如何简化空安全 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不能中断。

我想到的解决方案大致如下(我在这里使用了“ Guard 子句”,而其他人可能更喜欢单一的返回点,但那不是重点) :

// 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 标准库或 ApacheCommons。使这个(稍微)简单一点的唯一选择是实现我自己的“ NullSafeStringComparator”,并应用它来比较两个字段吗?

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

关于公认的答案

我在2009年的时候问过这个问题,当然是在 Java 1.6上,当时 Eddie 的纯 JDK 解决方案是我首选的答案。直到现在(2017年) ,我才抽出时间来改变这一点。

还有我发布的 第三方图书馆解决方案ーー2009年的 Apache Commons Collections1和2013年的 Guava ーー我在某个时候的确更喜欢它们。

我现在使干净的 Java 8解决方案作者: Lukasz Wiktor接受的答案。如果在 Java8上,这肯定是首选的,而且现在几乎所有的项目都可以使用 Java8。

198665 次浏览

您可以将类设计为不可变的(有效的 Java2nd Ed。有一个伟大的部分对此,项目15: 最大限度地减少可变性) ,并确保在构造时,没有空值是可能的(并使用 空对象模式,如果需要)。然后,您可以跳过所有这些检查,并安全地假定这些值不为空。

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

注意: 上面的比较器,如果 都有名称为 null,甚至不会比较值字段。我觉得这不是你想要的。

我将通过以下内容来实现这一点:

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

编辑: 修正了代码示例中的错误。这就是我没有先测试它的后果!

编辑: 将 nullSafeStringCompator 升级为 static。

你可以提取方法:

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

}

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

你感兴趣的类是 空比较器。它允许您将空值设置为高或低。当这两个值不为空时,您还为它提供了自己的比较器。

在您的情况下,您可以有一个静态成员变量来进行比较,然后您的 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);
}

}

即使你决定卷你自己的,记住这个类,因为它是非常有用的当排序列表包含空元素。

有关使用番石榴的更新(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);
}

这就是帮助器的定义方式(它被重载了,所以如果需要的话,您还可以定义 null 是先出现还是最后出现) :

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

因此,这与 Eddie 的回答(尽管我不会把静态助手方法称为 比较器)和 乌金的基本相同。

无论如何,总的来说,我会强烈支持 帕特里克的解决方案,因为我认为只要有可能就使用已建立的库是一个很好的实践。(正如 Josh Bloch 所说的 了解和使用库。)但在这种情况下,这不会产生最干净、最简单的代码。

编辑(2009) : ApacheCommons 集合版本

实际上,这里有一种使基于 ApacheCommons NullComparator的解决方案更简单的方法。将它与 String类中提供的 不区分大小写的 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 编码,我(当然)会使用 番石榴。(当然还有 没有 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);
}

当然,这与 ApacheCommons 版本几乎完全相同(两者都使用 JDK 的 不敏感顺序) ,使用 nullsLast()是唯一特定于番石榴的东西。这个版本更可取,仅仅是因为作为一个依赖项,番石榴比 Commons Collection 更可取。(作为 大家都同意)

如果您想了解 Ordering,请注意它实现了 Comparator。它非常方便,特别是对于更复杂的排序需求,例如允许您使用 compound()链接多个 Orderings。阅读 < em > 排序解释 了解更多!

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

result = ObjectUtils.compare(firstComparable, secondComparable)

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

但是我想指出的是,在 compareTo 中支持 null 并不符合 可比较的 javadocs官方描述的 compareTo 合同:

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

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

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

我正在寻找类似的东西,这似乎有点复杂,所以我这样做。我觉得这样更容易理解。您可以将它用作 Compaator 或一行程序。对于这个问题,您可以更改为 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);
}
}

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

如果有人使用 Spring,那么有一个 org.springframework.util.compator 类。NullSafe 比较器,它也可以为您完成这项工作。就这样装饰你自己吧

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

Https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/util/comparator/nullsafecomparator.html

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

对于我的情况,EntityPhone 扩展了 Entity摘要,我的容器是 List < Entity摘要 > 。

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

我们可以使用 java 8在对象之间进行空友好的比较。 假设我有一个 Boy 类,有两个字段: String name 和 Integer age,我想先比较名字,然后如果两者相等的话比较年龄。

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

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

使用 NullSafe 比较器的一个简单方法是使用 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;
}

如果你想要一个简单的黑客技巧:

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方法实现中使用的通用实用程序类如下所示:

/**
* 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;
}
}