当前位置: 首页 > 知识库问答 >
问题:

迭代器与Java 8的流

程祯
2023-03-14

为了利用Jdk 8的java.util.stream中包含的各种查询方法,我试图设计域模型,其中与*多重性(具有零个或多个实例)关系的获取器返回

我的疑问是Stream是否会产生任何额外的开销

那么,使用Stream破坏我的域模型是否有任何缺点

或者,我应该总是返回一个迭代器吗?

请注意,返回集合不是一个有效的选项,因为在这种情况下,大多数关系都是惰性的,并且大小未知。


共有2个答案

澹台镜
2023-03-14

让我们比较一下遍历所有元素的常见操作,假设源代码是< code>ArrayList。那么,有三种标准方法可以实现这一点:

>

  • Collection.forEach

    final E[] elementData = (E[]) this.elementData;
    final int size = this.size;
    for (int i=0; modCount == expectedModCount && i < size; i++) {
        action.accept(elementData[i]);
    }
    

    Iterator.forEachRemaining

    final Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length) {
        throw new ConcurrentModificationException();
    }
    while (i != size && modCount == expectedModCount) {
        consumer.accept((E) elementData[i++]);
    }
    

    Stream.forEach这将最终调用Spliterator.forEachRemaining

    if ((i = index) >= 0 && (index = hi) <= a.length) {
       for (; i < hi; ++i) {
           @SuppressWarnings("unchecked") E e = (E) a[i];
           action.accept(e);
       }
       if (lst.modCount == mc)
           return;
    }
    

    如您所见,实现代码的内部循环(这些操作结束的位置)基本上是相同的,它迭代索引并直接读取数组并将元素传递给 Consumer

    类似的事情也适用于JRE的所有标准集合,所有这些集合都针对所有方法进行了调整,即使您使用的是只读包装器。在后一种情况下,Stream API 甚至会稍微获胜,必须在只读视图上调用 Collection.forEach 才能委派给原始集合的 forEach。同样,迭代器必须包装以防止尝试调用 remove() 方法。相反,spliterator() 可以直接返回原始集合的 Spliterator,因为它不支持修改。因此,只读视图的流与原始集合的流完全相同。

    尽管在测量现实生活中的性能时几乎没有注意到所有这些差异,正如所说,内部循环,这是与性能最相关的东西,在所有情况下都是相同的。

    问题是从中得出什么结论。您仍然可以向原始集合返回只读包装视图,因为调用方仍然可以调用<code>stream()。forEach(…)在原始集合的上下文中直接迭代。

    由于性能并没有真正的不同,您应该专注于更高级别的设计,如“我应该返回集合还是流?”中讨论的那样。

  • 张嘉佑
    2023-03-14
    匿名用户

    这里有很多性能建议,但可悲的是,其中大部分都是猜测,很少指向真正的性能考虑因素。

    @Holger指出,我们应该抵制让性能尾巴摇摆API设计狗的似乎势不可挡的趋势,这是正确的。

    虽然在任何给定的情况下,有无数的考虑因素可以使流慢于、等于或快于其他形式的遍历,但有一些因素表明流在大数据集上具有性能优势。

    与创建迭代器相比,创建Stream有一些额外的固定启动开销,即在开始计算之前多创建几个对象。如果您的数据集很大,这并不重要;这是一个很小的启动成本,通过大量计算摊销。(如果您的数据集很小,这可能也不重要——因为如果您的程序在小数据集上运行,性能通常也不是您的首要问题。)重要的是并行时;建立管道所花费的任何时间都属于阿姆达尔定律的连续部分;如果你看一下实现,我们努力在流设置期间保持对象计数减少,但我很乐意找到减少的方法,因为这对盈亏平衡数据集大小有直接影响,其中并行开始战胜顺序。

    但是,比固定启动成本更重要的是每个元素的访问成本。在这里,流实际上赢了——而且往往赢得很大——有些人可能会感到惊讶。(在我们的性能测试中,我们经常看到流管道比<code>集合迭代器

    > < li>

    迭代器协议基本上效率较低。它需要调用两个方法来获取每个元素。此外,因为迭代器必须对不使用< code>hasNext()而多次调用< code>next()或不使用< code>next()而多次调用< code>hasNext()这样的事情具有鲁棒性,所以这两种方法通常都必须进行一些防御性编码(并且通常需要更多的有状态性和分支),这增加了效率。另一方面,即使是遍历spliterator的慢速方法(< code>tryAdvance)也没有这个负担。(这对于并发数据结构来说甚至更糟,因为< code > next /< code > has next 二元性从根本上来说是非常活泼的,并且< code>Iterator实现必须比< code>Spliterator实现做更多的工作来防止并发修改。)

    Spliterator进一步提供了一种“快速路径”迭代--forEachRemaining,它可以在大多数时间使用(reduce,forEach),进一步减少了迭代代码的开销,该迭代代码可以调解对数据结构内部的访问。这也倾向于非常好地内联,这反过来增加了其他优化的有效性,如代码运动、边界检查消除等。

    此外,通过Spliterator遍历的堆写入往往比使用Iterator少得多。使用Iterator,每个元素都会导致一个或多个堆写入(除非Iterator可以通过转义分析进行缩放并将其字段提升到寄存器中。)除其他问题外,这会导致GC卡标记活动,导致卡标记的缓存行争用。另一方面,Spliterator倾向于具有较少的状态,并且工业级的forEachRemning实现倾向于推迟将任何内容写入堆直到遍历结束,而不是将其迭代状态存储在自然映射到寄存器的局部变量中,从而减少内存总线活动。

    总结:不要担心,要快乐Spliterator是更好的迭代器,即使没有并行性。(它们通常也更容易编写,更难出错。)

     类似资料:
    • 问题内容: 在一次采访中有人问我,使用迭代器使用for循环有什么好处,或者使用循环比迭代器有什么好处? 任何人都可以回答这个问题,以便将来如果我遇到类似的问题,那么我可以回答 问题答案: 首先,有两种for循环,它们的行为非常不同。一种使用索引: 这种循环并非总是可能的。例如,列表具有索引,而集合没有索引,因为它们是无序集合。 另一个foreach循环在幕后使用Iterator: 这适用于每种It

    • 本文向大家介绍Python迭代器与可迭代与生成器,包括了Python迭代器与可迭代与生成器的使用技巧和注意事项,需要的朋友参考一下 示例 一个迭代是一个对象,可以返回一个迭代器。具有状态且具有__iter__  方法并返回迭代器的任何对象都是可迭代的。也可能是没有状态的对象,该对象实现了__getitem__方法。-该方法可以获取索引(从零开始),并IndexError在索引不再有效时引发。 Py

    • 我一直在玩Java新的和闪亮的功能部分,最让我困惑的事情之一是流? 它们有什么用? 在谷歌上,我主要找到了如何使用它们的解释和实际例子,我已经记下来了,没有关于幕后魔法的具体内容,这是我感兴趣的。 我的意思并不是说,从实际意义上讲,我从一些函数语言中找到了map/filter/reduce/etc。相当快,但为什么我们需要先转换为流?Java已经有了迭代器。流和迭代器之间是否有根本的区别,比如一个

    • 是否可以从迭代器创建一个流,其中对象的序列与通过反复调用迭代器的next()方法生成的序列相同?我所考虑的具体情况涉及到Treeset.desceningIterator()返回的迭代器的使用,但是我可以想象在其他情况下,迭代器是可用的,而不是它所引用的集合。 例如,对于,我们可以编写并按照该集合的排序顺序获取该集合中的对象流,但是如果我们希望它们按照不同的顺序,比如通过使用获得的顺序呢?我想象的

    • 我最近开始检查新的Java8特性。 我遇到过这个迭代器-它对集合进行迭代。 比如,我希望它从第2个值开始,然后迭代到第2个最后一个值。或者类似的东西--或者其他元素。 我要怎么做?

    • 这可能是一个基本问题,但是有人能解释一下和之间的主要区别并给出一些例子吗?