代码之家  ›  专栏  ›  技术社区  ›  Jonik

如何简化空安全compareto()实现?

  •  130
  • Jonik  · 技术社区  · 17 年前

    我正在实施 compareTo() 一个简单类的方法,例如 Collections.sort() Java平台提供的其他东西:

    public class Metadata implements Comparable<Metadata> {
        private String name;
        private String value;
    
    // Imagine basic constructor and accessors here
    // Irrelevant parts omitted
    }
    

    我想要 自然排序 对于这些对象:1)按名称排序,2)如果名称相同,则按值排序;两个比较应不区分大小写。对于这两个字段,空值完全可以接受,因此 compareTo 在这些情况下不得断裂。

    我们首先想到的解决方案是以下几点(我在这里使用的是“保护条款”,而其他人可能更喜欢单一的返回点,但这并不重要):

    // primarily by name, secondarily by value; null-safe; case-insensitive
    public int compareTo(Metadata other) {
        if (this.name == null && other.name != null){
            return -1;
        }
        else if (this.name != null && other.name == null){
            return 1;
        }
        else if (this.name != null && other.name != null) {
            int result = this.name.compareToIgnoreCase(other.name);
            if (result != 0){
                return result;
            }
        }
    
        if (this.value == null) {
            return other.value == null ? 0 : -1;
        }
        if (other.value == null){
            return 1;
        }
    
        return this.value.compareToIgnoreCase(other.value);
    }
    

    这可以完成任务,但我对这个代码不太满意。当然不是 非常 复杂,但相当冗长和乏味。

    问题是, 你怎么能让这个不那么冗长 (同时保留功能)?如果他们有帮助,请随意参考Java标准库或Apache公文。使这(稍微)简单一点的唯一选择是实现自己的“NullSafeStringComparator”,并将其应用于比较这两个字段吗?

    编辑1-3 :Eddie的权利;修正了上面的“两个名字都是空的”大小写

    关于接受的答案

    我在2009问了这个问题,当然是在Java 1.6上。 the pure JDK solution by Eddie 是我的首选答案。直到现在(2017年),我才开始改变这一点。

    也有 3rd party library solutions _ 2009年阿帕奇公共收藏1和2013年番石榴1,都是我发布的_我在某个时间点更喜欢。

    我现在做了清洁 Java 8 solution by Lukasz Wiktor 公认的答案。如果在Java 8上,这肯定是首选的,现在Java 8应该可以用于几乎所有的项目。

    15 回复  |  直到 9 年前
        1
  •  145
  •   xehpuk    12 年前

    使用 爪哇8 :

    private static Comparator<String> nullSafeStringComparator = Comparator
            .nullsFirst(String::compareToIgnoreCase); 
    
    private static Comparator<Metadata> metadataComparator = Comparator
            .comparing(Metadata::getName, nullSafeStringComparator)
            .thenComparing(Metadata::getValue, nullSafeStringComparator);
    
    public int compareTo(Metadata that) {
        return metadataComparator.compare(this, that);
    }
    
        2
  •  182
  •   bluish dmajkic    9 年前

    你可以简单地使用 Apache Commons Lang :

    result = ObjectUtils.compare(firstComparable, secondComparable)
    
        3
  •  88
  •   Sentinel    9 年前

    我将实现一个空安全比较器。可能会有一个实现,但这是如此简单的实现,我一直在滚动我自己的。

    注:如果你的比较器高于 二者都 名称为空,甚至无法比较值字段。我不认为这是你想要的。

    我将通过如下方式实现这一点:

    // primarily by name, secondarily by value; null-safe; case-insensitive
    public int compareTo(final Metadata other) {
    
        if (other == null) {
            throw new NullPointerException();
        }
    
        int result = nullSafeStringComparator(this.name, other.name);
        if (result != 0) {
            return result;
        }
    
        return nullSafeStringComparator(this.value, other.value);
    }
    
    public static int nullSafeStringComparator(final String one, final String two) {
        if (one == null ^ two == null) {
            return (one == null) ? -1 : 1;
        }
    
        if (one == null && two == null) {
            return 0;
        }
    
        return one.compareToIgnoreCase(two);
    }
    

    编辑:修正了代码示例中的拼写错误。这就是为什么我不先测试它!

    编辑:将NullSafeStringComparator提升为静态。

        4
  •  21
  •   Community Mohan Dere    9 年前

    请参阅此答案的底部,了解使用guava的更新(2013)解决方案。


    这就是我最终的选择。原来我们已经有了一个用于空安全字符串比较的实用方法,所以最简单的解决方案就是利用它。(这是一个很大的代码库;很容易错过这种事情:)

    public int compareTo(Metadata other) {
        int result = StringUtils.compare(this.getName(), other.getName(), true);
        if (result != 0) {
            return result;
        }
        return StringUtils.compare(this.getValue(), other.getValue(), true);
    }
    

    这是如何定义助手的方法(它被重载,以便您还可以定义空值是第一个还是最后一个,如果需要的话):

    public static int compare(String s1, String s2, boolean ignoreCase) { ... }
    

    所以这基本上和 Eddie's answer (尽管我不会调用静态助手方法a 比较器 ) that of uzhin 也是。

    总之,总的来说,我会非常喜欢 Patrick's solution 我认为,尽可能使用已建立的库是一个好的实践。( 了解并使用图书馆 正如JoshBloch所说),但在这种情况下,不会产生最干净、最简单的代码。

    编辑(2009):Apache Commons Collections版本

    实际上,这里有一种基于ApacheCommons的解决方案 NullComparator 更简单。把它和 case-insensitive Comparator 提供在 String 班级:

    public static final Comparator<String> NULL_SAFE_COMPARATOR 
        = new NullComparator(String.CASE_INSENSITIVE_ORDER);
    
    @Override
    public int compareTo(Metadata other) {
        int result = NULL_SAFE_COMPARATOR.compare(this.name, other.name);
        if (result != 0) {
            return result;
        }
        return NULL_SAFE_COMPARATOR.compare(this.value, other.value);
    }
    

    我觉得这很优雅。(只剩下一个小问题:下议院 零比较器 不支持泛型,因此存在未选中的分配。)

    更新(2013):番石榴版本

    近5年后,我将如何处理我最初的问题。如果在爪哇编码,我当然会使用 Guava . (当然 阿帕奇公地。)

    将这个常量放在某个地方,例如在“StringUtils”类中:

    public static final Ordering<String> CASE_INSENSITIVE_NULL_SAFE_ORDER =
        Ordering.from(String.CASE_INSENSITIVE_ORDER).nullsLast(); // or nullsFirst()
    

    然后,在 public class Metadata implements Comparable<Metadata> :

    @Override
    public int compareTo(Metadata other) {
        int result = CASE_INSENSITIVE_NULL_SAFE_ORDER.compare(this.name, other.name);
        if (result != 0) {
            return result;
        }
        return CASE_INSENSITIVE_NULL_SAFE_ORDER.compare(this.value, other.value);
    }    
    

    当然,这几乎与ApacheCommons版本相同(两者都使用 JDK公司 CASE_INSENSITIVE_ORDER )使用 nullsLast() 只是番石榴特有的东西。这个版本更可取,因为作为依赖关系,guava比commons集合更可取。(AS) everyone agrees )

    如果你想知道 Ordering ,注意它实现了 比较器 . 它非常方便,特别是对于更复杂的排序需求,允许您例如使用 compound() . 读 Ordering Explained 更多!

        5
  •  13
  •   Patrick Scott Bale    17 年前

    我总是推荐使用ApacheCommons,因为它很可能比您自己写的更好。另外,你可以做“真正的”工作,而不是重新发明。

    你感兴趣的班级是 Null Comparator . 它允许您将空值设为高或低。当这两个值不为空时,您还可以使用自己的比较器。

    在您的例子中,可以有一个静态成员变量来进行比较,然后 compareTo 方法只是引用它。

    形似

    class Metadata implements Comparable<Metadata> {
    private String name;
    private String value;
    
    static NullComparator nullAndCaseInsensitveComparator = new NullComparator(
            new Comparator<String>() {
    
                @Override
                public int compare(String o1, String o2) {
                    // inputs can't be null
                    return o1.compareToIgnoreCase(o2);
                }
    
            });
    
    @Override
    public int compareTo(Metadata other) {
        if (other == null) {
            return 1;
        }
        int res = nullAndCaseInsensitveComparator.compare(name, other.name);
        if (res != 0)
            return res;
    
        return nullAndCaseInsensitveComparator.compare(value, other.value);
    }
    

    }

    即使您决定滚动自己的类,也要记住这个类,因为它在排序包含空元素的列表时非常有用。

        6
  •  7
  •   Umpa Gourneau    11 年前

    我知道它可能不能直接回答您的问题,因为您说过必须支持空值。

    但我只想指出,CompareTo中支持空值与官方描述的CompareTo合同不一致。 javadocs for Comparable :

    注意,空不是任何类的实例,E.CompareTo(空) 即使E.Equals(null)返回,也应引发NullPointerException 错误的。

    所以我要么显式地抛出nullpointerException,要么在取消引用空参数时让它第一次被抛出。

        7
  •  4
  •   Yoni Roit    17 年前

    您可以提取方法:

    public int cmp(String txt, String otherTxt)
    {
        if ( txt == null )
            return otjerTxt == null ? 0 : 1;
    
        if ( otherTxt == null )
              return 1;
    
        return txt.compareToIgnoreCase(otherTxt);
    }
    
    public int compareTo(Metadata other) {
       int result = cmp( name, other.name); 
       if ( result != 0 )  return result;
       return cmp( value, other.value); 
    

    }

        8
  •  3
  •   Fabian Steeg    17 年前

    你可以设计你的类是不可变的(有效Java第二ED。在这方面有一个很好的部分,第15项:最小化易变性),并且确保在构造上没有空值(并且使用 null object pattern 如果需要的话)。然后您可以跳过所有这些检查,并安全地假定值不是空值。

        9
  •  2
  •   Dustin    11 年前

    我在找类似的东西,这看起来有点复杂,所以我这样做了。我觉得这有点容易理解。您可以将它用作比较器或一条直线。对于此问题,您将更改为CompareToIgnoreCase()。实际上,空值是向上浮动的。如果你想让它们下沉,你可以翻转1,-1。

    StringUtil.NULL_SAFE_COMPARATOR.compare(getName(), o.getName());
    

    .

    public class StringUtil {
        public static final Comparator<String> NULL_SAFE_COMPARATOR = new Comparator<String>() {
    
            @Override
            public int compare(final String s1, final String s2) {
                if (s1 == s2) {
                    //Nulls or exact equality
                    return 0;
                } else if (s1 == null) {
                    //s1 null and s2 not null, so s1 less
                    return -1;
                } else if (s2 == null) {
                    //s2 null and s1 not null, so s1 greater
                    return 1;
                } else {
                    return s1.compareTo(s2);
                }
            }
        }; 
    
        public static void main(String args[]) {
            final ArrayList<String> list = new ArrayList<String>(Arrays.asList(new String[]{"qad", "bad", "sad", null, "had"}));
            Collections.sort(list, NULL_SAFE_COMPARATOR);
    
            System.out.println(list);
        }
    }
    
        10
  •  2
  •   Leo Ng    10 年前

    我们可以使用Java 8对对象进行空友好的比较。 假设我有一个包含两个字段的boy类:字符串名称和整数年龄,如果两个字段都相等,我想首先比较名称,然后比较年龄。

    static void test2() {
        List<Boy> list = new ArrayList<>();
        list.add(new Boy("Peter", null));
        list.add(new Boy("Tom", 24));
        list.add(new Boy("Peter", 20));
        list.add(new Boy("Peter", 23));
        list.add(new Boy("Peter", 18));
        list.add(new Boy(null, 19));
        list.add(new Boy(null, 12));
        list.add(new Boy(null, 24));
        list.add(new Boy("Peter", null));
        list.add(new Boy(null, 21));
        list.add(new Boy("John", 30));
    
        List<Boy> list2 = list.stream()
                .sorted(comparing(Boy::getName, 
                            nullsLast(naturalOrder()))
                       .thenComparing(Boy::getAge, 
                            nullsLast(naturalOrder())))
                .collect(toList());
        list2.stream().forEach(System.out::println);
    
    }
    
    private static class Boy {
        private String name;
        private Integer age;
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public Integer getAge() {
            return age;
        }
        public void setAge(Integer age) {
            this.age = age;
        }
        public Boy(String name, Integer age) {
            this.name = name;
            this.age = age;
        }
    
        public String toString() {
            return "name: " + name + " age: " + age;
        }
    }
    

    结果:

        name: John age: 30
        name: Peter age: 18
        name: Peter age: 20
        name: Peter age: 23
        name: Peter age: null
        name: Peter age: null
        name: Tom age: 24
        name: null age: 12
        name: null age: 19
        name: null age: 21
        name: null age: 24
    
        11
  •  1
  •   Björn Bergenheim    10 年前

    在使用Spring的情况下,也有一个类org.springframework.util.comparator.nullsafecomparator可以为您实现这一点。把你自己的房子装饰成这样就可以了

    new NullSafeComparator<YourObject>(new YourComparable(), true)

    https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/util/comparator/NullSafeComparator.html

        12
  •  1
  •   Nikhil Kumar K    9 年前
    import java.util.ArrayList;
    import java.util.Iterator;
    import java.util.List;
    import java.util.Comparator;
    
    public class TestClass {
    
        public static void main(String[] args) {
    
            Student s1 = new Student("1","Nikhil");
            Student s2 = new Student("1","*");
            Student s3 = new Student("1",null);
            Student s11 = new Student("2","Nikhil");
            Student s12 = new Student("2","*");
            Student s13 = new Student("2",null);
            List<Student> list = new ArrayList<Student>();
            list.add(s1);
            list.add(s2);
            list.add(s3);
            list.add(s11);
            list.add(s12);
            list.add(s13);
    
            list.sort(Comparator.comparing(Student::getName,Comparator.nullsLast(Comparator.naturalOrder())));
    
            for (Iterator iterator = list.iterator(); iterator.hasNext();) {
                Student student = (Student) iterator.next();
                System.out.println(student);
            }
    
    
        }
    
    }
    

    输出是

    Student [name=*, id=1]
    Student [name=*, id=2]
    Student [name=Nikhil, id=1]
    Student [name=Nikhil, id=2]
    Student [name=null, id=1]
    Student [name=null, id=2]
    
        13
  •  0
  •   snp0k    10 年前

    另一个apache objectuils示例。能够对其他类型的对象进行排序。

    @Override
    public int compare(Object o1, Object o2) {
        String s1 = ObjectUtils.toString(o1);
        String s2 = ObjectUtils.toString(o2);
        return s1.toLowerCase().compareTo(s2.toLowerCase());
    }
    
        14
  •  0
  •   Angel Koh Sergey Kalinichenko    10 年前

    这是我用来排序数组列表的实现。空类被排序到最后一个。

    对于我的情况,entityphone扩展了entityabstract,我的容器是list<entityabstract>。

    “compareifnull()”方法用于空安全排序。其他方法是为了完整性,显示如何使用compareifnull。

    @Nullable
    private static Integer compareIfNull(EntityPhone ep1, EntityPhone ep2) {
    
        if (ep1 == null || ep2 == null) {
            if (ep1 == ep2) {
                return 0;
            }
            return ep1 == null ? -1 : 1;
        }
        return null;
    }
    
    private static final Comparator<EntityAbstract> AbsComparatorByName = = new Comparator<EntityAbstract>() {
        @Override
        public int compare(EntityAbstract ea1, EntityAbstract ea2) {
    
        //sort type Phone first.
        EntityPhone ep1 = getEntityPhone(ea1);
        EntityPhone ep2 = getEntityPhone(ea2);
    
        //null compare
        Integer x = compareIfNull(ep1, ep2);
        if (x != null) return x;
    
        String name1 = ep1.getName().toUpperCase();
        String name2 = ep2.getName().toUpperCase();
    
        return name1.compareTo(name2);
    }
    }
    
    
    private static EntityPhone getEntityPhone(EntityAbstract ea) { 
        return (ea != null && ea.getClass() == EntityPhone.class) ?
                (EntityPhone) ea : null;
    }
    
        15
  •  0
  •   kisna    9 年前

    对于您知道数据不会有空值的特定情况(对于字符串总是一个好主意),并且数据非常大,在实际比较值之前,您仍然要进行三次比较, 如果你确定这是你的案子 你可以稍微优化一下。YMMV作为可读代码胜过次要优化:

            if(o1.name != null && o2.name != null){
                return o1.name.compareToIgnoreCase(o2.name);
            }
            // at least one is null
            return (o1.name == o2.name) ? 0 : (o1.name != null ? 1 : -1);