二、java集合框架

集合類存放於java.util包中,主要有3種,set、list(包含Queue)和Map(映射)

1. Collection:Collection 是集合 List、Set、Queue 的最基本的接口。 2. Iterator:迭代器,可以通過迭代器遍歷集合中的數據 3. Map:是映射表的基礎接口。

(1)List

Java 的 List 是非常常用的數據類型。List 是有序的 Collection。Java List 一共三個實現類: 分別是 ArrayList、Vector 和 LinkedList。

ArrayList(數組)

ArrayList 是最常用的 List 實現類,內部是通過數組實現的,它允許對元素進行快速隨機訪問。數 組的缺點是每個元素之間不能有間隔,當數組大小不滿足時需要增加存儲能力,就要將已經有數 組的數據複製到新的存儲空間中。當從 ArrayList 的中間位置插入或者刪除元素時,需要對數組進 行復制、移動、代價比較高。因此,它適合隨機查找和遍歷,不適合插入和刪除。

Vector(數組實現、線程同步)

Vector 與 ArrayList 一樣,也是通過數組實現的,不同的是它支持線程的同步,即某一時刻只有一 個線程能夠寫 Vector,避免多線程同時寫而引起的不一致性,但實現同步需要很高的花費,因此, 訪問它比訪問 ArrayList 慢。

LinkList(鏈表)

LinkedList 是用鏈表結構存儲數據的,很適合數據的動態插入和刪除,隨機訪問和遍歷速度比較 慢。另外,他還提供了 List 接口中沒有定義的方法,專門用於操作表頭和表尾元素,可以當作堆 棧、隊列和雙向隊列使用。

(2)Set

Set 注重獨一無二的性質,該體系集合用於存儲無序(存入和取出的順序不一定相同)元素,值不能重 復。對象的相等性本質是對象 hashCode 值(java 是依據對象的內存地址計算出的此序號)判斷 的,如果想要讓兩個不同的對象視爲相等的,就必須覆蓋 Object 的 hashCode 方法和 equals 方 法。

HashSet(Hash表)

哈希表邊存放的是哈希值。HashSet 存儲元素的順序並不是按照存入時的順序(和 List 顯然不 同) 而是按照哈希值來存的所以取數據也是按照哈希值取得。元素的哈希值是通過元素的 hashcode 方法來獲取的, HashSet 首先判斷兩個元素的哈希值,如果哈希值一樣,接着會比較 equals 方法 如果 equls 結果爲 true ,HashSet 就視爲同一個元素。如果 equals 爲 false 就不是 同一個元素。

哈希值相同 equals 爲 false 的元素是怎麼存儲呢,就是在同樣的哈希值下順延(可以認爲哈希值相 同的元素放在一個哈希桶中)。也就是哈希一樣的存一列。如圖 1 表示 hashCode 值不相同的情 況;圖 2 表示 hashCode 值相同,但 equals 不相同的情況。

HashSet 通過 hashCode 值來確定元素在內存中的位置。一個 hashCode 位置上可以存放多個元 素。

TreeSet(二叉樹)

1.TreeSet()是使用二叉樹的原理對新 add()的對象按照指定的順序排序(升序、降序),每增 加一個對象都會進行排序,將對象插入的二叉樹指定的位置。

2. Integer 和 String 對象都可以進行默認的 TreeSet 排序,而自定義類的對象是不可以的,自 己定義的類必須實現 Comparable 接口,並且覆寫相應的 compareTo()函數,纔可以正常使 用。

3. 在覆寫 compare()函數時,要返回相應的值才能使 TreeSet 按照一定的規則來排序

4. 比較此對象與指定對象的順序。如果該對象小於、等於或大於指定對象,則分別返回負整 數、零或正整數。

LinkHashSet(HashSet+LinkedHashMap)

