Java 集合學習筆記

@

Java 集合(容器)

一、Java 集合框架概述

  • 一方面,面向對象語言對事務的體現都是以對象的形式,為了方便對多個對象的操作,就要對對象進行存儲。另一方面,使用 Array 存儲對象放面具有一些弊端,而 Java 集合就像一種容器,可以動態的把多個對象的引用放入容器中。

    • 數組在記憶體存儲方面的特點:
      • 數組初始化以後,長度就確定了
      • 數組聲明的類型,就決定了元素初始化時的類型
    • 數組在存儲數據方面的弊端:
      • 數組初始化以後,長度不可變,不便於擴展
      • 數組中提供的屬性和方法少,不便於進行添加、刪除、插入等操作,且效率不高。同時無法直接獲取存儲的元素的個數
      • 數組存儲的數據是有序的、可重複的。(存儲數據的特點單一)
    • Java 集合類可用於存儲數量不等的多個對象,還可用於保存具有映射關係的關聯數組。
  • Java 集合可分為 Collection 和 Map 兩種體系

    • Collection 介面:單列數據,定義了存取一組對象的方法和集合
      • List:元素有序、可重複的集合
      • Set:元素無需、不可重複的集合
    • Map 集合:雙列數據,保存具有映射關係 「 key – value 對」 的集合
  • Collection 介面繼承樹

    Collection 介面繼承樹

  • Map 介面繼承樹

    介面繼承樹

二、Collection 介面方法

Collection 介面中文文檔(JDK 11)

  1. 添加
    • add(Object obj)
    • addAll(Collection coll):將集合 coll 中的所有元素添加到當前集合中
  2. 獲取有效元素的個數
    • int size()
  3. 清空合集
    • void clear()
  4. 是否為空集合
    • boolean isEmpty()
  5. 是否包含某個元素
    • boolean contains(Object obj):是通過元素的 equals 方法來判斷是否是同一個對象
    • boolean containsAll(Collection c):也是調用 equals 方法來比較,用兩個集合的元素挨個比較。
  6. 刪除
    • boolean remove(Object obj):通過 equals 方法定位,並刪除,只會刪除找到的第一個元素
    • boolean removeAll(Collection coll):取當前集合的差集
  7. 取兩個集合的交集
    • boolean retainAll(Collection c):把交集的結果存在當前集合中,不影響集合 c
  8. 集合是否相等
    • boolean equals(Object obj)
  9. 轉成對象數組
    • Object[] toArray()
    • 數組 轉化成 集合:調用 Arrays 類的靜態方法 asList()。
      • 但要注意該方法直接寫入 new int[](123, 456) 只是把整體當成一個元素存入,此時用包裝類的方式即可解決。
  10. 獲取集合對象的哈希值
    • hashCode()
  11. 遍歷
    • iterator():返回迭代器對象,用於遍歷集合

三、Iterator 迭代器介面

1. 使用 Iterator 介面遍歷集合元素

  • Iterator 對象稱為迭代器(設計模式的一種),主要用於遍歷 Collection 集合中的元素。
  • GOF 給迭代器模式的定義為:提供一種方法訪問一個容器(container)對象中各個元素,而又不需要暴露該對象的內部細節。迭代器模式,便是為容器而生。類似於」公交車上的售票員」、」火車上的乘務員「、」空姐「。
  • Collection 介面繼承了 java.lang.Iterator 介面,該介面有一個 Iterator() 方法,那麼所有實現了 Collection 介面的集合類都有一個 Iterator() 方法,用以返回一個實現了 Iterator 介面的對象。

2. Iterator 介面的方法

