【问题标题】:Using Java Streams for Efficient Search使用 Java 流进行高效搜索
【发布时间】:2016-01-21 14:58:18
【问题描述】:

我有一个List 的元素,它们之间没有可测量的顺序。它们的属性也很复杂,我不能简单地将它们插入Set(因为不同的属性可能代表相同的元素)。

在我的程序过程中,我分析了列表中的每个元素,并在此基础上添加了其他元素(例如构建图表并转到每个节点以添加其他路径和节点)。但是,他们添加的元素可能等同于List 中的其他元素。在这种情况下,它们不会被添加,并且等效元素的属性已更改(假设是计数器)。

我一直在使用这段代码来查找是否存在等效状态:

public static State stateAlreadyExists(State current) {
    for (State any : list) {
        if (equivalencyMethod(any, current)) {
            return any;
        }
     }
     return null;
}

然而,这段代码虽然有O(n) 的复杂性,但对我的情况来说性能不够。我添加的每个元素都会执行此代码,并且我会为每个分析的元素添加大约 sqrt(N) 元素(因此,例如,分析元素 400 会创建 20 个新元素)。为了提高性能,我使用了 Java 的并行流:

public static State stateAlreadyExists(State current) {
    Optional<State> opt = list.parallelStream().filter(
        any -> equivalencyMethod(any, current)).findFirst();
    if (opt.isPresent()) {
        return opt.get();
    }
    return null;
}

并且性能显着提高。 问题是这段代码并不完全等价,因为我们在返回一个元素之前分析了整个流。大多数情况下,等效元素位于列表的第一个 sqrt(N) 元素中,因此在第一个匹配时停止的方法会更好。

我知道streams 有一个noneFound() 方法。一旦找到匹配项,它就会返回。但是,它返回一个boolean,而不是元素本身。有没有办法使用这个或类似构建的方法来返回找到的第一个匹配项?

根据JavaDoc,findFirst():

返回此流的第一个元素

而 findAny():

选择流中的任何元素。这是为了在并行操作中实现最大性能。

因此,通过使用findAny() 调用,我的代码可以变得更加高效,因为顺序对我的问题并不重要,因为任何时候都只有 1 个等效元素。

【问题讨论】:

  • findFirst() 在找到匹配项后立即返回(除非您的流已排序),因此除非必要,否则不会分析整个流。
  • findFirst() 不接受任何参数。它只返回流的第一个元素
  • 我不明白你最后的评论。第一个元素不是你想要的吗?
  • “因为我们在返回元素之前分析了整个流”——所以你的问题是这个假设,凭空而来,尽管你已经承认了性能提升?
  • 我没有意识到findFirst() 可以中断过滤操作

标签: performance java-8 java-stream


【解决方案1】:

findFirst 是一个短路终端操作(感谢Keppil)。

public static void main(String[] args) {
  final AtomicInteger countNew = new AtomicInteger();
  final AtomicInteger countDoStuff = new AtomicInteger();
  class A {
    A() { countNew.getAndIncrement(); }
    public boolean doStuff() { return countDoStuff.getAndIncrement() % 3 == 2; }
  }
  Stream.generate(A::new).limit(20).filter(A::doStuff).findFirst();
  System.out.println("Number of times an A was created: " + countNew);
  System.out.println("Number of times doStuff was called: " + countDoStuff);
}

此代码将打印

A 被创建的次数:3

doStuff 被调用的次数:3

但不是

创建 A 的次数:20

doStuff 被调用的次数:20

甚至更少

创建 A 的次数:20

doStuff 被调用的次数:3

【讨论】:

  • 它甚至是一个短路终端操作,这就是它找到匹配时停止的原因。
  • 是的,但它会搜索整个列表(所有 20 个元素),然后选择第一个匹配的。它不会在 3 点立即返回,对吗?
  • 是的,它会:doStuff 只被调用了 3 次。如果它按照您所说的那样执行,则意味着 findFirst 不会执行 1 个循环,而是执行 2 个循环。事实并非如此。
  • @BlueMoon93 我已经修改了我的代码,以向您展示 1. 仅生成了 3 个 A 实例,并且 2. 仅对 doStuff() 执行了 3 次调用。所以只完成了 1 个循环,代码不会创建 20 个 A 然后只过滤它们。
  • @BlueMoon93:想想你是否真的需要 first 匹配或任何匹配就足够了。在后一种情况下,您可以使用findAny() 而不是findFirst()。这可以提高并行性能。
【解决方案2】:

我会建议您另一种方法。尝试以某种方式制定您的等效标准,以便您可以创建一个散列函数,从而大大减少您必须执行的成对等效检查的数量。对于散列函数,如果两个不相等的项具有相同的散列是没有问题的,唯一重要的是两个相等的项具有相同的散列。然后,您可以简单地将元素存储在 HashSet 中,这将为您完成繁重的工作,您只需要实现 equals(Object other)hashCode() 的元素。

如果不可能找到合适的哈希码,您仍然可以考虑是否能够排序您的对象,也就是说,您可以制定一个比较函数来判断您的任意两个对象的顺序对象(或它们相等)。然后,您可以将TreeSet 与您的自定义比较器一起使用,这也将工作得非常快。

【讨论】:

  • 我考虑了您的建议,但不适用于我的情况。没有可以使用的有意义的顺序,也无法通过哈希进行比较。为这个想法+1
猜你喜欢
  • 2020-06-01
  • 2015-12-07
  • 2012-12-24
  • 2013-10-12
  • 1970-01-01
  • 2018-01-29
  • 1970-01-01
  • 1970-01-01
  • 2021-10-05
相关资源
最近更新 更多