跳到內容

集合

75% developed
來自華夏公益教科書

導航 Aggregate 主題: v  d  e )

最基本的集合介面稱為 Collection。此介面為使用者提供了集合的通用用法。所有集合都需要具有相同的基本操作。這些是

  • 將元素新增到集合中
  • 從集合中刪除元素
  • 獲取集合中元素的數量
  • 列出集合的內容(遍歷集合)
Computer code 程式碼清單 5.1: CollectionProgram.java
import java.util.Collection;   // Interface
import java.util.ArrayList;    // Implementation

public class CollectionProgram {

  public static void main(String[] args) {
    Collection myCollection = new ArrayList();
    myCollection.add("1");
    myCollection.add("2");
    myCollection.add("3");
    System.out.println("The collection contains " + myCollection.size() + " item(s).");

    myCollection.clear();
    if (myCollection.isEmpty()) {
      System.out.println("The collection is empty.");
    } else {
      System.out.println("The collection is not empty.");
    }
  }
}
Computer code 程式碼清單 5.1 的控制檯
The collection contains 3 item(s).
The collection is empty.

當您將物件放入集合中時,此物件實際上並不 *在* 集合中。只有它的物件引用被新增到集合中。這意味著如果物件在放入集合後被更改,則集合中的物件也會更改。這 程式碼清單 5.2 計算從明天開始的七天,並將每個日期儲存在一個列表中以便稍後讀取。看看發生了什麼

Computer code 程式碼清單 5.2: SevenNextDays.java
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.GregorianCalendar;

public class SevenNextDays {

  public static void main(String[] args) {
   
    // The calendar is set at the current date: today
    Calendar calendar = new GregorianCalendar();

    Collection collectionOfDays = new ArrayList();
    Date currentDate = new Date();
    for (int i = 0; i < 7; ++i) {
      // The calendar is now set to the next day
      calendar.add(Calendar.DATE, 1);
      currentDate.setTime(calendar.getTimeInMillis());

      collectionOfDays.add(currentDate);
    }

    for (Object oneDay : collectionOfDays) {
      System.out.println("The next day is: " + oneDay);
    }
  }
}
Computer code 程式碼清單 5.2 的控制檯

 第二天是: Tue Oct 15 9:09:19 UTC 2024
 第二天是: Tue Oct 15 9:09:19 UTC 2024
 第二天是: Tue Oct 15 9:09:19 UTC 2024
 第二天是: Tue Oct 15 9:09:19 UTC 2024
 第二天是: Tue Oct 15 9:09:19 UTC 2024
 第二天是: Tue Oct 15 9:09:19 UTC 2024
 第二天是: Tue Oct 15 9:09:19 UTC 2024

所有集合項都應該更新到不同的日期,但它們都被更新到最後一個日期。這意味著每次更新都會更新所有集合項。currentDate 已用於填充所有集合項。集合沒有跟蹤新增的值(七個日期之一),而是跟蹤新增的物件引用(currentDate)。所以集合中包含相同物件七次!為了避免這個問題,我們應該這樣編寫程式碼

Computer code 程式碼清單 5.3: ActualSevenNextDays.java
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.GregorianCalendar;

public class ActualSevenNextDays {

  public static void main(String[] args) {
   
    // The calendar is set at the current date: today
    Calendar calendar = new GregorianCalendar();

    Collection collectionOfDays = new ArrayList();
    for (int i = 0; i < 7; ++i) {
      Date currentDate = new Date();
      // The calendar is now set to the next day
      calendar.add(Calendar.DATE, 1);
      currentDate.setTime(calendar.getTimeInMillis());

      collectionOfDays.add(currentDate);
    }

    for (Object oneDay : collectionOfDays) {
      System.out.println("The next day is: " + oneDay);
    }
  }
}
Computer code 程式碼清單 5.3 的控制檯

 第二天是: Wed Oct 9 9:09:19 UTC 2024
 第二天是: Thu Oct 10 9:09:19 UTC 2024
 第二天是: Fri Oct 11 9:09:19 UTC 2024
 第二天是: Sat Oct 12 9:09:19 UTC 2024
 第二天是: Sun Oct 13 9:09:19 UTC 2024
 第二天是: Mon Oct 14 9:09:19 UTC 2024
 第二天是: Tue Oct 15 9:09:19 UTC 2024

