# JAVA集合基礎

[吃貓的大魚](https://blog.csdn.net/qq_32979219)  已於 2022-01-29 15:18:30 修改  120   收藏
分類專欄: [集合](https://blog.csdn.net/qq_32979219/category_11041859.html) [hashMap](https://blog.csdn.net/qq_32979219/category_11041861.html) [面試](https://blog.csdn.net/qq_32979219/category_11068351.html) 文章標簽: [java](https://so.csdn.net/so/search/s.do?q=java&t=blog&o=vip&s=&l=&f=&viparticle=) [hashmap](https://so.csdn.net/so/search/s.do?q=hashmap&t=blog&o=vip&s=&l=&f=&viparticle=) [數據結構](https://so.csdn.net/so/search/s.do?q=%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84&t=blog&o=vip&s=&l=&f=&viparticle=)
於 2021-05-07 23:20:43 首次發布
版權聲明:本文為博主原創文章,遵循 [CC 4.0 BY-SA](http://creativecommons.org/licenses/by-sa/4.0/) 版權協議,轉載請附上原文出處鏈接和本聲明。
本文鏈接:[https://blog.csdn.net/qq\_32979219/article/details/116432407](https://blog.csdn.net/qq_32979219/article/details/116432407)
版權
[ 集合 同時被 3 個專欄收錄](https://blog.csdn.net/qq_32979219/category_11041859.html "集合")
1 篇文章 0 訂閱
訂閱專欄
[ hashMap](https://blog.csdn.net/qq_32979219/category_11041861.html "hashMap")
1 篇文章 0 訂閱
訂閱專欄
[ 面試](https://blog.csdn.net/qq_32979219/category_11068351.html "面試")
25 篇文章 0 訂閱
訂閱專欄
# 前言
最近復習了一下數據結構,對數據結構有了更深了解,回頭再來看一下集合相關知識就感覺豁然開朗,面試中集合也是必考題,便有了這篇集合總結,其中HashMap(包括部分源碼分析)篇幅大概有6000+字,希望大家能耐心看完,看完后多少都會有一些收獲。
# 數據結構
先來簡單復習一下集合相關的數據結構。
一、數據結構的分類:
1.數據結構包括:邏輯結構和物理結構,其中邏輯結構稍復雜一些;
2.邏輯結構包括:線性結構(如 順序表、棧、隊列)和非線性結構(如 樹、圖);
3.物理結構包括:順序存儲結構(如 數組)、鏈式存儲結構(如 鏈表)。
二、集合數據結構:
1\. 數組:采用一段連續的存儲單元來存儲數據。對於指定下標的查找,時間復雜度為O(1);通過給定值進行查找,需要遍歷數組,逐一比對給定關鍵字和數組元素,時間復雜度為O(n);
2.鏈表:對於鏈表的新增,刪除等操作,僅需處理結點間的引用即可,時間復雜度為O(1),而查找操作需要遍歷鏈表逐一進行比對,復雜度為O(n);
3.二叉樹:對一棵相對平衡的有序二叉樹,對其進行插入,查找,刪除等操作,平均復雜度均為O(logn);
4.哈希表:在哈希表中進行添加,刪除,查找等操作,性能十分之高,不考慮哈希沖突的情況下,僅需一次定位即可完成,時間復雜度為O(1)。
數據結構本篇不做詳細說明,后續會有專題寫相關的數據結構及算法。
# 集合框架圖
Java 集合,也稱作容器就是用來存放數據的,主要是由兩大接口 派生出來的Collection 和 Map,具體請看下圖:

# Collection
collection主要存儲單值類型的數據,主要的子接口有 list、set和queue(隊列本篇不做介紹)。
操作集合,無非就是「增刪改查」四大類,也叫 CRUD,這里對集合API就不做介紹了。
## List
主要特點是 元素有序且元素可重復。
### 面試題:ArrayList、LinkList和Vector的區別?
ArrayList 底層是數組,查詢效率是O(1) ,新增和刪除效率是O(n),查詢效率高,線程不安全;
LinkList 底層是雙向鏈表,查詢效率是O(n),新增和刪除效率是O(1),新增和刪除效率高,線程不安全;
Vector 底層是數組結構,屬於線程安全集合,使用頻率較低。
## Set
主要特點是 元素無序且元素不可重復。
### 面試題:HashSet和TreeSet的區別?
HashSet :
1\. 數據結構 底層是HashMap實現;
2\. 順序性 不能保證元素的排列順;
3\. null元素 元素可以為null,但只能存放一個null元素;
4\. 時間復雜度 add(),remove(),contains()方法的時間復雜度是O(1)。
TreeSet:
1\. 數據結構 底層是treeMap(紅黑樹)實現;
2\. 順序性 元素是自動排好序的;
3\. null元素 不能存放null元素;
4\. 時間復雜度 add(),remove(),contains()方法的時間復雜度是O(logn)。
# Map
主要存放鍵值對數據,本篇會重點介紹面試率極高的hashmap。
## HashMap
JDK1.7 Hashmap由數組和鏈表組成,JDK1.8做了主要做了2處有優化: 1. 數據結構變成了數組、鏈表和紅黑樹,解決是鏈表太長查詢效率低問題;2. 在哈希沖突時頭插法變成了尾插法,解決是在hashmap擴容時形成環形鏈表問題。
以下篇幅介紹的都是JDK1.8的hashmap。
```java
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
...
}
```
### HashMap數據結構
JDK 1.8 hashMap的數據結構圖:

### 擴容條件
先看一下hashMap源碼中幾個關鍵的字段:
```java
/**實際存儲的key-value鍵值對的個數*/
transient int size;
/**閾值,當table == {}時,該值為初始容量(初始容量默認為16);當table被填充了,也就是為table分配內存空間后,
threshold一般為 capacity*loadFactory。HashMap在進行擴容時需要參考threshold,后面會詳細談到*/
int threshold;
/**負載因子,代表了table的填充度有多少,默認是0.75
加載因子存在的原因,還是因為減緩哈希沖突,如果初始桶為16,等到滿16個元素才擴容,某些桶里可能就有不止一個元素了。
所以加載因子默認為0.75,也就是說大小為16的HashMap,到了第13個元素,就會擴容成32。
*/
final float loadFactor;
/**HashMap被改變的次數,由於HashMap非線程安全,在對HashMap進行迭代時,
如果期間其他線程的參與導致HashMap的結構發生變化了(比如put,remove等操作),
需要拋出異常ConcurrentModificationException*/
transient int modCount;
```
hashMap擴容條件:
1. 當集合容量超過了閾值(threshold)就會進行擴容;
2. 當鏈表長度>=8且數組長度小於64時,也會擴容。
```java
/**
* Replaces all linked nodes in bin at index for given hash unless
* table is too small, in which case resizes instead.
* 新增數據時鏈表長度大於8時會進行樹化
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//樹化時 判斷 數組為空 或 數組長度 < MIN_TREEIFY_CAPACITY =64 則直接擴容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
```
### 擴容機制
1. 擴容:創建一個新的Entry空數組,長度是原數組的2倍;
```java
//hashmap擴容方法
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//原數組不為空 則原數組容量為 table.length
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//原數組閾值
int oldThr = threshold;
//新數組 容量和閾值 都默認為 0
int newCap, newThr = 0;
//原數組容量大於0
if (oldCap > 0) {
//原數組容量 大於等於 最大容量 MAXIMUM_CAPACITY = 1 << 30 則直接返回
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//設置新容量為舊容量的兩倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//閾值也變為原來的兩倍
newThr = oldThr << 1; // double threshold
}
...
```
2. 計算hash:index = HashCode(Key) & (Length - 1);
3. ReHash: 由於新數組的長度變了,則需要遍歷原Entry數組,重新計算hash把所有的Entry重新Hash到新數組。
小結: 可見在用hashMap時最好先計算好hashMap的容量,初始化帶上容量大小,畢竟擴容是非常消耗性能的。
### 樹化條件
樹化必須同時滿足2個條件:
1. 鏈表長度>=8 (binCount >=7 因為binCount 是從0開始算) ;
```java
//binCount 從0開始自增
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//當 binCount == 7時 則樹化 並跳出循環
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
```
2. 數組長度>=64(具體看以上treeifyBin源碼分析);
### 數據查詢
```java
/**
*
* @param hash 需要被獲取元素的hash值
* @param key 需要被獲取的元素
* @return 返回被需要到的元素,沒有獲取到則返回null
*/
final HashMap.Node<K, V> getNode(int hash, Object key) {
//臨時變量儲存table數組
HashMap.Node[] tab;
//臨時變量獲取第一個元素
HashMap.Node first;
//n為table的長度
int n;
// first = tab[n - 1 & hash]) 計算數組下標 獲取到數組上第一個node 且 node的key就是要查詢key 則直接返回 node數組
if ((tab = this.table) != null && (n = tab.length) > 0 && (first = tab[n - 1 & hash]) != null) {
Object k;
//first元素存在,切first元素即鎖需要查找的元素,直接返回first.
if (first.hash == hash && ((k = first.key) == key || key != null && key.equals(k))) {
return first;
}
//數組上的node不是查詢目標且指向下一個node不為空
HashMap.Node e;
if ((e = first.next) != null) {
//判斷node是否為紅黑樹
if (first instanceof HashMap.TreeNode) {
//按照紅黑樹方式去遍歷
return ((HashMap.TreeNode)first).getTreeNode(hash, key);
}
//如果鏈表沒有被樹化,則使用鏈表的方式查詢.
do {
//循環判斷當前的臨時變量e是否與所需元素相同
if (e.hash == hash && ((k = e.key) == key || key != null && key.equals(k))) {
return e;
}
} while((e = e.next) != null);
}
}
return null;
}
```
通過分析源碼查詢過程可以分為3部分:
1. 計算hashCode 獲取到數組上的node,且node.key==key則數組上的就是就是查詢目標;
2. 是鏈表結構則按照鏈表方式遍歷查詢目標;
3. 是紅黑樹結構則按照紅黑樹方式遍歷查詢目標。
### 數據存儲
先上源碼:
```java
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果存儲元素的table為空,則進行必要字段的初始化 默認為16
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 如果根據hash值獲取的node為空,則直接新增
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 如果新插入的結點和table中p結點的hash值,key值相同的話
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果是紅黑樹結點的話,進行紅黑樹插入
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//如果不是紅黑樹則為鏈表按照鏈表方式插入 binCount從0開始自增
for (int binCount = 0; ; ++binCount) {
// 代表這個單鏈表只有一個頭部結點,則直接新建一個結點即可
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 當binCount>=7 即 鏈表長度>=8時,將鏈表轉紅黑樹
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 及時更新p
p = e;
}
}
// 如果存在這個映射就覆蓋
if (e != null) { // existing mapping for key
V oldValue = e.value;
// 判斷是否允許覆蓋,並且value是否為空
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 回調以允許LinkedHashMap后置操作
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 如果容量超過閾值則進行擴容
if (++size > threshold)
resize();
// 回調以允許LinkedHashMap后置操作
afterNodeInsertion(evict);
return null;
}
```
從以上源碼分析hashMap數據存儲也可簡單分為4部分:
1. 在數組上計算hashCode的node為空則直接新增;
2. 獲取的node不為空,如果是紅黑樹就按照紅黑樹方式插入;
3. 獲取的node不為空,如果是鏈表則按照鏈表方式插入(會觸發樹化或擴容操作);
4. 新增成功后,還需要判斷數組容量是否有超過閾值,超過則需要擴容。
## 面試題:HashMap負載因子是多少?為什是這么多?
默認為0.75,這個是在時間和空間之間平衡的一個數值,負載因子是可以自定義的(不推薦)
## 面試題:HashMap和HashTable區別
1. 初始化容量 HashMap初始容量為16,HashTable初始容量為11(負載因子都相同);
2. 線程安全 HashMap為非線程安全,HashTable為線程安全(get和put方法都用synchronized修飾,效率較差);
3. 擴容容量 HashMap擴容時容量:capacity_2,HashTable擴容時容量:capacity_2+1
4. 遍歷方式 HashMap僅支持Iterator的遍歷方式,Hashtable支持Iterator和Enumeration兩種遍歷方式;
5. null值存儲 HashMap中key和value都允許為null,HashTable在遇到null時,會拋出NullPointerException異常。
ps:一般問到hashMap就一定會問到currentHashMap,由於篇幅較長了下次會單獨寫一篇currentHashMap。
# 總結思維導圖

最后奉上自己總結集合的思維導圖,希望能幫助大家。
既然都看到最后了請幫忙一鍵三連哦!