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

Posted

技术标签:

【中文标题】如何简化 null 安全的 compareTo() 实现?【英文标题】:How to simplify a null-safe compareTo() implementation? 【发布时间】:2010-10-03 16:00:54 【问题描述】:

我正在为这样一个简单的类实现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 和一个 2013 年的 Guava,都是我发布的——我在某个时间点确实更喜欢它们。

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

【问题讨论】:

***.com/questions/369383/… 【参考方案1】:

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

result = ObjectUtils.compare(firstComparable, secondComparable)

【讨论】:

(@Kong:这需要注意 null 安全性但不区分大小写,这是原始问题的另一个方面。因此不会改变接受的答案。) 另外,在我看来,Apache Commons 不应该是 2013 年公认的答案。(即使某些子项目比其他子项目维护得更好。)Guava can be used to achieve the same thing;见nullsFirst()/nullsLast() @Jonik 为什么你认为 Apache Commons 不应该成为 2013 年公认的答案? Apache Commons 的大部分是遗留/维护不善/低质量的东西。它提供的大多数东西都有更好的选择,例如Guava,这是一个非常高质量的库,并且越来越多地出现在 JDK 本身中。是的,在 2005 年左右,Apache Commons 是垃圾,但现在大多数项目都不需要它。 (当然,也有例外;例如,如果我出于某种原因需要一个 FTP 客户端,我可能会使用 Apache Commons Net 中的那个,等等。) @Jonik,你会如何使用 Guava 回答这个问题?您断言 Apache Commons Lang(包org.apache.commons.lang3)是“遗留/维护不善/质量低下”是错误的,或者充其量是毫无根据的。 Commons Lang3 易于理解和使用,并且得到积极维护。它可能是我最常用的库(除了 Spring Framework 和 Spring Security)——例如,StringUtils 类及其 null 安全方法使输入规范化变得微不足道。【参考方案2】:

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

【讨论】:

我支持使用 Java 8 内置的东西来支持 Apache Commons Lang,但是 Java 8 代码很丑陋,而且仍然很冗长。目前我会坚持使用 org.apache.commons.lang3.builder.CompareToBuilder。 这对 Collections.sort(Arrays.asList(null, val1, null, val2, null)) 不起作用,因为它会尝试在 null 对象上调用 compareTo()。老实说,它看起来像是集合框架的问题,试图弄清楚如何解决这个问题。 @PedroBorges 作者询问对拥有可排序字段(这些字段可能为空)的容器对象进行排序,而不是对空容器引用进行排序。因此,虽然您的评论是正确的,但当列表包含空值时 Collections.sort(List) 不起作用,评论与问题无关。 @PedroBorges null 值不能有自然顺序。如果要对包含null 的列表或数组进行排序,则必须使用Comparator【参考方案3】:

我会实现一个空安全比较器。那里可能有一个实现,但这实现起来非常简单,我总是自己动手。

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

我会用类似下面的东西来实现它:

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

【讨论】:

关于嵌套的 "if" ...我发现嵌套 if 在这种情况下可读性较差,所以我避免使用它。是的,有时会因此而进行不必要的比较。参数的 final 不是必需的,但它是个好主意。 @phihag - 我知道它已经超过 3 年了,但是...... final 关键字并不是真正需要的(Java 代码已经很冗长了。)但是,它确实阻止了参数的重用作为本地变量(一种糟糕的编码实践)。随着我们对软件的集体理解随着时间的推移而变得更好,我们知道默认情况下事物应该是 final/const/inmutable。所以我更喜欢在参数声明中使用final 来获得inmutability-by-quasi-default 时更加冗长(无论函数多么微不足道)。)它的可理解性/可维护性开销在总体方案中可以忽略不计。 @James McMahon 我不同意。 Xor (^) 可以简单地替换为不等于 (!=)。它甚至可以编译成相同的字节码。 != vs ^ 的使用只是品味和可读性的问题。所以,从你很惊讶的事实来看,我会说它不属于这里。尝试计算校验和时使用 xor。在大多数其他情况下(比如这个),让我们坚持使用 !=. @bvdb: 如果one==null &amp;&amp; two==null 测试首先完成,那么使用one==null || two==null 可以使其他情况更具可读性。就此而言,我建议:if (one==null || two==null) if (one==two) return 0; return lhs==null ? -1 : 1; 用 T 替换 String 很容易扩展这个答案,T 声明为 >... 然后我们可以安全地比较任何可为空的 Comparable 对象【参考方案4】:

有关使用 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&lt;Metadata&gt;

@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了解更多信息!

【讨论】:

String.CASE_INSENSITIVE_ORDER 确实使解决方案更加简洁。不错的更新。 如果您仍然使用 Apache Commons,则有一个 ComparatorChain,因此您不需要自己的 compareTo 方法。【参考方案5】:

我始终建议使用 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);

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

【讨论】:

谢谢,我有点希望在 Commons 中有这样的东西!但是,在这种情况下,我最终没有使用它:***.com/questions/481813/… 刚刚意识到您的方法可以通过使用 String.CASE_INSENSITIVE_ORDER 来简化;请参阅我编辑的后续答案。 这很好,但不应该有“if (other == null) ”检查。 Comparable 的 Javadoc 说,如果 other 为 null,compareTo 应该抛出 NullPointerException。【参考方案6】:

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

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

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

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

【讨论】:

【参考方案7】:

你可以提取方法:

public int cmp(String txt, String otherTxt)

    if ( txt == null )
        return otherTxt == 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); 

【讨论】:

“0:1”不应该是“0:-1”吗?【参考方案8】:

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

【讨论】:

是的,这通常是一个很好的解决方案,并且简化了许多事情 - 但在这里我更感兴趣的是允许空值的情况,出于某种原因,必须考虑到 :) 【参考方案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]

【讨论】:

【参考方案10】:

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

【讨论】:

【参考方案11】:

如果有人使用 Spring,还有一个类 org.springframework.util.comparator.NullSafeComparator 也可以为您执行此操作。像这样装饰你自己的与之媲美

new NullSafeComparator&lt;YourObject&gt;(new YourComparable(), true)

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

【讨论】:

【参考方案12】:

我们可以使用 java 8 在对象之间进行 null 友好的比较。 假设我有一个包含 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

【讨论】:

【参考方案13】:

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

【讨论】:

【参考方案14】:

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;
    

【讨论】:

【参考方案15】:

另一个 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());

【讨论】:

【参考方案16】:

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

就我而言,EntityPhone 扩展了 EntityAbstract,而我的容器是 List 。

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

【讨论】:

【参考方案17】:

如果你想要一个简单的 Hack:

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

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

如果你想把空值放在列表的末尾,只需在上面的方法中更改它

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

【讨论】:

以上是关于如何简化 null 安全的 compareTo() 实现?的主要内容,如果未能解决你的问题,请参考以下文章

Flutter:Geolocator返回方法'compareTo'在null上被调用

ArgumentNullException - 如何简化?

实例演示:如何简化生产中的Pod安全策略?

如何定义方法 compareTo()?

javascript原始数据类型compareto引用数据类型--近3天不太会的地方

FindBugs - 如何解决 EQ_COMPARETO_USE_OBJECT_EQUALS