卑微小吳勵志寫博客第2天

在這裏插入圖片描述

卑微小吳堅持博客第二天,今天想寫寫關於集合的一些知識,集合在java面試中是必問的一塊內容。
話不多說先上圖, Collection和Map的繼承體系

1、Collection和Map的繼承體系

在這裏插入圖片描述
在這裏插入圖片描述

2、ArrayList和LinkList的區別

ArrayList(數組結構):
優點:get和set調用花費常數時間,也就是查詢的速度快;
缺點:新項的插入和現有項的刪除代價昂貴,也就是添加刪除的速度慢
LinkedList(鏈表結構):
優點:新項的插入和和現有項的刪除開銷很小,即添加和刪除的速度快
缺點:對get和set的調用花費昂貴,不適合做查詢

2.1 是否保證線程安全

ArrayList 和 LinkedList 都是不同步的,也就是不保證線程安全

2.2 底層數據結構

Arraylist 底層使用的是Object數組;LinkedList 底層使用的是雙向循環鏈表數據結構

2.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)。
2.4 是否支持快速隨機訪問

LinkedList 不支持高效的隨機元素訪問,而ArrayList 實現了RandmoAccess 接口,所以有隨機訪問功能。快速隨機訪問就是通過元素的序號快速獲取元素對象(對應於 get(int index) 方法)。

2.5 內存空間佔用

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

3、HashMap底層原理

HashMap實際上是一個“鏈表散列”的數據結構,即數組和鏈表的結合體。這是jdk8之前的實現方式,但是在JDK8後對HashMap進行了底層優化,改爲了由數組+鏈表+紅黑樹實現,主要的目的是提高查找效率。
在這裏插入圖片描述
HashMap的主結構類似於一個數組,添加值時通過key確定儲存位置。
每個位置是一個Entry的數據結構,該結構可組成鏈表.當發生衝突時,相同hash值的鍵值對會組成鏈表。這種數組+鏈表的組合形式大部分情況下都能有不錯的性能效果,Java6、7就是這樣設計的。
然而,在極端情況下,一組(比如經過精心設計的)鍵值對都發生了衝突,這時的哈希結構就會退化成一個鏈表,使HashMap性能急劇下降。
所以在Java8中,HashMap的結構實現變爲數組+鏈表+紅黑樹。
在這裏插入圖片描述
可以看出,HashMap底層就是一個數組結構。
數組中的每一項又是一個鏈表,當新建一個HashMap時,就會初始化一個數組.
簡單地說,HashMap 在底層將 key-value 當成一個整體進行處理,這個整體就是一個 Entry 對象。HashMap 底層採用一個 Entry[] 數組來保存所有的 key-value 對,當需要存儲一個 Entry 對象時,會根據hash算法來決定其在數組中的存儲位置,在根據equals方法決定其在該數組位置上的鏈表中的存儲位置;當需要取出一個Entry時,也會根據hash算法找到其在數組中的存儲位置,再根據equals方法從該位置上的鏈表中取出該Entry。

4、HashMap的put()和get()原理

4.1 java7及以前:

get()方法
首先判斷輸入的key是否爲空,如果爲空,從hashmap數組下標爲0的位置獲取值返回。如果不爲空,根據key的值,從hashmap數組中獲取對應的entry對象,判斷這個對象是否爲空,爲空返回null,不爲空返回對應的value值, 獲取value的方法中key爲空和不爲空時的方法裏都先判斷數組中的元素是否爲0 ,如果不爲0,才繼續查找
put()方法
調用put方法的時候首先判斷hashmap數組是否爲空數組,
如果爲空,進行初始化,判斷key的值是否是null,如果是null,把對應的value值存進數組中下標爲0的位置,計算key的hash值,並計算出下標,遍歷下標對應的鏈表,匹配hash值和key的值,如果存在,則覆蓋,返回舊值,如果不存在,新添加一個,返回null
最後判斷數組大小,是否擴容

4.2 java8