現在,每次我們將專案新增到集合中時,它都是一個不同的例項。所有專案都獨立發展。為了將物件新增到集合中並避免每次源物件更改時此專案都被更改,您必須在將物件新增到集合之前複製或克隆該物件。

放入集合中的物件被向上轉換為 Object 類。這意味著您需要在從集合中取出元素時將物件引用轉換回來。這也意味著 **您需要知道** 取出物件時的型別。如果集合包含不同型別的物件,我們將在執行時難以找出從集合中獲取的物件的型別。例如,讓我們使用此集合,其中包含兩個物件

Example 程式碼部分 5.1: 集合饋送。
Collection ageList = new ArrayList();
ageList.add(new Integer(46));
ageList.add("50");
Example 程式碼部分 5.2: 集合讀取。
Integer sum = new Integer(0);
for (Object age : ageList) {
    sum = sum + ((Integer) age);
}

if (!ageList.isEmpty()) {
    System.out.println("The average age is " + sum / ageList.size());
}
Computer code 程式碼部分 5.2 的控制檯
ClassCastException.

此錯誤本可以在更早的時候,在編譯時透過使用泛型型別找到。這 泛型 是從 JDK 1.5 版本開始新增的。它是對 Java 語言型別系統的增強。從 1.5 開始的所有集合實現現在都具有 *引數化型別 <E>*。*E* 指的是 *元素* 型別。當建立集合時,實際的 *元素型別* 將替換 E。在集合中,物件現在被向上轉換為 *E* 類。

Example 程式碼部分 5.3: 帶有泛型的集合。
Collection<Integer> ageList = new ArrayList<Integer>();
ageList.add(new Integer(46));     // Integer can be added
ageList.add("50");                // Compilation error, ageList can have only Integers inside

ageList 是一個集合,它只能包含 Integer 物件作為元素。當我們取出元素時,不需要進行任何轉換。

Example 程式碼部分 5.4: 專案讀取。
Integer age = ageList.get(0);

泛型不是強制性的,但通常與集合類一起使用。

集合類

[編輯 | 編輯原始碼]

java.util.Collection 介面沒有直接的實現。集合介面有五個子介面。

圖 1: java.util.Collection 介面的五個子介面。


集集合包含唯一元素,因此不允許重複元素。它類似於數學集。當向集新增新專案時,集會呼叫專案的 int hashCode() 方法,並將它的結果與所有已插入專案的雜湊碼進行比較。如果未找到雜湊碼,則新增該專案。如果找到雜湊碼,則集會對所有具有與新專案相同的雜湊碼的集專案呼叫 boolean equals(Object obj); 方法。如果所有等於呼叫都返回 false,則將新專案插入到集中。如果等於呼叫返回 true,則不會將新專案插入到集中。

圖 2: 集類圖。


java.util.HashSet<E>
這是 Set 介面的基本實現。未同步。允許 null 元素
java.util.TreeSet<E>
元素已排序,未同步。null 不允許
java.util.CopyOnWriteArraySet<E>
執行緒安全,在修改操作期間會建立新的副本。新增、更新、刪除操作開銷很大。
java.util.EnumSet<E extends Enum<E>>
列舉集中的所有元素都必須來自在建立集時顯式或隱式指定的一個列舉型別。列舉集在內部表示為位向量。
java.util.LinkedHashSet<E>
與 HashSet 相同,此外還定義了迭代順序,即元素插入到集中的順序。

在集中檢測重複物件

[編輯 | 編輯原始碼]

Set 中不能包含重複元素。你可能會好奇,當我們向 Set 新增一個物件時,它是如何檢測重複的。我們必須檢視該物件是否已存在於 Set 中。僅僅檢查物件引用是不夠的,還需要檢查物件的**值**。

為了做到這一點,幸運的是,每個 Java 物件都從 Object 繼承了 boolean equals(Object obj) 方法。你需要覆蓋它。Set 實現將呼叫該方法來比較兩個物件,以檢視它們是否相等。

