Java深刻研究Collection集合框架

Java集合框架位於java.util包下,主要包含List、Set、Map、Iterator和Arrays、Collections集合工具類,涉及的數據結構有數組、鏈表、隊列、鍵值映射等,Collection是一個抽象接口,對應List、Set兩類子接口,Map是key-value形式的鍵值映射接口,Iterator是集合遍歷的迭代器,下面是總體框架圖java

集合框架總體框架圖

在util包下還涉及SortedMap、SortedSet接口,分別對應Map、Set接口,在concurrent包下有常見的 ArrayBlockingQueue、ConcurrentHashMap、CopyOnWriteArrayList等實現類對Queue、Map、List接口的擴展實現,下面分別從List\Queue\Set\Map接口經常使用實現類一探究竟

ArrayList實現

咱們先來看看初始化方式,node

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
private int size;

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
複製代碼

從源碼中定義的兩個Object數組可知ArrayList採用數組做爲基本存儲方式,在String字節碼中也有定義數組,不過是private final char[] value,transient關鍵字主要是序列化時忽略當前定義的變量;在ArrayList無參函數中給定默認數組長度爲10,在實際開發中,通常若是能預知數組長度則會調用帶有長度閾值的構造函數,算法

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);
    }
}
複製代碼

源碼方法中會直接按照指定長度建立Object數組並賦值給this.elementData,接下來繼續看看沒有指定數組長度時,數組是如何擴容從而知足可變長度?此時ArrayList中的add方法登場數組

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
複製代碼

size爲ArrayList中定義的int類型變量,默認爲0,當調用ensureCapacityInternal(1),繼續往下看bash

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}
複製代碼

該方法會判斷底層數組elementData與臨時數組DEFAULTCAPACITY_EMPTY_ELEMENTDATA是否相等,若是是就取兩個閾值中的最大值,minCapacity爲1,DEFAULT_CAPACITY爲10(默認值),而後繼續走ensureExplicitCapacity(10),數據結構

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
複製代碼

modCount用於記錄操做次數,若是minCapacity大於底層數組長度,開始調用擴容方法grow框架

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}
複製代碼

oldCapacity爲默認底層數組長度,newCapacity = oldCapacity + (oldCapacity >> 1)等價於 newCapacity = oldCapacity + (oldCapacity / 2);在Java位運算中,oldCapacity << 1 至關於oldCapacity乘以2;oldCapacity >> 1 至關於oldCapacity除以2 , 此時新的長度爲原始長度的1.5倍,若是擴容後的長度小於minCapacity,則直接賦值爲minCapacity,再往下的if判斷中是對int最大值的邊界斷定,能夠看到最後經過Arrays.copyOf進行數組的copy操做,這是Arrays工具類中的方法,該方法最終調用以下函數

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    @SuppressWarnings("unchecked")
    T[] copy = ((Object)newType == (Object)Object[].class)
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    System.arraycopy(original, 0, copy, 0,Math.min(original.length, newLength));
    return copy;
}
複製代碼

經過System.arraycopy進行數組複製並return this,System.arraycopy方法爲native方法,對應方法以下工具

//原始數組      //位置     //目標數組   //位置
 public static native void arraycopy(Object src,  int  srcPos, Object dest, int destPos,
                                    int length);//copy長度
複製代碼

調用一連串方法最終也只是copy一個默認長度爲10的空數組,咱們繼續看add方法中的 elementData[size++] = e;//把當前對象放置在elementData[0]上,在ArrayList中size()方法是直接返回定義的size值,即爲返回數組元素長度,而非底層數組長度(默認10),因此ArrayList在初始時就佔用必定空間,下面咱們看下ArrayList中的查詢方法oop

public E get(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    return (E) elementData[index];
}
複製代碼

若是要查找List中某個對象,假如已知對象在數組中的位置,則直接return (E) elementData[index]返回,在計算算法效率中以O表示時間,此時能夠以O(1)表示查詢到指定對象的時間複雜度,由於經過下標查找咱們只須要執行一次,若是咱們沒法得知具體下標,一般是for循環查找位置直到返回對象,假設數組長度爲n,此時的時間複雜度爲O(n),這種方式實則取的是查找到該對象所消耗的時間的最大值,有可能在for循環中第一個或是中間一個位置就已經查找到了,則可記爲O(1)或者O(n/2)