boolean hasNext(): Return true if the iterator has more elements.
E next(): Return the next element int the iteration.
void remove(): Removes from the underlying collection the last element returned by the iterator(optional operation).
  • 可採取 hasNext() 和 next() 方法配合遍歷集合

    在調用it.next()方法之前必須要調用it.hasNext()進行檢測。若不調用,且下一條記錄無效,直接調用it.next()會拋出NoSuchElementException異常。

    while(iterator.hasNext) {
        iterator.next();
    }
    

    兩種常見的錯誤遍歷方式:

    //方式一:
    Iterator iterator = coll.iterator();
    while(iterator.next() != null) {
        System.out.println(iterator.next());
    }
    	//while的條件會使 集合指針 移動一次,導致輸出結果跳躍
    
    //方式二:
    while(coll.iterator().hasNext()) {
        System.out.println(coll.iterator().next());
    }
    	//集合對象每次調用 iterator() 方法都會得到一個全新的迭代器對象,默認指針 都會從集合的第一個元素開始。
    
  • 有關 remove()

    Iterator iter = coll.iterator();
    while(iter.hasNext()) {
        Object obj = iter.next();
        if(obj,equals("Tom")) {
            iter.remove();
        }
    }
    
    • 注意
      • Iterator 可以刪除集合的元素,但是 是通過遍歷過程中通過迭代器對象的 remove 方法,不是集合對象的 remove 方法。
      • 如果還未調用 next() 或在上一次調用 next 方法之後已經調用了 remove 方法,再調用 remove 都會報IllegalStateException

3. 使用 foreach 循環遍歷集合元素

  • Java 5.0 提供了 foreach 循環迭代訪問 Collection 和 數組

  • 遍歷操作不需要獲取 Collection 或數組的長度,無需使用索引訪問元素

  • 遍歷集合和底層調用 Iterator 完成操作

  • foreach 還可以用來遍曆數組

四、Collection 子介面一:List

1. List 介面概述

  • 鑒於 Java 中數組用來存儲數據的局限性,我們通常使用List替代數組
  • List 集合類中 元素有序、且可重複,集合中的每個元素都有其對應的順序索引。
  • List容器中的元素都對應一個整數型的序號記載其在容器中的位置,可以根據序號存取容器中的元素。
  • JDK API中List介面的實現類常用的有:ArrayList、LinkedList和Vector。

2. List 介面方法

  • List 除了從 Collection 集合繼承的方法外,List 集合里添加了一些根據索引來操作集合元素的方法。
    • void add(int index, Object ele):在 index 位置插入 ele 元素
    • boolean addAll(int index, Collection eles):從 index 位置開始將 eles 中的所有元素添加進來
    • Object get(int index):獲取指定 index 位置的元素
    • int indexOf(Object obj):返回 obj 在集合中首次出現的位置
    • int lastIndexOf(Object obj):返回 obj 在當前集合中末次出現的位置
    • Object remove(int index):移除指定 index 位置的元素,並返回此元素
    • Object set(int index, Object ele):設置指定 index 位置的元素為 ele
    • List subList(int fromIndex, int toIndex):返回從 fromIndex 到 toIndex 位置的子集合

3. List 實現類之一:ArrayList(主要)

  • ArrayList 是 List 介面的典型實現類、主要實現類

  • 本質上,ArrayList 是對象引用的一個「變長「數組

  • ArrayList 的 JDK 1.8 之前與之後的 空參構造器 的實現區別(其他方面無異)?

    • JDK 1.7:類似餓漢式,直接創建一個初始容量為 10 的數組

      • ArrayList list = new ArrayList();
        list.add(123); // elementData[0] = new Integer(123);
        
      • 當數組 elementData 數組容量不夠時,默認情況下,擴容為原來的 1.5 倍,同時將原來數組中的數據複製到新的數組中。

      • 建議開發中使用帶參構造器:一開始人為的確定容量

    • JDK 1.8:類似懶漢式,一開始創建一個長度為 0 的數組,當添加第一個元素時再創建一個是容量為 10 的數組

  • Array.asList() 方法返回的 List 集合,既不是ArrayList 試里,也不是 Vector 實例。Array.asList() 返回值是一個固定長度的 List 集合。

  • 源碼分析傳送門(轉載)

