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

How to simplify a null-safe compareTo() implementation?

提问人:Jonik 提问时间:1/27/2009 最后编辑:CommunityJonik 更新时间:11/19/2022 访问量:212496

问:

我正在为一个简单的类实现方法,例如(以便能够使用 Java 平台提供的其他好东西):compareTo()Collections.sort()

public class Metadata implements Comparable<Metadata> {
    private String name;
    private String value;

// Imagine basic constructor and accessors here
// Irrelevant parts omitted
}

我希望这些对象的自然顺序是:1)按名称排序,2)如果名称相同,则按值排序;这两种比较都应该不区分大小写。对于这两个字段,null 值是完全可以接受的,因此在这些情况下不得中断。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 上,当时 Eddie 的纯 JDK 解决方案是我首选的答案。直到现在(2017 年),我才开始改变这一点。

还有第三方库解决方案——2009 年的 Apache Commons Collections 和 2013 年的 Guava,都是我发布的——我确实在某个时间点更喜欢它们。

现在,我把 Lukasz Wiktor 的 Java 8 解决方案作为公认的答案。如果在 Java 8 上,这绝对是首选,而现在 Java 8 应该可用于几乎所有项目。

Java 重构 比较 null compareto

评论

0赞 Ciro Santilli OurBigBook.com 6/2/2015
stackoverflow.com/questions/369383/......

答:

4赞 Fabian Steeg 1/27/2009 #1

你可以把你的类设计成不可变的(Effective Java 2nd Ed. 有一个很好的部分,第 15 项:最小化可变性),并确保在构造时没有 null 是可能的(如果需要,请使用 null 对象模式)。然后,您可以跳过所有这些检查,并安全地假设这些值不为 null。

评论

0赞 Jonik 1/27/2009
是的,这通常是一个很好的解决方案,并且简化了许多事情 - 但在这里,我更感兴趣的是出于某种原因允许 null 值的情况,并且必须考虑:)
96赞 Eddie 1/27/2009 #2

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

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

我会用如下方式实现它:

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

评论

2赞 Eddie 1/27/2009
关于嵌套的“如果”......我发现嵌套的 if 在这种情况下可读性较差,所以我避免了它。是的,有时会因此而进行不必要的比较。final for parameters 不是必需的,但是个好主意。
9赞 luis.espinal 7/13/2012
@phihag - 我知道已经超过 3 年了,但是......关键字不是真正必要的(Java 代码已经很冗长了。但是,它确实阻止了将参数重用为本地变量(一种糟糕的编码做法)。随着时间的流逝,我们对软件的集体理解越来越好,我们知道默认情况下,事情应该是最终的/恒定的/不可变的。因此,我更喜欢在参数声明中使用一些额外的冗长(无论函数可能多么微不足道)来获得 .)它的可理解性/可维护性开销在宏伟的计划中可以忽略不计。finalfinalinmutability-by-quasi-default
26赞 bvdb 7/27/2014
@James麦克马洪,我不得不不同意。Xor (^) 可以简单地替换为不等于 (!=)。它甚至可以编译为相同的字节码。!= vs ^ 的用法只是品味和可读性的问题。所以,从你感到惊讶的事实来看,我会说它不属于这里。尝试计算校验和时使用 xor。在大多数其他情况下(比如这个),让我们坚持使用 !=。
2赞 Harvey 11/24/2015
@bvdb:如果先进行测试,则使用可以使其他情况更具可读性。就此而言,我建议:one==null && two==nullone==null || two==nullif (one==null || two==null) { if (one==two) return 0; return lhs==null ? -1 : 1; }
5赞 Thierry 7/5/2016
通过将 String 替换为 T、T 声明为 <T extends Comparable<T>>...然后我们可以安全地比较任何可为 null 的 Comparable 对象
6赞 Yoni Roit 1/27/2009 #3

您可以提取方法:

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

}

评论

3赞 Rolf Kristensen 8/5/2011
“0 : 1” 不应该是 “0 : -1” 吗?
14赞 Patrick 1/27/2009 #4

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

您感兴趣的类是 Null Comparator。它允许您使 null 变高或变低。您还可以为它提供自己的比较器,以便在两个值不为 null 时使用。

在你的例子中,你可以有一个静态成员变量来进行比较,然后你的方法只是引用它。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 元素的列表进行排序时非常有用。

评论

0赞 Jonik 2/1/2009
谢谢,我有点希望共享资源中会有这样的东西!然而,在这种情况下,我最终没有使用它:stackoverflow.com/questions/481813/......
0赞 Jonik 2/2/2009
刚刚意识到您的方法可以通过使用String.CASE_INSENSITIVE_ORDER来简化;请参阅我编辑的后续答案。
0赞 Daniel Alexiuc 12/13/2012
这很好,但“if (other == null) {”检查不应该存在。Comparable 的 Javadoc 表示,如果 other 为 null,compareTo 应该抛出 NullPointerException。
23赞 Jonik 2/1/2009 #5

请参阅此答案的底部,了解使用番石榴的更新(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);
}

这是帮助程序的定义方式(它被重载,以便你还可以定义 null 是先出现还是最后出现,如果需要):

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

因此,这与 Eddie 的答案(尽管我不会将静态辅助方法称为比较器)和 uzhin 的答案基本相同。

无论如何,总的来说,我强烈赞成 Patrick 的解决方案,因为我认为尽可能使用已建立的库是一种很好的做法。(正如 Josh Bloch 所说,了解并使用这些库。但在这种情况下,这不会产生最干净、最简单的代码。

编辑 (2009): Apache Commons Collections version

实际上,这里有一种方法可以使基于 Apache Commons NullComparator 的解决方案更简单。将其与类中提供的不区分大小写的比较器结合使用: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);
}

