如何简化一个空安全的compareTo()实现?


156

我正在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:埃迪的权利;修复了上面的“名字都为空”的情况

关于接受的答案

我早在2009年就在Java 1.6上问了这个问题,当时我首选的是Eddie的纯JDK解决方案。直到现在(2017年),我再也没有回过头来改变它。

也有第三方库的解决方案 -a 2009年阿帕奇百科全书集合一年2013番石榴一个,两张贴由我,那我也喜欢在某个时间点。

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


Answers:


173

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

7
我支持使用Java 8内置的东西来支持Apache Commons Lang,但是Java 8代码很丑陋,而且仍然很冗长。我将暂时使用org.apache.commons.lang3.builder.CompareToBuilder。
jschreiner

1
这不适用于Collections.sort(Arrays.asList(null,val1,null,val2,null)),因为它将尝试在null对象上调用compareTo()。坦白地说,这似乎是集合框架的问题,试图找出解决方法。
Pedro Borges

3
@PedroBorges作者询问是否对具有可排序字段(其中的字段可能为null)的容器对象进行排序,而不是对空容器引用进行排序。因此,虽然您的注释正确,但是Collections.sort(List)当列表包含空值时,该注释不起作用,因此注释与问题无关。
Scrubbie

211

您可以简单地使用Apache Commons Lang

result = ObjectUtils.compare(firstComparable, secondComparable)

4
(@Kong:这是为了确保无效性,而不是大小写不敏感,这是原始问题的另一个方面。因此不会更改已接受的答案。)
Jonik

3
另外,在我看来,Apache Commons在2013年不应被接受。(即使某些子项目比其他子项目得到更好的维护。)Guava可用于实现同一目标;见nullsFirst()/ nullsLast()
Jonik 2013年

8
@Jonik为什么您认为Apache Commons在2013年不应该被接受?
2015年

1
Apache Commons的大部分是遗留的/维护不良的/低质量的东西。它提供的大多数功能都有更好的选择,例如,在Guava中,这是一个非常高质量的库,在JDK中也越来越多。是的,大约在2005年左右,Apache Commons很烂,但是如今,大多数项目中都不需要它了。(当然,有例外;例如,如果出于某种原因需要FTP客户端,则可能会在Apache Commons Net中使用该客户端,依此类推。)
Jonik

6
@Jonik,您如何用番石榴回答问题?您关于Apache Commons Lang(程序包org.apache.commons.lang3)是“旧版/维护不良/质量低下”的说法是错误的,或者充其量是毫无根据的。Commons Lang3易于理解和使用,并且得到了积极维护。它可能是我最常使用的库(除了Spring Framework和Spring Security)-例如,StringUtils类及其null安全方法使输入规范化变得微不足道。
保罗

93

我将实现一个空安全比较器。可能有一个实现,但是实现起来如此简单,以至于我总是自己动手做。

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

我将通过以下类似的方式实现此目的:

// 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
关于嵌套的“ if” ...在这种情况下,我发现嵌套的if可读性较低,因此我避免了它。是的,因此有时会进行不必要的比较。对于参数,final不是必需的,但这是一个好主意。
Eddie

31
很好地使用XOR
James McMahon

9
@phihag-我知道这已经超过3年了,但是... final关键字并不是真正必需的(Java代码已经很冗长了。)但是,它的确避免了将参数重用为局部变量(这是一种糟糕的编码习惯)。随着时间的推移,我们对软件的集体理解会越来越好,我们知道默认情况下事物应该是final / const / inmutable。因此,我更喜欢final在参数声明中使用一些冗长的语言(但是该函数可能微不足道)inmutability-by-quasi-default。)在总体方案中,它的可理解性/可维护性开销可以忽略不计。
luis.espinal

24
@詹姆斯·麦克马洪我不得不不同意。Xor(^)可以简单地用不等于(!=)代替。它甚至可以编译为相同的字节码。!= vs ^的用法只是味道和可读性的问题。因此,从您感到惊讶的事实来看,我会说它不属于这里。尝试计算校验和时,请使用xor。在大多数其他情况下(例如这种情况),请坚持使用!=。
bvdb 2014年

5
通过用T替换String来扩展此答案很容易,T声明为<Textended Comparable <T >> ...,然后我们可以安全地比较任何可空的Comparable对象
Thierry

20

有关使用Guava的更新(2013)解决方案,请参见此答案的底部。


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

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的答案相同(尽管我不会将静态辅助方法称为比较器),也与uzhin 的答案相同。

总之,总的来说,我会强烈支持Patrick的解决方案,因为我认为最好使用已建立的库。(如Josh Bloch所说,知道并使用这些库。)但是在这种情况下,这将不会产生最干净,最简单的代码。

编辑(2009):Apache Commons Collections版本

实际上,这是一种使基于Apache Commons的解决方案NullComparator更简单的方法。将其与类中提供的不区分大小写的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);
}

我认为现在这非常优雅。(仍然有一个小问题: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更可取。(大家都同意。)

如果您想知道Ordering,请注意它实现了Comparator。它非常方便,尤其是对于更复杂的排序需求,例如,您可以使用链接多个Ordering compound()。阅读订购说明,了解更多!


2
String.CASE_INSENSITIVE_ORDER确实使解决方案更加简洁。不错的更新。
Patrick

2
如果仍然使用Apache Commons,那么这里有一个,ComparatorChain因此您不需要自己的compareTo方法。
amoebe 2012年

13

我总是建议使用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);
}

}

即使您决定自己动手,也请记住该类,因为在订购包含空元素的列表时它非常有用。


谢谢,我有点希望下议院会有这样的事情!但是,在这种情况下,我并没有最终使用它:stackoverflow.com/questions/481813/…–
Jonik,

刚刚意识到,可以使用String.CASE_INSENSITIVE_ORDER简化您的方法;查看我编辑的后续答案。
约尼克,

这很好,但是“ if(other == null){”检查不应该存在。适用于Comparable的Javadoc说,如果other为null,则compareTo应该抛出NullPointerException。
Daniel Alexiuc 2012年

7

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

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

请注意,null不是任何类的实例,即使e.equals(null)返回false,e.compareTo(null)也应引发NullPointerException。

因此,我要么显式抛出NullPointerException,要么在取消引用null参数时使其第一次抛出。


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

}


2
不应将“ 0:1”设为“ 0:-1”吗?
罗夫·克里斯滕森

3

您可以将您的类设计为不可变的(Effective Java 2nd Ed。对此有很大的介绍,第15项:最小化可变性),并在构造时确保没有空值是可能的(并在需要时使用空对象模式)。然后,您可以跳过所有这些检查,并安全地假定值不为null。


是的,通常这是一个很好的解决方案,并且可以简化很多事情-但是在这里,我对允许空值(出于某种原因或其他原因而必须将其考虑在内)的情况更感兴趣:)
Jonik,

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

2

我们可以使用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

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

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]

1

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

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

0

这是我用来对ArrayList进行排序的实现。空类排在最后。

就我而言,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;
}

0

如果您想要一个简单的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());
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.