Java集合分析

一、JAVA集合的框架圖
 
常見集合的架構圖:
 
 
二、Set的底層實現
 
HashSet
它的構造函數
 
public HashSet() {
    map = new HashMap<>();
}

 

從上面的構造函數,我們可以得知,它的底層是一個HashMap
Map是存儲的鍵值對,但是Set只存儲了值,那它的key-value是如何設計的呢?
我們跟蹤一下HashSet的add方法
 
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

 

從上面的add方法我們可以看到,添加到HashSet的元素是作爲HashMap的Key,它的Value是一個Object類型的常量對象PRESENT,並且HashSet中所有元素key對應的value都是同一個。
 
LinkedHashSet
它的構造函數
 
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
    map = new LinkedHashMap<>(initialCapacity, loadFactor);
}

 

從上 面的構造函數,我們可以得知,它的底層實現是一個LinkedHashMap
 
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

 

從上面的add方法我們可以看到,添加到LinkedHashSet的元素是作爲LinkedHashMap的Key,它的Value是一個Object類型的常量對象PRESENT,並且LinkedHashSet中所有元素key對應的value都是同一個(LinkedHashSet繼承了父類HashSet的add方法沒有重寫)
 
TreeSet
它的構造函數
 
public TreeSet() {
    this(new TreeMap<>());
}

 

從上 面的構造函數,我們可以得知,它的底層實現是一個TreeMap
 
public boolean add(E e) {
    return m.put(e, PRESENT)==null;
}

 

從上面的add方法我們可以看到,添加到TreeSet的元素是作爲TreeMap的Key,它的Value是一個Object類型的常量對象PRESENT,並且TreeSet中所有元素key對應的value都是同一個
 
二、Iterator接口迭代器
 
java.util.Iterator接口有三個方法
boolean hasNext()
E next()
void remove()
所有的Collection系列的集合都會包含一個方法來獲取Iterator對象  Iterator iterator();
iterator() 這個方法返回的是哪個類的對象呢?
public Iterator<E> iterator() {
    return new Itr();
}

 

如:ArrayList,在其內部有一個內部類,Itr,它實現了Iterator接口
Vector中,在其內部有一個內部類,Itr,它實現了Iterator接口
LinkedList中,內部有一個Itr 實現了Iterator,它還有一個ListItr實現了ListIterator
……
每一種Collection系列集合的實現類中都有一個內部類實現Iterator接口
Iterator接口對象的作用是用來做遍歷,迭代集合的元素用,設計爲內部類的好處,就是可以方便直接訪問集合的內部元素
 
三、Iterator迭代器和foreach遍歷,多線程併發問題
 
public class TestIterator {


    public static void main(String[] args) {
        ArrayList list = new ArrayList();
        list.add("張三");
        list.add("李四");
        list.add("王五");
        list.add("趙六");


        Iterator iterator = list.iterator();
        while(iterator.hasNext()){
            Object next = iterator.next();
            //這裏使用集合的刪除方法,會產生異常java.util.ConcurrentModificationException
            list.remove(next);
        }
    }
}

 

在使用迭代器或者foreach遍歷時,再用集合對象的remove方法時會報ConcurrentModificationException異常
原因:因爲迭代器和集合兩個同時在操作元素
關於迭代器中的next方法
public E next() {
    checkForComodification();
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
}

 

在方法中它首先調用了checkForComodification()方法進行校驗,方法的實現如下:
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

 

這裏modCount表示修改的次數
觀察一下add添加方法
public boolean add(E e) {
    modCount++;
    add(e, elementData, size);
    return true;
}

 

觀察一下remove方法
public E remove(int index) {
    Objects.checkIndex(index, size);


    modCount++;
    E oldValue = elementData(index);


    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work


    return oldValue;
}

 

