【问题标题】:Java 8+ stream: Check if list is in the correct order for two fields of my object-instancesJava 8+ 流:检查我的对象实例的两个字段的列表是否顺序正确
【发布时间】:2018-08-24 09:53:52
【问题描述】:

标题可能有点含糊,但这是我所拥有的(私有化代码):

具有一些字段的类,包括 BigDecimal 和 Date:

class MyObj{
  private java.math.BigDecimal percentage;
  private java.util.Date date;
  // Some more irrelevant fields

  // Getters and Setters
}

在另一个类中,我有一个这些对象的列表(即java.util.List<MyObj> myList)。我现在想要的是一个 Java 8 流,用于检查列表是否符合我的验证器的日期和百分比顺序。

例如,以下列表是真实的:

[ MyObj { percentage = 25, date = 01-01-2018 },
  MyObj { percentage = 50, date = 01-02-2018 },
  MyObj { percentage = 100, date = 15-04-2019 } ]

但是这个列表是错误的,因为百分比的顺序不正确:

[ MyObj { percentage = 25, date = 01-01-2018 },
  MyObj { percentage = 20, date = 01-02-2018 },
  MyObj { percentage = 100, date = 15-04-2019 } ]

而且这个列表也是错误的,因为日期的顺序不正确:

[ MyObj { percentage = 25, date = 10-03-2018 },
  MyObj { percentage = 50, date = 01-02-2018 },
  MyObj { percentage = 100, date = 15-04-2019 } ]

一种可能的解决方案是创建Pairslike this,然后使用!.anyMatch 检查每个Pair<MyObj>。但如果可能的话,我真的不想为此目的创建一个Pair 类。

是否有可能使用.reduce 或其他方法来循环成对的MyObj 来检查它们?使用 Java 8 流检查列表中 MyObj 的所有日期和百分比是否按正确顺序的最佳方法是什么?

另一种可能是按日期对列表进行排序,然后检查它们是否都按百分比顺序排列,如果这比检查两个字段是否相同更容易的话。不过,比较成对的MyObj 的百分比仍然存在同样的问题。

(PS:我将它用于com.vaadin.server.SerializablePredicate<MyObj> validator,我更喜欢Java 8 lambda,因为我也为其他验证器使用了一些,所以它会更符合其余代码。然而,在我的问题中,Java 8 lambda 更像是一种偏好而不是要求。)

【问题讨论】:

  • 如果我理解正确,您希望这两个字段都是排序中的主键。除非你以某种方式将它们结合起来,否则这对我来说是不可能的。我看不出这怎么可能。还是我误会了?
  • @JackFlamp 据我了解,他只是想检查一下,即日期或百分比可能出现故障,但验证器应将其报告为无效。
  • 您能否详细说明您将如何使用SerializablePredicate(顺便说一句,它不是JDK 类)?我不确定你是否真的必须使用流。
  • 你能澄清一下:你想执行排序吗? - 或者只是验证现有订单是否满足您的要求?
  • 那么您希望百分比序列和日期序列不递减吗?我不知道您为什么开始谈论与验证无关的排序,并且正如其他人指出的那样,当某些元素在序列中时,这种比较是不可能的。

标签: java sorting java-8 java-stream boolean-expression


【解决方案1】:

好吧,如果你想要一个短路操作,我认为不存在使用 stream-api 的简单解决方案......我建议一个更简单的解决方案,首先定义一个方法,它会以短路的方式告诉你是否根据某些参数,您的列表是否已排序:

 private static <T, R extends Comparable<? super R>> boolean isSorted(List<T> list, Function<T, R> f) {
    Comparator<T> comp = Comparator.comparing(f);
    for (int i = 0; i < list.size() - 1; ++i) {
        T left = list.get(i);
        T right = list.get(i + 1);
        if (comp.compare(left, right) >= 0) {
            return false;
        }
    }

    return true;
}

并通过以下方式调用它:

 System.out.println(
          isSorted(myList, MyObj::getPercentage) && 
          isSorted(myList, MyObj::getDate));

