【问题标题】:comparator with null values具有空值的比较器
【发布时间】:2011-01-24 23:43:18
【问题描述】:

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

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

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

【问题讨论】:

  • 不太清楚您正在计算哪个距离...列表中的每个元素是一对坐标 - 每对一个距离吗?或者你是在计算列表的两个相邻坐标的距离(这有意义吗)?或者是相对于固定坐标计算的距离(每次排序后可能会改变)?
  • 按“坐标之间的距离”排序?您是将它们全部与一个固定点进行比较还是相互比较?因为如果您将它们相互比较,那么您的订单就会混乱,因为这种关系不会传递。
  • @Carlos:天哪!你更快;-)
  • 它与一个固定点比较是的

标签: java comparator


【解决方案1】:

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

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

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

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

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

【讨论】:

  • 方法不能返回 -1/1 或 true
【解决方案2】:

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

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

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

【讨论】:

  • ... 或 comp(null,null)==-1,取决于您是否希望两个无地址条目显示在同一个地方。
  • @izb,这违反了自反比较器的要求。
  • 只是大声疾呼,以防有人像我一样误读评论:@KevinBourrillion 是在回复第一条评论,而不是在回复@Sjoerd 选择的答案——@Sjoerd 的答案是正确的反身。 (comp(x,x)==0 代表所有 x
  • 从 Java 8 开始,您可以使用 Comparator.nullsLast​(anotherComparator) 创建具有所描述行为的比较器,而无需在 anotherComparator 中处理任何 null
【解决方案3】:

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

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

【讨论】:

    【解决方案4】:

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

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

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

    【讨论】:

      【解决方案5】:

      我对此的看法是,您为“改善”null 坐标所做的任何事情都只是掩盖了裂缝。您真正需要做的是找到并修复注入虚假null 坐标的错误。

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

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

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

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

      【讨论】:

      • 除非空坐标是有效值。
      • @SteveKuo - 在这种情况下,您最好将其替换为 Coordinate 实例,这意味着“未知坐标”。
      • (或者“空坐标”的实际含义。)
      【解决方案6】:

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

      【讨论】:

        【解决方案7】:

        只是为了扩展 Willi Schönborn 的回答,我来这里是说 google-collections 正是您在这里所追求的。

        一般情况下,你可以自己写Comparator忽略空值(假设非空,这样可以专注于重要的逻辑),然后使用Ordering处理空值:

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

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

        【讨论】:

        • 请注意,您可以首先使 COORDINATE_SORTED 扩展 Ordering 并跳过 Ordering.from()。
        • 你甚至可以使用另一个 .nullsLast() 和 Ordering.from(COORDINATE_SORTER).nullsLast().onResultOf(ADDRESS_TO_COORDINATES).nullsLast() 来允许列表中的空地址。对于这个问题可能没有意义,但总体上可能有意义。
        • 在 Java 8 中,Guava 的 Orderinghas been made obsolete 通过在标准 Java Comparator 类中包含一些相同的方法。
        【解决方案8】:

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

        【讨论】:

          【解决方案9】:

          如果您使用的是 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] [] [黑猩猩] [香蕉] [吃] [跑] [睡觉] [微笑] [扔香蕉皮]

          【讨论】:

            【解决方案10】:

            我知道...我知道...这篇文章很老了,已经回答了很多。但是 Guavas Ordering 已经过时了,Java 8 的 Comparator 内置了解决很多自定义比较问题的功能。

            我还想添加我的方法,以防有人有类似的需要通过对象中的多个字段来比较对象,这些字段可以为 null。

            设置

            让我们使用问题的示例数据。 我们有一个Address 列表,其中包含Coordinate 数据,在某些情况下可能为空。

            自定义比较器

            假设我们在类AddressSorter 中对列表进行排序,并且我们只想将具体对象的排序与那些为空的对象分开。我们可以使用自定义的Comparator 来实现这一点,该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&lt;? super T,? extends U&gt; keyExtractor, Comparator&lt;? super U&gt; keyComparator)

            这将构建一个列表,其中nullCoordinateAddresses 位于返回列表的开头。

            这将完全跳过任何两个具体Coordinate 之间的任何比较

            这可能看起来很奇怪,但在某些情况下跳过对象比较是有效的。例如,如果您需要通过可以为 null 的 LocalDateTimefield 分隔对象,则任何 LocalDateTime(或任何其他及时对象)与附加链接的比较都会导致意外行为。

            比较 Coordinate 与 null 安全

            因此,如果您需要比较 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());
                }
            

            编辑:如果您希望列表末尾带有Coordinate == nullAddresses,也可以使用nullsLast

            链接

            这样,我们还可以开始基于Addresses 的多个字段进行链式排序,例如:

                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 是一次包含 nullCoordinateid 排序,然后所有 Addresses 和具体的 Coordinate 也排序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(),因为它使用AddresscompareTo

                public List<Address> sortAddressList(List<Address> addresses) {
                    return addresses.stream()
                        .sorted()
                        .collect(Collectors.toList());
                }
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2022-11-02
              • 1970-01-01
              • 1970-01-01
              • 2021-09-06
              • 2014-12-11
              相关资源
              最近更新 更多