2016-11-30 33 views
6

我想知道如果Java 8流處理內存分配如果終端操作是一個列表收集器。Java 8流列表收集器內存分配速度與預分配的循環

例如,考慮

List<Integer> result = myList.stream().map(doWhatever).collect(Collectors.toList()); 

VS

List<Integer> result = new ArrayList<>(myList.size()); 
for(String s : myList) { 
    result.add(doWhatever.apply(s)); 
} 

在使用流的情況下,它是未知的名單將有多大增長,這意味着必須有某種再分配。這個假設是真的嗎?

結果列表的類型是什麼樣的鏈接列表,因此訪問元素的速度比ArrayList慢?

如果我從一開始就知道結果列表的大小,我不應該使用具有列表收集器的流嗎?

+5

'收藏家。 toList()'使用'ArrayList'。重新分配的方式與其他任何'ArrayList'完全相同。 –

回答

6

現場Collectors.toList()後面將允許收集所得的Stream元素與默認構造函數創建一個ArrayList所以用的10默認容量,從而確實是一個重新分配的情況下將所需的大小超過10

如果要使用不同的List的實現,使用toCollection(Supplier<C> collectionFactory)這是一個比較通用的集電極允許提供目標Collection的工廠。

例如,如果您要收集的元素融入一個LinkedList相反,你可以重寫你的代碼爲未來:

List<Integer> result = myList.stream() 
    .map(doWhatever) 
    .collect(Collectors.toCollection(LinkedList::new)); 

假設您想要與100默認容量ArrayList,集電極會Collectors.toCollection(() -> new ArrayList<>(100))

+4

您可能剛剛使用'LinkedList'作爲如何創建特定類型集合的示例。但是我會提醒讀者不要使用LinkedList,希望它比追加到ArrayList更快。它可能不會。還有另一件事基準.... –

+1

@StuartMarks是的,它只是作爲例子,我不知道OP的用例,所以我不能再去 –

3

如果您查看Collectors.toList()的源代碼,則不需要預先分配。

public static <T> Collector<T, ?, List<T>> toList() { 
     return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add, 
           (left, right) -> { left.addAll(right); return left; }, 
           CH_ID); 
    } 

它只是創建一個新的ArrayList使用默認大小,然後對add/addAll後續調用調整大小。

+6

你期望永遠保持這種情況嗎?它在方法合同中說什麼? –

6

Collectors.toList()沒有指定關於它的實現的任何東西。如果你在意,使用toCollection(ArrayList::new)

如果我從一開始就知道結果列表的大小,我不應該使用具有列表收集器的流嗎?

不,請繼續使用它們。相對於簡潔的勝利,分配很便宜,成本也很低。推行清單一般是不成熟的優化。

+3

是的。這個。 +1。對於有關讀者,請注意,即使在考慮到重新分配和複製時,向「ArrayList」添加N個元素仍然是O(N)。 –

+0

@StuartMarks老實說,我真的很驚訝'toList()'永遠得到'Collectors'批准。 –

+2

直接在Stream上有很多需求,如'stream.toList()'。人們抱怨'stream.collect(Collectors.toList())'很多。如果收集到列表的唯一方法是'stream.collect(Collectors.toCollection(ArrayList :: new))',那將會更糟糕。正如你所說的,'toList()'沒有指定它返回一個'ArrayList',但實際上它確實。我懷疑這些計劃已經發展到依賴於此。有一個希望,它可以返回像SpinedBuffer這樣的快速附加列表,但這可能太過於行爲不兼容。 –

2

在使用流的情況下,未知列表將增長多少,這意味着必須有某種重新分配。這個假設是真的嗎?

它知道以前的管道,它的大小,並創建一個ArrayList<>與默認配置不看那個。當您使用動態優化數組時,無關緊要。

結果列表的類型是什麼樣的鏈接列表,並因此比ArrayList更慢地訪問元素?

ArrayList默認情況下使用,但你可以自由地提供自己的供應商和累加器來改變這種行爲:

stream.collect(() -> new ArrayList<>(SIZE), ArrayList::add, ArrayList::addAll); 

如果,如果我知道我不使用流與列表收藏結果列表的大小從一開始就是?

不要這樣想。除了簡潔的語法,Stream API還提供了許多功能強大的功能(如並行化),您可以使用它們。

+2

如果流並行運行,則可以多次調用供應商,因爲流已被拆分。在這種情況下,很難知道預分配的大小。 –

2

目前,在toList()電器通過使用並返回ArrayList實現的(注意收集過程中使用的容器不總是有相匹配的最終結果的類型)。方式,收集器界面被定義,收集器沒有機會預先調整列表。

但在原則上,因爲標準流實現和預定toList()集電極實施是相同的庫的一部分,有可能是在未來的實現(或替代的JRE)非標準通信,其中該流檢測在toList()集電極collect方法並執行優化操作。但是當使用toList()收集器時,例如作爲groupingBy收集器的下游收集器,無論如何不存在可預測的尺寸。

如果假設流可以預測它的大小,就像你myList.stream().map(doWhatever)例如,最有效的解決辦法,因爲當前的實現,是

List<ElementType> result=Arrays.asList(stream.toArray(ElementType[]::new)); 

爲操作將利用已知的尺寸,即使在並行或,尤其是,當分裂的子尺寸是可預測的,因爲不需要合併步驟,即所有工作人員都將直接寫入結果數組中。

不幸的是,如果ElementType不是可確定類型,那麼您必須在此處求助於未選中的操作。

如果尺寸不可預測,與當前的toList()收集器相比,此解決方案可能仍然更有效,但與未來可使用非線性存儲的實施方案相比可能會有所鬆動。


因此,優化的變體只與某個設置有關。對於大多數情況下,toList()收集器是足夠的,或者可能比任何可能的未來實現中的替代方案更好。

1

對於大型並行數據流,我發現toList()實際上有嚴重的性能問題,因爲累加器列表正在重複組合 - 這導致了比O(N)更像O(N^2)的事情。

下面是在一個的ConcurrentLinkedQueue直至結束階段保持數據的替代toList()集電極 - 用於400000元素流中,收集操作時間從1500毫秒去約30:

http://pastebin.com/Bi93uig6

+0

很酷,這是一個非常好的主意! – Tobson