【讨论】:

  • if(comp.compare(left, right) &lt; 0) { continue; } else { return false; },真的吗?为什么不if(comp.compare(left, right) &gt;= 0) { return false; }
  • 其实应该只是if(comp.compare(left, right) &gt; 0) return false;,因为相等的元素不会违反排序的属性。返回 Supplier&lt;Boolean&gt; 而不是 boolean 的目的是什么?
  • 但是你可以直接写isSorted(...) &amp;&amp; isSorted(...)如果你真的想要这个,因为isSorted(x.thenComparing(y))isSorted(x) &amp;&amp; isSorted(y)不一样。
  • 我被 OP 的比较器示例误导了。实际上,isSorted(...) &amp;&amp; isSorted(...) 是有意的。我看不出有任何理由在这里使用Supplier。或者,您可以接受BiPredicate 而不是Comparator,让调用者提供一个检查这两个属性的函数。
  • 在这种情况下拥有Supplier&lt;Boolean&gt; 是没有用的,因为您评估整个列表以创建该供应商,并且评估供应商基本上是免费的(返回一个常量值)。它只会使所有内容的可读性降低。我认为不应该保留部分答案。
【解决方案2】:

我认为您几乎可以尝试使用Stream.anyMatch。你可以这样完成它:

private static boolean isNotOrdered(List<MyObj> myList) {
    return IntStream.range(1, myList.size()).anyMatch(i -> isNotOrdered(myList.get(i - 1), myList.get(i)));

}

private static boolean isNotOrdered(MyObj before, MyObj after) {
    return before.getPercentage().compareTo(after.getPercentage()) > 0 ||
            before.getDate().compareTo(after.getDate()) > 0;
}

我们可以使用IntStream.range 使用索引来遍历列表的元素。这样我们就可以引用列表中的任何元素,例如上一个来比较一下。

编辑添加更通用的版本:

private static boolean isNotOrderedAccordingTo(List<MyObj> myList, BiPredicate<MyObj, MyObj> predicate) {
    return IntStream.range(1, myList.size()).anyMatch(i-> predicate.test(myList.get(i - 1), myList.get(i)));
}

使用上述谓词可以如下调用:

isNotOrderedAccordingTo(myList1, (before, after) -> isNotOrdered(before, after));

或者在类ListNotOrdered中使用方法引用:

isNotOrderedAccordingTo(myList1, ListNotOrdered::isNotOrdered)

【讨论】:

  • 您甚至可以省略 myList.size() &gt; 1 &amp;&amp; 部分,因为空流上的 anyMatch 已经做了正确的事情。
  • @Holger 如果列表大小为 1,anyMatch 将起作用,但对 myList.get(i) 的内部调用将导致 AIOOB 异常。列表的大小必须至少为 2 才能正常工作。
  • @LuisG。不,试试吧,如果你不相信。 IntStream.range(0, 1)IntStream.range(1, 1) 是空流,它们永远不会在 List 上调用 get
  • 已验证@Holger 的建议。它适用于一个空列表和一个包含一个元素的列表。
  • 我最终使用了 Eugene 的方法,因为它是通用的并且更容易重复用于其他流。然而,刚刚验证了你的方法,它也像一个魅力一样工作,所以我投了赞成票。也感谢您的回答。
【解决方案3】:

既然你提到你不想为此创建一个单独的类Pairs,你可以为此目的使用一个内置类:AbstractMap.SimpleEntry

您可以创建一个BiPredicate 来检查您的比较条件并使用它来比较所有对。

BiPredicate<MyObj,MyObj> isIncorrectOrder = (o1,o2) -> {
    boolean wrongOrder = o1.getDate().after(o2.getDate());
    return wrongOrder ? wrongOrder : o1.getPercentage().compareTo(o2.getPercentage()) > 0;
};

boolean isNotSorted =  IntStream.range(1,myObjs.size())
        .anyMatch(i -> isIncorrectOrder.test(myObjs.get(i-1),myObjs.get(i)));

上面带有比较器的解决方案:

Comparator<MyObj> comparator = (o1, o2) -> {
    boolean wrongOrder = o1.getDate().after(o2.getDate());
    return wrongOrder ? 1 : o1.getPercentage().compareTo(o2.getPercentage());
};

Predicate<AbstractMap.SimpleEntry<MyObj,MyObj>> isIncorrectOrder = pair ->  comparator.compare(pair.getKey(),pair.getValue()) > 0;

boolean isNotSorted =  IntStream.range(1,myObjs.size())
         .mapToObj(i -> new AbstractMap.SimpleEntry<>(myObjs.get(i-1),myObjs.get(i)))
         .anyMatch(isIncorrectOrder);

