首頁 > 文學

在 Java 中如何加快大型集合的處理速度

作者:由 赫爾岑岑 發表于 文學日期:2022-12-09

什麼叫序列處理

本文討論了 Java Collections Framework 背後的目的、Java 集合的工作原理,以及開發人員和程式設計師如何最大限度地利用 Java 集合。

什麼是 Java 集合

儘管 Java 已經過了 25 歲生日,仍然是當今最受歡迎的程式語言之一。超過 100 萬個網站透過某種形式在使用 Java,超過

三分之一

的軟體開發人員的工具箱中有 Java。

Java 在它的整個生命歷程中經歷了重大的演變。一個早期的進步出現在 1998 年,當時 Java 引入了 Collections Framework(Java Collection Framework,JCF),簡化了操作 Java 物件的任務。JCF 為集合提供了標準化的介面和通用方法,減少了程式設計工作,並提升了 Java 程式的執行速度。

理解 Java 集合和 Java Collections Framework 之間的區別是至關重要的。Java 集合只是表示一組 Java 物件的資料結構。開發人員可以像處理其他資料型別一樣處理集合,執行搜尋或操作集合內容等常見任務。

Set 介面(java。util。Set)就是 Java 集合的一個例子。Set 是一種集合,不允許出現重複元素,也不以任何特定的順序儲存元素。Set 介面繼承了 Collection(java。util。Collection)的方法,並且只包含這些方法。

除了集合之外,還有佇列(java。util。Queue)和 Map(java。util。Map)。Map 並不是真正意義上的集合,因為它們沒有

繼承集合介面

,但開發人員可以像操作集合一樣操作 Map。集合、佇列、列表和 Map 都有後代,比如排序集合(java。util。SortedSet)和可導航 Map(java。util。NavigableMap)。

在使用集合時,開發人員需要熟悉和理解一些特定的集合相關術語。

可修改與不可修改——正如這些術語表面上所表明的,不同的集合可能支援也可能不支援修改操作。

可變集合與不可變集合——不可變集合在建立後不能被修改。雖然在某些情況下,不可修改的集合仍然可能由於其他程式碼的訪問而發生變化,但不可變集合會阻止這種變更。不可變集合是指能夠保證 Collection 物件中不會有任何變更的集合,而不可修改的集合是指不允許“add”或“clear”等修改操作的集合。

固定大小與可變大小——這些術語僅與集合的大小有關,與集合是可修改還是可變無關。

隨機訪問與順序訪問——如果一個集合允許為每一個元素建立索引,那麼它就是可隨機訪問的。在順序訪問集合中,必須透過所有前面的元素到達指定的元素。順序訪問集合更容易擴充套件,但搜尋時間更長。初學者可能會難以理解不可修改集合和不可變集合之間的區別。不可修改集合不一定是不可變的。實際上,不可修改集合通常是可修改集合的包裝器,其他程式碼仍然可以訪問和修改被包裝的可修改集合。通常需要使用集合一些時間才能在一定程度上理解不可修改集合和不可變集合。

例如,我們將建立一個可修改的按市值排名前五的加密貨幣列表。你可以使用 java。util。Collections。unmodifiableList()方法建立底層可修改列表的不可修改版本。你仍然可以修改底層列表,它只是被包裝成不可修改列表,但你不能直接修改不可修改的版本。

import

java。util。*;

public

class

UnmodifiableCryptoListExample

{

public

static

void

main

String

[] args)