get()方法
對輸入的key的值計算hash值,
首先判斷hashmap中的數組是否爲空和數組的長度是否爲0,如果爲空和爲0,則直接放回null
如果不爲空和0,計算key對應的數組下標,判斷對應位置上的第一個node是否滿足條件,如果滿足條件,直接返回
如果不滿足條件,判斷當前node是否是最後一個,如果是,說明不存在key,則返回null
如果不是最後一個,判斷是否是紅黑樹,如果是紅黑樹,則使用紅黑樹的方式獲取對應的key,
如果不是紅黑樹,遍歷鏈表是否有滿足條件的,如果有,直接放回,否則返回null
put()方法
首先計算key的hash值,獲取hashmap中的數組和數組長度,如果數組爲空,初始化計算key的下標
數組對應下標的位置是否爲空,如果爲空,則先添加一個,放在這個下標位置,然後判斷數組內元素是否大於閾值,如果大於,則進行擴容
如果數組對應下標不爲空,則先獲取對應鏈表的第一個值,判斷hash和key是否相同,如果相同,新value替換舊value,返回舊value
如果第一個值key不相同,判斷當前鏈表是否是紅黑樹,如果是紅黑樹,調用紅黑樹鏈表put的方法。如果也不是紅黑樹,遍歷鏈表,判斷當前node是否是最後一個,如果是,說明鏈表中沒有新添加的key,則在最後面新添加一個,然後判斷是否超過閾值(8-1),如果超過,則轉換成紅黑樹。
如果不是最後一個,說明在中間已經存在key了, 把新值賦值給舊值,並返回舊值,判斷是否需要擴容.。

給個例子:

  1. hashmap.put(“lisi”,22); 添加鍵值對時
    根據鍵的哈希碼,經過哈希函數計算得出hash值
    根據hash值計算數組下標 i=4
    訪問數組元素table[i],如果該元素爲null,就創建一個節點保存到table[i]中
  2. hashmap.put(“chen”,20); 添加鍵值對時
    根據鍵的哈希碼,經過哈希函數計算得出hash值
    根據hash值計算數組下標 i=0
    訪問數組元素table[i],如果該元素爲null,就創建一個節點保存到table[i]中
  3. hashmap.put(“wu”,36); 添加鍵值對時
    根據鍵的哈希碼,經過哈希函數計算得出hash值
    根據hash值計算數組下標 i=0
    訪問數組元素table[i],如果該元素不爲null,就遍歷table[i]鏈表中所有的節點,如果所有節點的key與當前key不同,就創建一個節插入到鏈表的尾部
  4. hashmap.put(“chen”,22); 添加鍵值對時
    根據鍵的哈希碼,經過哈希函數計算得出hash值
    根據hash值計算數組下標 i=0
    訪問數組元素table[i],如果該元素不爲null,就遍歷table[i]鏈表中所有的節點,如果所有節點的key與當前鍵equals()相等,就使用新的value值替換節點中的值

在這裏插入圖片描述

HashMap 的resize過程是什麼樣的(擴容)?
HashMap在put的時候會先檢查當前數組的length,如果插入新的值的時候使得length > 0.75f * size(f 爲加載因子,可以在創建hashMap時指定)的話,會將數組進行擴容爲當前容量的2倍。 擴容之後必定要將原有hashMap 中的值拷貝到新容量的hashMap 裏面,HashMap 默認的容量爲16,加載因子爲0.75, 也就是說當HashMap 中Entry的個數超過 16 * 0.75 = 12時, 會將容量擴充爲 16 * 2 = 32,然後重新計算元素在數組中的位置,這是一個非常耗時的操作,所以我們在使用HashMap的時候如果能預先知道Map中元素的大小,預設其大小能夠提升其性能。

 		//HashMap數組擴容
        void resize ( int newCapacity){
            Entry[] oldTable = table;
            int oldCapacity = oldTable.length;
			//如果當前的數組長度已經達到最大值,則不在進行調整
            if (oldCapacity == MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return;
            }
			//根據傳入參數的長度定義新的數組
            Entry[] newTable = new Entry[newCapacity];
			//按照新的規則,將舊數組中的元素轉移到新數組中
            transfer(newTable);
            table = newTable;
			//更新臨界值
            threshold = (int) (newCapacity * loadFactor);
        }
		//舊數組中元素往新數組中遷移
        void transfer (Entry[]newTable){
			//舊數組
            Entry[] src = table;
			//新數組長度
            int newCapacity = newTable.length;
			//遍歷舊數組
            for (int j = 0; j < src.length; j++) {
                Entry e = src[j];
                if (e != null) {
                    src[j] = null;
                    do {
                        Entry next = e.next;
                        int i = indexFor(e.hash, newCapacity);//放在新數組中的index位置
                        e.next = newTable[i];//實現鏈表結構,新加入的放在鏈頭,之前的的數據放在鏈尾
                        newTable[i] = e;
                        e = next;
                    } while (e != null);
                }
            }
        }
    }