4. List 實現類之二:LinkedList

  • 適合頻繁的插入或刪除元素的操作,效率較高

  • 雙向鏈表

    在這裡插入圖片描述

5. List 實現類之三:Vector

  • Vector 很古老,JDK 1.0 就有了。大多數操作與 ArrayList 相同,區別在於 Vector 是執行緒安全的,但效率總是比 ArrayList 慢,一般不用。

6. List 三種實現類的異同

  1. ArrayList 與 LinkedList 區別:(參考javaGuide)
    1. 是否執行緒安全:二者執行緒不完全,相對執行緒安全的 Vector,執行效率更高
    2. 底層數據結構:ArrayList 和Vector 實現了基於動態數組的數據結構,LinkedList是基於鏈表的數據結構(JDK1.6之前為循環鏈表,JDK1.7取消了循環)。
    3. 插入和刪除元素
      • 對於 ArrayList ,由於是數組結構,故在指定位置 i 插入或刪除元素的畫,時間複雜度為 O(n-i) ,即操作位置之後的所有元素後移或前移。
      • 對於 LinkedList ,由於採用鏈表結構,插入刪除元素的時間複雜度近似為 O(1) , 若在指定位置插入元素,時間複雜度近似為 O(n) ,因為要先移動到指定位置,再進行操作。
    4. 是否支援快速隨機訪問:LinkedList 不支援高效的隨即元素訪問,而 ArrayList 支援。快速隨機訪問就是通過元素的序號開速獲取元素對象。
    5. 記憶體空間佔用:ArrayList 的空間浪費主要體現在 list 列表的結尾會預留一定的容量空間,而 LinkedList 的空間花費則體現在它的每一個元素都需要消耗比 ArrayList 更多的空間(因為要存放直接後繼和直接前驅以及數據)。
  2. ArrayList 和 Vector 的區別
    Vector 和 ArrayList 幾乎完全相同,最大區別為 Vector 是同步類(synchronized),屬於強同步類。因此開銷比 ArrayList 要大,訪問要慢。正常情況下,一般使用 ArrayList 而不是 Vector,因為同步完全可以由程式設計師自己來控制(如使用Collections的方法)。
    Vector 每次擴容請求其大小的 2 倍空間,而 ArrayList 是 1.5 倍。
    Vector還有一個子類 Stack

五、Collection 子介面二:Set

1. Set 介面概述

  • Set 介面是 Collection 的子介面,Set 介面沒有提供額外的方法。
  • Set 集合不允許包含相同的元素
  • Set 判斷兩個對象是否相同,使用的是 equals() 方法。