對於 LinkedHashSet 而言,它繼承與 HashSet、又基於 LinkedHashMap 來實現的。 LinkedHashSet 底層使用 LinkedHashMap 來保存所有元素,它繼承與 HashSet,其所有的方法 操作上又與 HashSet 相同,因此 LinkedHashSet 的實現上非常簡單,只提供了四個構造方法,並 通過傳遞一個標識參數,調用父類的構造器,底層構造一個 LinkedHashMap 來實現,在相關操 作上與父類 HashSet 的操作相同,直接調用父類 HashSet 的方法即可。

(3)Map

HashMap(數組+鏈表+紅黑樹)

HashMap 根據鍵的 hashCode 值存儲數據,大多數情況下可以直接定位到它的值,因而具有很快 的訪問速度,但遍歷順序卻是不確定的。 HashMap 最多隻允許一條記錄的鍵爲 null,允許多條記 錄的值爲 null。HashMap 非線程安全,即任一時刻可以有多個線程同時寫 HashMap,可能會導 致數據的不一致。如果需要滿足線程安全,可以用 Collections 的 synchronizedMap 方法使 HashMap 具有線程安全的能力,或者使用 ConcurrentHashMap。我們用下面這張圖來介紹 HashMap 的結構。

file:///D:/work-doc/java-learning.pdf

ConcurrentHashMap

file:///D:/work-doc/java-learning.pdf

HashTable(線程安全)

Hashtable 是遺留類,很多映射的常用功能與 HashMap 類似,不同的是它承自 Dictionary 類, 並且是線程安全的,任一時間只有一個線程能寫 Hashtable,併發性不如 ConcurrentHashMap, 因爲 ConcurrentHashMap 引入了分段鎖。Hashtable 不建議在新代碼中使用,不需要線程安全 的場合可以用 HashMap 替換,需要線程安全的場合可以用 ConcurrentHashMap 替換。

TreeMap(可排序)

TreeMap 實現 SortedMap 接口,能夠把它保存的記錄根據鍵排序,默認是按鍵值的升序排序, 也可以指定排序的比較器,當用 Iterator 遍歷 TreeMap 時,得到的記錄是排過序的。 如果使用排序的映射,建議使用 TreeMap。 在使用 TreeMap 時,key 必須實現 Comparable 接口或者在構造 TreeMap 傳入自定義的 Comparator,否則會在運行時拋出 java.lang.ClassCastException 類型的異常。 參考:https://www.ibm.com/developerworks/cn/java/j-lo-tree/index.html

LinkHashMap(記錄插入順序)

LinkedHashMap 是 HashMap 的一個子類,保存了記錄的插入順序,在用 Iterator 遍歷 LinkedHashMap 時,先得到的記錄肯定是先插入的,也可以在構造時帶參數,按照訪問次序排序。 參考 1:http://www.importnew.com/28263.html 參考 2:http://www.importnew.com/20386.html#comment-648123

1.ArrayList與LinkedList異同

1. 是否保證線程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保證線程安全;

2. 底層數據結構: Arraylist 底層使用的是Object數組;LinkedList 底層使用的是雙向鏈表數據結構(JDK1.6之 前爲循環鏈表,JDK1.7取消了循環。注意雙向鏈表和雙向循環鏈表的區別:); 詳細可閱讀JDK1.7-LinkedList 循環鏈表優化 

3. 插入和刪除是否受元素位置的影響: ① ArrayList 採用數組存儲,所以插入和刪除元素的時間複雜度受元素 位置的影響。 比如:執行 add(E e) 方法的時候, ArrayList 會默認在將指定的元素追加到此列表的末尾,這種 情況時間複雜度就是O(1)。但是如果要在指定位置 i 插入和刪除元素的話( add(int index, E element) )時 間複雜度就爲 O(n-i)。因爲在進行上述操作的時候集合中第 i 和第 i 個元素之後的(n-i)個元素都要執行向後位/向 前移一位的操作。 ② LinkedList 採用鏈表存儲,所以插入,刪除元素時間複雜度不受元素位置的影響,都是 近似 O(1)而數組爲近似 O(n)。

