如何简化空安全的compareTo()实现?
-
20-08-2019 - |
题
我正在实施 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 年)我才抽出时间来改变这一点。
还有 第三方图书馆解决方案— 2009 年的 Apache Commons Collections 和 2013 年的 Guava 都由我发布 — 在某个时间点我确实更喜欢它们。
我现在已经清理干净了 Lukasz Wiktor 的 Java 8 解决方案 接受的答案。如果在 Java 8 上,这绝对是首选,而且现在 Java 8 应该可用于几乎所有项目。
解决方案
使用的爪哇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);
}
其他提示
你可以简单地使用 阿帕奇通用语言:
result = ObjectUtils.compare(firstComparable, secondComparable)
我想实现一个空的安全比较。有可能是一个实现在那里,但这是如此直白地实现,我一直滚我自己。
请注意:您比较以上,如果的两个的名称是空的,甚至不会比较值的字段。我不认为这是你想要的。
我会像下面这样实现这个:
// 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静态
请参阅此答案的底部,了解使用 Guava 的更新(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) { ... }
所以这本质上是一样的 艾迪的回答 (尽管我不会将静态辅助方法称为 比较器) 和 乌津的 也。
无论如何,总的来说,我会强烈支持 帕特里克的解决方案, ,因为我认为尽可能使用已建立的库是一个很好的做法。(了解并使用图书馆 正如 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);
}
我认为这非常优雅。(只剩下一个小问题:下议院 NullComparator
不支持泛型,因此存在未经检查的分配。)
更新(2013):番石榴版
近 5 年后,我将如何解决我最初的问题。如果用 Java 编码,我(当然)会使用 番石榴. 。(而且相当肯定的是 不是 阿帕奇共享资源。)
把这个常数放在某个地方,例如在“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 作为依赖项比 Commons Collections 更可取。(作为 每个人都同意.)
如果您想知道 Ordering
, ,注意它实现了 Comparator
. 。它非常方便,特别是对于更复杂的排序需求,例如,允许您使用链接多个排序 compound()
. 。读 订购说明 了解更多!
我总是建议使用 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);
}
}
即使您决定推出自己的类,也请记住此类,因为它在对包含空元素的列表进行排序时非常有用。
我知道,它可能没有直接回答你的问题,因为你说,空值都必须支持。
但我只是想指出,在支持的compareTo空是不是在官方的 JavaDoc以可比:
请注意null不是任何类的一个实例,和e.compareTo(空) 应该抛出一个NullPointerException即使e.equals(空)的回报 假的。
所以,我要么抛出NullPointerException明确,还是让它被抛出时,空的说法被取消引用的第一次。
您可以提取方法:
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);
}
您可以设计你的类是不可变的(有效的Java第二版对此有很大的部分,第15项:最大限度地减少可变性),并确保在施工,没有空是可能的(和使用的空对象图案如果需要)。然后你就可以跳过所有这些检查和安全地假定值不为空。
我一直在寻找类似的东西,这似乎有点复杂,所以我这样做。我认为这是一个比较容易理解。您可以使用它作为一个比较器或一个衬垫。对于这个问题,你会改变对与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);
}
}
我们可以使用Java 8做对象之间的空友好比较讨论。 本来我哈瓦与2场一个男孩等级:字符串名称和整型年龄,我想先比较名称,然后年龄,如果双方都是平等的。
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
在使用Spring的情况下任何人,还有一类org.springframework.util.comparator.NullSafeComparator,这是否对你。只是装饰自己比得上像这样
new NullSafeComparator<YourObject>(new YourComparable(), true)
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;
}
另一个阿帕奇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());
}
这是我的实现,我用我的排序ArrayList的。空类分拣到最后。
对于我的情况下,延伸EntityPhone和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;
}
有关,你知道数据会不会有空值(总是字符串是个好主意)和数据是真的大,你其实之前比较值还在做三个比较,的如果您知道具体情况相信这是你的情况的,可以优化一点点位。 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);