2. Set 實現類之一:HashSet

  • HashSet 是 Set 介面的典型實現,大多數使用 Set 集合時都使用該實現類。

  • HashSet 按 Hash 演算法來存儲集合種的元素,因此具有很好的存取、查找、刪除性能。

  • HashSet 具有以下特點:

    • 不能保證元素的排列順序
    • HashSet 不是執行緒同步的
    • 集合元素可以時null
  • HashSet 集合判斷兩個元素相等的標準:

    通過 hashCode() 方法比較相等,並且兩個對象的 equals() 方法返回值也相等。

  • 對於存放在 Set 容器種的對象,對應的類一定要重寫 equals() 和 hashCode(Object obj) 方法,以實現對象相等規則。即:」相等的對象必須具有相等的散列碼「。

  • HashSet添加元素的過程:

    添加元素a,首先調用元素 a 所在類的 hashcode() 方法,計算元素a的哈希值,通過此哈希值和某種散列函數計算出在 HashSet 底層數組的存放位置(即為:索引位置),判斷此位置上是否已經又元素:

    ​ 如果此位置上沒有其他元素,則元素 a 添加成功(情況1)

    ​ 如果此位置上有其他元素 b (或以鏈表形式存在的多個元素), 則比較元素 a 與元素 b 的哈希值:

    ​ 若哈希值不同:則元素 a 添加成功(情況2)

    ​ 若哈希值相同:需要調用元素 a 所在類的 equals() 方法

    ​ equals() 返回 true :添加失敗

    ​ equals() 返回 false:添加成功(情況3)

    • 對於添加成功的 情況2 和 情況3 而言:元素 a 與已經存在指定索引位置上數據以鏈表的方式存儲。

      • jdk 7 :元素 a 放到數組中,指向原來的元素

      • jdk 8 :原來的元素在數組中,指向元素 a

        (七上八下)

    在這裡插入圖片描述

    底層也是數組,初始容量為16,當如果使用率超過0.75(16*0.75=12)就會擴大容量為原來的2倍。(16擴容為32,依次為64,128….等)

    • 上述提到的散列函數,會與底層數組的長度相計算得到在數組中的下標,並且這種散列函數計算還儘可能保證能均勻存儲元素,越是散列分布,該散列函數設計的越好。
  • 重寫 hashCode() 方法的基本原則

    • 在程式運行時,同時一個對象多次調用 hashCode() 方法應該返回相同的值。
    • 當兩個對象的 equals() 方法比較返回 true 時,這兩個對象的 hashCode() 方法的返回值也相等
    • 對象中作用 equals() 方法比較的 Field,都應該用來計算 hashCode 值
  • 重寫 equals() 方法的基本原則

    • 當一個類有自己獨特的 「邏輯相等」 概念,當該重寫 equals() 的時候,總要重寫 hashCode() ,根據一個類的 equals 方法(重寫後),兩個截然不同的實例有可能在邏輯上時相等的,但是根據 Object.hashCode() 方法,它們僅僅是兩個對象。
    • 因此,違反了 「相等的對象必須具有相等的散列碼」
    • 結論:重寫 equals 方法的時候一般都需要同時重寫 hashCode 方法,通常參與計算 hashCode 的對象的屬性也應該參與到 equals() 中進行計算。
    • 為什麼用 Eclipse/IDEA 重寫的 hashCode 方法,有31這個數字?
      • 選擇係數的時候要盡量選擇最大的係數,因為如果計算出來的 hash 地址越大,所謂的「衝突」就越少。查找起來效率也會提高(減少衝突)
      • 31 只佔用 5 bits,相乘造成數據溢出的概率較小
      • 31 可以由 i * 31 == (i << 5) – 1,現在很多虛擬機裡面都有相關優化。(提高演算法效率)
      • 31 是一個素數,某個數與素數相乘,結果只能被該素數,和被乘數還有 1 來整除(減少衝突)

3. Set 實現類之二:LinkedHashSet

  • LinkedHashSet 是 HashSet 的子類

  • LinkedHashSet 根據元素的 hashCode 值來決定元素的存儲位置,但它同時使用雙向鏈表維護元素的次序,這使得元素看起來是以插入順序保存

  • LinkedHashSet 插入性能略低於 HashSet,但在迭代訪問 Set 里的全部元素時有更好的性能

  • LinkedHashSet 不允許集合元素重複

在這裡插入圖片描述