不過,這裡存在一個問題。如果我把兩種不同型別的物件放入 Set 中呢?例如,我放了一個蘋果和一個橘子。它們不能被比較。呼叫 equals() 方法會導致 ClassCastException。這個問題有兩個解決方案。

  • 解決方案一 : 覆蓋 int hashCode() 方法,對於相同型別的物件返回相同的值,對於不同型別的物件返回不同的值。equals() 方法僅用於比較具有相同 hashCode 值的物件。因此,在新增物件之前,Set 實現需要:
    • 找到 Set 中所有與候選物件 hashCode 值相同的物件。
    • 對這些物件呼叫 equals() 方法,並將候選物件作為引數傳遞。
    • 如果其中任何一個方法返回 true,則不會將該物件新增到 Set 中。
  • 解決方案二 : 為蘋果和橘子建立一個父類,我們稱之為水果類。將水果放入 Set 中。你需要做以下操作:
    • 不要覆蓋蘋果類和橘子類中的 equals()hashCode() 方法。
    • 在蘋果類中建立 appleEquals() 方法,在橘子類中建立 orangeEquals() 方法。
    • 覆蓋水果類中的 hashCode() 方法,返回相同的值,這樣 Set 實現就會呼叫 equals() 方法。
    • 覆蓋水果類中的 equals() 方法,類似於這樣。
Example 程式碼部分 5.5:equals 方法實現。
public boolean equals(Object obj) {
    boolean ret = false;
    if (this instanceof Apple &&
          obj instanceof Apple) {
        ret = this.appleEquals(obj);
    } else if (this instanceof Orange &&
              obj  instanceof Orange) {
        ret = this.orangeEquals(obj);  
    } else {
        // Can not compare Orange to Apple
       ret = false;
    }
    return ret;
}

注意

  • 只有 hashCode 值相同的物件才會被比較。
  • 你有責任覆蓋 equals()hashCode() 方法。Object 中的預設實現不起作用。
  • 只有在你想消除值重複時才覆蓋 hashCode() 方法。
  • 如果你知道物件的**值**不同,或者你只是想阻止新增完全相同的物件,不要覆蓋 hashCode() 方法。
  • 注意,hashCode() 可能在其他集合實現中使用,例如在 Hashtable 中快速查詢物件。覆蓋預設的 hashCode() 方法可能會影響那裡的效能。
  • 對於每個建立的物件,預設的 hashCodes 都是唯一的。所以,如果你決定不覆蓋 hashCode() 方法,就沒有必要覆蓋 equals() 方法,因為它不會被呼叫。

SortedSet

[edit | edit source]

SortedSet 介面與 Set 介面相同,只是 SortedSet 中的元素是有序的。它擴充套件了 Set 介面。SortedSet 中的所有元素必須實現 Comparable 介面,此外,所有元素必須是相互可比較的。

注意,如果 SortedSet 要正確地實現 Set 介面,SortedSet 保持的排序必須與 equals 一致。這是因為 Set 介面是根據 equals 操作定義的,但 SortedSet 使用其 compare 方法執行所有元素比較,所以根據此方法認為相等的兩個元素,從 SortedSet 的角度來看是相等的。

由於 SortedSet 的排序特性,它還包含一些額外的操作。

E first(); 返回第一個元素。
E last(); 返回最後一個元素。
SortedSet headSet(E toElement); 返回從第一個元素到不包含 toElement 的元素。
SortedSet tailSet(E fromElement); 返回從包含 fromElement 的元素到最後一個元素。
SortedSet subSet(E fromElement, E toElement); 返回從包含 fromElement 的元素到不包含 toElement 的元素範圍內的元素。(如果 fromElement 和 toElement 相等,則返回的 SortedSet 為空。)

在 List 集合中,元素按特定順序排列,可以透過索引訪問。允許重複,同一個元素可以被新增到 List 中多次。它有以下實現:

圖 3: List 類圖。


