java原始碼集合體系解析
一、集合體系
1.1 Collection體系
1.1.1 ArrayList & Vector
1)底層實現和特點
底層實現:動態陣列實現
特點:元素有序且可重複。Vector是執行緒安全,ArrayList是執行緒不安全的。
關係:Vector是JDK1.0就有個類,但是現在所學習的集合體系是JDK1.2才產生的。因此在JDK1.2之後,sun公司強行的讓Vector實現了List介面。所以Vector內部出現了很多工程重複的方法。
2)什麼是動態陣列?
注意:Java中本質沒有動態陣列的,一旦陣列的長度確定就不可改變(原因是因為陣列是一段連續的記憶體地址)
,那為什麼不設定一個邏輯連續,物理不連續?這樣的陣列是沒有辦法通過陣列下標定位的。下標1萬和找下標為0的速度是一樣快的。因為他在記憶體中是連續的地址,只要在最開始的元素位置只要加上1萬的4個位元組就是4萬個位元組就你夠立馬找到。動態陣列的實現:一旦原來的陣列空間不夠時,建立一個長度更長的新陣列,然後將舊的陣列元素移動到新陣列中,以此來實現陣列的"動態擴容",舊的陣列會因為沒有引用,而被垃圾回收器回收掉
3)ArrayList原始碼解析
基本屬性介紹
/**
* 預設的初始化容量
*/
private static final int DEFAULT_CAPACITY = 10;
/**
*
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 預設的空節點陣列
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 底層的核心動態陣列的引用
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* 陣列的元素個數(不是陣列的長度)
*/
private int size;構造方法
/**
有參構造方法。
引數為初始化陣列的長度
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {//直接初始化指定長度的陣列,賦值給elementData
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
/**
無參構造方法
*/
public ArrayList() {
//直接將預設的空陣列賦值給elementData變數
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
疑問:為什麼在兩個構造方法給了不同的空陣列?
因為從道理將給同一個是沒有問題的,但是從設計角度來講,這兩個陣列他會有一定的區別,一個是當陣列引數零也就是指定了容量為0進行賦值空節點陣列給elementData ,一個是預設引數為0賦值空節點陣列給elementData,這個版本來看作用是同一個,但是當以後版本出現了變化是當陣列引數零給的是進行列外一個操作這樣DEFAULTCAPACITY_EMPTY_ELEMENTDATA和EMPTY_ELEMENTDATA就互不影響了。就是為了以後的擴充套件
元素新增(add)
/**
新增元素
*/
public boolean add(E e) {
//判斷是否容量足夠,如果不夠就需要擴容
ensureCapacityInternal(size + 1);
//將元素e放入底層陣列,下標為size的位置,然後size自增
elementData[size++] = e;
return true;
}
/**
容量判斷
引數:本次至少需要的容量大小
*/
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
/**
重新計算陣列需要的長度,如果是第一次新增元素,則返回預設長度10
*/
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
/**
判斷是否需要擴容
*/
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 如果需要的最小容量超過了底層陣列的實際長度,則進行擴容
if (minCapacity - elementData.length > 0)
//擴容方法
grow(minCapacity);
}
/**
陣列擴容的方法
引數:為至少需要的容量大小
*/
private void grow(int minCapacity) {
//舊的容量
int oldCapacity = elementData.length;
//新的容量(按照原容量1.5倍擴容),右移一位相當於除以二,左移一位相當於乘以二
int newCapacity = oldCapacity + (oldCapacity >> 1);
//擴容的容量如果沒有達到最小容量
if (newCapacity - minCapacity < 0)
//直接將最小容量變成新容量
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//重要:陣列擴容的方式
//copyOf - 將引數1的陣列元素,拷貝到一個新陣列中,新陣列的容量為引數2,並且將新陣列返回
elementData = Arrays.copyOf(elementData, newCapacity);
}
注意:copyOf底層是有native本地方法實現的,Java所有的native方法都是呼叫c/c++實現的
元素插入方法(add)
/**
將元素e插入到index位置
*/
public void add(int index, E element) {
//檢測index是否越界
rangeCheckForAdd(index);
//檢測是否需要擴容
ensureCapacityInternal(size + 1);
//引數1的陣列,從引數2的位置開始
//複製到引數3的陣列中,從引數3的引數4的位置開始設定,
//複製的元素總長度為引數5
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//將元素覆蓋到index位置
elementData[index] = element;
//元素總數量加1
size++;
}獲取元素(get)
public E get(int index) {
//檢查下標越界
rangeCheck(index);
//獲得下標index處的元素
return elementData(index);
}
作業:自行檢視remove方法的原始碼
1.1.2 LinkedList
1)底層實現特點
特點:元素有序且可重複
底層實現:雙向連結串列
2)什麼是雙向連結串列?
連結串列在記憶體中不是一串連續的地址,由一個一個節點組成,每個節點可以分為3部分(資料部分,頭指標,尾指標)
優勢:從中間插入和中間刪除,只需要移動節點的指標指向,無需移動節點的位置
缺點:查詢一個元素時,必須從頭/尾依次往後/往前遍歷
如何用Java程式碼實現一個雙向連結串列?
//雙向連結串列的節點物件
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
3)LinkedList的原始碼解析
常用的變數
//元素的個數/連結串列的節點數
transient int size = 0;
//指向雙向連結串列頭指標的引用
transient Node<E> first;
//指向雙向連結串列尾指標的引用
transient Node<E> last;疑問為什麼要做雙向連結串列頭指標和尾指標的引用?
因為沒有這兩個變數都找不到雙向連結串列
新增元素的方法(add)
//新增元素
public boolean add(E e) {
linkLast(e);
return true;
}
//新增元素到連結串列的末尾
void linkLast(E e) {
//讓l變數指向last所指向的最後一個節點
final Node<E> l = last;
//建立一個新的節點,資料部分就是新增的元素
//讓新節點的頭指標指向l
final Node<E> newNode = new Node<>(l, e, null);
//將新節點賦值給last
last = newNode;
//判斷當前是否新增的第一個元素
if (l == null)
//如果是第一個元素,那麼newNode也要賦值給first
first = newNode;
else
//如果不是,
l.next = newNode;
//元素個數++
size++;
modCount++;
}插入元素(add)
//插入元素到index的位置
public void add(int index, E element) {
//檢查下標是否越界
checkPositionIndex(index);
//判斷index是否在末端
if (index == size)
//尾部的追加
linkLast(element);
else
//插入
//引數1:插入的元素
//引數2:index位置的現有節點
linkBefore(element, node(index));
}
//獲得index位置的元素
Node<E> node(int index) {
if (index < (size >> 1)) {
//查詢的元素在前半段,從頭開始依次往後變數
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
//查詢的元素在後半段,從尾開始依次往前遍歷
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
//將元素e,插入到節點succ的前面
void linkBefore(E e, Node<E> succ) {
//succ的當前節點的上一個節點的指標的物件賦值給pred
final Node<E> pred = succ.prev;
//創建出一個新的節點,e代表元素部分,pred指向上一個節點,succ指向下一個節點
final Node<E> newNode = new Node<>(pred, e, succ);
//這個新的節點又指向succ的上一個指標(說明原先succ不指向上一個指標了而轉向指向這個新的節點)
succ.prev = newNode;
//如果pred == null這就說明了succ是第一個節點,
if (pred == null)
//那麼newNode就是第一個節點
first = newNode;
else
//新節點指向了pred指向的下一個的指標
pred.next = newNode;
size++;
modCount++;
}
獲得元素(get)
public E get(int index) {
//下標越界
checkElementIndex(index);
//獲得index位置的節點,方式同上,再返回節點的資料部分
return node(index).item;
}
作業:自行檢視remove方法的實現
1.1.3 ArrayList VS LinkedList效能分析
ArrayList查詢速度很快,往中間插入/移除元素很慢。 LinkedList往中間插入/移除元素很快,查詢元素很慢。
不確切
效能對比
插入效能對比: 尾部:ArrayList和LinkedList效能差異不大,幾乎一樣 頭部:LinkedList效能 遠遠大於ArrayList效能,因為ArrayList需要進行陣列的擴容 + 元素的位移(已經通過C優化) 中間:ArrayList效能 大於 LinkedList效能,原因在於LinkedList在插入中間位置時,需要查詢中間的元素,LinkedList查詢中間元素是最忙的操作
讀取效能對比: ArrayList:讀取任何位置效能差異不大,速度很快 LinkedList:讀取越靠中間的元素,效能越差,越靠兩邊,效能越好(ArrayList效能差不多)
1.1.4 HashSet、LinkedHashSet、TreeSet
1)特點
HashSet:無序,不可重複 LinkedHashSet:不可重複,有序(插入順序) TreeSet:不可重複,有序(字典序)
2)底層實現
Set集合的底層實現都是由Map集合實現的
1.2 Map體系
1.2.1 HashMap & Hashtable
1)底層實現和特點
HashMap和Hashtable底層都是由雜湊表實現,HashMap和Hashtable的關係與ArrayList和Vector的關係是一樣的。HashMap執行緒不安全,Hashtable執行緒安全。
特點:HashMap的key無序不可重複,value可以重複
2)雜湊表的介紹
什麼是雜湊表?
雜湊表是一種用於快速查詢的資料結構,在精準定位方面效能非常的好(通過key找value),查詢速度和元素的個數無關(理想狀態,實際過程中,多少還是有點關係),時間複雜度為O(1)
優勢:可以快速的通過key找到value。 快速定位、大資料去重、判斷是否存在.... 雜湊表???
雜湊表的底層是一個位數組?他是通過雜湊函式轉換成下標隨機匹配座標。這就導致了同一個位置可能會有重複元素,那麼這個key有不一樣怎麼可能會相同呢因為雜湊函式實現了key.hashcode() % array.length**不是按key實現的。這就導致雜湊碰撞
什麼是雜湊函式?
可以將任意型別的key轉換成int型別下標
特點: 1、任何型別 -> int型別 2、同一個值 在 任何時候,轉換的下標必須一樣 3、轉成的下標必須落在雜湊表的有效範圍之內
自己實現一個雜湊函式? 實現:key.hashcode() % array.length
什麼是雜湊碰撞?(雜湊衝突 - 重要)
兩個元素(key-value),通過雜湊函式計算出同一個下標,如果key相同,則後面的元素value覆蓋前面的元素value,如果key不同,則發生了所謂的雜湊碰撞。雜湊碰撞不是好事,而是因為不可避免。
問題1:雜湊表是如何判斷key是否相同的?
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
先判斷兩個key的hash值是否相同,如果相同,再判斷兩個元素的key是否相等(== 或者 equals)問題2:雜湊碰撞發生後該怎麼辦?
雜湊衝突的解決
1、開發地址法 - 發生碰撞後,新的元素自動往後移位 2、鏈地址法 - 將發生碰撞的元素通過連結串列連線起來(優勢,不佔其他的桶,劣勢,影響查詢效能)
注意:JDK1.8之後,引入了連結串列 + 紅黑樹的方式解決雜湊衝突
雜湊表的擴容
為什麼雜湊表要擴容? 隨著新增元素越來越多,適當的擴容可以降低發生雜湊碰撞概率。以及重新計算原來節點的桶位置,打散原來連結串列的長度,起到提高查詢效率的作用。
擴容閾值:當元素個數達到擴容閾值之後,就會觸發一次雜湊表的擴容。 填充因子:擴容的元素比例,擴容閾值 = 雜湊表容量 * 填充因子
3)紅-黑樹 - (簡單介紹)
什麼是紅-黑樹?
紅-黑樹是一種特殊的二叉搜尋樹,也是一種便於快速查詢的資料結構,但是查詢效能會比雜湊表略低。
什麼是二叉搜尋樹?
二叉樹 -> 二叉搜尋樹。在二叉搜尋樹中,任何一個節點的所有左子節點都小於該節點,所有的右子節點都大於該節點,這種二叉樹,就稱之為二叉搜尋樹。
缺點:害怕樹的失衡
紅-黑樹就是一個永遠平衡的二叉搜尋樹
紅黑規則
只要遵循了紅黑規則的二叉搜尋樹就一定是平衡的 1、根節點一定是黑色 2、紅色節點不能有紅色的子節點(紅紅衝突) 3、從根節點觸發,到任意一個葉子節點,經過的黑節點數量必須相同 4、新增的節點預設為紅節點
紅黑樹的平衡手段
變色 + 旋轉
4)HashMap原始碼解析 - JDK1.8
常用屬性
/**
* 雜湊表的預設初始長度 - 16(2的4次方)
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
/**
* 雜湊表的最大長度 - 2^30
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 預設填充因子 - 0.75
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 連結串列長度到8時轉成紅黑樹
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 紅黑樹的個數到6時轉成連結串列
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 雜湊表的元素達到64時,連結串列才會轉紅黑樹
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
底層雜湊表
*/
transient Node<K,V>[] table;
/**
當前的拓展閾值,當元素個數達到這個值時,就觸發擴容
*/
int threshold;
/**
當前的填充因子
*/
final float loadFactor;雜湊表中的節點元素
//雜湊表的節點
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //當前節點的hash值,通過key計算而來
final K key; //key
V value;//值
Node<K,V> next;//下個節點的引用
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}構造方法
/**
* 無參構造
*/
public HashMap() {
//設定當前的填充因子為預設的填充因子 0.75f
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
/**
有參構造
引數1:初始化的雜湊表的長度
引數2:填充因子
*/
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 <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//1 2 4 8 16 32 64 128 ......
//根據初始容量,計算離這個容量最近的2的N次方的結果值
//設定給當前的擴容閾值
this.threshold = tableSizeFor(initialCapacity);
}
注意:JDK1.8之後,雜湊表的容量必須是2的N次方
新增元素(put)
/**
新增元素
引數4:false代表,key相同時,value覆蓋,如果為true,表示value不覆蓋
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
雜湊函式
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
核心的新增元素的方法
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
//tab - 代表當前的雜湊表
//p - 代表當前key對應的雜湊桶中的元素
//n - 代表雜湊表的長度
//i - 代表key對應的雜湊桶的下標
Node<K,V>[] tab; Node<K,V> p; int n, i;
//------------------第一次新增元素時觸發-----------------------
//判斷雜湊表是否為空,如果為空表示當前第一次新增元素(table是)
if ((tab = table) == null || (n = tab.length) == 0)
//如果雜湊表還沒有初始化,就呼叫resize方法初始化雜湊表
n = (tab = resize()).length;
//------------------第一次新增元素時觸發-----------------------
//任何一個數字 & n 結果一定是0 ~ n範圍
//(n - 1) & hash 通過key的雜湊值計算下標,賦值給i
//從雜湊表tab,下標為i的元素賦值給p
//判斷p是否為null
if ((p = tab[i = (n - 1) & hash]) == null)
//說明當前雜湊桶為空,沒有發生雜湊碰撞
//新建一個Node,將key,value等都儲存到節點中
//將新的節點放入桶i的位置
tab[i] = newNode(hash, key, value, null);
else {
//桶i的位置不為空
//e - 雜湊碰撞的桶的第一個元素
//k - 下標為i的雜湊桶的第一個元素的key值
Node<K,V> e; K k;
//判斷新增的key和p是否相同
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//如果相等 將p賦值給e
e = p;
else if (p instanceof TreeNode)
//判斷當前雜湊桶中是否為紅黑樹
//走紅黑樹的邏輯
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//雜湊桶中是連結串列,並且發生了雜湊碰撞
//遍歷當前的桶的連結串列
for (int binCount = 0; ; ++binCount) {
//e一直指向p的下一個節點
if ((e = p.next) == null) {
//表示走到了最後一個節點,說明整個連結串列都沒有發現相等的key
//建立一個新的節點,放入連結串列的尾端
p.next = newNode(hash, key, value, null);
//binCount - 迴圈的連結串列數量
//判斷連結串列的長度是否達到轉樹的條件(有沒有超過8)
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//可能轉紅黑樹???
treeifyBin(tab, hash);
break;
}
//判斷節點e是否和新增的元素key相等
if (e.hash == hash &&
((k = e.key)