62

我们有一些代码可以根据坐标之间的距离对地址列表进行排序。这是通过带有自定义比较器的 collections.sort 完成的。

但是,有时没有坐标的地址在列表中会导致 NullPointerException。我解决此问题的最初想法是让比较器返回 0 作为至少一个坐标为空的地址的距离。我担心这可能会导致列表中“有效”元素的顺序损坏。

那么在比较器中为空数据返回“0”值是否可以,或者是否有更简洁的方法来解决这个问题?

4

10 回答 10

88

像处理它一样null意味着无限远。因此:

  • comp(1234, null) == -1
  • comp(null, null) == 0
  • comp(null, 1234) == 1

这样,您可以获得一致的排序。

于 2010-03-08T13:39:05.563 回答
26

只是为了扩展 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));

这就是谷歌收藏的力量真正开始发挥作用的地方。

于 2010-03-08T23:03:27.177 回答
10

我对此的看法是,你试图做的任何事情来“弥补”null坐标只是在裂缝上盖上纸。您真正需要做的是找到并修复注入虚假null坐标的错误。

根据我的经验,NPE bug 的侵扰通常是由以下不良编码习惯引起的:

  • 输入参数验证不足,
  • 用于null避免创建空数组或集合,
  • null在应该抛出异常时返回,或者
  • null当有更好的解决方案时,用它来表示“没有价值”。

(“无值”问题的更好解决方案通常涉及重写代码,这样您就不需要表示它和/或使用非空值代替;例如,空字符串、特殊实例、保留值。您可以并非总能找到更好的解决方案,但您通常可以。)

如果这描述了您的应用程序,您应该花时间尝试查找和纠正注入null值的代码问题,而不是考虑避免它们引起的 NPE 的方法。

于 2010-03-08T14:04:57.757 回答
10

如果您使用的是 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]

于 2016-10-24T14:53:20.730 回答
9

我的解决方案(可能对查看这里的人有用)是进行正常比较,空值不是由 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,具体取决于您要将空对象设置在哪里 - 到列表的头部或尾部。

于 2012-03-09T10:29:23.727 回答
3

您可能不想返回 0,因为这意味着地址是等距的,您真的不知道。这是一个非常经典的问题,您正在尝试处理错误的输入数据。当您不知道距离时,我认为比较器没有责任尝试确定地址的实际距离。我会在排序之前从列表中删除这些地址。

破解方法是将它们移到列表的底部(但这很难看!)

于 2010-03-08T13:41:38.973 回答
1

不,没有更清洁的方法。也许:

  • 如果两个比较对象的坐标都为空,则返回 0
  • 如果其中一个对象的坐标为空,则返回 -1 / 1(取决于它是第一个参数还是第二个参数)

但更重要的是 - 尝试摆脱/填写丢失的坐标,或者更好:不要将缺少坐标的地址放在列表中。

实际上,不将它们放在列表中是最合乎逻辑的行为。如果将它们放在列表中,则结果实际上不会按距离排序。

您可以创建另一个列表,其中包含缺少坐标的地址,并向需要该信息的任何人(最终用户、API 用户)说明,第一个列表仅包含具有所需数据的地址,而第二个列表包含缺乏必要的信息。

于 2010-03-08T13:39:02.850 回答
1

与其把它看成是比较器的技术问题,不如再看看需求:你在这里真正想要做什么,你打算用这个排序列表做什么?

  • 如果您尝试对它们进行排序以首先向用户显示最相关的解决方案,那么将未知位置放在最后可能是个好主意,因此将其视为无穷大(返回 0/-1/1 取决于它们中的哪一个是空值)。
  • 如果您要使用此结果来绘制一些图形或进行一些其他计算,这些计算取决于它们真正按距离排序,那么空值可能不应该在那里(所以要么先删除它们,要么抛出一个如果那时实际上不应该有任何地址为空的地址,则例外)。

正如您已经意识到的那样,当其中一个为 null 时始终返回 0 在这里不是一个好主意。它确实会破坏结果。但是你应该做什么取决于你需要什么,而不是其他人通常做什么/需要什么。您的程序如何处理没有位置的地址(因此用户将看到的内容)不应取决于某些技术细节,例如比较器的“最佳实践”是什么。(对我来说,在这里问“最佳实践”是什么,听起来像是在问“最佳要求”是什么)。

于 2010-03-08T14:03:56.963 回答
1

我个人讨厌在比较器中到处处理特殊的 null 情况,所以我一直在寻找更干净的解决方案,最后找到了 google 集合。他们的订购真是太棒了。它们支持复合比较器,提供将空值排序到顶部和末尾,并允许在比较之前运行某些功能。编写比较器从未如此简单。你应该试一试。

于 2010-03-08T20:24:50.240 回答
0

我知道......我知道......这篇文章很老,已经回答了很多。但是 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的Addresses位于返回列表的开头。nullCoordinate

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

所以你最终会得到一个列表,其中前导Addresses 是一次包含nullCoordinate排序的id,然后所有Address带有具体的 esCoordinate也按排序id

可比和 Apache Commons

如果您希望将此行为作为自然顺序,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()在流中使用,因为它利用了compareToof Address

    public List<Address> sortAddressList(List<Address> addresses) {
        return addresses.stream()
            .sorted()
            .collect(Collectors.toList());
    }
于 2021-12-01T13:42:20.127 回答