4. 是否支持快速隨機訪問: LinkedList 不支持高效的隨機元素訪問,而 ArrayList 支持。快速隨機訪問就是通 過元素的序號快速獲取元素對象(對應於 get(int index) 方法)。

5. 內存空間佔用: ArrayList的空間浪費主要體現在在list列表的結尾會預留一定的容量空間,而LinkedList的空間花費則體現在它的每一個元素都需要消耗比ArrayList更多的空間(因爲要存放直接後繼和直接前驅以及數據)。

補充內容:RandomAccess接口

public interface RandomAccess {

}

查看源碼我們發現實際上RandomAccess 接口中什麼都沒有定義。所以,在我看來 RandomAccess 接口不過是一個 標識罷了。標識什麼? 標識實現這個接口的類具有隨機訪問功能。 在binarySearch()方法中,它要判斷傳入的list 是否RamdomAccess的實例,如果是,調用 indexedBinarySearch()方法,如果不是,那麼調用iteratorBinarySearch()方法

ArrayList 實現了 RandomAccess 接口, 而 LinkedList 沒有實現。ArrayList 實現了 RandomAccess 接口,就表明了他具有快速隨機訪問功能。 RandomAccess 接口只是標識,並不 是說 ArrayList 實現 RandomAccess 接口才具有快速隨機訪問功能的!

下面再總結一下list的遍歷方式選擇:

實現了RandomAccess接口的list,優先選擇普通for循環 ,其次foreach,

未實現RandomAccess接口的list, 優先選擇iterator遍歷(foreach遍歷底層也是通過iterator實現的),大 size的數據,千萬不要使用普通for循環

2.ArrayList與Vector區別

Vector類的所有方法都是同步的。可以由兩個線程安全地訪問一個Vector對象、但是一個線程訪問Vector的話代碼要 在同步操作上耗費大量的時間。

Arraylist不是同步的,所以在不需要保證線程安全時時建議使用Arraylist。

3.HashMap的底層實現

JDK1.8 之前 HashMap 底層是 數組和鏈表 結合在一起使用也就是 鏈表散列。HashMap 通過 key 的 hashCode 經 過擾動函數處理過後得到 hash 值,然後通過 (n - 1) & hash 判斷當前元素存放的位置(這裏的 n 指的是數組的 長度),如果當前位置存在元素的話,就判斷該元素與要存入的元素的 hash 值以及 key 是否相同,如果相同的 話,直接覆蓋,不相同就通過拉鍊法解決衝突。

所謂擾動函數指的就是 HashMap 的 hash 方法。使用 hash 方法也就是擾動函數是爲了防止一些實現比較差的 hashCode() 方法 換句話說使用擾動函數之後可以減少碰撞。

JDK 1.8 HashMap 的 hash 方法源碼:

JDK 1.8 的 hash方法 相比於 JDK 1.7 hash 方法更加簡化,但是原理不變。

相比於 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能會稍差一點點,因爲畢竟擾動了 4 次。 所謂 “拉鍊法” 就是:將鏈表和數組相結合。也就是說創建一個鏈表數組,數組中每一格就是一個鏈表。若遇到哈希衝突,則將衝突的值加到鏈表中即可。

JDK1.8之後

相比於之前的版本,JDK1.8之後在解決哈希衝突時有了較大的變化,當鏈表長度大於閾值(默認爲8)時,將鏈表轉化爲紅黑樹,以減少搜索時間。

4. HashMap 和 Hashtable 的區別

1. 線程是否安全: HashMap 是非線程安全的,HashTable 是線程安全的;HashTable 內部的方法基本都經過 synchronized 修飾。(如果你要保證線程安全的話就使用 ConcurrentHashMap 吧!);

2. 效率: 因爲線程安全的問題,HashMap 要比 HashTable 效率高一點。另外,HashTable 基本被淘汰,不要在 代碼中使用它; 3. 對Null key 和Null value的支持: HashMap 中,null 可以作爲鍵,這樣的鍵只有一個,可以有一個或多個鍵 所對應的值爲 null。。但是在 HashTable 中 put 進的鍵值只要有一個 null,直接拋出 NullPointerException。

