推薦學習:《》
Java集合框架提供了一套效能優良,使用方便的介面和類,他們位於java.util
包中。容器主要包括 Collection 和 Map 兩種,Collection 儲存著物件的集合,而 Map 儲存著鍵值對(兩個物件)的對映表
ConcurrentHashMap
來支援執行緒安全,並且 ConcurrentHashMap
的效率會更高,因為 ConcurrentHashMap
引入了分段鎖。方法 | 說明 |
---|---|
boolean add(Object o) | 在列表的末尾順序新增元素,起始索引位置從0開始 |
void add(int index, Object o) | 在指定的索引位置新增元素,索引位置必須介於0和列表中元素個數之間 |
int size() | 返回列表中的元素個數 |
Object get(int index) | 返回指定索引位置處的元素。取出的元素是Object型別,使用前品要進行益制型別轉換 |
boolean contains(Object o) | 判斷列表中是否存在指定元素 |
boolean remove(Object o) | 從列表中刪除元素 |
Object remove(int index) | 從列表中刪除指定位置元素,起始索引位量從0開始 |
另外,ArrayList和Vector的區別是:ArrayList是執行緒不安全的,當多條執行緒存取同一個ArrayList集合時,程式需要手動保證該集合的同步性,而Vector則是執行緒安全的。
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
這裡簡單解釋一下幾個介面
這裡的繼承結構可通過IDEA中Navigate>Type Hierarchy檢視
//版本號 private static final long serialVersionUID = 8683452581122892189L; //預設容量 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 //實際元素大小,預設為0 private int size; //最大陣列容量 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/** * 構造具有指定初始容量的空列表 * 如果指定的初始容量為負,則為IllegalArgumentException */public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); }}/** * 預設空陣列的大小為10 * ArrayList中儲存資料的其實就是一個陣列,這個陣列就是elementData */public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;}/** * 按照集合迭代器返回元素的順序構造包含指定集合的元素的列表 */public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); if ((size = elementData.length) != 0) { // 轉換為陣列 //每個集合的toarray()的實現方法不一樣,所以需要判斷一下,如果不是Object[].class型別,那麼久需要使用ArrayList中的方法去改造一下。 if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // 否則就用空陣列代替 this.elementData = EMPTY_ELEMENTDATA; }}
每當向陣列中新增元素時,都要去檢查新增後元素的個數是否會超出當前陣列的長度,如果超出,陣列將會進行擴容,以滿足新增資料的需求。陣列擴容通過一個公開的方法ensureCapacity(int minCapacity)
來實現。在實際新增大量元素前,我也可以使用ensureCapacity
來手動增加ArrayList範例的容量,以減少遞增式再分配的數量。
陣列進行擴容時,會將**老陣列中的元素重新拷貝一份到新的陣列中,每次陣列容量的增長大約是其原容量的1.5倍。**這種操作的代價是很高的,因此在實際使用時,我們應該儘量避免陣列容量的擴張。當我們可預知要儲存的元素的多少時,要在構造ArrayList範例時,就指定其容量,以避免陣列擴容的發生。或者根據實際需求,通過呼叫ensureCapacity方法來手動增加ArrayList範例的容量。
private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));}private static int calculateCapacity(Object[] elementData, int minCapacity) { //判斷初始化的elementData是不是空的陣列,也就是沒有長度 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { //因為如果是空的話,minCapacity=size+1;其實就是等於1,空的陣列沒有長度就存放不了 //所以就將minCapacity變成10,也就是預設大小,但是在這裡,還沒有真正的初始化這個elementData的大小 return Math.max(DEFAULT_CAPACITY, minCapacity); } //確認實際的容量,上面只是將minCapacity=10,這個方法就是真正的判斷elementData是否夠用 return minCapacity;}private void ensureExplicitCapacity(int minCapacity) { modCount++; //minCapacity如果大於了實際elementData的長度,那麼就說明elementData陣列的長度不夠用 /*第一種情況:由於elementData初始化時是空的陣列,那麼第一次add的時候, minCapacity=size+1;也就minCapacity=1,在上一個方法(確定內部容量ensureCapacityInternal) 就會判斷出是空的陣列,就會給將minCapacity=10,到這一步為止,還沒有改變elementData的大小。 第二種情況:elementData不是空的陣列了,那麼在add的時候,minCapacity=size+1;也就是 minCapacity代表著elementData中增加之後的實際資料個數,拿著它判斷elementData的length 是否夠用,如果length不夠用,那麼肯定要擴大容量,不然增加的這個元素就會溢位。*/ if (minCapacity - elementData.length > 0) grow(minCapacity);}//ArrayList核心的方法,能擴充套件陣列大小的真正祕密。private void grow(int minCapacity) { //將擴充前的elementData大小給oldCapacity int oldCapacity = elementData.length; //newCapacity就是1.5倍的oldCapacity int newCapacity = oldCapacity + (oldCapacity >> 1); /*這句話就是適應於elementData就空陣列的時候,length=0,那麼oldCapacity=0,newCapacity=0, 所以這個判斷成立,在這裡就是真正的初始化elementData的大小了,就是為10.前面的工作都是準備工作。 */ if (newCapacity - minCapacity < 0) newCapacity = minCapacity; //如果newCapacity超過了最大的容量限制,就呼叫hugeCapacity,也就是將能給的最大值給newCapacity if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); //新的容量大小已經確定好就copy陣列,改變容量大小。 elementData = Arrays.copyOf(elementData, newCapacity);}//用來賦最大值private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); //如果minCapacity都大於MAX_ARRAY_SIZE,那麼就Integer.MAX_VALUE返回,反之將MAX_ARRAY_SIZE返回。 //相當於給ArrayList上了兩層防護 return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;}
/** * 新增一個特定的元素到list的末尾。 * 先size+1判斷陣列容量是否夠用,最後加入元素 */public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true;}/** * Inserts the specified element at the specified position in this * list. Shifts the element currently at that position (if any) and * any subsequent elements to the right (adds one to their indices). * * @param index index at which the specified element is to be inserted * @param element element to be inserted * @throws IndexOutOfBoundsException {@inheritDoc} */public void add(int index, E element) { //檢查index也就是插入的位置是否合理。 rangeCheckForAdd(index); //檢查容量是否夠用,不夠就自動擴容 ensureCapacityInternal(size + 1); // Increments modCount!! //這個方法就是用來在插入元素之後,要將index之後的元素都往後移一位 System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++;}
當呼叫add()方法時,實際函數呼叫:
add→ensureCapacityInternal→ensureExplicitCapacity(→grow→hugeCapacity)
例如剛開始初始化一個空陣列後add一個值,會首先進行自動擴容
將底層陣列的容量調整為當前列表儲存的實際元素的大小的功能
public void trimToSize() { modCount++; if (size < elementData.length) { elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size); }}
remove()
方法也有兩個版本,一個是remove(int index)
刪除指定位置的元素,另一個是remove(Object o)
刪除第一個滿足o.equals(elementData[index])
的元素。刪除操作是add()
操作的逆過程,需要將刪除點之後的元素向前移動一個位置。需要注意的是為了讓GC起作用,必須顯式的為最後一個位置賦null
值。
public E remove(int index) { rangeCheck(index); modCount++; E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; //清除該位置的參照,讓GC起作用 return oldValue; }
這裡簡單介紹了核心方法,其他方法檢視原始碼可以很快了解
ArrayList採用了快速失敗的機制,通過記錄modCount
引數來實現。在面對並行的修改時,迭代器很快就會完全失敗,並丟擲ConcurrentModificationException
異常,而不是冒著在將來某個不確定時間發生任意不確定行為的風險
方法名 | 說明 |
---|---|
void addFirst(Object o) | 在列表的首部新增元素 |
void addLast(Object o) | 在列表的未尾新增元素 |
Object getFirst() | 返回列表中的第一個元素 |
Object getLast() | 返回列表中的最後一個元素 |
Object removeFirst() | 刪除並返回列表中的第一個元素 |
Object removeLast() | 刪除並返回列表中的最後一個元素 |
LinkedList
同時實現了List介面和Deque介面,也就是說它既可以看作一個順序容器,又可以看作一個佇列(Queue),同時又可以看作一個棧(Stack)。這樣看來,LinkedList簡直就是個全能冠軍。當你需要使用棧或者佇列時,可以考慮使用LinkedList
,一方面是因為Java官方已經宣告不建議使用Stack類,更遺憾的是,Java里根本沒有一個叫做Queue_的類(它是個介面名字)。關於棧或佇列,現在的首選是ArrayDeque
,它有著比LinkedList
(當作棧或佇列使用時)有著更好的效能。
LinkedList的實現方式決定了所有跟下標相關的操作都是線性時間,而在首段或者末尾刪除元素只需要常數時間。為追求效率LinkedList沒有實現同步(synchronized),如果需要多個執行緒並行存取,可以先採用Collections.synchronizedList()
方法對其進行包裝
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable
這裡可以發現LinkedList多了一層AbstractSequentialList
的抽象類,這是為了減少實現順序存取(例如LinkedList)這種類的工作。如果自己想實現順序存取這種特性的類(就是連結串列形式),那麼就繼承 這個AbstractSequentialList抽象類,如果想像陣列那樣的隨機存取的類,那麼就去實現AbstracList抽象類。
transient關鍵字修飾,這也意味著在序列化時該域是不會序列化的
//實際元素個數transient int size = 0; //頭結點transient Node<E> first; //尾結點transient Node<E> last;
public LinkedList() {}public LinkedList(Collection<? extends E> c) { this(); //將集合c中的各個元素構建成LinkedList連結串列 addAll(c);}
//根據前面介紹雙向連結串列就知道這個代表什麼了,linkedList的奧祕就在這裡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; }}
public boolean add(E e) { linkLast(e); return true;}void linkLast(E e) { //臨時節點l(L的小寫)儲存last,也就是l指向了最後一個節點 final Node<E> l = last; //將e封裝為節點,並且e.prev指向了最後一個節點 final Node<E> newNode = new Node<>(l, e, null); //newNode成為了最後一個節點,所以last指向了它 last = newNode; if (l == null) //判斷是不是一開始連結串列中就什麼都沒有,如果沒有,則new Node就成為了第一個結點,first和last都指向它 first = newNode; else //正常的在最後一個節點後追加,那麼原先的最後一個節點的next就要指向現在真正的 最後一個節點,原先的最後一個節點就變成了倒數第二個節點 l.next = newNode; //新增一個節點,size自增 size++; modCount++;}
addAll()
有兩個過載函數,addAll(Collection<? extends E>)
型和addAll(int,Collection<? extends E>)
型,我們平時習慣呼叫的addAll(Collection<?extends E>)
型會轉化為addAll(int,Collection<? extends<E>)
型
public boolean addAll(Collection<? extends E> c) { return addAll(size, c);}public boolean addAll(int index, Collection<? extends E> c) { //檢查index這個是否為合理 checkPositionIndex(index); //將集合c轉換為Object陣列 Object[] a = c.toArray(); //陣列a的長度numNew,也就是由多少個元素 int numNew = a.length; if (numNew == 0) //如果空的就什麼也不做 return false; Node<E> pred, succ; //構造方法中傳過來的就是index==size //情況一:構造方法建立的一個空的連結串列,那麼size=0,last、和first都為null。linkedList中是空的。 //什麼節點都沒有。succ=null、pred=last=null //情況二:連結串列中有節點,size就不是為0,first和last都分別指向第一個節點,和最後一個節點, //在最後一個節點之後追加元素,就得記錄一下最後一個節點是什麼,所以把last儲存到pred臨時節點中。 //情況三index!=size,說明不是前面兩種情況,而是在連結串列中間插入元素,那麼就得知道index上的節點是誰, //儲存到succ臨時節點中,然後將succ的前一個節點儲存到pred中,這樣儲存了這兩個節點,就能夠準確的插入節點了 if (index == size) { succ = null; pred = last; } else { succ = node(index); pred = succ.prev; } for (Object o : a) { @SuppressWarnings("unchecked") E e = (E) o; Node<E> newNode = new Node<>(pred, e, null); if (pred == null) first = newNode; else pred.next = newNode; pred = newNode; } if (succ == null) { /*如果succ==null,說明是情況一或者情況二, 情況一、構造方法,也就是剛建立的一個空連結串列,pred已經是newNode了, last=newNode,所以linkedList的first、last都指向第一個節點。 情況二、在最後節後之後追加節點,那麼原先的last就應該指向現在的最後一個節點了, 就是newNode。*/ last = pred; } else { pred.next = succ; succ.prev = pred; } size += numNew; modCount++; return true;}//根據引下標找到該結點並返回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; }}
/*如果我們要移除的值在連結串列中存在多個一樣的值,那麼我們 會移除index最小的那個,也就是最先找到的那個值,如果不存在這個值,那麼什麼也不做 */public boolean remove(Object o) { if (o == null) { for (Node<E> x = first; x != null; x = x.next) { if (x.item == null) { unlink(x); return true; } } } else { for (Node<E> x = first; x != null; x = x.next) { if (o.equals(x.item)) { unlink(x); return true; } } } return false;}不能傳一個null值E unlink(Node<E> x) { // assert x != null; final E element = x.item; final Node<E> next = x.next; final Node<E> prev = x.prev; if (prev == null) { first = next; } else { prev.next = next; x.prev = null; } if (next == null) { last = prev; } else { next.prev = prev; x.next = null; } //x的前後指向都為null了,也把item為null,讓gc回收它 x.item = null; size--; modCount++; return element;}
**get(index)、indexOf(Object o)**等檢視原始碼即可
在LinkedList中除了有一個Node的內部類外,應該還能看到另外兩個內部類,那就是ListItr
,還有一個是DescendingIterator
內部類
/*這個類,還是呼叫的ListItr,作用是封裝一下Itr中幾個方法,讓使用者以正常的思維去寫程式碼, 例如,在從後往前遍歷的時候,也是跟從前往後遍歷一樣,使用next等操作,而不用使用特殊的previous。 */private class DescendingIterator implements Iterator<E> { private final ListItr itr = new ListItr(size()); public boolean hasNext() { return itr.hasPrevious(); } public E next() { return itr.previous(); } public void remove() { itr.remove(); }}
兩個都是執行緒不安全的,在iterator時,會發生fail-fast:快速失效。
在java.util下的集合都是發生fail-fast,而在java.util.concurrent下的發生的都是fail-safe
java.util.ConcurrentModificationException
異常(並行修改異常),這就是快速失敗java.util.concurrent
下的類,都是執行緒安全的類,他們在迭代的過程中,如果有執行緒進行結構的改變,不會報異常,而是正常遍歷,這就是安全失敗Arrays.copyOf()
來拷貝副本,在副本上增加元素,如果有其他執行緒在此改變了集合的結構,那也是在副本上的改變,而不是影響到原集合,迭代器還是照常遍歷,遍歷完之後,改變原參照指向副本,所以總的一句話就是如果在此包下的類進行增加刪除,就會出現一個副本。所以能防止fail-fast,這種機制並不會出錯,所以我們叫這種現象為fail-safe總結:Vector在你不需要進行執行緒安全的時候,也會給你加鎖,也就導致了額外開銷,所以在jdk1.5之後就被棄用了,現在如果要用到執行緒安全的集合,都是從java.util.concurrent
包下去拿相應的類。
通過key、value封裝成一個entry物件,然後通過key的值來計算該entry的hash值,通過entry的hash 值和陣列的長度length來計算出entry放在陣列中的哪個位置上面,每次存放都是將entry放在第一個位置。
HashMap實現了Map介面,即允許放入key
為null
的元素,也允許插入value
為null
的元素;除該類未實現同步外,其餘跟Hashtable
大致相同;跟TreeMap不同,該容器不保證元素順序,根據需要該容器可能會對元素重新雜湊,元素的順序也會被重新打散,因此不同時間迭代同一個HashMap的順序可能會不同。 根據對衝突的處理方式不同,雜湊表有兩種實現方式,一種開放地址方式(Open addressing),另一種是衝突連結串列方式(Separate chaining with linked lists)。Java7 HashMap採用的是衝突連結串列方式。
Java8 對 HashMap 進行了一些修改,最大的不同就是利用了紅黑樹,所以其由 陣列+連結串列+紅黑樹 組成。根據 Java7 HashMap 的介紹,我們知道,查詢的時候,根據 hash 值我們能夠快速定位到陣列的具體下標,但是之後的話,需要順著連結串列一個個比較下去才能找到我們需要的,時間複雜度取決於連結串列的長度為 O(n)。為了降低這部分的開銷,在 Java8 中,當連結串列中的元素達到了 8 個時,會將連結串列轉換為紅黑樹,在這些位置進行查詢的時候可以降低時間複雜度為 O(logN)。
Java7 中使用 Entry 來代表每個 HashMap 中的資料節點,Java8 中使用 Node,基本沒有區別,都是 key,value,hash 和 next 這四個屬性,不過,Node 只能用於連結串列的情況,紅黑樹的情況需要使用 TreeNode
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
//序列號private static final long serialVersionUID = 362498820763181265L; //預設的初始容量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; //當桶(bucket)上的結點數大於這個值時會轉成紅黑樹static final int TREEIFY_THRESHOLD = 8; //當桶(bucket)上的結點數小於這個值時樹轉連結串列static final int UNTREEIFY_THRESHOLD = 6; //桶中結構轉化為紅黑樹對應的table的最小大小static final int MIN_TREEIFY_CAPACITY = 64; //儲存元素的陣列,總是2的冪次倍transient Node<K,V>[] table; //存放具體元素的集transient Set<Map.Entry<K,V>> entrySet; //存放元素的個數,注意這個不等於陣列的長度transient int size; //每次擴容和更改map結構的計數器transient int modCount; //臨界值,當實際大小(容量*填充因子)超過臨界值時,會進行擴容int threshold; //填充因子,計算HashMap的實時裝載因子的方法為:size/capacityfinal float loadFactor;
public HashMap(int initialCapacity, float loadFactor) { // 初始容量不能小於0,否則報錯 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); // 初始容量不能大於最大值,否則為最大值 if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; //填充因子不能小於或等於0,不能為非數位 if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); //初始化填充因子 this.loadFactor = loadFactor; //初始化threshold大小 this.threshold = tableSizeFor(initialCapacity);}//這個方法將傳進來的引數轉變為2的n次方的數值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;}/** * 自定義初始容量,載入因子為預設 */public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR);}/** * 使用預設的載入因子等欄位 */public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted}public HashMap(Map<? extends K, ? extends V> m) { //初始化填充因子 this.loadFactor = DEFAULT_LOAD_FACTOR; //將m中的所有元素新增至HashMap中 putMapEntries(m, false);}//將m的所有元素存入該範例final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0) { //判斷table是否已經初始化 if (table == null) { // pre-size //未初始化,s為m的實際元素個數 float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); //計算得到的t大於閾值,則初始化閾值 if (t > threshold) threshold = tableSizeFor(t); } else if (s > threshold) resize(); //將m中的所有元素新增至HashMap中 for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); } }}
put()方法
先計算key的hash值,然後根據hash值搜尋在table陣列中的索引位置,如果table陣列在該位置處有元素,則查詢是否存在相同的key,若存在則覆蓋原來key的value,否則將該元素儲存在連結串列尾部,注意JDK1.7中採用的是頭插法,即每次都將衝突的鍵值對放置在連結串列頭,這樣最初的那個鍵值對最終就會成為鏈尾,而JDK1.8中使用的是尾插法。此外,若table在該處沒有元素,則直接儲存。
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) { Node<K,V>[] tab; Node<K,V> p; int n, i; //第一次put元素時,table陣列為空,先呼叫resize生成一個指定容量的陣列 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //hash值和n-1的與運算結果為桶的位置,如果該位置空就直接放置一個Node if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); //如果計算出的bucket不空,即發生雜湊衝突,就要進一步判斷 else { Node<K,V> e; K k; //判斷當前Node的key與要put的key是否相等 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //判斷當前Node是否是紅黑樹的節點 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //以上都不是,說明要new一個Node,加入到連結串列中 else { for (int binCount = 0; ; ++binCount) { //在連結串列尾部插入新節點,注意jdk1.8是在連結串列尾部插入新節點 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); // 如果當前連結串列中的元素大於樹化的閾值,進行連結串列轉樹的操作 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //在連結串列中繼續判斷是否已經存在完全相同的key if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //走到這裡,說明本次put是更新一個已存在的鍵值對的value if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; //在hashMap中,afterNodeAccess方法體為空,交給子類去實現 afterNodeAccess(e); return oldValue; } } ++modCount; //如果當前size超過臨界值,就擴容。注意是先插入節點再擴容 if (++size > threshold) resize(); //在hashMap中,afterNodeInsertion方法體為空,交給子類去實現 afterNodeInsertion(evict); return null;}
resize() 陣列擴容
用於初始化陣列或陣列擴容,每次擴容後,容量為原來的 2 倍,並進行資料遷移
final Node<K,V>[] resize() { 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; } // 將陣列大小擴大一倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) // 將閾值擴大一倍 newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // 對應使用 new HashMap(int initialCapacity) 初始化後,第一次 put 的時候 newCap = oldThr; else {// 對應使用 new HashMap() 初始化後,第一次 put 的時候 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; // 用新的陣列大小初始化新的陣列 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; // 如果是初始化陣列,到這裡就結束了,返回 newTab 即可 if (oldTab != null) { // 開始遍歷原陣列,進行資料遷移。 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; // 如果該陣列位置上只有單個元素,那就簡單了,簡單遷移這個元素就可以了 if (e.next == null) newTab[e.hash & (newCap - 1)] = e; // 如果是紅黑樹,具體我們就不展開了 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // 這塊是處理連結串列的情況, // 需要將此連結串列拆成兩個連結串列,放到新的陣列中,並且保留原來的先後順序 // loHead、loTail 對應一條連結串列,hiHead、hiTail 對應另一條連結串列,程式碼還是比較簡單的 Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; // 第一條連結串列 newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; // 第二條連結串列的新的位置是 j + oldCap,這個很好理解 newTab[j + oldCap] = hiHead; } } } } } return newTab;}
get()過程
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value;}final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { // 判斷第一個節點是不是就是需要的 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { // 判斷是否是紅黑樹 if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); // 連結串列遍歷 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null;}
HashSet是對HashMap的簡單包裝,其他還有迭代器等
關於陣列擴容,從putVal原始碼中我們可以知道,當插入一個元素的時候size就加1,若size大於threshold的時候,就會進行擴容。假設我們的capacity大小為32,loadFator為0.75,則threshold為24 = 32 * 0.75,此時,插入了25個元素,並且插入的這25個元素都在同一個桶中,桶中的資料結構為紅黑樹,則還有31個桶是空的,也會進行擴容處理,其實此時,還有31個桶是空的,好像似乎不需要進行擴容處理,但是是需要擴容處理的,因為此時我們的capacity大小可能不適當。我們前面知道,擴容處理會遍歷所有的元素,時間複雜度很高;前面我們還知道,經過一次擴容處理後,元素會更加均勻的分佈在各個桶中,會提升存取效率。所以說盡量避免進行擴容處理,也就意味著,遍歷元素所帶來的壞處大於元素在桶中均勻分佈所帶來的好處。
另外LinkedHashMap是HashMap的直接子類,二者唯一的區別是LinkedHashMap在HashMap的基礎上,採用雙向連結串列(doubly-linked list)的形式將所有**entry**
連線起來,這樣是為保證元素的迭代順序跟插入順序相同
此類完全由在 collection 上進行操作或返回 collection 的靜態方法組成。它包含在 collection 上操作的多型演演算法,即「包裝器」,包裝器返回由指定 collection 支援的新 collection,以及少數其他內容。如果為此類的方法所提供的 collection 或類物件為 null,則這些方法都將丟擲NullPointerException
//反轉列表中元素的順序 static void reverse(List<?> list) //對List集合元素進行隨機排序 static void shuffle(List<?> list) //根據元素的自然順序 對指定列表按升序進行排序 static void sort(List<T> list) //根據指定比較器產生的順序對指定列表進行排序 static <T> void sort(List<T> list, Comparator<? super T> c) //在指定List的指定位置i,j處交換元素 static void swap(List<?> list, int i, int j) //當distance為正數時,將List集合的後distance個元素「整體」移到前面;當distance為負數時,將list集合的前distance個元素「整體」移到後邊。該方法不會改變集合的長度 static void rotate(List<?> list, int distance)
//使用二分搜尋法搜尋指定列表,以獲得指定物件在List集合中的索引 //注意:此前必須保證List集合中的元素已經處於有序狀態 static <T> int binarySearch(List<? extends Comparable<? super T>>list, T key) //根據元素的自然順序,返回給定collection 的最大元素 static Object max(Collection coll) //根據指定比較器產生的順序,返回給定 collection 的最大元素 static Object max(Collection coll,Comparator comp): //根據元素的自然順序,返回給定collection 的最小元素 static Object min(Collection coll): //根據指定比較器產生的順序,返回給定 collection 的最小元素 static Object min(Collection coll,Comparator comp): //使用指定元素替換指定列表中的所有元素 static <T> void fill(List<? super T> list,T obj) //返回指定co1lection中等於指定物件的出現次數 static int frequency(collection<?>c,object o) //返回指定源列表中第一次出現指定目標列表的起始位置;如果沒有出現這樣的列表,則返回-1 static int indexofsubList(List<?>source, List<?>target) //返回指定源列表中最後一次出現指定目標列表的起始位置;如果沒有出現這樣的列表,則返回-1 static int lastIndexofsubList(List<?>source,List<?>target) //使用一個新值替換List物件的所有舊值o1dval static <T> boolean replaceA1l(list<T> list,T oldval,T newval)
Collectons提供了多個synchronizedXxx()方法,該方法可以將指定集合包裝成執行緒同步的集合,從而解決多執行緒並行存取集合時的執行緒安全問題。正如前面介紹的HashSet,TreeSet,arrayList,LinkedList,HashMap,TreeMap都是執行緒不安全的。Collections提供了多個靜態方法可以把他們包裝成執行緒同步的集合。
//返回指定 Collection 支援的同步(執行緒安全的)collection static <T> Collection<T> synchronizedCollection(Collection<T> c) //返回指定列表支援的同步(執行緒安全的)列表 static <T> List<T> synchronizedList(List<T> list) //返回由指定對映支援的同步(執行緒安全的)對映 static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) //返回指定 set 支援的同步(執行緒安全的)set static <T> Set<T> synchronizedSet(Set<T> s)
//返回一個空的、不可變的集合物件,此處的集合既可以是List,也可以是Set,還可以是Map。 emptyXxx() //返回一個只包含指定物件(只有一個或一個元素)的不可變的集合物件,此處的集合可以是:List,Set,Map。 singletonXxx(): //返回指定集合物件的不可變檢視,此處的集合可以是:List,Set,Map unmodifiableXxx()
推薦學習:《》
以上就是詳細解析Java集合框架的詳細內容,更多請關注TW511.COM其它相關文章!