如何简化null安全的compareTo()实现?

时间:2009-01-26 23:25:15

标签: java refactoring comparison null compareto

我正在为一个简单的类实现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 Commons。使(稍微)更简单的唯一选择是实现我自己的“NullSafeStringComparator”,并将其应用于比较两个字段吗?

编辑1-3 :Eddie是对的;修复了“两个名字都为空”的情况

关于接受的答案

我在2009年就Java 1.6问了这个问题,当时 the pure JDK solution by Eddie 是我首选的答案。直到现在(2017年)我都没有改变它。

还有3rd party library solutions - 一个2009年的Apache Commons Collections one和一个2013 Guava one,两者都是我发布的 - 我确实喜欢在某个时间点。

我现在将干净的 Java 8 solution by Lukasz Wiktor 作为接受的答案。如果在Java 8上,这绝对应该是首选,而且现在几乎所有项目都可以使用Java 8。

17 个答案:

答案 0 :(得分:189)

您只需使用Apache Commons Lang

即可
result = ObjectUtils.compare(firstComparable, secondComparable)

答案 1 :(得分:147)

使用 Java 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 :(得分:90)

我会实现一个空安全比较器。可能有一个实现,但实现起来非常简单,我总是推出自己的实现。

注意:如果两个名称均为空,则上面的比较器甚至不会比较值字段。我认为这不是你想要的。

我会用以下内容实现:

// 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提升为静态。

答案 3 :(得分:21)

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


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

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基本相同(虽然我不会将静态辅助方法称为比较器)和that of uzhin

无论如何,总的来说,我会强烈赞成Patrick's solution,因为我认为尽可能使用已建立的库是一种好习惯。 (知道并使用库,正如Josh Bloch所说。)但在这种情况下,它不会产生最干净,最简单的代码。

编辑(2009):Apache Commons Collections版本

实际上,这是一种使基于Apache Commons NullComparator的解决方案更简单的方法。将其与String类中提供的case-insensitive Comparator结合使用:

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);
}

我觉得这很优雅。 (只剩下一个小问题:Commons NullComparator不支持泛型,因此有一个未经检查的赋值。)

更新(2013):番石榴版

将近5年后,我就是这样解决原始问题的方法。如果用Java编码,我(当然)会使用Guava。 (当然不是 Apache Commons。)

将此值保持在某个位置,例如在“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);
}    

当然,这几乎与Apache Commons版本相同(两者都使用 JDK的CASE_INSENSITIVE_ORDER),使用nullsLast()是唯一的Guava特定的东西。这个版本更可取,因为Guava是Commons Collections的优先选择。 (作为everyone agrees。)

如果您对Ordering感到疑惑,请注意它实现了Comparator。它非常方便,特别是对于更复杂的排序需求,例如,您可以使用compound()链接多个排序。阅读Ordering Explained了解更多信息!

答案 4 :(得分:13)

我总是建议使用Apache commons,因为它很可能比你自己编写的更好。此外,你可以做“真正的”工作,而不是重新发明。

您感兴趣的课程是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);
}

}

即使你决定自己动手,也要记住这个类,因为它在订购包含null元素的列表时非常有用。

答案 5 :(得分:7)

我知道可能没有直接回答你的问题,因为你说必须支持空值。

但我只想指出,在compareTo中支持null并不符合官方javadocs for Comparable中描述的compareTo合约:

  

请注意,null不是任何类的实例,而e.compareTo(null)   即使e.equals(null)返回,也应抛出NullPointerException   假的。

所以我要么显式抛出NullPointerException,要么只是在解除引用null参数时抛出它。

答案 6 :(得分:4)

您可以提取方法:

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); 

}

答案 7 :(得分:3)

你可以将你的类设计为不可变的(Effective Java 2nd Ed。有一个很好的部分,第15项:最小化可变性)并确保在构造时不可能有空(并使用null object pattern if需要)。然后,您可以跳过所有这些检查并安全地假设值不为空。

答案 8 :(得分:2)

我正在寻找类似的东西,这看起来有点复杂所以我这样做了。我认为它更容易理解。您可以将它用作比较器或单线。对于这个问题,您将更改为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);
    }
}

答案 9 :(得分:2)

我们可以使用java 8在对象之间做一个无效的比较。 假设我有一个带有2个字段的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

答案 10 :(得分:1)

如果有人使用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

答案 11 :(得分:1)

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]

答案 12 :(得分:1)

using NullSafe Comparator的一种简单方法是使用它的Spring实现,下面是一个引用的简单示例:

public int compare(Object o1, Object o2) {
        ValidationMessage m1 = (ValidationMessage) o1;
        ValidationMessage m2 = (ValidationMessage) o2;
        int c;
        if (m1.getTimestamp() == m2.getTimestamp()) {
            c = NullSafeComparator.NULLS_HIGH.compare(m1.getProperty(), m2.getProperty());
            if (c == 0) {
                c = m1.getSeverity().compareTo(m2.getSeverity());
                if (c == 0) {
                    c = m1.getMessage().compareTo(m2.getMessage());
                }
            }
        }
        else {
            c = (m1.getTimestamp() > m2.getTimestamp()) ? -1 : 1;
        }
        return c;
    }

答案 13 :(得分:0)

这是我用来对ArrayList进行排序的实现。 null类被排序到最后一个。

对于我的情况,EntityPhone扩展了EntityAbstract,我的容器是List&lt; EntityAbstract&GT;

&#34; compareIfNull()&#34;方法用于null安全排序。其他方法是为了完整性,显示了如何使用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;
}

答案 14 :(得分:0)

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

@Override
public int compare(Object o1, Object o2) {
    String s1 = ObjectUtils.toString(o1);
    String s2 = ObjectUtils.toString(o2);
    return s1.toLowerCase().compareTo(s2.toLowerCase());
}

答案 15 :(得分:0)

对于您知道数据不会为空(对字符串总是一个好主意)并且数据非常大的特定情况,您在实际比较值之前仍然进行三次比较,如果您知道的话,确定这是你的情况,你可以优化一点点。 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);

答案 16 :(得分:0)

如果您想要简单的Hack:

arrlist.sort((o1, o2) -> {
    if (o1.getName() == null) o1.setName("");
    if (o2.getName() == null) o2.setName("");

    return o1.getName().compareTo(o2.getName());
})

如果您想将null放在列表的末尾,只需在方法上方更改

return o2.getName().compareTo(o1.getName());