{

List

<

String

> cryptoList =

new

ArrayList<>();

Collections。addAll(cryptoList,

“BTC”

“ETH”

“USDT”

“USDC”

“BNB”

);

List

<

String

> unmodifiableCryptoList = Collections。unmodifiableList(cryptoList);

System。out。println(

“Unmodifiable crypto List: ”

+ unmodifiableCryptoList);

// 嘗試在可修改列表中再新增一種加密貨幣,並顯示在不可修改列表中

cryptoList。add(

“BUSD”

);

System。out。println(

“New unmodifiable crypto List with new element:”

+ unmodifiableCryptoList);

// 嘗試新增並顯示一個額外的加密貨幣到不可修改列表中——unmodifiableCryptoList。add將丟擲一個未捕獲的異常,println程式碼將無法被執行

unmodifiableCryptoList。add(

“XRP”

);

System。out。println(

“New unmodifiable crypto List with new element:”

+ unmodifiableCryptoList);

}

}

複製程式碼

在執行程式碼時,你將看到對底層可修改列表新增的內容顯示為對不可修改列表的修改。

但這與你建立了一個不可變列表並試圖修改底層列表不同。有許多種方法可以基於現有的

可修改列表建立不可變列表

,下面我們使用 List。copyOf()方法建立了一個不可變列表。

import

java。util。*;

public

class

UnmodifiableCryptoListExample

{

public

static

void

main

String

[] args)

{

List

<

String

> cryptoList =

new

ArrayList<>();

Collections。addAll(cryptoList,

“BTC”

“ETH”

“USDT”

“USDC”

“BNB”

);

List

immutableCryptoList =

List

。copyOf(cryptoList);

System。out。println(

“Underlying crypto list:”

+ cryptoList)

System。out。println(

“Immutable crypto ist: ”

+ immutableCryptoList);

// 嘗試新增更多的加密貨幣到可修改列表,但不可變列表並沒有顯示變化

cryptoList。add(

“BUSD”

);

System。out。println(

“New underlying list:”

+ cryptoList);

System。out。println(

“New immutable crypto List:”

+ immutableCryptoList);

// 嘗試新增並顯示一個新的加密貨幣到不可修改的列表中

immutableCryptoList。add(

“XRP”

);

System。out。println(

“New unmodifiable crypto List with new element:”

+ immutableCryptoList);

}

}

複製程式碼

修改底層的列表後,不可變列表不顯示變更。嘗試修改不可變列表會直接導致丟擲 UnsupportedOperationException。

在 Java 中如何加快大型集合的處理速度

集合與 Java Collections Framework 的關係

在引入 JCF 之前,開發人員可以使用幾個特殊的類,即 Array、Vector 和 Hashtable。但這些類有很大的侷限性,除了缺乏公共介面之外,它們還難以擴充套件。

JCF 提供了一個用於處理集合的通用架構。集合介面包含了幾個不同的元件。

公共介面——主要集合型別的表示,包括集合、列表和 Map;

實現——集合介面的特定實現,從通用的到特殊的再到抽象的。此外,還有一些與舊的 Array、Vector 和 Hashtable 類相關的遺留實現;

演算法——用於操作集合的靜態方法;

基礎結構——對各種集合介面的底層支援。與之前相比,JCF 為開發人員提供了許多好處。值得注意的是,JCF 降低了開發人員對自己編寫資料結構的需求,從而提高了 Java 程式設計的效率。

但是,JCF 也從根本上改變了開發人員使用 API 的方式。JCF 透過提供一組新的公共介面來處理不同的 API,簡化了開發人員學習、設計和實現 API 的過程。此外,API 的互操作性也大大提升了。

Eclipse Collections就是一個例子

,它是一個完全相容不同 Java 集合型別的開源 Java 集合庫。

由於 JCF 提供了更容易重用程式碼的結構,從而進一步提升了開發效率。其結果就是開發時間縮短了,程式質量也得到了提升。

JCF 有一個定義良好的介面層次結構。java。util。Collection 擴充套件了超介面 Iterable,Collection 有許

多子介面和子類

,如下所示。

在 Java 中如何加快大型集合的處理速度

如前所述,集合是唯一性物件的無序容器,而列表是可能包含重複項的有序集合。你可以在列表中的任何位置新增元素,但其他部分仍然保留了順序。