4. 初始容量大小和每次擴充容量大小的不同 : ①創建時如果不指定容量初始值,Hashtable 默認的初始大小爲 11,之後每次擴充,容量變爲原來的2n+1。HashMap 默認的初始化大小爲16。之後每次擴充,容量變爲原來 的2倍。②創建時如果給定了容量初始值,那麼 Hashtable 會直接使用你給定的大小,而 HashMap 會將其擴充 爲2的冪次方大小(HashMap 中的 tableSizeFor() 方法保證,下面給出了源代碼)。也就是說 HashMap 總 是使用2的冪作爲哈希表的大小,後面會介紹到爲什麼是2的冪次方。 5. 底層數據結構: JDK1.8 以後的 HashMap 在解決哈希衝突時有了較大的變化,當鏈表長度大於閾值(默認爲 8)時,將鏈表轉化爲紅黑樹,以減少搜索時間。Hashtable 沒有這樣的機制。

HasMap 中帶有初始容量的構造函數:

下面這個方法保證了 HashMap 總是使用2的冪作爲哈希表的大小

5.HashMap的長度爲什麼是2的冪次方

爲了能讓 HashMap 存取高效,儘量較少碰撞,也就是要儘量把數據分配均勻。我們上面也講到了過了,Hash 值的 範圍值-2147483648到2147483647,前後加起來大概40億的映射空間,只要哈希函數映射得比較均勻鬆散,一般應 用是很難出現碰撞的。但問題是一個40億長度的數組,內存是放不下的。所以這個散列值是不能直接拿來用的。用之 前還要先做對數組的長度取模運算,得到的餘數才能用來要存放的位置也就是對應的數組下標。這個數組下標的計算 方法是“ (n - 1) & hash ”。(n代表數組長度)。這也就解釋了 HashMap 的長度爲什麼是2的冪次方。

這個算法應該如何設計呢?

我們首先可能會想到採用%取餘的操作來實現。但是,重點來了:“取餘(%)操作中如果除數是2的冪次則等價於與其 除數減一的與(&)操作(也就是說 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 並且 採 用二進制位操作 &,相對於%能夠提高運算效率,這就解釋了 HashMap 的長度爲什麼是2的冪次方。

6. HashMap 多線程操作導致死循環問題

在多線程下,進行 put 操作會導致 HashMap 死循環,原因在於 HashMap 的擴容 resize()方法。由於擴容是新建一 個數組,複製原數據到數組。由於數組下標掛有鏈表,所以需要複製鏈表,但是多線程操作有可能導致環形鏈表。復 制鏈表過程如下:

以下模擬2個線程同時擴容。假設,當前 HashMap 的空間爲2(臨界值爲1),hashcode 分別爲 0 和 1,在散列地 public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor >> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; } 址 0 處有元素 A 和 B,這時候要添加元素 C,C 經過 hash 運算,得到散列地址爲 1,這時候由於超過了臨界值,空 間不夠,需要調用 resize 方法進行擴容,那麼在多線程條件下,會出現條件競爭,模擬過程如下:

線程一:讀取到當前的 HashMap 情況,在準備擴容時,線程二介入

這個過程爲,先將 A 複製到新的 hash 表中,然後接着複製 B 到鏈頭(A 的前邊:B.next=A),本來 B.next=null, 到此也就結束了(跟線程二一樣的過程),但是,由於線程二擴容的原因,將 B.next=A,所以,這裏繼續複製A,讓 A.next=B,由此,環形鏈表出現:B.next=A; A.next=B 注意:jdk1.8已經解決了死循環的問題。

7.HashSet和HashMap的區別

8. ConcurrentHashMap和Hashtable的區別