LinkedList實現

LinkedList基於雙向鏈表+內部類Node<>泛型集合實現,初始化沒有默認空間大小,根據頭尾節點查找元素,下面先看下雙向鏈表的數據結構圖

鏈表中elem爲當前元素,prev爲當前元素的上一個節點,next爲下一個節點,LinkedList初始化時鏈表是空的,因此firs頭節點、last尾節點都是null,下面看下初始化源碼,

transient int size = 0;  //transient標記序列化時忽略
transient Node<E> first; //頭節點
transient Node<E> last; //尾節點

public LinkedList() {
}

public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}
複製代碼

有參函數對應加入整個集合,下面先看下內部類Node的定義,

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;
    }
}
複製代碼

E表示泛型元素類型,prev記錄當前元素的上一個節點,next記錄下一個節點,當咱們往LinkedList中add一個元素時,看下源碼是怎麼處理Node節點,

public boolean add(E e) {
    linkLast(e);
    return true;
}
    
void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}
複製代碼

在進行添加操做時默認是追加至last節點,linkLast方法中首先將當前數組中last節點賦值臨時變量,而後調用Node<>構造函數將當前添加元素與last關聯,此時l賦值給Node<>中的perv節點,接着判斷l是否爲null,若是是表示數組中沒有元素,就直接賦值給first做爲第一個,不然就追加至原數組最後一個元素的next節點,從而完成add操做,下面咱們看下指定插入位置add方法,

public void add(int index, E element) {
    checkPositionIndex(index);   //校驗index邊界>=0 && <=size
    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}
Node<E> node(int index) {
    // assert isElementIndex(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;
    }
}
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    final Node<E> pred = succ.prev;
    final Node<E> newNode = new Node<>(pred, e, succ);
    succ.prev = newNode;
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}
複製代碼

linkBefore方法中首先會調用node(int index)節點生成方法,該方法中首先經過二分法的方式斷定元素插入位置,而後分別對first、last節點中的next、prev節點進行賦值操做,最後返回插入的節點元素,傳遞給linkBefore方法,該方法會判斷傳入的節點元素的上一個節點是否爲null,對節點進行相應賦值操做,從而完成指定下標插入元素,下面繼續看下LinkedList刪除元素方法remove(int index),

public E remove(int index) {
    checkElementIndex(index);  //index邊界斷定>=0 && <size
    return unlink(node(index));
}

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.item = null;
    size--;
    modCount++;
    return element;
}
複製代碼

刪除元素方法基本都會調用unlink(Node x),原理就是把x元素的先後節點指向關係進行替換,而後將當前x元素全部屬性置空,達到刪除元素目的同時等待GC回收,順帶看下LinkedList查詢方法

public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}
複製代碼

核心是經過相似二分法定位下標對應的元素並返回該對象的element值,能夠看到LinkedList在增刪元素時,只是修改當前下標所在元素的先後節點指向關係,相對於ArrayList的copy數組效率要高,而查詢元素時雖採用二分法提升查詢效率,但其時間複雜度仍是O(logN),二分法找一次就排除一半的可能,log是以2做爲底數,相對於ArrayList直接索引查詢要慢得多

Queue、Deque的實現

PriorityQueue是基於優先堆的一個無界隊列,該優先隊列中的元素默認以天然排序方式或者經過傳入可比較的Comparator比較器進行排序,下面看下PriorityQueue的add方法源碼

private static final int DEFAULT_INITIAL_CAPACITY = 11;   //隊列默認大小
transient Object[] queue;                                 //隊列底層數組存儲結構
int size;                                                 

public boolean add(E e) {
    return offer(e);
}
public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    modCount++;
    int i = size;
    if (i >= queue.length)
        grow(i + 1);
    size = i + 1;
    if (i == 0)
        queue[0] = e;
    else
        siftUp(i, e);
    return true;
}
複製代碼