4. Set 實現類之三:TreeSet

  • TreeSet 時 SortedSet 介面的實現類,TreeSet 可以確保集合元素處於排序狀態。
  • TreeSet 底層使用 紅黑樹 結構存儲數據
  • TreeSet 兩種排序方法:自然排序訂製排序。默認情況下,TreeSet 採用自然排序。
    • 自然排序:
      • 向 TreeSet 中添加元素時,只有第一個元素無須比較 compareTo() 方法,後面添加的元素都會調用 compareTo() 方法進行比較。
      • 因為只有相同類的兩個實例才會比較大小,所以向 TreeSet 中添加的應該時同一個類的對象。
      • 對於 TreeSet 集合而言,它判斷兩個對象是否相等的唯一標準是:兩個對象通過 compareTo(Object obj) 方法比較返回值
      • 當需要把一個對象放入 TreeSet 中,重寫該對象對應的 equals 方法時,應保證該方法與 compareTo(Object obj) 方法有一致的結果:如果兩個對象通過 equals() 方法比較返回 true,則通過 compareTo(Object obj) 方法比較 應返回 0 ,否則可讀性較差。
    • 訂製排序:
      • TreeSet 的自然排序要求元素所屬的類實現 Comparable 介面,如果元素所屬的類沒有實現 Comparable 介面,或不希望按照升序(默認情況)的方式排列元素或希望按照其他屬性大小進行排序,則考慮使用訂製排序。訂製排序,通過 Comparator 介面來實現。需要重寫 compae(T o1, T o2) 方法。
      • 利用 int compare(T o1, T o2) 方法, 比較 o1 和 o2 的大小:如果方法返回正整數,則表示 o1 大於 o2;如果返回 0,表示相等;返回負整數,表示 o1 小於 o2。
      • 要實現訂製排序,需要將實現 Comparator 介面的實例作為參數傳遞給 TreeSet 的構造器
      • 此時,仍然只能向 TreeSet 中添加類型相同的對象。否則發生 ClassCastException 異常。
      • 使用訂製排序判斷兩個元素相等的標準是:通過 Comparator 比較兩個元素返回了 0。

六、Map 介面

1. Map 介面概述

  • Map 與 Collection 並列存在。用於保存具有映射關係的數據: key – value

  • Map 中的 key 和 value 都可以是任何引用類型的數據

  • Map 中的 key 用 Set 來存放,不允許重複,即同一個 Map 對象所對應的類,必須重寫 hashCode() 和 equals() 方法

  • 常用 String 類作為 Map 的 」鍵「

  • key 和 value 之間存在單向一對一關係,即通過指定的 key 總能找到唯一的、確定的 value

  • Map 介面的常用實現類:HashMap、TreeMap、LinkedHashMap 和 Properties。其中,HashMap 是 Map 介面使用頻率最高的實現類。

  • Map結構的理解:

    • Map 中的 key 無序的、不可重複的,使用 Set 存儲所有的 key (key 所在類要重寫 equals() 和 hashCode() )
    • Map 中的 value:無序的、可重複的,使用 Collection 存儲所有的 value (value 所在的類要重寫 equals() )
    • 一個鍵值對:key – value 構成了一個 Entry 對象。
    • Map 中的 Entry :無序的、不可重複的,使用 Set 存儲所有的 Entry。
    • 在這裡插入圖片描述
  • Map 介面常用方法:

    • 在這裡插入圖片描述