【讨论】:

  • 我的主要意思是为此目的创建一个Pair 类。像你一样使用AbstractMap.SimpleEntry 对我来说完全没问题,所以也很赞成。 :)
  • @KevinCruijssen :)
  • @KevinCruijssen 通过BiPredicate 提供了一个更小的解决方案
【解决方案4】:

这是pairMapStreamEx 中的解决方案

StreamEx.of(1, 2, 3, 5).pairMap((a, b) -> a <= b).allMatch(e -> e); // true

StreamEx.of(1, 2, 5, 3).pairMap((a, b) -> a <= b).allMatch(e -> e); // false

// your example:
StreamEx.of(myList)
   .pairMap((a, b) -> a.getPercentage().compareTo(b.getPercentage()) <= 0 && !a.getDate().after(b.getDate()))
   .allMatch(e -> e);

【讨论】:

    【解决方案5】:

    是的,您可以使用reduce 比较两个项目(尽管它不是最佳选择)。当您发现某个项目乱序时,您只需要创建一个新的“空”项目,如下所示:

        boolean fullyOrdered = myList.stream()
                .reduce((a, b) -> {
                    if ((a.percentage == null && a.date == null) || // previous item already failed 
                            a.percentage.compareTo(b.percentage) > 0 || // pct out of order
                            a.date.after(b.date)) { // date out of order
                        return new MyObj(null, null); // return new empty MyObj
                    } else {
                        return b;
                    }
                })
                .filter(a -> a.percentage != null || a.date != null)
                .isPresent();
    
        System.out.println("fullyOrdered = " + fullyOrdered);
    

    只有当你的两个条件都满足时才会打印true,否则会打印false

    当然,您可以通过在MyObj 中包含一些辅助方法来使代码更好:

    class MyObj {
        // ...
        public MyObj() {}
        public static MyObj EMPTY = new MyObj();
        public boolean isEmpty() {
            return percentage == null && date == null;
        }
        public boolean comesAfter(MyObj other) {
            return this.percentage.compareTo(other.percentage) > 0 ||
                   this.date.after(other.date);
        }
    }
    // ...
            boolean fullyOrdered = myList.stream()
                    .reduce((a, b) -> (a.isEmpty() || a.comesAfter(b)) ? MyObj.EMPTY : b)
                    .filter(b -> !b.isEmpty())
                    .isPresent();
    
            System.out.println("fullyOrdered = " + fullyOrdered);
    

    请记住,这不是短路,即即使找到无序项,它也会遍历整个列表。在使用reduce 时尽早退出流的唯一方法是在找到第一个无序项时抛出RuntimeException,这将使用异常来控制程序流,这被认为是不好的做法。尽管如此,我还是想向您展示,您确实可以为此目的使用 reduce

    对于一旦找到第一个不合适的项目就会完成的短路方法,请查看@LuCio 的答案。

    【讨论】:

    • 这可能不适用于并行流,并且由于累加器不是关联的,因此会破坏 reduce 方法契约。
    • @Alex Fair 积分。正如我所说,这只是一种 PoC,表明是的,您可以使用 reduce 做到这一点,尽管其他答案更好。
    【解决方案6】:

    我不认为这是一个应该使用流来解决的问题。流在将它们再次收集到新集合中或将它们减少到某种累积值之前,独立地将映射和过滤应用于集合的元素(甚至可能将不同元素的处理分配到不同的 CPU 内核)。您的问题涉及与流的目的相矛盾的集合的不同元素之间的关系。虽然可能存在涉及流的解决方案,但就像用钳子将钉子钉在墙上一样。一个经典的循环将非常适合您:找到第一次出现的破坏顺序的元素并返回所需的结果!因此,您甚至不需要创建一对。

    【讨论】:

      【解决方案7】:

      与@luis g. 的回答类似,您也可以将reduceOptional(空表示未排序)和“最小”MyObj 结合使用作为标识:

       boolean isSorted = list.stream()
                  .map(Optional::of)
                  .reduce(Optional.of(new MyObj(BigDecimal.ZERO, Date.from(Instant.EPOCH))),
                          (left, right) -> left.flatMap(l -> right.map(r -> l.date.compareTo(r.date)<= 0 && l.percentage.compareTo(r.percentage) <= 0 ? r : null)))
                  .isPresent();
      

      请注意,累加函数 (BinaryOperator) 应该是关联的,在这种情况下不是。此外,它也没有短路。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-07-31
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-12-03
        相关资源
        最近更新 更多