如果您确定输出元素的数量等于输入元素的数量,并且您满意使用数组作为结果,那么一定要使用
toArray
而不是收集器。如果管道在整个过程中具有固定的大小,则目标数组将预先分配正确的大小,并且并行操作将其结果直接存储到目标数组的正确位置:无需复制、重新分配或合并。
如果您想要一个
List
,则可以始终使用
Arrays.asList
包装结果,但是当然不能向结果添加或删除元素。
收集器
如果上述条件之一不成立,则需要处理收集器,它们具有不同的权衡。
收集器通过以线程限制的方式在中间结果上操作来并行工作。然后将中间结果合并到最终结果中。有两个操作需要考虑:1)将单个元素累积到中间结果中,2)将中间结果合并(或组合)到最终结果中。
在
LinkedList
和
ArrayList
之间,
ArrayList
很可能更快,但您应该进行基准测试以确保。请注意,
Collectors.toList
默认使用
ArrayList
,尽管这可能会在未来版本中更改。
LinkedList
每个被累加的元素(
LinkedList.add
)都涉及分配一个新的列表节点并将其连接到列表的末尾。将节点连接到列表非常快,但这涉及为每个单独的流元素分配内存,这可能会在累积过程中产生轻微的垃圾收集。
合并(
LinkedList.addAll
)也非常昂贵。第一步是将源列表转换为数组;这是通过循环遍历列表的每个节点并将元素存储到临时数组中来完成的。然后,代码遍历此临时数组,并将每个元素添加到目标列表的末尾。如上所述,这会导致为每个元素分配一个新节点。因此,合并操作非常昂贵,因为它两次迭代源列表中的每个元素,并为每个元素分配内存,这可能会引入垃圾收集开销。
ArrayList
每个元素的累加通常涉及将其附加到
ArrayList
中包含的数组的末尾。这通常非常快,但如果数组已满,则必须重新分配并复制到更大的数组中。
ArrayList
的增长策略是将新数组分配为当前数组的50%以上,因此重新分配与添加的元素数量的对数成比例,这不太糟糕。但是,所有元素都必须复制过去,这意味着早期的元素可能需要多次复制。
合并一个
ArrayList
可能比
LinkedList
更便宜。将
ArrayList
转换为数组涉及从源中批量复制(而不是逐个复制)元素到临时数组中。如果需要,目标数组将调整大小(在这种情况下很可能),需要批量复制所有元素。然后,源元素从临时数组批量复制到已经预先调整大小以容纳它们的目标中。
讨论
根据上述内容,似乎
ArrayList
比
LinkedList
快。但是,即使收集到
ArrayList
中也需要一些不必要的重新分配和复制多个元素,可能会进行多次。潜在的未来优化是,
Collectors.toList
积累元素到一个针对快速追加访问进行了优化的数据结构中,最好是一个已经预先调整大小以容纳预期元素数量的数据结构。支持快速合并的数据结构也是一种可能性。
如果您只需要遍历最终结果,那么编写具有这些属性的自定义数据结构应该不太困难。如果它不需要成为完整的列表,那么可以积累到预先调整大小的列表中以避免重新分配,并且合并将简单地将它们聚集到树结构或列表中。请参见 JDK 的
SpinedBuffer(一个私有实现类)以获取想法。