2. Map 實現類之一:HashMap

  1. Some Tips

    • HashMap 是 Map介面使用頻率最高的實現類
    • 允許使用 null 鍵和 null 值,與 Hash Set 一樣
    • 所有的 key 構成的集合是 Set :無序的、不可重複的。所以,key 所在的類要重寫 equals() 和 hashCode()
    • 所有的 value 構成的集合是 Collection:無序的、可以重複的。所以,value所在類要重寫 equals()
    • 一個 key-value 構成一個 entry
    • 所有的 entry 構成的集合是 Set:無需的、不可重複的
    • HashMap 判斷兩個 key 相等的標準是:兩個 key 通過 equals() 返回 true
    • HashMap 判斷兩個 value 相等的標準是:兩個 value 通過 equals() 返回 true
  2. JDK 7 及以前

    1. 添加元素的過程(jdk7)

      HashMap map = new HashMap();

      在實例化後,底層創建了長度為 16 的一維數組 Entry[] table

      (可能已經執行多次 put )

      map.put(key1, value1);

      首先,調用 key1 所在類的 hashCode() 計算 key1 哈希值,此哈希值經過某種演算法計算後,得到在 Entry 數組中存放的位置

      如果此位置上的數據為空,此時的 key1-value1 添加成功(情況1)

      如果此位置上的數據不為空,(意味著此位置上存在一個或多個數據(以鏈表形式存在)),比較 key1 和已經存在的一個或多個數據的哈希值:

      ​ 如果 key1 的哈希值與已經存在的數據的哈希值都不相同,key1-value1 添加成功(情況2)

      ​ 如果 key1 的哈希值和已經存在的某一個數據(key2-value2)的哈希值相同,就繼續比較:調用 key1 所在類的 equals(key2):

      ​ 如果 equals() 返回 false:key1-value1添加成功。(情況3)

      ​ 如果 equals() 返回 true:使用 value1 替換 value2

      • (情況 2 與情況 3 :key1-value1 和原來的數據以鏈表的形式存儲)
      • 擴容(resize):轉移到新的容量為原來的兩倍的數組中。
    2. 擴容時機

      當 HashMap 中的元素個數超過數組大小(數組總大小length,不是數組中個數 size ) * loadFactor 時 , 就會 進行數組擴容, loadFactor 的默認值( DEFAULT_LOAD_FACTOR)為 0.75,這是一個折中的取值。也就是說,默認情況下,數組大小(DEFAULT_INITIAL_CAPACITY)為 16,那麼當 HashMap 中元素個數超過16 * 0.75=12(這個值就是程式碼中的 threshold 值,也叫做臨界值)的時候,就把數組的大小擴展為2*16=32,即擴大一倍,然後重新計算每個元素在數組中的位置,而這是一個非常消耗性能的操作,所以如果我們已經預知 HashMap 中元素的個數,那麼預設元素的個數能夠有效的提高 HashMap 的性能。

  3. jdk 8 與jdk 7 的區別

    1. new HashMap() 時:底層沒有創建一個長度為 16 的數組

    2. 首次調用 put() 方法時,底層創建長度為 16 的數組

    3. Entry() 改為 Node[]

    4. JDK 8 形成鏈表結構時,新添加的 key – value 對在鏈表的尾部 (7 上 8 下 )

    5. jdk 7 底層結構只有:數組 + 鏈表

      jdk 8 底層結構:數組 + 鏈表 + 紅黑樹

      當數組的某一個索引位置上的元素以鏈表形式存在的數據個數 > 8 且當前這個數組的長度超過 64 時,這個鏈會變成樹,結點類型由 Node 變成 TreeNode 類型。當然,如果當映射關係被移除後,下次 resize 方法時判斷樹的結點個數低於6個,也會把樹再轉為鏈表。

  4. 部分主要常量

    • DEFAULT_INITIAL_CAPACITY : HashMap的默認容量,16
    • MAXIMUM_CAPACITY : HashMap的最大支援容量,2^30
    • DEFAULT_LOAD_FACTOR :HashMap的默認載入因子:0.75
    • TREEIFY_THRESHOLD :Bucket中鏈表長度大於該默認值,轉化為紅黑樹(8)
    • UNTREEIFY_THRESHOLD :Bucket中紅黑樹存儲的Node小於該默認值,轉化為鏈表
    • MIN_TREEIFY_CAPACITY :桶中的Node被樹化時最小的hash表容量(64)。(當桶中Node的數量大到需要變紅黑樹時,若hash表容量小於MIN_TREEIFY_CAPACITY時,此時應執行resize擴容操作這個MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍。)
    • table :存儲元素的數組,總是2的n次冪
    • entrySet: :存儲具體元素的集
    • size :HashMap中存儲的鍵值對的數量
    • modCount :HashMap擴容和結構改變的次數。
    • threshold擴容的臨界值 = 容量 * 填充因子 :16 * 0.75 = 12
      • 提前擴容,可以讓鏈表少一些
    • loadFactor: :填充因子
  5. LoadFactor(負載因子值、 填充因子值、填充比)的大小,對 HashMap的影響

    • 決定 HashMap 的數據密度
    • LoadFactor 越大密度越大,發生碰撞的幾率越高,數組中的鏈表越容易長,造成查詢或插入時的比較次數增多,性能下降
    • LoadFactor 越小,越容易觸發擴容,數據密度也越小,碰撞幾率越小,數組中的鏈表也越短,查詢和插入時比較的次數也越小,性能會較高。但是會浪費更多的記憶體。而且經常擴容也會影響性能,故建議初始化預設大一點的空間。
    • 設置為 0.7 ~ 0.75 較為常規:此時平均檢索長度接近於常數。

