面試資料-JAVA基礎知識-JAVA容器
JAVA容器
容器家族:
容器主要包括 Collection 和 Map 兩種,
Collection 儲存著物件的集合,而 Map 儲存著鍵值對(兩個物件)的對映表。
List,Set,Map 三者的區別
List(對付順序的好幫手): 儲存的元素是有序的、可重複的。
Set(注重獨一無二的性質): 儲存的元素是無序的、不可重複的。
Map(用 Key 來搜尋的專家): 使用鍵值對(kye-value)儲存,類似於數學上的函式 y=f(x),“x”代表 key,"y"代表 value,Key 是無序的、不可重複的,value 是無序的、可重複的,每個鍵最多對映到一個值。
Set是最簡單的一種集合。集合中的物件不按特定的方式排序,並且沒有重複物件。
Set是一個繼承於Collection的介面,Set是一種不包括重複元素的Collection。
Set介面主要實現了兩個實現類:
HashSet: HashSet類按照雜湊演算法來存取集合中的物件,存取速度比較快
TreeSet :TreeSet類實現了SortedSet介面,能夠對集合中的物件進行排序。
Set<Integer> set=new HashSet<> ();
set.add(s1);
set.contains(xxx)
list
List的特徵是其元素以線性方式儲存,集合中可以存放重複物件。
List介面主要實現類包括:
ArrayList() : 代表長度可以改變得陣列。可以對元素進行隨機的訪問,向ArrayList()中插入與刪除元素的速度慢。
LinkedList(): 在實現中採用連結串列資料結構。插入和刪除速度快,訪問速度慢。
List<Integer> aList=new ArrayList<>();
list.add(value) ; // list.add(index,value);
list.get(index)
map
Map 是一種把鍵物件和值物件對映的集合,它的每一個元素都包含一對鍵物件和值物件。
Map與List、Set介面不同,它是由一系列鍵值對組成的集合,提供了key到Value的對映。在Map中它保證了key與value之間的一一對應關係。也就是說一個key對應一個value,所以它不能存在相同的key值,當然value值可以相同。
Map<Integer, Integer> num = new HashMap<>();
num.put(key, value);
num.containsKey(key);
num.get(key); 得到value
String
StringBuilder sb = new StringBuilder();
char c = str.charAt(i);
sb.append(c);
return sb.toString();
StringBuffer str;
return str.toString().replace(" ", "%20");
幾個常用類的區別
1.ArrayList: 元素單個,效率高,多用於查詢
2.Vector: 元素單個,執行緒安全,多用於查詢
3.LinkedList:元素單個,多用於插入和刪除
4.HashMap: 元素成對,元素可為空
5.HashTable: 元素成對,執行緒安全,元素不可為空
二、Vector、ArrayList和LinkedList
大多數情況下,從效能上來說ArrayList最好,但是當集合內的元素需要頻繁插入、刪除時LinkedList會有比較好的表現,但是它們三個效能都比不上陣列,另外Vector是執行緒同步的。所以:
如果能用陣列的時候(元素型別固定,陣列長度固定),請儘量使用陣列來代替List;
如果沒有頻繁的刪除插入操作,又不用考慮多執行緒問題,優先選擇ArrayList;
如果在多執行緒條件下使用,可以考慮Vector;
如果需要頻繁地刪除插入,LinkedList就有了用武之地;
如果你什麼都不知道,用ArrayList沒錯。
三、Collections和Arrays
在 Java集合類框架裡有兩個類叫做Collections(注意,不是Collection!)和Arrays,這是JCF裡面功能強大的工具,但初學者往往會忽視。按JCF文件的說法,這兩個類提供了封裝器實現(Wrapper Implementations)、資料結構演算法和陣列相關的應用。
想必大家不會忘記上面談到的“折半查詢”、“排序”等經典演算法吧,Collections類提供了豐富的靜態方法幫助我們輕鬆完成這些在資料結構課上煩人的工作:
binarySearch:折半查詢。
sort:排序,這裡是一種類似於快速排序的方法,效率仍然是O(n * log n),但卻是一種穩定的排序方法。
reverse:將線性表進行逆序操作,這個可是從前資料結構的經典考題哦!
rotate:以某個元素為軸心將線性表“旋轉”。
swap:交換一個線性表中兩個元素的位置。
……
Collections還有一個重要功能就是“封裝器”(Wrapper),它提供了一些方法可以把一個集合轉換成一個特殊的集合,如下:
unmodifiableXXX:轉換成只讀集合,這裡XXX代表六種基本集合介面:Collection、List、Map、Set、SortedMap和SortedSet。如果你對只讀集合進行插入刪除操作,將會丟擲UnsupportedOperationException異常。
synchronizedXXX:轉換成同步集合。
singleton:建立一個僅有一個元素的集合,這裡singleton生成的是單元素Set,
singletonList和singletonMap分別生成單元素的List和Map。
空集:由Collections的靜態屬性EMPTY_SET、EMPTY_LIST和EMPTY_MAP表示。
set
Set是最簡單的一種集合。集合中的物件不按特定的方式排序,並且沒有重複物件。
Set是一個繼承於Collection的介面,Set是一種不包括重複元素的Collection。
HashSet: HashSet類按照雜湊演算法來存取集合中的物件,存取速度比較快
TreeSet :TreeSet類實現了SortedSet介面,能夠對集合中的物件進行排序。
HashSet和TreeSet的區別
1、TreeSet 是二差樹實現的,Treeset中的資料是自動排好序的,不允許放入null值。
2、HashSet 是雜湊表實現的,HashSet中的資料是無序的,可以放入null,但只能放入一個null,兩者中的值都不能重複,就如資料庫中唯一約束。
3、HashSet要求放入的物件必須實現HashCode()方法,放入的物件,是以hashcode碼作為標識的,而具有相同內容的 String物件,hashcode是一樣,所以放入的內容不能重複。但是同一個類的物件可以放入不同的例項 。
HashSet:
內部的資料結構是雜湊表,是執行緒不安全的。
HashSet中保證集合中元素是唯一的方法:通過物件的hashCode和equals方法來完成物件唯一性的判斷。如果物件的hashCode值不同,則不用判斷equals方法,就直接存到HashSet中。
TreeSet:
是SortedSet介面的唯一實現類,TreeSet可以保證集合元素處於排序狀態。
是執行緒不安全的。
TreeSet存放物件時,是根據物件的compareTo方法比較兩個物件是否相等,並進行比較。
提供一個使用樹結構儲存set介面的實現(紅黑樹演算法)。物件以升序順序儲存,訪問和遍歷的時間很快。
HashSet的實現:
對於HashSet而言,它是基於HashMap實現的,HashSet底層使用HashMap來儲存所有元素,更確切的說,HashSet中的元素,只是存放在了底層HashMap的key上, 而value使用一個static final的Object物件標識。因此HashSet 的實現比較簡單,相關HashSet的操作,基本上都是直接呼叫底層HashMap的相關方法來完成。
Set的實現類的集合物件中不能夠有重複元素,HashSet也一樣他是使用了一種標識來確定元素的不重複,HashSet用一種演算法來保證HashSet中的元素是不重複的,HashSet採用雜湊演算法,底層用陣列儲存資料。預設初始化容量16,載入因子0.75。
list
List的特徵是其元素以線性方式儲存,集合中可以存放重複物件。
List是一個繼承於Collection的介面,即List是集合中的一種。List是有序的佇列,List中的每一個元素都有一個索引;第一個元素的索引值是0,往後的元素的索引值依次+1。和Set不同,List中允許有重複的元素。
ArrayList() :
代表長度可以改變得陣列。可以對元素進行隨機的訪問,向ArrayList()中插入與刪除元素的速度慢。
執行緒不同步,執行緒不安全,但是高效。
LinkedList():
在實現中採用連結串列資料結構。插入和刪除速度快,訪問速度慢。
執行緒不同步,執行緒不安全,但是高效。
vector
與ArrayList實現原理相同,都是通過陣列形式實現的。
只不過vector執行緒安全(方法加synchronized修飾)。
ArrayList和LinkedList和vector區別
對於隨機訪問get和set,ArrayList優於LinkedList,因為LinkedList要移動指標。
對於新增和刪除操作add和remove,LinedList比較佔優勢,因為ArrayList要移動資料。
ArrayList和Vector中,從指定的位置檢索一個物件,或在集合的末尾插入、刪除一個元素的時間是一樣的,時間複雜度都是O(1)。但是如果在其他位置增加或者刪除元素花費的時間是O(n)
LinkedList中,在插入、刪除任何位置的元素所花費的時間都是一樣的,時間複雜度都為O(1),但是他在檢索一個元素的時間複雜度為O(n).
所以如果只是查詢特定位置的元素或只在集合的末端增加移動元素,那麼使用ArrayList或Vector都是一樣的。如果是在指定位置的插入、刪除元素,最好選擇LinkedList
三者都屬於List的子類,方法基本相同。比如都可以實現資料的新增、刪除、定位以及都有迭代器進行資料的查詢,但是每個類在安全,效能,行為上有著不同的表現。
Vector是Java中執行緒安全的集合類,如果不是非要執行緒安全,不必選擇使用,畢竟同步需要額外的效能開銷,底部實現也是陣列來操作,再新增資料時,會自動根據需要建立新陣列增加長度來儲存資料,並拷貝原有陣列資料
ArrayList是應用廣泛的動態陣列實現的集合類,不過執行緒不安全,所以效能要好的多。擴容上:也可以根據需要增加陣列容量,不過與Vector的調整邏輯不同,ArrayList增加50%,而Vector會擴容1倍。這樣,ArrayList就有利於節約記憶體空間。
ArrayList擴容:
擴容時機當陣列的大小大於初始容量的時候(比如初始為10,當新增第11個元素的時候),就會進行擴容。
1、擴容
確定新陣列的長度,確定之後就是把老陣列copy到新陣列中(Arrays.copy()),這樣陣列的擴容就結束了。
2、新增元素
把新元素新增到擴容以後的陣列中。
LinkedList是基於雙向連結串列實現,不需要增加長度,也不是執行緒安全的
Vector與ArrayList在使用的時候,應保證對資料的刪除、插入操作的減少,因為每次對改集合類進行這些操作時,都會使原有資料
進行移動除了對尾部資料的操作,所以非常適合隨機訪問的場合。
LinkedList進行節點的插入、刪除卻要高效的多,但是隨機訪問對於該集合類要慢的多。
map
Map 是一種把鍵物件和值物件對映的集合,它的每一個元素都包含一對鍵物件和值物件。
Map與List、Set介面不同,它是由一系列鍵值對組成的集合,提供了key到Value的對映。在Map中它保證了key與value之間的一一對應關係。也就是說一個key對應一個value,所以它不能存在相同的key值,當然value值可以相同。
傳統集合中只有Hash Table 和 Vector 兩個執行緒安全。
map是鍵值對的集合介面,它的實現類主要包括:HashMap,TreeMap,Hashtable以及LinkedHashMap等。其中這四者的區別如下(簡單介紹):
HashMap:我們最常用的Map,它根據key的HashCode 值來儲存資料,根據key可以直接獲取它的Value,同時它具有很快的訪問速度。HashMap最多隻允許一條記錄的key值為Null(多條會覆蓋);允許多條記錄的Value為 Null。非同步的。HashMap是無序的。
TreeMap: 能夠把它儲存的記錄根據key排序,預設是按升序排序,也可以指定排序的比較器,當用Iterator 遍歷TreeMap時,得到的記錄是排過序的。TreeMap不允許key的值為null。非同步的。(底層紅黑樹)是有序的,按照key值排列,預設是升序。二叉樹的中序遍歷保證了資料的有序性。
Hashtable: 與 HashMap類似,不同的是:key和value的值均不允許為null;它支援執行緒的同步,即任一時刻只有一個執行緒能寫Hashtable,因此也導致了Hashtale在寫入時會比較慢。
LinkedHashMap: 儲存了記錄的插入順序,在用Iterator遍歷LinkedHashMap時,先得到的記錄肯定是先插入的.在遍歷的時候會比HashMap慢。key和value均允許為空,非同步的。底層儲存結構是雜湊表+雙向連結串列,連結串列記錄了新增資料的順序。是有序的。(按照插入元素順序進行排序的;也可以按照訪問順序進行有序排列的)
HashMap
1.7中:陣列+連結串列
java7中先hash位運算(不是取模),然後存入陣列,衝突的話用連結串列繼續存(頭插法,新資料插入這一格的連結串列的頭部!)
put:
判斷當前陣列是否需要初始化。
如果 key 為空,則 put 一個空值進去。
根據 key 計算出 hashcode。
根據計算出的 hashcode 定位出所在桶。
如果桶是一個連結串列則需要遍歷判斷裡面的 hashcode、key 是否和傳入 key 相等,如果相等則進行覆蓋,並返回原來的值。
如果桶是空的,說明當前位置沒有資料存入;新增一個 Entry 物件寫入當前位置。
get:
首先也是根據 key 計算出 hashcode,然後定位到具體的桶中。
判斷該位置是否為連結串列。
不是連結串列就根據 key、key 的 hashcode 是否相等來返回值。
為連結串列則需要遍歷直到 key 及 hashcode 相等時候就返回值。
啥都沒取到就直接返回 null 。
雜湊衝突解決:
HashMap即是採用了鏈地址法,也就是陣列+連結串列的方式。
容量:
HashMap要求容量必須是2的冪:
因為hash計算的時候用的與運算,不是2的指數次冪會陣列越界!
引數:
size:實際儲存的key-value鍵值對的個數
threshold:閾值,當table == {}時,該值為初始容量(初始容量預設為16);當table被填充了,也就是為table分配記憶體空間後,threshold=capacity*loadFactory。
loadFactor:負載因子,代表了table的填充度有多少,預設是0.75。為了減緩雜湊衝突,如果初始桶為16,等到滿16個元素才擴容,某些桶裡可能就有不止一個元素了。預設為0.75,也就是說大小為16的HashMap,到了第13個元素,就會擴容成32。
負載因子值的大小,對HashMap有什麼影響?
負載因子的大小決定了HashMap的資料密度。
負載因子越大,密度越大,發生碰撞的機率越高,陣列中的連結串列越容易長,造成查詢或插入時的比較次數增多,效能會下降。
負載因子越小,就越容易觸發擴容,資料密度也越小,意味著發生碰撞的機率越小,陣列中的連結串列也就越短,查詢和插入時比較的次數也越小,效能會更高。但是會浪費一定的內容空間。而且經常擴容也會影響效能,建議初始化預設大一點的空間。
按照其他語言的參考及研究經驗,會考慮將負載因子設定為0.7~0.75,此時平均檢索長度接近於常數。
HashMap的擴容機制
當前存放新值(注意不是替換已有元素位置時)的時候當前已有元素的個數必須大於等於閾值(容量*負載因子)
(已有元素等於閾值,下一個存放後必然觸發擴容機制)
擴容發生在存放後,即是資料存放後(先存放後擴容),判斷當前存入物件的個數,如果大於閾值則進行擴容。
當發生雜湊衝突並且size大於閾值的時候,需要進行陣列擴容,擴容時,需要新建一個長度為之前陣列2倍的新的陣列,然後將當前的Entry陣列中的元素全部傳輸過去,擴容後的新陣列長度為之前的2倍,所以擴容相對來說是個耗資源的操作。
擴容。
這個過程也叫作rehashing,因為它重建內部資料結構,並呼叫hash方法找到新的bucket位置。
大致分兩步:
1.擴容:容量擴充為原來的兩倍(2 * table.length);
2.移動:對每個節點重新計算雜湊值,重新計算每個元素在陣列中的位置,將原來的元素移動到新的雜湊表中。在前面提到,HashMap 使用 hash%capacity 來確定桶下標。HashMap capacity 為 2 的 n 次方這一特點能夠極大降低重新計算桶下標操作的複雜度。
先按照陣列的順序依次遍歷,在對每個陣列中的連結串列/紅黑樹進行rehash,從連結串列的尾結點起。
1.8中:陣列+連結串列+紅黑樹
put:
1)判斷table是否為空,為空表明這是第一個元素插入,則初始化,使用resize()進行擴容,初始大小預設16
2)根據當前 key執行hash(Object key)得到一個int型別的hash值,然後根據這個hash值就可以找到Node節點的位置了,確定元素存放在哪個桶中
3)如果當前桶為空,表明沒有 Hash 衝突
新生成結點放入桶中(放在陣列中),跳到第6步
4)如果當前桶有值(Hash 衝突),用一個節點e作為返回來記錄當前操作後的節點。
桶中第一個元素(陣列中的結點)的hash值相等,key相等,賦值給node節點e; (新值覆蓋舊值)
hash值不相等,說明key不相等:
如果當前桶為紅黑樹,
那就要按照紅黑樹的方式寫入資料,賦值給node節點e;
如果為連結串列,則遍歷連結串列:
過程中,如果有key滿足相等條件,替換舊值,返回node節點e;
否則插入key-value到連結串列最後(尾插法)並返回node節點e;
插入之後如果當前連結串列長度大於TREEIFY_THRESHOLD(8),轉換成紅黑樹結構。
5)如果 返回的 e != null 就相當於存在相同的 key,還要判斷一個引數onlyIfAbsent(決定要不要新值替換舊值),如果要替換舊值,就將舊值記錄並返回,隨後這裡直接將舊值覆蓋。如果是有值的話,在上面就已經插入了,所以現在也不再需要插入的操作了。
6)最後判斷是否需要進行擴容。實際大小大於閾值則擴容。
get:
首先將 key hash 之後取得所定位的桶。
如果桶為空則直接返回 null 。
否則判斷桶的第一個位置(有可能是連結串列、紅黑樹)的 key 是否為查詢的 key,是就直接返回 value。
如果第一個不匹配,則判斷它的下一個是紅黑樹還是連結串列。
紅黑樹就按照樹的查詢方式返回值。
不然就按照連結串列的方式遍歷匹配返回值。
連結串列和紅黑樹:
為什麼連結串列是8次以後就轉換為紅黑樹?紅黑樹什麼時候轉回連結串列?
當連結串列已經有8個元素了,此時put進第9個元素,先完成第9個元素的put,然後立刻做連結串列轉紅黑樹。
表示若桶中連結串列元素超過8時,會自動轉化成紅黑樹;若桶中元素小於等於6時,樹結構還原成連結串列形式。
原因:
紅黑樹的平均查詢長度是log(n),長度為8,查詢長度為log(8)=3,連結串列的平均查詢長度為n/2,當長度為8時,平均查詢長度為8/2=4,這才有轉換成樹的必要;連結串列長度如果是小於等於6,6/2=3,雖然速度也很快的,但是轉化為樹結構和生成樹的時間並不會太短。
還有選擇6和8的原因是:
中間有個差值7可以防止連結串列和樹之間頻繁的轉換。假設一下,如果設計成連結串列個數超過8則連結串列轉換成樹結構,連結串列個數小於8則樹結構轉換成連結串列,如果一個HashMap不停的插入、刪除元素,連結串列個數在8左右徘徊,就會頻繁的發生樹轉連結串列、連結串列轉樹,效率會很低。
問題1:為什麼不使用二叉查詢樹?
問題主要出現在二叉排序樹在新增元素的時候極端情況下會出現線性結構。
舉例說明:由於二叉排序樹左子樹所有節點的值均小於根節點的特點,如果我們新增的元素都比根節點小,會導致左子樹線性增長,這樣就失去了用樹型結構替換連結串列的初衷,導致查詢時間增長。所以這是不用二叉查詢樹的原因。
問題2:為什麼不使用平衡二叉樹呢?
①紅黑樹不追求"完全平衡",即不像AVL那樣要求節點的 |balFact| <= 1,它只要求部分達到平衡,但是提出了為節點增加顏色,紅黑是用非嚴格的平衡來換取增刪節點時候旋轉次數的降低,任何不平衡都會在三次旋轉之內解決,而AVL是嚴格平衡樹,因此在增加或者刪除節點的時候,根據不同情況,旋轉的次數比紅黑樹要多。
就插入節點導致樹失衡的情況,AVL和RB-Tree都是最多兩次樹旋轉來實現復衡rebalance,旋轉的量級是O(1)
但是,刪除節點導致失衡,AVL需要維護從被刪除節點到根節點root這條路徑上所有節點的平衡,旋轉的量級為O(logN),而RB-Tree最多隻需要旋轉3次實現復衡,只需O(1),所以說RB-Tree刪除節點的rebalance的效率更高,開銷更小!
AVL的結構相較於RB-Tree更為平衡,插入和刪除引起失衡,如2所述,RB-Tree復衡效率更高;當然,由於AVL高度平衡,因此AVL的Search效率更高,紅黑樹查詢效率低。
針對插入和刪除節點導致失衡後的rebalance操作,紅黑樹能夠提供一個比較"便宜"的解決方案,降低開銷,是對search,insert ,以及delete效率的折中,總體來說,RB-Tree的統計效能高於AVL.
故引入RB-Tree是功能、效能、空間開銷的折中結果。
② AVL更平衡,結構上更加直觀,時間效能針對讀取而言更高;維護稍慢,空間開銷較大。
③ 紅黑樹,讀取略遜於AVL,維護強於AVL,空間開銷與AVL類似,內容極多時略優於AVL,維護優於AVL。
基本上主要的幾種平衡樹看來,紅黑樹有著良好的穩定性和完整的功能,效能表現也很不錯,綜合實力強,在諸如STL的場景中需要穩定表現。
hashmap的key是否可以為null?
可以的,hashtable不可以。
計算雜湊的方法見下方,自動轉為0。
儲存時存在0的桶裡。
hash的計算方法:
//重新計算雜湊值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
//key如果是null 新hashcode是0 ;否則 計算新的hashcode
}
通過hash計算出來的值將會使用indexFor方法找到它應該所在的table下標:
static int indexFor(int h, int length) {
return h & (length-1);
}
為什麼要無符號右移16位後做異或運算?
將h無符號右移16為相當於將高區16位移動到了低區的16位,再與原hashcode做異或運算,可以將高低位二進位制特徵混合起來。
從上文可知高區的16位與原hashcode相比沒有發生變化,低區的16位發生了變化。
原因:
我們都知道重新計算出的新雜湊值在後面將會參與hashmap中陣列槽位(桶)的計算,計算公式:(n - 1) & hash,【這裡n是總長度,2的倍數】假如這時陣列槽位有16個,則槽位計算如下:
仔細觀察上文不難發現,高區的16位很有可能會被陣列槽位數的二進位制碼鎖遮蔽(相當於沒用上高16位的那些數),如果我們不做剛才移位異或運算,那麼在計算槽位時將丟失高區特徵。
也許你可能會說,即使丟失了高區特徵不同hashcode也可以計算出不同的槽位來,但是細想當兩個雜湊碼很接近時,那麼這高區的一點點差異就可能導致一次雜湊碰撞,所以這也是將效能做到極致的一種體現。
使用異或運算的原因:
^ : 位異或 第一個運算元的的第n位於第二個運算元的第n位相反,那麼結果的第n為也為1,否則為0
0^0=0, 1^0=1, 0^1=1, 1^1=0
& : 與運算 第一個運算元的的第n位於第二個運算元的第n位如果都是1,那麼結果的第n為也為1,否則為0
0&0=0, 0&1=0, 1&0=0, 1&1=1 eg:用if ((a & 1) == 0) 代替 if (a % 2 == 0)來判斷a是不是偶數。
| : 或運算 第一個運算元的的第n位於第二個運算元的第n位 只要有一個是1,那麼結果的第n為也為1,否則為0
0|0=0, 0|1=1, 1|0=1, 1|1=1
異或運算能更好的保留各部分的特徵,如果採用&運算計算出來的值會向0靠攏,採用|運算計算出來的值會向1靠攏
為什麼槽位數必須使用2^n?
為了讓雜湊後的結果更加均勻
假如槽位數不是16,而是17,則槽位計算公式變成:(17 - 1) & hash
從上文可以看出,計算結果將會大大趨同,hashcode參加&運算後被更多位的0遮蔽,計算結果只剩下兩種0和16,這對於hashmap來說是一種災難
上面提到的所有問題,最終目的還是為了讓雜湊後的結果更均勻的分部,減少雜湊碰撞,提升hashmap的執行效率
hashmap為什麼執行緒不安全?
resize擴容死迴圈:(這個問題只針對1.7版本以前的,因為當時是頭插法)
我們都知道HashMap初始容量大小為16。1.7版本頭插法。
while(null != e) {
Entry<K,V> next = e.next; //執行緒1執行到這裡被排程掛起了
e.next = newTable[i];
newTable[i] = e;
e = next;
}
假設這裡有兩個執行緒同時執行了put()操作,並進入了transfer()環節:
Entry<K,V> next = e.next; //執行緒1執行到這裡被排程掛起了
這時候2搞完是這樣的:
然後執行緒1被喚醒了:
1)執行e.next = newTable[i],於是 key(3)的 next 指向了執行緒1的新 Hash 表,因為新 Hash 表為空,所以e.next = null,
2)執行newTable[i] = e,所以執行緒1的新 Hash 表第一個元素指向了執行緒2新 Hash 表的 key(3)。好了,e 處理完畢。
3)執行e = next,將 e 指向 next,所以新的 e 是 key(7)
然後該執行 key(3)的 next 節點 key(7)了:
1)現在的 e 節點是 key(7),首先執行Entry<K,V> next = e.next,那麼 next 就是 key(3)了
2)執行e.next = newTable[i],於是key(7) 的 next 就成了 key(3)
3)執行newTable[i] = e,那麼執行緒1的新 Hash 表第一個元素變成了 key(7)
4)執行e = next,將 e 指向 next,所以新的 e 是 key(3)
然後又該執行 key(7)的 next 節點 key(3)了:
1)現在的 e 節點是 key(3),首先執行Entry<K,V> next = e.next,那麼 next 就是 null
2)執行e.next = newTable[i],於是key(3) 的 next 就成了 key(7)
3)執行newTable[i] = e,那麼執行緒1的新 Hash 表第一個元素變成了 key(3)
4)執行e = next,將 e 指向 next,所以新的 e 是 key(7)
很明顯,環形連結串列出現了!!當然,現在還沒有事情,因為下一個節點是 null,所以transfer()就完成了,等put()的其餘過程搞定後,HashMap 的底層實現就是執行緒1的新 Hash 表了。
HashMap是頭插法還是尾插法?
JDK8以前是頭插法,JDK8開始是尾插法
頭插法的原因:
認為後來的值被查詢的可能性更大一點,提升查詢的效率。
為什麼要從頭插法改成尾插法?
A. 因為頭插法會造成死鏈,為了安全,防止環化。擴容的時候會有循壞鏈。
假如擴容時使用了單鏈表的頭插入方式,同一位置上新元素總會被放在連結串列的頭部位置,在舊陣列中同一條Entry鏈上的元素,通過重新計算索引位置後,有可能被放到了新陣列的不同位置上。
B.JDK7用頭插是考慮到了一個所謂的熱點資料的點(新插入的資料可能會更早用到),但這其實是個偽命題,因為JDK7中rehash的時候,舊連結串列遷移新連結串列的時候,如果在新表的陣列索引位置相同,則連結串列元素會倒置(就是因為頭插) 所以最後的結果 還是打亂了插入的順序 所以總的來看支撐JDK7使用頭插的這點原因也不足以支撐下去了 所以就乾脆換成尾插 一舉多得
但是,JDK1.8仍然存在多執行緒不安全的情況(put操作):
if ((p = tab[i = (n - 1) & hash]) == null) // 如果沒有hash碰撞則直接插入元素
tab[i] = newNode(hash, key, value, null);
如果沒有hash碰撞則會直接插入元素。如果執行緒A和執行緒B同時進行put操作,剛好這兩條不同的資料hash值一樣,並且該位置資料為null,所以這執行緒A、B都會進入第6行程式碼中。假設一種情況,執行緒A進入後還未進行資料插入時掛起,而執行緒B正常執行,從而正常插入資料,然後執行緒A獲取CPU時間片,此時執行緒A不用再進行hash判斷了,問題出現:執行緒A會把執行緒B插入的資料給覆蓋,發生執行緒不安全。
總結
首先HashMap是執行緒不安全的,其主要體現:
#1.在jdk1.7中,在多執行緒環境下,擴容時會造成環形鏈或資料丟失。
#2.在jdk1.8中,在多執行緒環境下,會發生資料覆蓋的情況。
HashMap的時間複雜度問題:
put操作的流程:
第一步:key.hashcode(),時間複雜度O(1)。
第二步:找到桶以後,判斷桶裡是否有元素,如果沒有,直接new一個entey節點插入到陣列中。時間複雜度O(1)。
第三步:如果桶裡有元素,並且元素個數小於6,則呼叫equals方法,比較是否存在相同名字的key,不存在則new一個entry插入都連結串列尾部。時間複雜度O(1)+O(n)=O(n)。
第四步:如果桶裡有元素,並且元素個數大於6,則呼叫equals方法,比較是否存在相同名字的key,不存在則new一個entry插入都連結串列尾部。時間複雜度O(1)+O(logn)=O(logn)。紅黑樹查詢的時間複雜度是logn。
通過上面的分析,我們可以得出結論,HashMap新增元素的時間複雜度是不固定的,可能的值有O(1)、O(logn)、O(n)。
jdk1.7的concurrentHashmap
陣列+連結串列:
唯一的區別就是其中的核心資料如 value ,以及連結串列都是 volatile 修飾的,保證了獲取時的可見性。
容器裡有多把鎖,每一把鎖用於鎖容器其中一部分資料,那麼當多執行緒訪問容器裡不同資料段的資料時,執行緒間就不會存在鎖競爭,從而可以有效的提高併發訪問效率,這就是ConcurrentHashMap所使用的鎖分段技術,首先將資料分成一段一段的儲存,然後給每一段資料配一把鎖,當一個執行緒佔用鎖訪問其中一個段資料的時候,其他段的資料也能被其他執行緒訪問。有些方法需要跨段,按順序鎖定所有段,操作完畢後,又按順序釋放所有段的鎖。
原理上來說:ConcurrentHashMap 採用了分段鎖技術,其中 Segment 繼承於 ReentrantLock。不會像 HashTable 那樣不管是 put 還是 get 操作都需要做同步處理,理論上 ConcurrentHashMap 支援 CurrencyLevel (Segment 陣列數量)的執行緒併發。每當一個執行緒佔用鎖訪問一個 Segment 時,不會影響到其他的 Segment。
put要加分段鎖。
當執行put方法插入資料時,根據key的hash值,在Segment陣列中找到相應的位置,如果相應位置的Segment還未初始化,則通過CAS進行賦值,接著執行Segment物件的put方法通過加鎖機制插入資料,實現如下:
場景:執行緒A和執行緒B同時執行相同Segment物件的put方法
1、執行緒A執行tryLock()方法成功獲取鎖,則把HashEntry物件插入到相應的位置;
2、執行緒B獲取鎖失敗,則執行scanAndLockForPut()方法,在scanAndLockForPut方法中,會通過重複執行tryLock()方法嘗試獲取鎖,在多處理器環境下,重複次數為64,單處理器重複次數為1,當執行tryLock()方法的次數超過上限時,則執行lock()方法掛起執行緒B;
3、當執行緒A執行完插入操作時,會通過unlock()方法釋放鎖,接著喚醒執行緒B繼續執行。
get 方法是非常高效的,因為整個過程都不需要加鎖(由於 HashEntry 中的 value 屬性是用 volatile 關鍵詞修飾的,保證了記憶體可見性,所以每次獲取時都是最新值)。
如上圖所示,ConcurrentHashMap預設分成了16個segment,每個Segment都對應一個Hash表,且都有獨立的鎖。所以這樣就可以每個執行緒訪問一個Segment,就可以並行訪問了,從而提高了效率。這就是鎖分段。
jdk1.8的concurrentHashmap
1.8仍然考慮到連結串列查詢效率太低,陣列+連結串列+紅黑樹
拋棄了原有的 Segment 分段鎖,而採用了 CAS + synchronized 來保證併發安全性。
hash演算法:
int hash = spread(key.hashCode());
static final int spread(int h) {return (h ^ (h >>> 16)) & HASH_BITS;}
與HashMap類似這裡計算hash的方式和hashmap基本相同,都是右移16位後異或,只不過不允許key為null。
(key和value都不允許為null)
定位索引:
int index = (n - 1) & hash // n為bucket的個數
put:
檢查傳入的引數, ConcurrentHashmap不允許null值作key,但是value可以。
判斷是否需要進行初始化。
f 即為當前 key 定位出的 Node,
1)如果為空:-- 沒有發生hash衝突
表示當前位置可以寫入資料,利用 CAS 嘗試寫入,失敗則自旋保證成功。
2)如果當前map正在擴容,
當前位置的f.hash == MOVED, 則跟其他執行緒一起協助他們進行擴容。
3)如果都不滿足(不為空,出現hash衝突):
則利用 synchronized 鎖只鎖住當前節點(桶)寫入資料。
連結串列的話插入到尾巴,紅黑樹的話加入紅黑樹再平衡。
如果是連結串列的話,數量大於 TREEIFY_THRESHOLD 則要轉換為紅黑樹。
4)統計節點個數,檢查是否需要resize擴容,擴容兩倍
get:
(檢索操作不用加鎖,get方法是非阻塞的)
根據計算出來的 hashcode 定址,如果就在桶上那麼直接返回值。
如果是紅黑樹那就按照樹的方式獲取值。
就不滿足那就按照連結串列的方式遍歷獲取值。
equals()方法
Object類中的equals方法和“==”是一樣的,沒有區別,即倆個物件的比較是比較他們的棧記憶體中儲存的記憶體地址。而String類,Integer類等等一些類,是重寫了equals方法,才使得equals和“==不同”,他們比較的是值是不是相等。所以,當自己建立類時,自動繼承了Object的equals方法,要想實現不同的等於比較,必須重寫equals方法。
當我們new一個物件時,將在記憶體里加載一份它自己的記憶體,而不是共用!對於static修飾的變數和方法則儲存在方法區中,只加載一次,不會再多copy一份記憶體。所以我們在判斷倆個物件邏輯上是否相等,即物件的內容是否相等不能直接使用繼承於Object類的equals()方法,我們必須得重寫equals()方法,改變這個方法預設的實現。
重寫equals方法:先判斷比較物件是否為null—>判斷比較物件是否為要比較類的例項—–>比較倆個成員變數是否完全相等。
hashcode()方法:
當Set接收一個元素時根據該物件的記憶體地址算出hashCode,看它屬於哪一個區間,再這個區間裡呼叫equeals方法。這裡需要注意的是:當倆個物件的hashCode值相同的時候,Hashset會將物件儲存在同一個位置,但是他們equals返回false,所以實際上這個位置採用鏈式結構來儲存多個物件。
當你把物件加入HashSet時,HashSet 會先計算物件的hashcode值來判斷物件加入的位置,同時也會與其他加入的物件的 hashcode 值作比較,如果沒有相符的 hashcode,HashSet 會假設物件沒有重複出現。但是如果發現有相同 hashcode 值的物件,這時會再呼叫equals()方法來檢查 hashcode 相等的物件是否真的相同。如果兩者相同,HashSet 就不會讓加入操作成功。
但一個面臨問題:若兩個物件equals相等,但不在一個區間,因為hashCode的值在重寫之前是對記憶體地址計算得出,所以根本沒有機會進行比較,會被認為是不同的物件。所以Java對於eqauls方法和hashCode方法是這樣規定的:
1. 如果兩個物件相同,那麼它們的hashCode值一定要相同。也告訴我們重寫equals方法,一定要重寫hashCode方法,也就是說hashCode值要和類中的成員變數掛上鉤,物件相同–>成員變數相同—->hashCode值一定相同。
2. 如果兩個物件的hashCode相同,它們並不一定相同,這裡的物件相同指的是用eqauls方法比較。
注:如果我們將物件的屬性值參與了hashCode的運算中,在進行刪除的時候,就不能對其屬性值進行修改,否則會出現嚴重的問題。
為什麼在重寫equals()方法時,一般都會重寫HashCode()方法?
重寫equals()方法主要是為了方便比較兩個物件內容是否相等。
hashCode()方法用於返回呼叫該方法的物件的雜湊碼值,此方法將返回整數形式的雜湊碼值。
在JAVA的集合中,每次要對全部元素進行equal的比較,挨個比較太麻煩了。因此採用hashcode先進行比較,相同的情況下,在對裡面所有元素進行比較。
是為了提高效率,採取重寫hashcode方法,先進行hashcode比較,如果不同,那麼就沒必要在進行equals的比較了,這樣就大大減少了equals比較的次數,這對比需要比較的數量很大的效率提高是很明顯的,一個很好的例子就是在集合中的使用。set集合是不能重複的,那麼怎麼能保證不能被放入重複的元素呢,但靠equals方法一樣比較的話,太麻煩, java就採用了hash表,利用雜湊演算法(也叫雜湊演算法),只要看對應的hashcode地址上是否有值,有的話就用equals比較,如果沒有則直接插入,只要就大大減少了equals的使用次數,執行效率就大大提高了。
總結:
1.使用hashcode方法提前校驗,可以避免每一次比對都呼叫equals方法,提高效率
2.保證是同一個物件,如果重寫了equals方法,而沒有重寫hashcode方法,會出現equals相等的物件,hashcode不相等的情況,重寫hashcode方法就是為了避免這種情況的出現。
hashtable與hashmap區別:
0底層資料結構不同
Hashtable同樣是基於雜湊表實現的,同樣每個元素是一個key-value對,其內部也是通過單鏈表解決衝突問題,容量不足(超過了閥值)時,同樣會自動增長。
HashMap採用雜湊表+連結串列+紅黑樹。
1 執行緒安全性不同
Hashtable是執行緒安全的,它的每個方法中都加入了Synchronize方法。在多執行緒併發的環境下,可以直接使用Hashtable,不需要自己為它的方法實現同步HashMap不是執行緒安全的,在多執行緒併發的環境下,可能會產生死鎖等問題。具體的原因在下一篇文章中會詳細進行分析。使用HashMap時就必須要自己增加同步處理,雖然HashMap不是執行緒安全的,但是它的效率會比Hashtable要好很多。這樣設計是合理的。在我們的日常使用當中,大部分時間是單執行緒操作的。HashMap把這部分操作解放出來了。當需要多執行緒操作的時候可以使用執行緒安全的ConcurrentHashMap。ConcurrentHashMap雖然也是執行緒安全的,但是它的效率比Hashtable要高好多倍。因為ConcurrentHashMap使用了分段鎖,並不對整個資料進行鎖定。
2. 計算hash值的方法不同
為了得到元素的位置,首先需要根據元素的 KEY計算出一個hash值,然後再用這個hash值來計算得到最終的位置。Hashtable直接使用物件的hashCode。hashCode是JDK根據物件的地址或者字串或者數字算出來的int型別的數值。然後再使用除留餘數發來獲得最終的位置。
Hashtable在計算元素的位置時需要進行一次除法運算,而除法運算是比較耗時的。HashMap為了提高計算效率,將雜湊表的大小固定為了2的冪,這樣在取模預算時,不需要做除法,只需要做位運算。位運算比除法的效率要高很多。HashMap的效率雖然提高了,但是hash衝突卻也增加了。因為它得出的hash值的低位相同的概率比較高,而計算位運算為了解決這個問題,HashMap重新根據hashcode計算hash值後,又對hash值做了一些運算來打散資料。使得取得的位置更加分散,從而減少了hash衝突。當然了,為了高效,HashMap只做了一些簡單的位處理。從而不至於把使用2 的冪次方帶來的效率提升給抵消掉。
3、HashMap是繼承自AbstractMap類,而HashTable是繼承自Dictionary類。不過它們都實現了同時實現了map、Cloneable(可複製)、Serializable(可序列化)這三個介面。
4、key和value是否允許null值
其中key和value都是物件,並且不能包含重複key,但可以包含重複的value。
Hashtable中,key和value都不允許出現null值。但是如果在Hashtable中有類似put(null,null)的操作,編譯同樣可以通過,因為key和value都是Object型別,但執行時會丟擲NullPointerException異常,這是JDK的規範規定的。
原始碼:
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
}
可以看出,當時不允許是因為希望每個 key 都會實現 hashCode 和 equals 方法。而 null 顯然沒有。後來,大家都意識到 null 值的重要性,所以 HashMap 允許 null 值的 key 和 value。當 key 為 null 時,HashMap 將會把 key-value pair 存放在一個特殊的位置,比如 第一個槽位 裡。
HashMap中,key和value都可以為null值,這樣的鍵只有一個;可以有一個或多個鍵所對應的值為null。當get()方法返回null值時,可能是 HashMap中沒有該鍵,也可能使該鍵所對應的值為null。因此,在HashMap中不能由get()方法來判斷HashMap中是否存在某個鍵, 而應該用containsKey()方法來判斷。
5 內部實現使用的陣列初始化和擴容方式不同
HashTable在不指定容量的情況下的預設容量為11,而HashMap為16,Hashtable不要求底層陣列的容量一定要為2的整數次冪,而HashMap則要求一定為2的整數次冪。
Hashtable擴容時,將容量變為原來的2倍加1,而HashMap擴容時,將容量變為原來的2倍。
Hashtable和HashMap它們兩個內部實現方式的陣列的初始大小和擴容的方式。HashTable中hash陣列預設大小是11,增加的方式是 old*2+1。
HashMap和HashTable和concurrentHashMap異同點:
相同點:
1.底層資料結構相同,都為陣列+連結串列( ConcurrentHashMap為陣列+連結串列)
2.key都不能重複
3.插入元素不能保證有序
4.都是通過key進行雜湊
不同點:
1.安全性問題
HashMap是非執行緒安全的集合,若想讓其在多執行緒下具有安全性:使用集合工具類collection.SyncnizedMap;
HashTable中的方法都有Synchronized修飾,多執行緒訪問時執行緒安全;
ConcurrentHashMap中通過分段鎖機制保證執行緒安全
2.繼承關係:
hashMap和ConcurrentHashMap繼承AbstractMap
hashTable繼承Dictionary
3.擴容方式:
HashMap和 ConcurrentHashMap為2倍(2table.length)
HashTable為2的倍數+1 [(2table.length)+1]
4.null值:
HashTable的鍵值不能為null
hashMap鍵值可以為null
ConcurrentHashMap鍵可以為null,值不能為null。
5.預設值:
hashTable陣列預設大小11
hashMap陣列預設大小16
ConcurrentHashMap陣列預設大小16
6.hash演算法不同:
7.效率不同:
hashMap在單執行緒下效率高
hashTable和ConcurrentHashMap在多執行緒下效率高(ConcurrentHashMap更高)
LinkedHashMap:
LinkedHashMap是繼承於HashMap,是基於HashMap和雙向連結串列來實現的。
HashMap無序;LinkedHashMap有序,可分為插入順序和訪問順序兩種。如果是訪問順序,那put和get操作已存在的Entry時,都會把Entry移動到雙向連結串列的表尾(其實是先刪除再插入)。
LinkedHashMap存取資料,還是跟HashMap一樣使用的Entry[]的方式,雙向連結串列只是為了保證順序。
LinkedHashMap是執行緒不安全的。
1.LinkedHashMap 繼承自 HashMap,是基於HashMap和雙向連結串列來實現的。該結構由陣列和連結串列+紅黑樹 在此基礎上LinkedHashMap 增加了一條雙向連結串列,保持遍歷順序和插入順序一致的問題。
2.在實現上,LinkedHashMap 很多方法直接繼承自 HashMap(比如put remove方法就是直接用的父類的),僅為維護雙向連結串列覆寫了部分方法(get()方法是重寫的)。
3.LinkedHashMap使用的鍵值對節點是Entity 他繼承了hashMap 的Node,並新增了兩個引用,分別是 before 和 after。這兩個引用的用途不難理解,也就是用於維護雙向連結串列.
4.連結串列的建立過程是在插入鍵值對節點時開始的,初始情況下,讓 LinkedHashMap 的 head 和 tail 引用同時指向新節點,連結串列就算建立起來了。隨後不斷有新節點插入,通過將新節點接在 tail 引用指向節點的後面,即可實現連結串列的更新
5.LinkedHashMap 允許使用null值和null鍵, 執行緒是不安全的,雖然底層使用了雙線連結串列,但是增刪相快了。因為他底層的Entity 保留了hashMap node 的next 屬性。
6.如何實現迭代有序?
重新定義了陣列中儲存的元素Entry(繼承於HashMap.node),該Entry除了儲存當前物件的引用外,還儲存了其上一個元素before和下一個元素after的引用,從而在雜湊表的基礎上又構成了雙向連結列表。仍然保留next屬性,所以既可像HashMap一樣快速查詢,
用next獲取該連結串列下一個Entry,也可以通過雙向連結,通過after完成所有資料的有序迭代.
7.竟然inkHashMap 的put 方法是直接呼叫父類hashMap的,但在 HashMap 中,put 方法插入的是 HashMap 內部類 Node 型別的節點,該型別的節點並不具備與 LinkedHashMap 內部類 Entry 及其子型別節點組成連結串列的能力。那麼,LinkedHashMap 是怎樣建立連結串列的呢?
雖然linkHashMap 呼叫的是hashMap中的put 方法,但是linkHashMap 重寫了,了一部分方法,其中就有 newNode(int hash, K key, V value, Node<K,V> e)linkNodeLast(LinkedHashMap.Entry<K,V> p)
這兩個方法就是 第一個方法就是新建一個 linkHasnMap 的Entity 方法,而 linkNodeLast 方法就是為了把Entity 接在連結串列的尾部。
8.連結串列節點的刪除過程
與插入操作一樣,LinkedHashMap 刪除操作相關的程式碼也是直接用父類的實現,但是LinkHashMap 重寫了removeNode()方法 afterNodeRemoval()方法,該removeNode方法在hashMap 刪除的基礎上有呼叫了afterNodeRemoval 回撥方法。完成刪除。
刪除的過程並不複雜,上面這麼多程式碼其實就做了三件事:
根據 hash 定位到桶位置
遍歷連結串列或呼叫紅黑樹相關的刪除方法
從 LinkedHashMap 維護的雙鏈表中移除要刪除的節點
TreeMap
1.TreeMap實現了SortedMap介面,保證了有序性。預設的排序是根據key值進行升序排序,也可以重寫comparator方法來根據value進行排序具體取決於使用的構造方法。不允許有null值null鍵。TreeMap是執行緒不安全的。
2.TreeMap基於紅黑樹(Red-Black tree)實現。TreeMap的基本操作 containsKey、get、put 和 remove 的時間複雜度是 log(n) 。