我们有一些代码可以根据坐标之间的距离对地址列表进行排序。这是通过带有自定义比较器的 collections.sort 完成的。
但是,有时没有坐标的地址在列表中会导致 NullPointerException。我解决此问题的最初想法是让比较器返回 0 作为至少一个坐标为空的地址的距离。我担心这可能会导致列表中“有效”元素的顺序损坏。
那么在比较器中为空数据返回“0”值是否可以,或者是否有更简洁的方法来解决这个问题?
我们有一些代码可以根据坐标之间的距离对地址列表进行排序。这是通过带有自定义比较器的 collections.sort 完成的。
但是,有时没有坐标的地址在列表中会导致 NullPointerException。我解决此问题的最初想法是让比较器返回 0 作为至少一个坐标为空的地址的距离。我担心这可能会导致列表中“有效”元素的顺序损坏。
那么在比较器中为空数据返回“0”值是否可以,或者是否有更简洁的方法来解决这个问题?
像处理它一样null
意味着无限远。因此:
comp(1234, null) == -1
comp(null, null) == 0
comp(null, 1234) == 1
这样,您可以获得一致的排序。
只是为了扩展 Willi Schönborn 的答案,我来这里是说google-collections正是您在这里所追求的。
在一般情况下,您可以自己编写Comparator
忽略空值(假设非空,因此可以专注于重要逻辑),然后使用排序来处理空值:
Collections.sort(addresses, Ordering.from(new AddressComparator()).nullsLast());
但是,在您的情况下,它是用于排序的地址(坐标)内的数据,对吗?google-collections在这种情况下更加有用。所以你可能有更多类似的东西:
// Seems verbose at first glance, but you'll probably find yourself reusing
// this a lot and it will pay off quickly.
private static final Function<Address, Coordinates> ADDRESS_TO_COORDINATES =
new Function<Address, Coordinates>() {
public Coordinates apply(Address in) {
return in.getCoordinates();
}
};
private static final Comparator<Coordinates> COORDINATE_SORTER = .... // existing
那么当你想排序时:
Collections.sort(addresses,
Ordering.from(COORDINATE_SORTER)
.nullsLast()
.onResultOf(ADDRESS_TO_COORDINATES));
这就是谷歌收藏的力量真正开始发挥作用的地方。
我对此的看法是,你试图做的任何事情来“弥补”null
坐标只是在裂缝上盖上纸。您真正需要做的是找到并修复注入虚假null
坐标的错误。
根据我的经验,NPE bug 的侵扰通常是由以下不良编码习惯引起的:
null
避免创建空数组或集合,null
在应该抛出异常时返回,或者null
当有更好的解决方案时,用它来表示“没有价值”。(“无值”问题的更好解决方案通常涉及重写代码,这样您就不需要表示它和/或使用非空值代替;例如,空字符串、特殊实例、保留值。您可以并非总能找到更好的解决方案,但您通常可以。)
如果这描述了您的应用程序,您应该花时间尝试查找和纠正注入null
值的代码问题,而不是考虑避免它们引起的 NPE 的方法。
如果您使用的是 Java 8,那么 Comparator 类中有 2 个新的静态方法,它们会派上用场:
public static <T> Comparator<T> nullsFirst(Comparator<? super T> comparator)
public static <T> Comparator<T> nullsLast(Comparator<? super T> comparator)
比较将是空值安全的,您可以选择将空值放置在排序序列中的位置。
下面的例子:
List<String> monkeyBusiness = Arrays.asList("Chimp", "eat", "sleep", "", null, "banana",
"throw banana peel", null, "smile", "run");
Comparator<? super String> comparator = (a, b) -> a.compareTo(b);
monkeyBusiness.stream().sorted(Comparator.nullsFirst(comparator))
.forEach(x -> System.out.print("[" + x + "] "));
将打印:[null] [null] [] [Chimp] [banana] [eat] [run] [sleep] [smile] [throw Banana]
我的解决方案(可能对查看这里的人有用)是进行正常比较,空值不是由 0 代替,而是可能的最大值(例如 Integer.MAX_VALUE)。如果您的值本身为 0,则返回 0 不一致。这是一个正确的示例:
public int compare(YourObject lhs, YourObject rhs) {
Integer l = Integer.MAX_VALUE;
Integer r = Integer.MAX_VALUE;
if (lhs != null) {
l = lhs.giveMeSomeMeasure();
}
if (rhs != null) {
r = rhs.giveMeSomeMeasure();
}
return l.compareTo(r);
}
我只是想补充一点,您不需要整数的最大值。这取决于您的 giveMeSomeMeasure() 方法可以返回什么。例如,如果您比较天气的摄氏温度,您可以将 l 和 r 设置为 -300 或 +300,具体取决于您要将空对象设置在哪里 - 到列表的头部或尾部。
您可能不想返回 0,因为这意味着地址是等距的,您真的不知道。这是一个非常经典的问题,您正在尝试处理错误的输入数据。当您不知道距离时,我认为比较器没有责任尝试确定地址的实际距离。我会在排序之前从列表中删除这些地址。
破解方法是将它们移到列表的底部(但这很难看!)
不,没有更清洁的方法。也许:
但更重要的是 - 尝试摆脱/填写丢失的坐标,或者更好:不要将缺少坐标的地址放在列表中。
实际上,不将它们放在列表中是最合乎逻辑的行为。如果将它们放在列表中,则结果实际上不会按距离排序。
您可以创建另一个列表,其中包含缺少坐标的地址,并向需要该信息的任何人(最终用户、API 用户)说明,第一个列表仅包含具有所需数据的地址,而第二个列表包含缺乏必要的信息。
与其把它看成是比较器的技术问题,不如再看看需求:你在这里真正想要做什么,你打算用这个排序列表做什么?
正如您已经意识到的那样,当其中一个为 null 时始终返回 0 在这里不是一个好主意。它确实会破坏结果。但是你应该做什么取决于你需要什么,而不是其他人通常做什么/需要什么。您的程序如何处理没有位置的地址(因此用户将看到的内容)不应取决于某些技术细节,例如比较器的“最佳实践”是什么。(对我来说,在这里问“最佳实践”是什么,听起来像是在问“最佳要求”是什么)。
我个人讨厌在比较器中到处处理特殊的 null 情况,所以我一直在寻找更干净的解决方案,最后找到了 google 集合。他们的订购真是太棒了。它们支持复合比较器,提供将空值排序到顶部和末尾,并允许在比较之前运行某些功能。编写比较器从未如此简单。你应该试一试。
我知道......我知道......这篇文章很老,已经回答了很多。但是 GuavasOrdering
已经过时了,Java 8Comparator
已经内置了解决很多自定义比较问题的功能。
此外,我想添加我的方法,以防任何人有类似的需要通过对象中的多个字段来比较对象,这些字段可以为空。
让我们使用问题的示例数据。我们有一个Address
包含Coordinate
数据的列表,在某些情况下可以为空。
假设我们在一个类中对列表进行排序,AddressSorter
并且我们只想将具体对象的排序与那些为空的对象分开。我们可以通过使用Comparator
进行基本空检查的自定义来实现这一点。
public class AddressSorter {
private static final Comparator<Coordinate> COORDINATE_NULL_COMPARATOR = (c1, c2) -> {
if (c1 != null && c2 == null) {
return 1;
}
if (c1 == null && c2 != null) {
return -1;
}
return 0;
}
public List<Address> sortAddressList(List<Address> addresses) {
return addresses.stream()
.sorted(Comparator.compare(Address::getCoordinate, COORDINATE_NULL_COMPARATOR))
.collect(Collectors.toList());
}
}
在这个例子中,我们使用内置的Comparator.comparing(Function<? super T,? extends U> keyExtractor, Comparator<? super U> keyComparator)
这将构造一个列表,其中带有as的Address
es位于返回列表的开头。null
Coordinate
Coordinate
这将完全跳过任何两个混凝土之间的任何比较
这可能看起来很奇怪,但在某些情况下跳过对象比较是有效的。例如,如果您需要通过可以为空LocalDateTime
的字段分隔对象,则与附加链接的任何(或任何其他及时对象)比较将导致意外行为。LocalDateTime
Coordinate
与零安全性比较因此,如果您需要比较Coordinate
对象,包括 null 安全性,您可以使用自然顺序和 null 检查,如下所示:
public List<Address> sortAddressList(List<Address> addresses) {
return addresses.stream()
.sorted(Comparator.compare(Address::getCoordinate, Comparator.nullsFirst(Comparator.naturalOrder())
.collect(Collectors.toList());
}
nullsLast
编辑:如果您希望在列表末尾Address
添加 es,也可以使用。Coordinate == null
有了它,我们也可以开始基于我们的多个字段进行链式排序Address
,例如:
public List<Address> sortAddressList(List<Address> addresses) {
return addresses.stream()
.sorted(Comparator.compare(Address::getCoordinate, Comparator.nullsFirst(Comparator.naturalOrder())
.thenCompare(Address::getId))
.collect(Collectors.toList());
}
所以你最终会得到一个列表,其中前导Address
es 是一次包含null
按Coordinate
排序的id
,然后所有Address
带有具体的 esCoordinate
也按排序id
。
如果您希望将此行为作为自然顺序,Address
您可以Address
实施Comparable
,然后使用 Apache Commons 进行工作CompareToBuilder
:
@Override
public int compareTo(Address address) {
return new CompareToBuilder()
.append(this.coordinate, address.coordinate, Comparator.nullsFirst(Comparator.naturalOrder())
.append(this.id, address.id)
.toComparison();
然后,这使您可以sorted()
在流中使用,因为它利用了compareTo
of Address
:
public List<Address> sortAddressList(List<Address> addresses) {
return addresses.stream()
.sorted()
.collect(Collectors.toList());
}