3. Map 實現類之二:LinkedHashMap

  • 是 HashMap 的子類

  • 在 HashMap 存儲結構的基礎上,使用了一對雙向鏈表來記錄添加元素的順序。

  • 與 LinkedHashSet 類似,其可以維護 Map 的迭代順序:迭代順序與 Key-Value 對的插入順序一致。

  • HashMap 中的內部類:Node

    static class Node<K, V> implements Map.Entry<K, V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
    }
    

    LinkedHashMap 中的內部類:Entry

    static class Entry<K, V> extends HashMap.Node<K, V> {
        Entry<K, V> before, after;
        Entry(int hash, K key, V value, Node<K, v> next) {
            super(hash, key, value, next);
        }
    }
    

4. Map 實現類之三:TreeMap

  • TreeMap 存儲 Key-Value 對時,需要根據 key-value 對進行排序
    TreeMap 可以保證所有的 Key-Value 對處於有序狀態。
  • TreeMap 底層使用 紅黑樹 結構存儲數據。
  • TreeMap 的 Key 的排序:
    • 自然排序:TreeMap 的所有的 Key 必須實現 Comparable 介面,而且所有的 Key 應該是同一個類的對象,否則將會拋出 ClassCastException
    • 訂製排序:創建 TreeMap 時,傳入一個 Comparator 對象,該對象負責對 TreeMap 中的所有 key 進行排序。此時不需要 Map 的 Key 實現Comparable 介面
  • TreeMap 判斷兩個 key 相等的標準:兩個 key 通過 compareTo() 方法或者 compare() 方法返回 0 。

5. Map 實現類之四:Hashtable

  • Hashtable 是個功勞的 Map 實現類(JDK 1.0),相比於 HashMap,Hashtable 時執行緒安全的。
  • 實現原理和 HashMap 相同,功能相同。底層都使用哈希表結構,查詢速度快,很多情況下可以相互用。
  • 與 HashMap 不同,Hashtable 不允許使用 null 作為 key 和 value
  • 與HashMap 一樣, Hashtable 也不能保證其中 Key-Value 對的順序。
  • Hashtable 判斷兩個 key 相等、 兩個 value 相等的標準,與 HashMap 一致。

6. Map 實現類之五:Properties

  • Properties 類是 Hashtable 的子類,該對象用於處理屬性文件

  • 由於屬性文件里的 key、value 都是字元串類型,所以 Properties 里的 key 和 value 都是字元串類型

  • 存儲數據時,建議使用 setProperty (String key, String value)方法和 getProperty (String key) 方法

    #jdbc.properties
    name = Tom
    passWord == 123abc
    
    Properties pros = new Properties();
    pros.load(new FileInputStream("jdbc.properties"));
    String user = pros.getProperty("user");
    System.out.println(user)
    

七、Collections 工具類

1. 概述

  • Collections 是一個操作 Set、List 和 Map 等集合的工具類
  • Collections 中提供了一系列靜態的方法對集合元素進行排序、查詢和修改等操作,還提供了對集合對象設置不可變、對集合對象實現同步控制等方法。
  • 排序操作(均為 static 方法)
    • reverse(List) : 反轉 List 中元素的順序
    • shuffle(List) : 對 List 集合元素進行隨機排序
    • sort(List) : 根據元素的自然順序對指定 List 集合元素按升序排序
    • sort(List, Comparator) : 根據指定的 Comparator 產生的順序對 List 集合元素進行排序
    • swap(List, int, int) : 將指定的 List 集合中的 i 處元素和 j 處元素進行交換