這是1.7中的代碼,1.8中引入了紅黑樹的概念,代碼會相對複雜一些。

5、HashMap在擴容的時候爲什麼容量都是原來的2倍,即容量爲2^n

HashMap 在計算數組中key的位置時,使用的算法爲:

/* * Returns index for hash code h. */
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : “length must be a non-zero power of 2”; return h & (length-1); }

即對key的hashcode 與當前數組容量 -1 進行與操作 我們假設有一個容量爲分別爲15 和 16 的hashMap ,有兩個key的hashcode 分別爲4和5,進行indexFor操作之後:

H & (length -1) hash & table.length-1 4 & (15 - 1) 0100 & 1110 = 0100 5 & ( 15 -1 ) 0101 & 1110 = 0100
4 & (16 - 1) 0100 & 1111 = 0100 5 & ( 16 -1 ) 0101 & 1111 = 0101

我們能夠看到在容量爲16時進行indexFor操作之後獲得相同結果的機率要比容量爲15時的機率要小,這樣能夠減少出現hash衝突的機率,從而提高查詢效率。2 ^ n是一個非常神奇的數字。

6、ArrayList、HashSet、HashMap 是線程安全的嗎?如果不是我想要線程安全的集合怎麼辦?

每個方法都沒有加鎖,顯然都是線程不安全的。話又說過來,如果他們安全了也就沒第二問了。 在集合中 Vector 和 HashTable 倒是線程安全的。打開源碼會發現其實就是把各自核心方法添加上了synchronized 關鍵字。
Collections 工具類提供了相關的 API,可以讓上面那 3 個不安全的集合變爲安全的。

1. // 	 	Collections.synchronizedCollection(c) 
2. // 	 	Collections.synchronizedList(list) 
3. // 	 	Collections.synchronizedMap(m) 
4. // 	 	Collections.synchronizedSet(s) 

上面幾個函數都有對應的返回值類型,傳入什麼類型返回什麼類型。打開源碼其實實現原理非常簡單,就是將集合的核心方法添加上了 synchronized 關鍵字。
常用的三個結合都是現成不安全的,只有vector和hashtable是線程安全的。線程安全就在方法上加上了synchronize。可以用Collections中的方法添加上synchronized關鍵字。

7、ConcurrentHashMap 的工作原理及代碼實現

HashTable裏使用的是synchronized關鍵字,這其實是對對象加鎖,鎖住的都是對象整體,當Hashtable的大小增加到一定的時候,性能會急劇下降,因爲迭代時需要被鎖定很長的時間。
ConcurrentHashMap算是對上述問題的優化,其構造函數如下,默認傳入的是16,0.75,16。
在這裏插入圖片描述
在這裏插入圖片描述

ConcurrentHashMap引入了分割(Segment),上面代碼中的最後一行其實就可以理解爲把一個大的Map拆分成N個小的HashTable。
在put方法中,會根據hash(paramK.hashCode())來決定具體存放進哪個Segment,如果查看Segment的put操作,我們會發現內部使用的同步機制是基於lock操作的,這樣就可以對Map的一部分(Segment)進行上鎖,這樣影響的只是將要放入同一個Segment的元素的put操作,保證同步的時候,鎖住的不是整個Map(HashTable就是這麼做的)。相對於HashTable提高了多線程環境下的性能,因此HashTable已經被淘汰了。
ConcurrentHashMap其實就是對hashtable優化了,是現成安全的。它將一個map拆分爲N個小的hashtable,不是隻會鎖住一個小塊,不是全部都鎖住,提高了性能。簡單說就是線程安全,效率優化了。
在這裏插入圖片描述

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