在offer方法中若是插入的元素爲null則會直接拋出異常,當隊列長度大於等於容量值時開始自動擴容,grow方法與ArrayList的擴容方法相似,最後都調用了Arrays.copyOf方法,惟一區別在於擴容長度不同,可自行查看此處源碼,offer方法中i=0時標記爲隊列第一個元素,直接賦值queue[0],若是不是第一個,則開始調用siftUp()上浮方法,

private void siftUp(int k, E x) {        // k != 0
    if (comparator != null)
        siftUpUsingComparator(k, x);    //指定排序比較器
    else
        siftUpComparable(k, x);         //使用默認天然順序比較器
}
private void siftUpComparable(int k, E x) {
    Comparable<? super E> key = (Comparable,<? super E>) x;
    while (k > 0) {
        int parent = (k - 1) >>> 1;
        Object e = queue[parent];
        if (key.compareTo((E) e) >= 0)
            break;
        queue[k] = e;
        k = parent;
    }
    queue[k] = key;
}
複製代碼

在siftUpComparable方法中,將傳入的可比較的對象轉換爲Comparable,若是k下標大於0,計算父節點的下標int parent = (k - 1) >>> 1 等價於int parent = (k - 1)/2;而後取出父一級的節點對象,經過compareTo方法對插入的對象於當前對象比較是否>=0,若是不大於則把當前對象賦值給k位置,再把parent位置賦值給k作替換,最後經過queue[k] = key實現元素上浮排序,繼續看下remove方法

public boolean remove(Object o) {
    int i = indexOf(o);               //遍歷數組找到第一個知足o.equals(queue[i])元素的下標
    if (i == -1)               
        return false;
    else {
        removeAt(i);
        return true;
    }
}
E removeAt(int i) {
    // assert i >= 0 && i < size;
    modCount++;
    int s = --size;
    if (s == i)                         // removed last element
        queue[i] = null;
    else {
        E moved = (E) queue[s];
        queue[s] = null;
        siftDown(i, moved);             //調整順序
        if (queue[i] == moved) {
            siftUp(i, moved);
            if (queue[i] != moved)
                return moved;
        }
    }
    return null;
}
複製代碼

刪除元素時下標是從後往前,當i = s是最後一個元素下標時直接置空,不然從隊列數組中取出要刪除的元素,調用一次siftDown下沉方法對最小堆節點位置進行調整,以不指定比較器的方法源碼分析

private void siftDownComparable(int k, E x) {
    Comparable<? super E> key = (Comparable<? super E>)x;
    int half = size >>> 1;                          // loop while a non-leaf
    while (k < half) {
        int child = (k << 1) + 1;                   // assume left child is least
        Object c = queue[child];
        int right = child + 1;
        if (right < size &&                         //對比左右節點大小
            ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
            c = queue[child = right];
        if (key.compareTo((E) c) <= 0)
            break;
        queue[k] = c;                               //將子節點c上移
        k = child;
    }
    queue[k] = key;                                 //key向下移動到k位置
}
複製代碼

根據int half = size/2 找到非葉子節點的最後一個節點下標並與當前k的位置做比較,從k的位置開始,將x逐層向下與當前節點的左右節點中較小的那個交換,直到x小於或等於左右節點中的任何一個爲止,從而達到刪除非最後一個元素的節點排序,相應的時間複雜度也是O(logN),經過此處的方法也能夠得知在數組下標從0開始狀況下,節點下標計算方式爲

left = k * 2 + 1 ,right = k * 2 + 2, parent = (k -1) / 2, 固然PriorityQueue還有一些其它Queue接口的實現方法,如poll、peek方法,包括在concurrent包下的PriorityBlockingQueue,DelayQueue,ConcurrentLinkedDeque等實現Queue、Deque接口的擴展類,可自行去看下jdk 1.8源碼實現,加深二叉隊列排序原理的理解

HashSet、HashMap、ConcurrentHashMap的實現

HashSet底層是基於HashMap實現,限於本文篇幅過長,剩餘源碼分析參見下篇

《Java深刻研究HashMap實現原理》

以上涉及JDK源碼部分均來自 JDK 1.8

個人我的新球

加入星球一塊兒討論項目、研究新技術,共同成長!

相關文章
相關標籤/搜索