2. Collections 常用方法

查找、替換

  • Object max(Collection):根據元素的自然順序,返回給定集合中的最大元素
  • Object max(Collection, Comparator): 根據 Comparator 指定的順序,返回給定集合中的最大元素
  • Object min (Collection)
  • Object min (Collection, Comparator)
  • int frequency(Collection, Object) : 返回指定集合中指定元素的出現次數
  • void copy(List dest, List src) :將 src 中的內容賦值到 dest 中
  • boolean replaceALl(List list, Object oldVal, Object newVal):使用新值替換 List 對象的所有舊值

3. 同步控制

  • Collections 類提供了多個關於同步控制的方法,該方法可使將指定集合包裝成執行緒同步的集合,從而可以解決多執行緒並發訪問集合時的執行緒安全問題。

4. Enumeration

  • Enumeration 介面是 Iterator 迭代器的」古老版本「

    Enumeration stringEnum = new StringTokenizer("a-b*c-d-e-g", "-");
    while(stringEnum.hasMoreElements()) {
    	Object obj = stringEnum.nextElement();
    	System.out.println(obj);
    }
    

八、關於HashSet的一道面試題

  1. hashCode 理解

    package com.exer03;
    
    import java.util.HashSet;
    import java.util.Objects;
    import java.util.Set;
    
    public class Test {
        @org.junit.Test
        public void test() {
            Person p1 = new Person(1001, "AA");
            Person p2 = new Person(1002, "BB");
            Set set = new HashSet();
    
            set.add(p1);
            set.add(p2);
            System.out.println(set);
    
            // ①
            p1.name = "CC";
            set.remove(p1);
            System.out.println(set);
    
            // ②
            set.add(new Person(1001, "CC"));
            System.out.println(set);
    
            // ③
            set.add(new Person(1001, "AA"));
            System.out.println(set);
    
        }
    }
    
    class Person {
        int num;
        String name;
    
        public Person(int num, String name) {
            this.num = num;
            this.name = name;
        }
    
        public Person() {
        }
    
        @Override
        public String toString() {
            return "Person{" +
                    "num=" + num +
                    ", name='" + name + '\'' +
                    '}';
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
    
            Person person = (Person) o;
    
            if (num != person.num) return false;
            return name != null ? name.equals(person.name) : person.name == null;
        }
    
        @Override
        public int hashCode() {
            int result = num;
            result = 31 * result + (name != null ? name.hashCode() : 0);
            return result;
        }
    }
    
    
    // ① 輸出結果:
    /*
    [Person{num=1002, name='BB'}, Person{num=1001, name='CC'}]
    
    解析:關鍵是注意 hashCode 的重寫演算法,p1.name 改為 「CC」 後,remove()方法調用的hashCode()使用的name為CC的hashcode ,該位置和一開始存入p1位置不同(是AA和1001 hash編碼),故remove() 過程實際上結果為,刪除 1001 和 CC hash編碼的位置上的元素。
    */
    
    // ② 輸出結果:
    /*
    [Person{num=1002, name='BB'}, Person{num=1001, name='CC'}, Person{num=1001, name='CC'}]
    
    解析;添加位置是1001和CC編碼位置,p1的name雖然被改為CC,但位置在1001和AA編碼位置。
    向set里add元素的流程:首先靠 hashCode 尋找某位置是否存在元素,若有則調用 equals() 判斷,若無則直接添加。
    */
    
    // ③ 輸出結果:
    /*
    [Person{num=1002, name='BB'}, Person{num=1001, name='CC'}, Person{num=1001, name='CC'}, Person{num=1001, name='AA'}]
    
    解析:雖然1001 和 AA 編碼位置上有元素,但是經過 equals() 返回false,故可以添加成功。
    */