跳轉到內容

集合

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

導航 聚合 主題: 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 介面沒有直接實現。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 物件都有一個 boolean equals(Object obj) 方法,它繼承自 Object。您需要重寫它。該方法將被 Set 實現呼叫,以比較兩個物件,以檢視它們是否相等。

不過,存在一個問題。如果我將兩種不同型別的物件放入 Set 中怎麼辦。我放了一個蘋果和一個橘子。它們無法比較。呼叫 equals() 方法會導致 ClassCastException。為此有兩種解決方案

  • 解決方案一 : 重寫 int hashCode() 方法,對於相同型別的物件返回相同的值,對於不同型別的物件返回不同的值。該 equals() 方法僅用於比較具有相同 hashCode 值的物件。因此,在新增物件之前,Set 實現需要
    • 在 Set 中查詢所有與候選物件 hashCode 相同的 hashCode 的物件
    • 並對這些物件呼叫 equals() 方法,傳入候選物件
    • 如果任何一個返回 true,則該物件不會被新增到 Set 中。
  • 解決方案二 : 為 Apple 和 Orange 建立一個超類,我們將其稱為 Fruit 類。將 Fruits 放入 Set 中。您需要執行以下操作
    • 不要重寫 Apple 和 Orange 類中的 equals()hashCode() 方法
    • 在 Apple 類中建立 appleEquals() 方法,並在 Orange 類中建立 orangeEquals() 方法
    • 重寫 Fruit 類中的 hashCode() 方法並返回相同的值,以便 Set 實現呼叫 equals()
    • 重寫 Fruit 類中的 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() 可能用於其他集合實現,例如在雜湊表中快速查詢物件。重寫預設的 hashCode() 方法可能會影響那裡的效能。
  • 預設的 hashCodes 對於每個建立的物件都是唯一的,因此如果您決定不重寫 hashCode() 方法,則沒有必要重寫 equals() 方法,因為它不會被呼叫。

SortedSet

[edit | edit source]

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

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

由於 'Set' 的排序性質,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 相等,則返回的排序集為空。)

在列表集合中,元素按特定順序放置,並且可以透過索引訪問。允許重複,相同的元素可以新增到列表中兩次。它具有以下實現

圖 3: List 類圖。


java.util.Vector<E>
同步,在多執行緒訪問中使用,否則使用 ArrayList
java.util.Stack<E>
它擴充套件了 Vector 類,並具有五個操作,允許將向量視為堆疊。它表示物件的先進後出 (LIFO) 堆疊。
java.util.ArrayList<E>
List 介面的基本實現是 ArrayList。ArrayList 不是同步的,也不是執行緒安全的。Vector 是同步的,並且執行緒安全的。Vector 較慢,因為額外的開銷使其執行緒安全。當只有一個執行緒訪問列表時,請使用 ArrayList。每當您從列表中插入或刪除元素時,都會有額外的開銷來重新索引列表。當您有一個大型列表並且您有很多插入和刪除操作時,請考慮使用 LinkedList
java.util.LinkedList<E>
非同步,更新操作比其他列表更快,易於用於堆疊、佇列、雙端佇列。名稱 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,反之亦然,沒有內部容量,即使是容量為一的容量,元素只在您嘗試獲取它時才會出現;除非另一個執行緒試圖刪除它,否則您無法新增元素(使用任何方法)。

完整的 UML 類圖

[edit | edit source]
圖 5: Collection 介面及其實現的 UML 類圖。


同步

[edit | edit source]

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

Java 1.5 引入了新的 Java JDK 包,即java.util.concurrent。此包提供了一些專為多執行緒環境設計的集合實現。

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

同步 非同步
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)方法並傳入一個列表。您可以傳入VectorArrayListHashSetTreeSetYourImpOfCollection等。所有這些不同型別的集合將被神奇地轉換為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

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


華夏公益教科書