java.util.Vector<E>
同步的,在多執行緒訪問中使用,否則使用 ArrayList
java.util.Stack<E>
它擴充套件了 Vector 類,並增加了五個操作,允許將 Vector 視為一個棧。它表示一個後進先出 (LIFO) 物件棧。
java.util.ArrayList<E>
List 介面的基本實現是 ArrayList。ArrayList 不是同步的,不是執行緒安全的。Vector 是同步的,執行緒安全的。Vector 速度較慢,因為為了使其執行緒安全而增加了額外的開銷。當只有一個執行緒訪問 List 時,使用 ArrayList。每當你向 List 中插入或刪除元素時,都會有額外的開銷來重新索引 List。當你有很大的 List 並且有很多插入和刪除操作時,可以考慮使用 LinkedList
java.util.LinkedList<E>
非同步的,更新操作比其他 List 更快,易於用作棧、佇列、雙端佇列。LinkedList 的名稱暗示了一種特殊的資料結構,其中元素/節點透過指標連線。
 Head               Node 1                   Node 2                     Node n
  ______
 | Size |          _________________        _______________            _____________
 |______|         |      | point   |       |      | point  |          |      |      |  
 | First|-------->| Data | to next |------>| Data | to next|-- ... -->| Data | null |
 | elem |         |______|_________|       |______|________|          |______|______|
 |______|                                                                 ^
 | Last |                                                                 |
 | elem |-----------------------------------------------------------------
 |______|

每個節點與連結列表中的一個專案相關聯。要從連結列表中刪除元素,需要重新排列指標。刪除節點 2 之後

 Head               Node 1                   Node 2                     Node n
  ______                                 _____________________
 | Size |          _________________    |   _______________   |       ______________
 |_- 1__|         |      | point   |    |  |      | point  |  |       |      |      |  
 | First|-------->| Data | to next |----   | Data | to next|   -...-->| Data | null |
 | elem |         |______|_________|       |______|________|          |______|______|
 |______|                                                                 ^
 | Last |                                                                 |
 | elem |-----------------------------------------------------------------
 |______|
javax.management.AtributeList<E>
表示 MBean 屬性值的列表。用於在 AttributeList 中插入 Attribute 物件的方法覆蓋了超類 ArrayList 中的相應方法。這是為了確保 AttributeList 中包含的物件只有 Attribute 物件。
javax.management.relation.RoleList<E>
RoleList 表示角色 (Role 物件) 列表。它用作建立關係時的引數,以及嘗試在關係中設定多個角色 (透過 'setRoles()' 方法) 時。它作為 RoleResult 的一部分返回,以提供成功檢索的角色。
javax.management.relation.RoleUnresolvedList<E>
RoleUnresolvedList 表示 RoleUnresolved 物件列表,表示由於在嘗試訪問 (讀寫角色) 時遇到問題而未從關係中檢索到的角色。

Queue

[edit | edit source]

Queue 介面提供了額外的插入、提取和檢查操作。有 FIFO (先進先出) 和 LIFO (後進先出) 佇列。該介面在 Collection 介面的基礎上增加了以下操作:

E element() 檢索,但不刪除此佇列的頭部。此方法與 peek 方法的不同之處在於,如果此佇列為空,則它會丟擲異常。
boolean offer(E o) 如果可能,將指定元素插入此佇列中。
E peek() 檢索,但不刪除此佇列的頭部,如果此佇列為空,則返回 null。
E poll() 檢索並刪除此佇列的頭部,如果此佇列為空,則返回 null。
E remove() 檢索並刪除此佇列的頭部。此方法與 poll 方法的不同之處在於,如果此佇列為空,則它會丟擲異常。
圖 4: Queue 類圖。


java.util.BlockingQueue<E>
在檢索元素時等待佇列變為非空,在儲存元素時等待佇列中有空間。最適合生產者-消費者佇列。
java.util.PriorityQueue<E>
根據構造時指定的順序/優先順序對元素進行排序,不允許 null 元素。
java.util.concurrent.ArrayBlockingQueue<E>
按 FIFO 順序對元素進行排序;同步的,執行緒安全的。
java.util.concurrent.SynchronousQueue<E>
每個 put 操作都必須等待一個 take 操作,反之亦然,沒有內部容量,即使是容量為 1 的情況也是如此,元素只有在你嘗試取它時才會存在;除非另一個執行緒試圖刪除它,否則你不能新增元素 (使用任何方法)。