佇列也是集合,元素被新增到一端,並在另一端被刪除。也就是說,它是一種先進先出(FIFO)介面。Deque(雙端佇列)允許從任意一端新增或刪除元素。

使用 Java 集合的方法

JCF 中的每一個介面,包括 java。util。Collection,都提供了特定的方法用於訪問和操作集合的各個元素。集合提供的常用的方法有:

size()——返回集合中元素的個數;

add(Collection element) / remove(Collection object)——這些方法用於修改集合的內容。需要注意的是,當集合中有重複元素時,移除只會影響元素的單個例項;

equals(Collection object)——比較物件與集合是否等價;

clear()——刪除集合中的所有元素。每個子介面也可以有其他方法。例如,儘管 Set 介面只包含來自 Collection 介面的方法,但 List 介面包含了許多用於訪問特定列表元素的方法。

get(int index)——返回指定索引位置的元素;

set(int index, element)——設定指定索引位置的元素;

remove(int,index)——移除指定索引位置的元素。

Java 集合的效能

隨著集合元素數量的增長,它們可能會出現明顯的效能問題。事實證明,

集合型別的選擇

和集合的相關設計也會極大地影響集合的效能。

隨著需要處理的資料量不斷增加,Java 引入了新的處理集合的方法來提升整體效能。在 2014 年釋出的 Java 8 引入了 Streams——旨在簡化和提高批次處理物件的速度。自從推出以來,Streams 已經有了許多

改進

需要注意的是,流本身並不是資料結構,而是“對流中的元素進行函式式操作(例如對集合進行 map-reduce 轉換)的類。”

Streams 使用方法管道來處理從資料來源(如集合)接收到的資料。Streams 的每一個方法要麼是一箇中間方法(返回可以進一步處理的流),要麼是一個終端方法(在此之後不可能進行其他流處理)。管道中的中間方法是惰性的,也就是說,它們只在必要時才進行求值。

並行執行和

序列執行

都存在於流中。預設情況下,流是序列的。

在 Java 中如何加快大型集合的處理速度

透過並行處理來提升效能

在 Java 中處理大型集合可能很麻煩。雖然 Streams 簡化了大型集合的處理和編碼工作,但並不總是能保證效能上的提升。事實上,程式設計師經常發現使用 Streams 反而會

減慢處理速度

眾所周知,網站使用者只會等待幾秒鐘的載入時間,然後他們就會離開。因此,為了提供最好的使用者體驗並

維護開發人員

提供高質量產品的聲譽,開發人員必須考慮如何最佳化大型資料集合的處理。雖然並行處理並不總能保證提高速度,但至少是有希望的。

並行處理,即將處理任務分解為更小的塊並同時執行它們,提供了一種在處理大型集合時減少處理開銷的方法。但是,即使並行流處理簡化了程式碼編寫,也會

導致效能下降

。本質上,多執行緒管理開銷會抵消並行執行執行緒所帶來的好處。

因為集合不是執行緒安全的,並行處理可能會導致執行緒干擾或記憶體不一致(當並行執行緒看不到其他執行緒所做的修改,對相同的資料有不同的檢視時)。Collections Framework 試圖透過使用同步包裝器在並行處理期間防止執行緒不一致。雖然包裝器可以讓集合變成執行緒安全的,從而實現更高效的並行處理,但它可能會產生不良的效能影響。具體來說,同步可能會導致執行緒爭用,從而導致執行緒執行得更慢或停止執行。

Java 有一個用於集合的元素並行處理函式 Collection。parallelstream。預設的序列處理和並行處理之間的一個顯著區別是,序列處理時總是相同的執行和輸出順序在並行處理時可能會有不同。

因此,在處理順序不影響最終輸出的場景中,並行處理會特別有效。但是,在一個執行緒的狀態可能會影響另一個執行緒狀態的場景中,並行處理可能會有問題。

