日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

Java基础知识——集合类

發布時間:2024/1/18 java 24 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Java基础知识——集合类 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

工具類:Collection和Map,兩者是同一級別的

知識點:
expectedModCount
ArrayList:懶加載,transient,System.arraycopy()
LinkedList:雙向鏈表
Vector:鎖機制
Stack:繼承Vector,
知識點:
初始化:tableForSize
put:計算hash/如何轉樹擴容
擴容:如何定位
遍歷:迭代器
為什么是2
1.7/1.8d的區別
對比區別
LinkedListHashMap:結構,LRU
TreeMap:比較器,一致性哈希
Queue:隊列
Set:內置HashMap

List

arraylist源碼閱讀:
知識點一:

private transient Object[] elementData;

elementData,假如現在實際有了5個元素,而elementData的大小可能是10,那么在序列化時只需要儲存5個元素,數組中的最后五個元素是沒有實際意義的,不需要儲存。所以ArrayList的設計者將elementData設計為transient,然后在writeObject方法中手動將其序列化,并且只序列化了實際存儲的那些元素,而不是整個數組

知識點二:
初始化時,如果指定Capacity則生成Capacity大小的數組,如果沒有指定則為空,等到添加元素的時候再擴容,節約內存懶加載機制?

參考資料

// 空的數組private static final Object[] EMPTY_ELEMENTDATA = {};// 默認容量的空數組private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

這里有兩個空數組的定義,注意到是static,final不可改變的常量。所以說我們可以推斷,當定義很多個空的ArrayList,他們都指向這兩個數組節約內存

知識點三:
所以indexOf(Object o),remove等都可以穿入null
o==null
null既不是對象也不是一種類型,它僅是一種特殊的值,你可以將其賦予任何引用類型,它還僅僅是一個特殊值,并不屬于任何類型,用instanceof永遠返回false

知識點四:

參考資料
操作中實現可拓展數組,根本在于System.arraycopy()
是native方法,一般是借助C/C++實現的