完整的 UML 類圖

[編輯 | 編輯原始碼]
圖 5:Collection 介面及其實現的 UML 類圖。


當您執行多個執行緒時,同步非常重要。注意,同步並不意味著您的集合是執行緒安全的。執行緒安全的集合也稱為併發集合。大多數流行的集合類都提供單執行緒和多執行緒環境的實現。非同步實現總是更快。您可以在多執行緒環境中使用非同步實現,只要確保在任何給定時間只有一個執行緒更新集合。

Java JDK 包在 Java 1.5 中引入了一個新包,即 java.util.concurrent。此包提供了一些專為在多執行緒環境中使用而設計的 Collection 實現。

下表列出了所有同步集合類

同步 非同步
List java.util.Vector java.util.ArrayList
java.util.Stack
java.util.LinkedList
java.util.concurrent.CopyOnWriteArrayList
java.util.TreeSet
java.util.HashSet
java.util.LinkHashSet
java.util.concurrent.CopyOnWriteArraySet

自定義集合

[編輯 | 編輯原始碼]

Java JDK 集合實現非常強大且完善,因此您不太可能需要編寫自己的實現。不同集合的用法相同,但實現不同。如果現有集合實現無法滿足您的需求,您可以編寫自己的實現版本。您版本的實現只需實現相同的 java.util.Collection 介面,然後您就可以切換到使用您的實現,而使用該集合的程式碼不需要更改。

如果您需要將相關(通常是相同型別)的物件儲存在一個集合中,可以使用 Collection 介面,您可以在其中

  • 搜尋特定元素
  • 列出元素
  • 使用集合基本操作(新增、刪除、更新等)維護和/或更改元素的順序
  • 透過索引號訪問元素

使用 Collection 介面的優勢在於

  • 提供通用用法,正如我們上面所討論的,它易於切換實現
  • 它使將一種型別的集合轉換為另一種型別變得容易。

Collection 介面定義了以下基本操作

boolean add(E o); 使用元素型別 E
boolean addAll(Collection c);
boolean remove(Object o);
boolean removeAll(Collection c);
boolean retainAll(Collection c); 如果集合因操作而發生更改,則返回 true

請注意,在 addAll() 中,我們可以新增任何型別的集合。這就是使用 Collection 介面的美妙之處。您可以擁有一個 LinkedList,只需呼叫 addAll(list) 方法,傳入一個列表即可。您可以傳入一個 Vector、一個 ArrayList、一個 HashSet、一個 TreeSet、一個 YourImpOfCollection……所有這些不同型別的集合將神奇地轉換為一個 LinkedList

讓我們更仔細地看看這種魔法。轉換很簡單,因為 Collection 介面定義了一種迴圈遍歷元素的標準方法。以下程式碼是 LinkedListaddAll() 方法的可能實現。

Example 程式碼部分 5.6:集合傳輸。
import java.util.Collection
import java.util.Iterator
...
public boolean addAll(Collection coll) {
   int sizeBefore = this.size();
   Iterator iter = coll.iterator();
   while(iter.hasNext()) {
      this.add(iter.next());
   }
   if (sizeBefore > this.size()) {
      return true;
   } else {
      return false;
   }
}

上面的程式碼只是遍歷傳入的集合並將元素新增到連結串列中。您不必這樣做,因為這已經定義過了。您可能需要編碼的是迴圈遍歷一個 Customer 集合

Example 程式碼部分 5.7:對集合的迭代。
import java.util.Collection
import java.util.Iterator
import java.yourcompany.Customer
...
public String printCustomerNames(Collection customerColl) {
   StringBuffer buf = new StringBuffer();

   Iterator iter = customerColl.iterator();
   while(iter.hasNext()) {
      Customer cust = (Customer) iter.next();
      buf.append(cust.getName());
      buf.append( "\n" );
   }
  return buf.toString();
}

請注意兩點

  • 上面的程式碼將適用於所有型別的集合。
  • 我們必須知道集合中物件的型別,因為我們對其呼叫了一個方法。


Clipboard

待辦事項
新增一些類似於 變數 中的練習


華夏公益教科書