這樣我們可以看到只要是我們修改了集合中的元素都會修改modcount
關於expectedModCount
在創建迭代器的時候就記錄下來了集合的修改次數
當我們在遍歷過程中 modCount變化了也就是集合中的元素變化了則會導致如下判斷成立
modCount != expectedModCount
這個時候則會拋出異常
throw new ConcurrentModificationException(); 
去們用集合的remove方法會報錯,但是在遍歷時使用Iterator的remove方法則不會報錯,可以看Iterator中的remove方法如下:
public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();


    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

 

從上面可以看到在執行了集合的remove後進行了一個賦值操作 expectedModCount = modCount; 從而保證這兩個值一致,所以在checkForComodification()校驗時不會拋出異常
設計者爲了避免將來其它的線程修改了集合的元素,導致當前這個操作的數據不正確風險,則快速拋出異常而失敗掉。
Enumration就不是這樣設計的,可能會存在數據的不一致性。
 
四、ArrayList
ArrayList:動態數組
它的內部實現是數組
內部初始化時數組的大小
在它的內部會創建一個數組
transient Object[] elementData;
它的構造函數如下:
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

 

DEFAULTCAPACITY_EMPTY_ELEMENTDATA 這個是一個空數組
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

 

在jdk1.8以上可以看到源碼中如上代碼所表述,它初始化時會置爲一個長度爲0的空數組:DEFAULTCAPACITY_EMPTY_ELEMENTDATA
在jdk1.6的時候在創建ArrayList對象時,它是直接創建一個長度爲10的數組
如果在jdk1.7的時候在創建ArrayList時,它初始化時會創建一個長度爲0的數組:EMPTY_ELEMENTDATA
 
數組擴容方式
當調用add方法時可以看到會調用ArrayList類中方法如下:
public boolean add(E e) {
    modCount++;
    add(e, elementData, size);
    return true;
}

private void add(E e, Object[] elementData, int s) {
    if (s == elementData.length)
        elementData = grow();
    elementData[s] = e;
    size = s + 1;
}

private Object[] grow() {
    return grow(size + 1);
}

private Object[] grow(int minCapacity) {
    return elementData = Arrays.copyOf(elementData,
                                       newCapacity(minCapacity));
}

private int newCapacity(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);    //這裏可以看到是擴容爲1.5倍
    if (newCapacity - minCapacity <= 0) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            return Math.max(DEFAULT_CAPACITY, minCapacity); //這一行代碼是關鍵DEFAULT_CAPACITY的常量值被定義爲10
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return minCapacity;
    }
    return (newCapacity - MAX_ARRAY_SIZE <= 0) //Integer.MAX_VALUE - 8
        ? newCapacity
        : hugeCapacity(minCapacity);
}

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE)
        ? Integer.MAX_VALUE
        : MAX_ARRAY_SIZE;
}

 

由於jdk1.7後,ArrayList創建對象時它的數據初始化爲一個空數組,所以第一次它會擴展長度爲10
在後續如果又不夠了這個時候會再擴展爲1.5倍
如下測試可以看到確實當開始第二次擴容後,它擴容的長度是原來的1.5倍
public static void main(String[] args) {
    ArrayList arrayList = new ArrayList();

    System.out.println("在最開始創建ArrayList對象,它的底層數組容量是:" + getCapacity(arrayList));
    arrayList.add("張三");
    System.out.println("在添加了一個元素後ArrayList對象,它的底層數組容量是:" + getCapacity(arrayList));

    arrayList.add("張三");
    arrayList.add("張三");
    arrayList.add("張三");
    arrayList.add("張三");
    arrayList.add("張三");
    arrayList.add("張三");
    arrayList.add("張三");
    arrayList.add("張三");
    arrayList.add("張三");
    arrayList.add("張三"); //這是第11個添加的元素,已經超過10個元素了

    System.out.println("當添加第11個元素後,ArrayList做底層數組的擴容,擴容後的容量是:" + getCapacity(arrayList));
}