现在我认为这很优雅。(只剩下一个小问题:共享资源不支持泛型,所以有一个未经检查的分配。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()

如果您想知道 Ordering,请注意它实现了 .它非常方便,特别是对于更复杂的排序需求,例如,允许您使用 .阅读订购说明以了解更多信息!Comparatorcompound()

评论

2赞 Patrick 2/3/2009
String.CASE_INSENSITIVE_ORDER确实使解决方案更加清洁。不错的更新。
2赞 amoebe 9/24/2012
如果你仍然使用Apache Commons,有一个ComparatorChain,所以你不需要自己的方法。compareTo
244赞 Dag 4/5/2012 #6

您可以简单地使用 Apache Commons Lang

result = ObjectUtils.compare(firstComparable, secondComparable)

评论

6赞 Jonik 7/27/2013
(@Kong:这照顾了空安全性,但不区分大小写,这是原始问题的另一个方面。因此不会改变公认的答案。
3赞 Jonik 12/19/2013
另外,在我看来,Apache Commons不应该是2013年公认的答案。(即使某些子项目比其他子项目维护得更好。番石榴可以用来实现同样的事情;看/。nullsFirst()nullsLast()
10赞 reallynice 8/7/2015
@Jonik 为什么你认为 Apache Commons 不应该成为 2013 年公认的答案?
3赞 Jonik 7/13/2016
Apache Commons 的大部分内容都是遗留的/维护不善的/低质量的东西。对于它提供的大多数东西,都有更好的选择,例如在 Guava 中,它自始至终都是一个非常高质量的库,并且在 JDK 本身中越来越多。在 2005 年左右,是的,Apache Commons 是狗屎,但现在大多数项目都不需要它。(当然,也有例外;例如,如果我出于某种原因需要FTP客户端,我可能会使用Apache Commons Net中的那个客户端,等等。
8赞 Paul 4/1/2017
@Jonik,你会如何用番石榴回答这个问题?您断言 Apache Commons Lang(软件包)是“遗留/维护不良/质量低”是错误的,或者充其量是毫无根据的。Commons Lang3 易于理解和使用,并且得到了积极的维护。它可能是我最常使用的库(除了 Spring Framework 和 Spring Security)——例如,带有 null 安全方法的 StringUtils 类使输入规范化变得微不足道。org.apache.commons.lang3
7赞 Piotr Sobczyk 7/24/2013 #7

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

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

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

因此,我要么显式抛出 NullPointerException,要么在取消引用 null 参数时首次抛出它。

224赞 Lukasz Wiktor 5/28/2014 #8

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

评论

12赞 jschreiner 5/12/2017
我支持使用 Java 8 内置的东西来支持 Apache Commons Lang,但 Java 8 代码非常丑陋,而且仍然很冗长。我暂时坚持使用org.apache.commons.lang3.builder.CompareToBuilder。
2赞 Pedro Borges 6/12/2018
这不适用于 Collections.sort(Arrays.asList(null, val1, null, val2, null)),因为它会尝试在 null 对象上调用 compareTo()。老实说,这看起来像是集合框架的问题,试图弄清楚如何解决这个问题。
3赞 Scrubbie 1/28/2020
@PedroBorges 作者询问了对具有可排序字段的容器对象(这些字段可能为 null)进行排序的问题,而不是对 null 容器引用进行排序。因此,虽然您的注释是正确的,但当列表包含 null 时,它不起作用,因此注释与问题无关。Collections.sort(List)
2赞 Holger 4/9/2021
@PedroBorges值不能有自然的顺序。如果要对包含 的列表或数组进行排序,则必须使用 。nullnullComparator
2赞 Dustin 10/8/2014 #9

我一直在寻找类似的东西,这似乎有点复杂,所以我这样做了。我认为这更容易理解。您可以将其用作比较器或单衬。对于这个问题,您将更改为 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);
    }
}
0赞 snp0k 8/11/2015 #10

另一个 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());
}
2赞 Björn Bergenheim 1/19/2016 #11

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

0赞 Angel Koh 3/16/2016 #12

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

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

“compareIfNull()” 方法用于 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;
}
2赞 Leo Ng 3/19/2016 #13

我们可以使用 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
1赞 kisna 7/12/2016 #14

对于您知道数据不会有空值(对于字符串来说总是一个好主意)并且数据非常大的特定情况,在实际比较值之前,您仍然要进行三次比较,如果您确定这是您的情况,您可以稍微优化一下。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);
4赞 Nikhil Kumar K 2/14/2017 #15
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]
1赞 Amandeep Singh 3/9/2019 #16

使用 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;
    }
-1赞 MarsPeople 3/21/2020 #17

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

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

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

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

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

评论

0赞 Marian Klühspies 10/26/2022
在比较对象时修改对象是一个非常糟糕的黑客 imo。你永远不知道系统的其余部分会用这些值做什么
0赞 lars 11/19/2022 #18

可以处理 null 方面并在自定义方法实现中使用的泛型实用程序类可能如下所示:compareTo

/**
 * Generic utility class for null-safe comparison.
 */
public class Comparing
{
    /**
     * Compares two objects for order. Returns a negative integer, zero, or a
     * positive integer if the first object is less than, equal to, or greater
     * than the second object. Any of the objects can be null. A null value is
     * considered to be less than a non-null value.
     * 
     * @param <T>
     * @param a the first object.
     * @param b the second object.
     * @return an integer value.
     */
    public static <T extends Comparable<T>> int compareTo( T a, T b )
    {
        if ( a == b )
        {
            return 0;
        }

        return a != null ? b != null ? a.compareTo( b ) : 1 : -1;
    }
}