復制方法有四種:
1、for循環,手動復制
2、System.arraycopy()方法
3、Arrays.copyOf()方法
4、clone()方法
由于System.arraycopy()是最貼近底層的,其使用的是內存復制,省去了大量的數組尋址訪問等時間,故效率最高。
對于Arrays.copyOf()方法查看源碼可以看到:
public static int[] copyOf(int[] original, int newLength) {
int[] copy = new int[newLength];
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
它是借助System.arraycopy()方法實現的,故效率次于System.arraycopy()
clone()方法效率是最低的,一般需要重寫

LinkedList源碼閱讀
知識點一:可以看到LinkedList基于鏈表,并且是雙向循環鏈表,每一個節點是一個Entry,element,next,previous,構造方法和entry(int index) ,entry(int index) 找index位置的元素并返回。其他的操作比如add,remove都是符合雙向循環鏈表基本操作

private static class Entry<E> { E element; Entry<E> next; Entry<E> previous; Entry(E element, Entry<E> next, Entry<E> previous) {this.element = element;this.next = next;this.previous = previous; } private Entry<E> entry(int index) {if (index < 0 || index >= size)throw new IndexOutOfBoundsException("Index: "+index+ ", Size: "+size);Entry<E> e = header;// 根據這個判斷決定從哪個方向遍歷這個鏈表if (index < (size >> 1)) {for (int i = 0; i <= index; i++)e = e.next;} else {// 可以通過header節點向前遍歷,說明這個一個循環雙向鏈表,header的previous指向鏈表的最后一個節點,這也驗證了構造方法中對于header節點的前后節點均指向自己的解釋for (int i = size; i > index; i--)e = e.previous;}return e;}}

知識點二“:
expectedModCount字段,
modCount,用于記錄對象的修改次數,比如增、刪、改
可以理解成version,在特定的操作下需要對version進行檢查,適用于Fail-Fast機制。
Fail-Fast 機制
比如當A通過iterator去遍歷某集合的過程中,因為iterator長時間擁有對象,而且是線程安全即其他線程也可以操作對象,所以為了防止便利的時候讀取臟數據,通過checkForComodification()方法,判斷modCount==expectedModCount,若其他線程修改了此集合,此時會拋出ConcurrentModificationException異常。

知識點三:重寫clone方法,利用clone.add(e.element);

public Object clone() {LinkedList<E> clone = null;try {clone = (LinkedList<E>) super.clone();} catch (CloneNotSupportedException e) {throw new InternalError();}clone.header = new Entry<E>(null, null, null);clone.header.next = clone.header.previous = clone.header;clone.size = 0;clone.modCount = 0;for (Entry<E> e = header.next; e != header; e = e.next)clone.add(e.element);return clone; }

Iterator:

參考資料


參考資料
知識點一:
接口中的default方法,能夠在借口中直接寫方法體和抽象類的區別又縮小了
但是如果實現A,B兩個接口都有相同函數簽名的default的方法,必須重寫因為不知道應該繼承哪個
如果繼承父類A,和實現接口B都有default方法,則會實際為父類A的方法

知識點二:
首先Java提供兩個迭代器接口Iterator和ListIterator
Iterator方法比較少:next,hasNext,remove
ListIterator方法比較多:add,previous等等

參考資料

AbstractList:是ArrayList和LinkedList的父類
實現了兩個內部類Itr和ListItr
Itr:實現Iterator接口
ListItr:實現ListIterator并且繼承自Itr

ArrayList:直接使用AbstractList的Itr
Itr:cursor lastRet(remove時移除此元素)利用數組特性遍歷

checkForComodification(); try { Object next = get(cursor);//先取當前光標所在位置后面的元素 lastRet = cursor++;//然后把最后一次操作所在光標設置成當前光標位置,再把當前光標后移一 return next; } catch (IndexOutOfBoundsException e) { checkForComodification(); throw new NoSuchElementException(); }

LinkedList:
單獨定義內部類ListItr,它實現了ListIterator接口,用來對LinkedList進行專門迭代,因為LinkedList與ArrayList還不同,它是使用鏈表結構實現的,所以需專門的迭代器。
ListItr:
lastReturned:最近一次操作返回的元素
Entry next:即將返回的元素
nextIndex:即將返回的元素的編號

public Object next() { //檢查外部是否修改了集合結構,即modCount是否與expectedModCount相等 checkForComodification(); if (nextIndex == size) throw new NoSuchElementException(); lastReturned = next;//先記錄next所在位置,并把它賦給lastReturned next = next.next;//然后再把next指向下一個元素 nextIndex++;//nextIndex與next操作需同步,所以也要增一 return lastReturned.element; }

Vector
參考資料
1:底層實現與ArrayList類似,基于數組
2:線程安全,add,remove方法都是synchronized方法。但是其實并不是真正的意義安全,對方法加鎖實際上是沒有太大意義的。因為如果你申請遍歷Vector,同樣需要鎖來禁止修改Vector遍歷沒加鎖而修改加鎖。注意add,remov仍然是不能同時執行的,因為synchronized非靜態方法是對對象實例加鎖,并且效率低下
3: Vector擴容由oldCapacity 與 capacityIncrement共同決定,

int newCapacity = oldCapacity + ((capacityIncrement > 0) ?capacityIncrement : oldCapacity);

而ArrayList為

int newCapacity = (oldCapacity * 3)/2 + 1;

參考資料
stack:
public class Stack extends Vector
Vector:
public class Vector extends AbstractList implements List, RandomAccess, Cloneable, Serializable
繼承Vector 并實現pop,push等方法
問題一:為什么說Java的Stack類實現List接口是個笑話?
因為Stack是FILO,但是LIst是RandomAccess,從設計角度上講不合理。接口的實現,不取決于類的應用場景,而是接口的契約的應用場景,固然棧有些時候是需要隨機訪問,但是他本質還是FILO
就像牙刷是用來刷牙的,但是有些時候我們的確也可以用來洗衣服
問題二:為什么stack是一個類?
因為collection體系是在jdk1.2被設計出來的,而vector,stack,hashtable這些是隨java的第一個版本就發布了的。簡而言之,在最初的java版本中,提供了基本的容器實現,但未經良好設計,因此在革命性的1.2版本中,重新設計了后來獲得廣泛好評的Collection體系

HashMap


參考資料

參考資料
前沿:首先HashMap具體實現是由鏈表+數組的實現方式,并且采用了動態擴容技術
數據結構:

特點:
1:動態擴充
數組是否需要擴充是通過負載因子判斷的,如果當前元素個數為數組容量的0.75時,就會擴充數組。這個0.75就是默認的負載因子,可由構造傳入。我們也可以設置大于1的負載因子,這樣數組就不會擴充,犧牲性能,節省內存。
2:鏈表和紅黑樹轉化:
為了解決碰撞,數組中的元素是單向鏈表類型。當1:鏈表長度到達一個閾值時(7或8)TREEIFY_THRESHOLD并且2:總數量是否到達一個閾值(64),會將鏈表轉換成紅黑樹提高性能。而當鏈表長度縮小到另一個閾值時(6)UNTREEIFY_THRESHOLD,又會將紅黑樹轉換回單向鏈表提高性能
TreeNodes占用空間是普通Nodes的兩倍(兩個指針和一個指針)所以剛開始用Nodes節省空間,后面當鏈表長度大泊松分布幾率小時再用紅黑樹

3:數據結構是數組+鏈表+紅黑樹
拉鏈法進行沖突處理

內部類:
繼承關系如下:Node是單向鏈表節點,Entry是雙向鏈表節點,TreeNode是紅黑樹節點。
Node->Entry->TreeNode
注意Entry是JDK1.7之前的數據機構,1.8后主要是用Node和TreeNode不用Entry可能認為雙向循環鏈表意義不大,效率不如Node

Node

static class Node<K,V> implements Map.Entry<K,V> {// hash是經過hash()方法處理過的hashCode,為了使hashCode分布更加隨機,// 待會會深入這個hash()方法。final int hash; final K 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 int hashCode() {// key的hashCode異或value的哈希Codereturn Objects.hashCode(key) ^ Objects.hashCode(value);}// 其余略}

HashMap方法:

成員變量

/*** 數組的默認初始長度,java規定hashMap的數組長度必須是2的次方* 擴展長度時也是當前長度 << 1。*/ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16// 數組的最大長度 static final int MAXIMUM_CAPACITY = 1 << 30;// 默認負載因子,當元素個數超過這個比例則會執行數組擴充操作。 static final float DEFAULT_LOAD_FACTOR = 0.75f;// 樹形化閾值,當鏈表節點個大于等于TREEIFY_THRESHOLD - 1時, // 會將該鏈表換成紅黑樹。 static final int TREEIFY_THRESHOLD = 8;// 解除樹形化閾值,當鏈表節點小于等于這個值時,會將紅黑樹轉換成普通的鏈表。 static final int UNTREEIFY_THRESHOLD = 6;// 最小樹形化的容量,即:當內部數組長度小于64時,不會將鏈表轉化成紅黑樹,而是優先擴充數組。 static final int MIN_TREEIFY_CAPACITY = 64;// 這個就是hashMap的內部數組了,而Node則是鏈表節點對象。 transient Node<K,V>[] table;// 下面三個容器類成員,作用相同,實際類型為HashMap的內部類KeySet、Values、EntrySet。 // 他們的作用并不是緩存所有的key或者所有的value,內部并沒有持有任何元素。 // 而是通過他們內部定義的方法,從三個角度(視圖)操作HashMap,更加方便的迭代。 // 關注點分別是鍵,值,映射。 transient Set<K> keySet; // AbstractMap的成員 transient Collection<V> values; // AbstractMap的成員 transient Set<Map.Entry<K,V>> entrySet;// 元素個數,注意和內部數組長度區分開來。 transient int size;// 再上一篇文章中說過,是容器結構的修改次數,fail-fast機制。 transient int modCount;// 閾值,超過這個值時擴充數組。 threshold = capacity * load factor,具體看上面的靜態常量。 int threshold;// 裝在因子,具體看上面的靜態常量。 final float loadFactor;

其中,Node等為transient變量
1:有很多空的元素,不需要序列化
2:HashCode依賴于不同的虛擬機,常規序列化可能導致錯誤

1:構造方法
//主要是對傳入的initialCapacity和loadFactor進行參數檢驗,沒有為數組table分配內存空間而是在執行put操作的時候才真正構建table數組

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;this.threshold = tableSizeFor(initialCapacity);    init();//init方法在HashMap中沒有實際實現,不過在其子類如 linkedHashMap中就會有對應實現}

public HashMap(int initialCapacity, float loadFactor)這個構造可以由我們指定數組的初始容量和負載因子。
但是前面說過,數組容量必須是2的次方。所以就需要通過某個算法將我們給的數值轉換成2的次方。
tableSizeFor(int cap)就是這個作用。

// 這個方法可以將任意一個整數轉換成2的次方。
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
0000 0100 0000 0000
0000 0110 0000 0000
0000 0111 1000 0000
0000 0111 1111 1000
0000 0111 1111 1111
0000 1000 0000 0000 可以看到變成2的次方
為什么要 int n = cap - 1?
我的理解是,因為給定的MAXIMUM_CAPACITY = 1 << 30,相當于在說明文檔中說,最大值可以取1<<30。如果不減1,那么將1<<30帶入后結果為0,與要求不符,所以-1后計算。

當然,如果未指定容量,則同樣也是懶加載機制

public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted}

特點

2:put
檢查數組是否為空,執行resize()擴充;
通過hash值計算數組索引,獲取該索引位的首節點。
如果首節點為null,直接添加節點到該索引位。
如果首節點不為null,那么有3種情況
① key和首節點的key相同,覆蓋value;否則執行②或③
② 如果首節點是紅黑樹節點(TreeNode),將鍵值對添加到紅黑樹。
③ 如果首節點是鏈表,將鍵值對添加到鏈表。添加之后會判斷鏈表長度是否到達TREEIFY_THRESHOLD - 1這個閾值,“嘗試”將鏈表轉換成紅黑樹。
最后判斷當前元素個數是否大于threshold,擴充數組。

public V put(K key, V value) {return putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {//tab存放當前的哈希桶,p用作臨時鏈表節點 Node<K,V>[] tab; Node<K,V> p; int n, i;//如果當前哈希表是空的,代表是初始化//table為Entry,tab為Node,Entry是Node的子類,上轉型對象if ((tab = table) == null || (n = tab.length) == 0)//那么直接去擴容哈希表,并且將擴容后的哈希桶長度賦值給nn = (tab = resize()).length;//如果當前index的節點是空的,表示沒有發生哈希碰撞。直接構建一個新節點Node,掛載在index處即可。if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {//否則 發生了哈希沖突。Node<K,V> e; K k;//如果哈希值相等,key也相等,則是覆蓋value操作if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;//將當前節點引用賦值給eelse if (p instance of TreeNode)//如果是紅黑樹節點e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {//不是紅黑樹節點//遍歷鏈表for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {//遍歷到尾部,,仍沒有key與其相等,說明是新節點,追加新節點到尾部p.next = newNode(hash, key, value, null);//如果追加節點后,鏈表數量>=8,則轉化為紅黑樹if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}//如果找到key與其相同,說明是曾經插入過的節點if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}//如果e不是null,說明有需要覆蓋的節點,由上面的break;彈出if (e != null) { // existing mapping for key//則覆蓋節點值,并返回原oldValueV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;//這是一個空實現的函數,用作LinkedHashMap重寫使用。afterNodeAccess(e);return oldValue;}}//如果執行到了這里,說明插入了一個新的節點,所以會修改modCount,以及返回null。++modCount;//更新size,并判斷是否需要擴容。if (++size > threshold)resize();//這是一個空實現的函數,用作LinkedHashMap重寫使用。afterNodeInsertion(evict);return null; }

計算實際存儲位置:
1:利用Java重寫的HashCode計算哈希值
2:對哈希值進行擾動處理 (h = key.hashCode()) ^ (h >>> 16)
3:與運算代替模計算(n - 1) & hash

hash():轉為hash值

static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }

hashCode的高16位與低16位進行異或運算注意h進行h >>> 16已經被賦值,因為內部數組的容量一般都不會很大,基本分布在16~256之間。所以一個32位的hashCode,一直都用最低的4到8位進行與運算,而高位幾乎沒有參與。“擾動函數”
獲得索引值:

(p = tab[i = (n - 1) & hash]) == null

(n - 1) & hash實際上=hash%(n)n是長度,為一個2的次方數
利用與運算代替模運算可以極大增加運算速度

Hash表擴容:
1:計算新數組的容量和閾值
2:將原數組的元素拷貝到新數組中
這里JDK1.7之前利用的是重新計算Hash索引,即rehash操作
1.8后,由于數組容量是2的次方且擴充后翻倍,則只需要判斷原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”

final HashMap.Node<K,V>[] resize() {HashMap.Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;if (oldCap > 0) {// 如果數組已經是最大長度,不進行擴充。if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}// 否則數組容量擴充一倍。(2的N次方)else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold}// 如果數組還沒創建,但是已經指定了threshold(這種情況是帶參構造創建的對象),threshold的值為數組長度// 在 "構造函數" 那塊內容進行過說明。else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;// 這種情況是通過無參構造創建的對象else { // zero initial threshold signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}// 可能是上面newThr = oldThr << 1時,最高位被移除了,變為0。if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}threshold = newThr;// 到了這里,新的數組長度已經被計算出來,創建一個新的數組。@SuppressWarnings({"rawtypes","unchecked"})HashMap.Node<K,V>[] newTab = (HashMap.Node<K,V>[])new HashMap.Node[newCap];table = newTab;// 下面代碼是將原來數組的元素轉移到新數組中。問題在于,數組長度發生變化。 // 那么通過hash%數組長度計算的索引也將和原來的不同。// jdk 1.7中是通過重新計算每個元素的索引,重新存入新的數組,稱為rehash操作。// 這也是hashMap無序性的原因之一。而現在jdk 1.8對此做了優化,非常的巧妙。if (oldTab != null) {// 遍歷原數組for (int j = 0; j < oldCap; ++j) {// 取出首節點HashMap.Node<K,V> e;if ((e = oldTab[j]) != null) {oldTab[j] = null;// 如果鏈表只有一個節點,那么直接重新計算索引存入新數組。if (e.next == null)newTab[e.hash & (newCap - 1)] = e;// 如果該節點是紅黑樹,執行split方法,和鏈表類似的處理。else if (e instanceof HashMap.TreeNode)((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap);// 此時節點是鏈表else { // preserve order// loHead,loTail為原鏈表的節點,索引不變。HashMap.Node<K,V> loHead = null, loTail = null;// hiHeadm, hiTail為新鏈表節點,原索引 + 原數組長度。HashMap.Node<K,V> hiHead = null, hiTail = null;HashMap.Node<K,V> next;// 遍歷鏈表do {next = e.next;// 新增bit為0的節點,存入原鏈表。if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}// 新增bit為1的節點,存入新鏈表。else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);// 原鏈表存回原索引位if (loTail != null) {loTail.next = null;newTab[j] = loHead;}// 新鏈表存到:原索引位 + 原數組長度if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab; }

4:遍歷
參考資料
KeySet,values,EntrySet
Set< String>,Collections< String>,Set< Entry< String,String>>

在HashMap構造時,只有一個KeySet的引用,而只有當用戶調用Map.KeySet()方法時,才實例化一個最新的KeySet懶加載機制
public Set keySet() {
Set ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}
而在KeySet是一個內部類
擁有
一:iterator,即可以通過遍歷器遍歷
public final Iterator iterator() { return new KeyIterator(); }
返回一個KeyIterator,而KeyIterator有nextNode方法進行遍歷
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
// fast-fail 機制的實現 即在迭代器往后遍歷時,每次都檢測expectedModCount是否和modCount相等
// 不相等則拋出ConcurrentModificationException異常
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
//如果遍歷越界,則拋出NoSuchElementException異常
if (e == null)
throw new NoSuchElementException();
if ((next = (current = e).next) == null && (t = table) != null) {
//如果遍歷到末尾,則跳到table中下一個不為null的節點處
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
二:調用forEach方法
public final void forEach(Consumer<? super V> action) {—}思想和上述類似,都是利用tab[index]進行遍歷訪問,并沒有涉及Set元素的增添

五:化樹

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {TreeNode<K,V> parent; // 紅黑樹父節點TreeNode<K,V> left;TreeNode<K,V> right;TreeNode<K,V> prev; // 刪除后需要取消鏈接boolean red;TreeNode(int hash, K key, V val, Node<K,V> next) {super(hash, key, val, next);}

我們知道當一個桶中節點數量超過8時就會轉化為紅黑樹,過程如下:
步驟一:treeifyBin()先將所有節點替換為TreeNode,然后再將單鏈表轉為雙鏈表
步驟二:treeify(tab);依次比較節點,分別比較Hash值(HashCode異或16位的值),是否有比較器,兩者的類名(),對象的HashCode
步驟三:進行平衡調整
d = a.getClass().getName().
compareTo(b.getClass().getName())) == 0

關鍵點:為什么實際大小總是2的冪?
我的理解:

原因一:
因為index=hash%length-1
比如對于:
length=16 則length-1 =15 即 00001111
length=32 則length-1=31 即 00011111
可以看到,對于一個相同的hash,分別&兩個不同的length-1,得到的結果result1,result2 差別就只在右數第5位,相當于擴容后只要修改一位就可以改變索引

原因二:
因為length=2^n,則length-1的位都是保持00001111111的形狀,而hash&length-1中,高位不會產生影響,而低位任意一個變化變化都會產生影響,減少沖突的概率。
在h<length的情況下:
對于長度:23 length-1則比特為 10110,則對于結果result=10110
既可以是h=11110 10110 11111 10111 都對應相同的結果沖突增加
而對于長度32 length-1 則比特位11111 ,則對于結果10110
只有h=10110才可以與其對應

原因三:可以用與運算代替模運算,增加了效率

JDK1.7和JDK1.8中HashMap的區別
底層:
最重要的一點是底層結構不一樣,1.7是數組+鏈表,1.8則是數組+鏈表+紅黑樹結構;并且內部類的實現也有區別
1.7:Entry
1.8:Node-Entry-TreeNode 兩個Entry是不同的

put:
1:1.8中會將節點插入到鏈表尾部,而1.7中是采用頭插
頭插法:
1:擴容時顛倒鏈表順序
2:擴容時形成閉環
線程1插入節點A,線程2插入節點B。順序
1A 2B 2A 此時成環
而尾插法會遍歷鏈表,如果發現已經在則停止
為什么頭插法形成閉環
2:jdk1.7中的hash函數對哈希值的計算直接使用key的hashCode值,而1.8中則是采用key的hashCode異或上key的hashCode進行無符號右移16位的結果,避免了只靠低位數據來計算哈希時導致的沖突,計算結果由高低位結合決定,使元素分布更均勻;

擴容:
1:擴容時1.8會保持原鏈表的順序,而1.7會顛倒鏈表的順序;
2:jdk1.8是擴容時通過hash&cap是否等于0將鏈表分散,無需改變hash值,而1.7是通過更新hashSeed來修改hash值達到分散的目的;
3:擴容策略:1.7中是只要不小于閾值就直接擴容2倍;而1.8的擴容策略會更優化,當數組容量未達到64時,以2倍進行擴容,超過64之后若桶中元素個數不小于7就將鏈表轉換為紅黑樹,但如果紅黑樹中的元素個數小于6就會還原為鏈表,當紅黑樹中元素大于32的時候才會再次擴容。

HashTable:
Hashtable 是遺留類
很多映射的常用功能與 HashMap 類似,不同的是它承自 Dictionary 類,
線程安全的,并發性不如 ConcurrentHashMap,
HashTable和HashMap的區別:
1:HashTable線程安全,HashMap線程不安全
2:HashTable不允許鍵和值為Null,而HashMap允許。
兩點原因:
1:源碼角度
HashTable:可以看到會直接調用hash=key.hashCode();當key為null時報錯
而HashMap會進行一個判斷,如果key為null則設為0

2:設計角度,hashtable為早期版本,可能認為null不可以作為分類依據。而后面的hashmap可能認為null也可以作為分類依據

HashTable、ConcurrentHashMap以及Collections中的靜態方法SynchronizedMap比較:
HashTable:對set/get方法都使用synchronized
SynchronizedMap:內部封裝HashMap,加了synchronized
ConcurrentHashMap:synchronized+CAS

紅黑樹:
參考資料
參考資料
性質:
1:根節點和葉子結點是黑色
2:不能出現連續的兩個紅色結點
3:從根到任意葉子結點上的黑色結點數相同
根據定義:
若節點只有一棵子樹,那么一定是黑根+紅子

查找:O(log2(N))的時間復雜度
插入:插入節點都是紅色結點
判斷父親節點
1:若是黑色,直接插入紅色節點
2:若是紅色,判斷叔叔節點。插入紅色節點后
若叔叔節點是紅色:交換色(父叔節點和祖父節點換色)
若叔叔節點是黑色(或者不存在):交換色+旋轉 四種情況
其實不可能是黑色,只有不存在的情形。因為父親節點是單子樹或者子節點,但是如果是單子樹只能是黑色(這樣卻不滿足性質3),所以父親節點是葉子結點,那么叔叔只能是紅色或者無
刪除:
步驟1:確定實際的刪除位置
步驟2:當進入情形1時,進行樹的平衡調整

步驟1:
情形1:葉子節點:如果是紅色直接刪除,如果是黑色那么需要進行平衡
情形2:有一個葉子:子節點代替(變為刪除子節點)——轉化為情形1或2或3
情形3:有兩個葉子:最鄰近代替(一般是右子樹,變為刪除替換節點)——直接轉化為情形1
葉子代替的時候,將代替節點轉化為被代替的顏色

樹的平衡調整
被刪除結點:
若被刪除結點兄弟節點在右子樹
被刪除結點節點紅色:直接刪除
被刪除結點節點黑色:
1 被刪除結點兄弟節點為紅色:父親兄弟改色+旋轉——2.3
2 被刪除結點兄弟節點是黑色:
2.1兄弟節點的右子節點為紅色:兄弟黑,父親兄右紅+旋轉(唯一終態)
2.2兄弟節點的右子為黑色,但是左子為紅色:換色后旋轉——變為2.1
2.3兄弟節點雙子都是黑色:改色+父節點作為替換節點
優勢:
平衡二叉樹的插入/刪除操作帶來的旋轉操作可能會達到logn次,而紅黑樹的插入/刪除操作帶來的旋轉操作最多為2/3次。

平衡二叉樹的刪除:

參考資料
左子樹上節點的刪除相當于我們在右子樹上插入了一個新節點,右子樹上節點的刪除相當于在左子樹上插入了一個新節點
三種情況:
1:被刪節點為葉節點
2:被刪節點只一個葉節點
3:被刪節點有兩個葉子結點

LInkedHashMap:
HashMap和LinkedList的結合體,可以看到節點含有三個指針(鏈表),四個指針(樹)

static class Entry<K,V> extends Node<K,V> {Entry<K,V> before, after;Entry(int hash, K key, V value, Node<K,V> next) {super(hash, key, value, next);}}

accessOrder兩種順序進行存儲
一種是元素的插入順序,另一種便是元素的訪問順序(LRU緩存實現)
LRU的實現:最久未訪問元素
addBefore(lm.header)是把當前訪問的元素挪到head的前面,即最近訪問的元素被放到了鏈表頭,如此要實現LRU算法只需要從鏈表末尾往前刪除就可以
而刪除如何實現的呢?

void addEntry(int hash, K key, V value, int bucketIndex) {super.addEntry(hash, key, value, bucketIndex);// Remove eldest entry if instructedEntry<K,V> eldest = header.after;if (removeEldestEntry(eldest)) {removeEntryForKey(eldest.key);}}

可以看到判斷removeEldestEntry(eldest)即是否需要刪除最晚元素
LinkedHashMap默認的removeEldestEntry方法如下

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {return false;}

所以開發者需要實現LRU算法只需要繼承LinkedHashMap并重寫removeEldestEntry方法

內部比較器和外部比較器:
參考資料
內部:實現Comparable接口并重寫compareTo
外部:單獨定義一個類實現Comparator接口并重寫compare

TreeMap:底層架構是紅黑樹
數據結構是Entry,并沒有復用HashMap的TreeNode節省內存?
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
Entry<K,V> left;
Entry<K,V> right;
Entry<K,V> parent;
boolean color = BLACK;
}
比較是先進行外部比較器的比較,再進行內部比較器的比較
如果沒有定義外部比較器,key=null則報錯
如果沒有定義外部比較器,key也沒有實現Comparable接口,報
問題一:
基本數據結構都實現了Comparable,但是如果是某個類,需要實現Comparable接口?
因為其會調用compareTo的方法
問題二:如何實現一致性哈希
tailMap(K fromKey)方法,支持從紅黑樹中查找比fromKey大的值的集合,但并不需要遍歷整個數據結構
第一個順時針元素Integer i = subMap.firstKey();即是我們的存儲節點

我們只要把服務器的節點名字取hashCode(需要加虛擬節點,簡便實現方式為+i)put進TreeMap中,之后對于查找的點調用tailMap/subMap.firstKey即可

LinkedListHashMap:結構,實現LRU
TreeMap:比較器,實現一致性哈希

Queue

超級接口:
Queue 接口
Deque 雙端接口

ArrayQueue:普通隊列
參考資料
1:不可擴容
2:循環隊列(數組)
一般來說,隊空隊滿head==tail的區分法有三種常用方法,但是ArrayQueue并沒有使用,而是利用this.capacity = capacity + 1;
當add后if (newtail == head)表示此時已經有 capacity + 1個元素,直接報錯 不具有容錯性,感覺不咋樣

public boolean add(T o) {queue[tail] = o;// 通過除余來實現下標的循環;int newtail = (tail + 1) % capacity;if (newtail == head)throw new IndexOutOfBoundsException("Queue full");tail = newtail;return true; // we did add something}public T remove(int i) {if (i != 0)throw new IllegalArgumentException("Can only remove head of queue");if (head == tail)throw new IndexOutOfBoundsException("Queue empty");T removed = queue[head];queue[head] = null;// 通過除余來實現下標的循環;head = (head + 1) % capacity;return removed;}

ArrayDeque:雙端隊列,實現了Deque超級接口
參考資料
1:可以擴充,方式為2的次方容積和HashMap一致
區別在于如果輸入的大于1<<30,則返回1<<30 理解為10000–為-0,小于0

if (initialCapacity < 0) initialCapacity >>>= 1;

2:利用循環數組,且是雙端循環數組理解為兩個棧
開始的時候head=0,tail=0。當要在head插入數值時,是先移動位置再賦值
步驟一:head = (head - 1),此時head=-1,而負數是以偽碼形似存儲1111 1111 1111 1111 1111 1111 1111 1111
步驟二:1111 1111 1111 1111 1111 1111 1111 1111 & (elements.length - 1)
其實本質就是對elements[elements.length - 1]=e
步驟三:當再插入時,-2偽碼1111 1111 1111 1111 1111 1111 1111 1110
與之后相當于elements[elements.length - 2]=e
可以看到實際就是上圖循環數組的龍尾部分

同理,我們可以對tail進行分析,先賦值再移動位置,實際上就是上圖的龍首部分

public void addFirst(E e) {
if (e == null)
throw new NullPointerException();
elements[head = (head - 1) & (elements.length - 1)] = e;
if (head == tail)
doubleCapacity();
}
public void addLast(E e) {
if (e == null)
throw new NullPointerException();
elements[tail] = e;
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
doubleCapacity();

而當 if (head == tail)時,擴充容newCapacity = n << 1
并將head之后的元素復制到新數組的開頭,把剩余的元素復制到新數組之后
原數組長度為n,(tail=head)——0為tail棧,(tail=head)——n-1為head棧。記head=tail=flag

新數組相當于交叉原數組
新數組長度為2n,head=0——flag為head棧,tail——flag為tail棧

3:刪除元素。同樣分為pollFirst()和pollLast() 。并且實現了如棧操作:pop,push,peek,隊列操作:add,offer,remove,poll,peek,element,雙端隊列操作:addFirst,addLast,getFirst,getLast等方法,目的是模擬其他數據結構

4:遍歷:可以看到從head開始遍歷到tail。沒有使用fast-fail機制,而是利用
if (tail != fence || result == null),即判斷head和tail和記錄的有無更改和modCount一個思想

private class DeqIterator implements Iterator<E> {private int cursor = head;private int fence = tail;private int lastRet = -1;public boolean hasNext() {return cursor != fence;}public E next() {if (cursor == fence)throw new NoSuchElementException();@SuppressWarnings("unchecked")E result = (E) elements[cursor];if (tail != fence || result == null)throw new ConcurrentModificationException();lastRet = cursor;

PriorityQueue:
1:PriorityQueue是優先級隊列,取出元素時會根據元素的優先級進行排序。其內部內部是一個用數組實現的小頂堆
2:使用場景:N取K,動態插入含有優先級的數組

private void grow(int minCapacity) {int oldCapacity = queue.length;// 如果當前容量比較小(小于64)的話進行雙倍擴容,否則擴容50%int newCapacity = oldCapacity + ((oldCapacity < 64) ?(oldCapacity +2) :(oldCapacity >> 1));// 如果發現擴容后溢出了,則進行調整if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);queue = Arrays.copyOf(queue, newCapacity);}

Set

基本類
AbstractSet:提供 Set 接口的骨干實現,從而最大限度地減少了實現此接口所需的工作。實現了HashCode,equals,removeAll
SortedSet:按照對象的比較函數對元素排序
NavigableSet:接口繼承自SortedSet接口

引申類:
HashSet
通過內部成員變量HashMap實現其功能
public Iterator iterator() {
return map.keySet().iterator();
}
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
其中PRESENT是一個Object 對象

LinkedHashSet:擁有HashSet的功能,且能實現插入有序或者訪問有序。依靠成員變量LinkedHashMap實現
TreeSet:可自定義排序的Set
擁有成員變量NavigableMap
private transient NavigableMap<E,Object> m;
// map中共用的一個value
private static final Object PRESENT = new Object();

KeySet:HashMap的內部類,繼承AbstractSet

總結

以上是生活随笔為你收集整理的Java基础知识——集合类的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。