/**
* 通過反射的方式把ArrayList對象中的elementData數組拿出來看看現這個數組的長度
* @param obj 一個ArrayList的對象
* @return length 返回的是當前ArrayList對象底層數組的容量
*/
public static int getCapacity(Object obj){

    int length = -1;
    Class c = obj.getClass();
    Field[] declaredFields = c.getDeclaredFields();
    for (Field declaredField : declaredFields) {
        if("elementData".equals(declaredField.getName())){
            //打開私有訪問
            declaredField.setAccessible(true);
            try {
                Object value = declaredField.get(obj);
                Object o[] = (Object[]) value;
                length = o.length;
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
    return length;
}

 

通過運行上面的代碼輸出結果如下:
可以從上面返回的結果可以看到當最初new ArrayList對象的時候底層的那個數據容量默認是0,當添加第一個元素後,它的容量設置爲了10,當繼續添加元素以至於超過了10元素後則需要進行再次擴容,這個時候的擴容按舊的容量的1.5倍進行擴容。
 
當我們再次把元素刪除後它的容量會不會縮小?ArrayList原碼如下:
public E remove(int index) {
    Objects.checkIndex(index, size);


    modCount++;
    E oldValue = elementData(index);


    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work


    return oldValue;
}

 

刪除的時候會先對底層數組的元素進行整理(如果刪除的不是最後一個則需要進行移動元素)
從上面的源碼可以看到--size表示size是減少了,但是底層數組的容量並不會減少
同樣使用自己測試類進行刪除操作後測試
public static void main(String[] args) {
    ArrayList arrayList = new ArrayList();


    System.out.println("在最開始創建ArrayList對象,它的底層數組容量是:" + getCapacity(arrayList));
    arrayList.add("張三");
    System.out.println("在添加了一個元素後ArrayList對象,它的底層數組容量是:" + getCapacity(arrayList));


    arrayList.add("張三");
    arrayList.add("張三");
    arrayList.add("張三");
    arrayList.add("張三");
    arrayList.add("張三");
    arrayList.add("張三");
    arrayList.add("張三");
    arrayList.add("張三");
    arrayList.add("張三");
    arrayList.add("張三"); //這是第11個添加的元素,已經超過10個元素了


    System.out.println("當添加第11個元素後,ArrayList做底層數組的擴容,擴容後的容量是:" + getCapacity(arrayList));


    arrayList.remove(0);
    System.out.println("刪除一個元素,此時元素只有" + arrayList.size() +",ArrayList做底層數組當前容量是:" + getCapacity(arrayList));


    arrayList.remove(0);
    System.out.println("刪除一個元素,此時元素只有" + arrayList.size() +",ArrayList做底層數組當前容量是:" + getCapacity(arrayList));


}


/**
* 通過反射的方式把ArrayList對象中的elementData數組拿出來看看現這個數組的長度
* @param obj 一個ArrayList的對象
* @return length 返回的是當前ArrayList對象底層數組的容量
*/
public static int getCapacity(Object obj){


    int length = -1;


    Class c = obj.getClass();
    Field[] declaredFields = c.getDeclaredFields();
    for (Field declaredField : declaredFields) {
        if("elementData".equals(declaredField.getName())){
            //打開私有訪問
            declaredField.setAccessible(true);
            try {
                Object value = declaredField.get(obj);
                Object o[] = (Object[]) value;
                length = o.length;


            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
    return length;
}

 

輸出的結果如下:
ArrayList中預留了一個方法(trimToSize)可以把底層數組的容量爲當前元素的大小 源碼 如下
public void trimToSize() {
    modCount++;
    if (size < elementData.length) {
        elementData = (size == 0)
          ? EMPTY_ELEMENTDATA
          : Arrays.copyOf(elementData, size);
    }
}

//再次調用自己的測試類進行驗證
public static void main(String[] args) {
    ArrayList arrayList = new ArrayList();


    System.out.println("在最開始創建ArrayList對象,它的底層數組容量是:" + getCapacity(arrayList));
    arrayList.add("張三");
    System.out.println("在添加了一個元素後ArrayList對象,它的底層數組容量是:" + getCapacity(arrayList));


    arrayList.add("張三");
    arrayList.add("張三");
    arrayList.add("張三");
    arrayList.add("張三");
    arrayList.add("張三");
    arrayList.add("張三");
    arrayList.add("張三");
    arrayList.add("張三");
    arrayList.add("張三");
    arrayList.add("張三"); //這是第11個添加的元素,已經超過10個元素了


    System.out.println("當添加第11個元素後,ArrayList做底層數組的擴容,擴容後的容量是:" + getCapacity(arrayList));


    arrayList.remove(0);
    System.out.println("刪除一個元素,此時元素只有" + arrayList.size() +",ArrayList做底層數組當前容量是:" + getCapacity(arrayList));


    arrayList.remove(0);
    System.out.println("刪除一個元素,此時元素只有" + arrayList.size() +",ArrayList做底層數組當前容量是:" + getCapacity(arrayList));


    arrayList.trimToSize();
    System.out.println("調用trimToSize方法後,此時元素只有" + arrayList.size() +",ArrayList做底層數組當前容量是:" + getCapacity(arrayList));
}


/**
* 通過反射的方式把ArrayList對象中的elementData數組拿出來看看現這個數組的長度
* @param obj 一個ArrayList的對象
* @return length 返回的是當前ArrayList對象底層數組的容量
*/
public static int getCapacity(Object obj){


    int length = -1;


    Class c = obj.getClass();
    Field[] declaredFields = c.getDeclaredFields();
    for (Field declaredField : declaredFields) {
        if("elementData".equals(declaredField.getName())){
            //打開私有訪問
            declaredField.setAccessible(true);
            try {
                Object value = declaredField.get(obj);
                Object o[] = (Object[]) value;
                length = o.length;


            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
    return length;
}

 

運行後輸出結果如下:
五、LinkedList
當我們去new一個LinkedList的時候它的無參構造函數如下:
public LinkedList() {
}

 

它的內部實現是鏈表,記錄了Node有兩個,一個頭部的一個尾部的
transient int size = 0;

/**
* Pointer to first node.
*/
transient Node<E> first;

/**
* Pointer to last node.
*/
transient Node<E> last;

//調用add方法,它默認添加的位置是在鏈表的最後
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++;
}

 

調用add的方法,此方法中我們指定添加元素的索引位置時
public void add(int index, E element) {
    checkPositionIndex(index); //檢查指定的索引位置是否合法

    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}

private void checkPositionIndex(int index) {
    if (!isPositionIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

private boolean isPositionIndex(int index) {
    return index >= 0 && index <= size;
}

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++;
}

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;
    }
}
remove方法,remove(int index)
public E remove(int index) {
    checkElementIndex(index); //檢查要刪除元素位置
    return unlink(node(index));
}
private void checkElementIndex(int index) {
    if (!isElementIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
    return index >= 0 && index < size;
}


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;
}

remove方法,remove(Object obj) 
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;
}

 

 
這裏要注意了,根據元素來進行刪除的時候它是從前往後找的,如果LinkedList中存在多個相同的元素,則只會刪除第一個匹配到的元素刪除,後面相同的元素並不會做刪除操作
 
六、HashMap
在JDK1.7及以前的版本
HashMap的底層實現是數組+鏈表,其結構類似如下:
數組的數據類型:Entry<k,v>,Entry實現了Map.Entry<k,v>,Entry<k,v>是HashMap的內部類類型
Entry<k,v>中不僅僅有key,value還有next,這裏的next又是一個Entry<k,v>
數組和名稱爲table
鏈表的存在意義
 計算出每一對映射關係的key的hash值,然後根據hash值決定存在table數組的哪一個位置(index)
兩個key的hash值相同,但是equals不一樣,這裏算出來的index會一樣
兩個key的hash值不一樣,equals出不一樣,但是是在運算後index也一樣
那麼當index中的值一樣的時候,但這時需要存儲多個值,這時就使用鏈表的方式把它們串起來
 
數組的初始化長度:16,在源碼中的寫法如下(注意:長度可以在new時進行手動指定,當指定的長度不是2的n次方則會進行糾正)
static final int DEFAULT_INITIAL_CAPACITY = 1<<4;

 

這裏也體現出長度必須是2的n次方(這裏也是因爲後面在計算存儲數據計算數組index時的位運算有關)
 
數組的擴容方式
擴容的原因是,如果數組不進行擴容,會導致每一個index下的鏈表會很長,在進行查找、添加時效率都會很低
擴容是的時機點:有一個變量:threshold閾值來判斷是否擴容
當變量的值達到臨界值時,則會考慮進行擴容
DEFAULT_LOAD_FACTOR:默認加載因子,它的值是0.75
threshold = table.length * DEFAULT_LOAD_FACTOR
默認:16*0.75 = 12,當size達到12個,則會考慮擴容
還有一個需要判斷當前添加(key,value)時:table[index] == null,是否成立,如果成立則本次暫時不做擴容否則會進行擴容
注意,當擴容後,後來數組中存放的鏈表的數據也會進行調整(會重新進行計算)
 
index值的計算方式
當key的值是null,則會直接把index置爲0
如果key的值不是null,通過key拿到hash值(它確保可以相對均勻的分散數值),再通過hashCode與table.length-1進行按位與計算index。
 
key是不可以重複的
如果key相等時,則會使用新的 元素進行替換
key的判斷:先判斷hash是否相等,如果相等,再判斷key的地址值 或 equals值是否相同,如果滿足同表示key相等
 
在存入元素(key,value)時,添加到table[index]時,發現table[index]不爲空,則會在鏈表上鍊接新的元素
鏈接的方式是新的元素會做爲table[index]的頭,而原來下面的元素會做爲新元素的next
 
JDK1.8後版本
底層實現:數組+鏈表/紅黑樹
這裏修改爲鏈表/紅黑樹的原因:
當鏈表比較長的時候,我們查找的效率會比較慢,爲了提高查詢的效率,那麼則把table[index]下的鏈表做調整
如果table[index]的鏈表的節點的個數少(8個以內),則保持鏈表結構;如果超過8個則考慮把鏈表轉爲紅黑樹
static final int TREEIFY_THRESHOLD = 8; //樹化的閾值,從鏈表轉爲紅黑樹的長度

 

紅黑樹:它是一個自平衡的二叉樹,它可以自動調整左右節點的個數,儘量保存節點在兩個是均勻
 
樹化的注意點:
當table[index]下的節點數達到8個不一定會做樹化,還要判斷是:table.length是否達到64,如果沒有達到則先做擴容不做樹化
static final int MIN_TREEIFY_CAPACITY = 64; //這是表示最小樹化的容量

 

 
注意:與jdk1.7的版本不一樣的是,它在新增元素時會做爲table[index]原來元素中的next
 
樹化是否會變化鏈表
當刪除結節點,當樹的節點個數少於6個,會把樹變成鏈表(要做反樹是因爲樹的結構複雜,當節點元數少時可以把複雜度降低)
static final int UNTREEIFY_THRESHOLD = 6;

 

 
使用put方法存儲元素時
 
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) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        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 {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    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;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

 

put的過程:
        index的計算
  1. 使用key的hashCode值調用hash()函數,干擾hash值,使得(key,value)更加均勻分佈table數組中(jdk1.8後的版本對hash()算法做了優化)
  2.  使用hash值與table.length-1進行按位與運算,保證index在[0,length-1]的範圍
 
        擴容
        第一種:當某個table[index]的鏈表個數達到8個,並且table.length<64,那麼會擴容爲鏈表(table.length>=64時會轉爲樹)
        第二種:當size>=threshold,並且table[index] !=null
            threshold = table.length*loadFator(這個的默認值是0.75)
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章