我們來考慮一個簡單的示例,在這個示例中,我們為包含 1000 個客戶建立了一個應收賬款列表。我們想要知道這些客戶中有多少人的應收賬款超過 25000 美元。我們可以按照序列或並行的處理方式檢查這個列表。

import

java。util。Random;

import

java。util。ArrayList;

import

java。util。List;

class

Customer

{

static

int

customernumber;

static

int

receivables;

Customer(

int

customernumber,

int

receivables) {

this

。customernumber = customernumber;

this

。receivables = receivables;

}

public

int

getCustomernumber

{

return

customernumber;

}

public

void

setCustomernumber

int

customernumber

{

this

。customernumber = customernumber;

}

public

int

getReceivables

{

return

receivables;

}

public

void

setReceivables

{

this

。receivables = receivables;

}

}

public

class

ParallelStreamTest

{

public

static

void

main

String args[]

{

Random receivable =

new

Random();

int

upperbound =

1000000

List < Customer > custlist =

new

ArrayList < Customer > ();

for

int

i =

0

; i < upperbound; i++) {

int

custnumber = i +

1

int

custreceivable = receivable。nextInt(upperbound);

custlist。

add

new

Customer(custnumber, custreceivable));

}

long

t1 = System。currentTimeMillis();

System。

out

。println(

“Sequential Stream count: ”

+ custlist。stream()。filter(c ->

c。getReceivables() >

25000

)。count());

long

t2 = System。currentTimeMillis();

System。

out

。println(

“Sequential Stream Time taken:”

+ (t2 - t1));

t1 = System。currentTimeMillis();

System。

out

。println(

“Parallel Stream count: ”

+ custlist。parallelStream()。filter(c ->

c。getReceivables() >

25000

)。count());

t2 = System。currentTimeMillis();

System。

out

。println(

“Parallel Stream Time taken:”

+ (t2 - t1));

}

}

複製程式碼

程式碼執行結果表明,在處理資料集合時,

並行處理

可能會提升效能:

在 Java 中如何加快大型集合的處理速度

但需要注意的是,每次執行程式碼時,你可能獲得不同的結果。在某些情況下,序列處理仍然優於並行處理。

在 Java 中如何加快大型集合的處理速度

在本例中,我們使用 Java 的原生程序來分割資料和分配執行緒。

不幸的是,對於上述兩種情況,Java 的原生並行處理並不總是比序列處理更快。實際上,經常會更慢。

例如,並行處理對於連結串列沒有什麼用。雖然 ArrayList 很容易被分割成並行處理,但 LinkedList 卻不是這樣的。TreeMap 和 HashSet 介於兩者之間。

Oracle 的

NQ模型

是決定是否使用並行處理的一種方法。在 NQ 模型中,N 表示需要處理的資料元素數量,Q 表示每個資料元素所需的計算量。在 NQ 模型中,計算 N 和 Q 的乘積,數值越大,說明並行處理提高效能的可能性越大。

在使用 NQ 模型時,N 和 Q 之間存在反比關係,即每個元素所需的計算量越高,並行處理的資料集就越小。經驗法則是,對於較低的計算需求,包含 10000 個元素的資料集是使用並行處理的基線。

除此之外,還有其他更高階的方法來最佳化 Java 集合中的並行處理。例如,高階開發人員可以調整集合中資料元素的分割槽,以最大化並行處理效能。還有一些

第三方的JCF外掛

和替代品可以提升效能。但是,初學者和中級開發人員應該重點了解哪些操作可以從 Java 的原生並行處理特性中受益。

結論

在大資料世界裡,想要建立高效能的網頁和應用程式,必須找到改進大量資料處理的方法。Java 提供了內建的集合處理特性幫助開發人員改進資料處理,包括 Collections Framework 和原生並行處理功能。開發人員需要熟悉如何使用這些特性,並瞭解可以時候可以使用原生特性,什麼時候應該使用並行處理。