實現線程安全的方式(重要): ① 在JDK1.7的時候,ConcurrentHashMap(分段鎖) 對整個桶數組進行了 分割分段(Segment),每一把鎖只鎖容器其中一部分數據,多線程訪問容器裏不同數據段的數據,就不會存在鎖 競爭,提高併發訪問率。 到了 JDK1.8 的時候已經摒棄了Segment的概念,而是直接用 Node 數組+鏈表+紅黑 樹的數據結構來實現,併發控制使用 synchronized 和 CAS 來操作。(JDK1.6以後 對 synchronized鎖做了很 多優化) 整個看起來就像是優化過且線程安全的 HashMap,雖然在JDK1.8中還能看到 Segment 的數據結構, 但是已經簡化了屬性,只是爲了兼容舊版本;② Hashtable(同一把鎖) :使用 synchronized 來保證線程安全, 效率非常低下。當一個線程訪問同步方法時,其他線程也訪問同步方法,可能會進入阻塞或輪詢狀態,如使用 put 添加元素,另一個線程不能使用 put 添加元素,也不能使用 get,競爭會越來越激烈效率越低。

9.ConcurrentHashMap線程安全的具體實現方式/底層具體實現

首先將數據分爲一段一段的存儲,然後給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據時,其他段的 數據也能被其他線程訪問。

ConcurrentHashMap 是由 Segment 數組結構和 HashEntry 數組結構組成。

Segment 實現了 ReentrantLock,所以 Segment 是一種可重入鎖,扮演鎖的角色。HashEntry 用於存儲鍵值對數 據

static class Segment extends ReentrantLock implements Serializable { }

一個 ConcurrentHashMap 裏包含一個 Segment 數組。Segment 的結構和HashMap類似,是一種數組和鏈表結 構,一個 Segment 包含一個 HashEntry 數組,每個 HashEntry 是一個鏈表結構的元素,每個 Segment 守護着一個 HashEntry數組裏的元素,當對 HashEntry 數組的數據進行修改時,必須首先獲得對應的 Segment的鎖。

JDK1.8 (上面有示意圖)

ConcurrentHashMap取消了Segment分段鎖,採用CAS和synchronized來保證併發安全。數據結構跟HashMap1.8 的結構類似,數組+鏈表/紅黑二叉樹。 synchronized只鎖定當前鏈表或紅黑二叉樹的首節點,這樣只要hash不衝突,就不會產生併發,效率又提升N倍。

10. 集合框架底層數據結構總結

1. List

Arraylist: Object數組

Vector: Object數組

LinkedList: 雙向鏈表(JDK1.6之前爲循環鏈表,JDK1.7取消了循環) 詳細可閱讀JDK1.7-LinkedList循環鏈表優化。

2. Set

HashSet(無序,唯一): 基於 HashMap 實現的,底層採用 HashMap 來保存元素

LinkedHashSet: LinkedHashSet 繼承與 HashSet,並且其內部是通過 LinkedHashMap 來實現的。有點類 似於我們之前說的LinkedHashMap 其內部是基於 Hashmap 實現一樣,不過還是有一點點區別的。

TreeSet(有序,唯一): 紅黑樹(自平衡的排序二叉樹。)

3.Map

HashMap: JDK1.8之前HashMap由數組+鏈表組成的,數組是HashMap的主體,鏈表則是主要爲了解決哈希 衝突而存在的(“拉鍊法”解決衝突).JDK1.8以後在解決哈希衝突時有了較大的變化,當鏈表長度大於閾值(默 認爲8)時,將鏈表轉化爲紅黑樹,以減少搜索時間

LinkedHashMap: LinkedHashMap 繼承自 HashMap,所以它的底層仍然是基於拉鍊式散列結構即由數組和 鏈表或紅黑樹組成。另外,LinkedHashMap 在上面結構的基礎上,增加了一條雙向鏈表,使得上面的結構可以 保持鍵值對的插入順序。同時通過對鏈表進行相應的操作,實現了訪問順序相關邏輯。詳細可以查看: 《LinkedHashMap 源碼詳細分析(JDK1.8)》

HashTable: 數組+鏈表組成的,數組是 HashMap 的主體,鏈表則是主要爲了解決哈希衝突而存在的

TreeMap: 紅黑樹(自平衡的排序二叉樹)

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章