1. 程式人生 > 其它 >《面試補習》- Java集合知識梳理

《面試補習》- Java集合知識梳理

一、ArrayList

ArrayList 底層資料結構為 動態陣列 ,所以我們可以將之稱為陣列佇列。
ArrayList 的依賴關係:

public class ArrayList<E> extends AbstractList<E>
    	implements List<E>, RandomAccess, Cloneable, java.io.Serializable

從依賴關係可以看出,ArrayList 首先是一個列表,其次,他具有列表的相關功能,支援快速(固定時間)定位資源位置。可以進行拷貝操作,同時支援序列化。這裡我們需要重點關注的是 AbstractLit 以及 RandomAccess 。這個類,一個是定義了列表的基本屬性,以及確定我們列表中的常規動作。而RandomAccess 主要是提供了快速定位資源位置的功能。

1.1、ArrayList 成員變數

  /**
     * Default initial capacity.陣列預設大小
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     空佇列
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
        如果使用預設構造方法,則預設物件內容是該值
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
        用於儲存資料
     */
    transient Object[] elementData; 

     // 當前佇列有效資料長度
      private int size;

     // 陣列最大值
     private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

在ArrayList 的原始碼中,主要有上述的幾個成員變數:

  • elementData : 動態陣列,也就是我們儲存資料的核心陣列
  • DEFAULT_CAPACITY:陣列預設長度,在呼叫預設構造器的時候會有介紹
  • size:記錄有效資料長度,size()方法直接返回該值
  • MAX_ARRAY_SIZE:陣列最大長度,如果擴容超過該值,則設定長度為 Integer.MAX_VALUE

拓展思考:
EMPTY_ELEMENTDATADEFAULTCAPACITY_EMPTY_ELEMENTDATA 都是兩個空的陣列物件,他們到底有什麼區別呢?我們在下一節講解構造方法的時候,會做詳細對比。

1.2、構造方法

ArrayList 中提供了三種構造方法:

  • ArrayList()
  • ArrayList(int initialCapacity)
  • ArrayList(Collection c)

根據構造器的不同,構造方法會有所區別。我們在平常開發中,可能會出現在預設構造器內部呼叫了 ArrayList(int capacity) 這種方式,但是ArrayList 中對於不同的構造器的內部實現都有所區別,主要跟上述提到的成員變數有關。

1.2.1 ArrayList()

在原始碼給出的註釋中這樣描述:構造一個初始容量為十的空列表

    /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

從原始碼可以看到,它只是將 elementData 指向了 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的儲存地址,而 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 其實是一個空的陣列物件,那麼它為什麼說建立一個預設大小為10 的列表呢?

或者我們從別的角度思考一下,如果這個空的陣列,需要新增元素,會怎麼樣?

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  //確認內部容量
        elementData[size++] = e;
        return true;
    }
    
    private void ensureCapacityInternal(int minCapacity) {
        // 如果elementData 指向的是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的地址
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            設定預設大小 為DEFAULT_CAPACITY
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        //確定實際容量
        ensureExplicitCapacity(minCapacity);
    }
    
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // 如果超出了容量,進行擴充套件
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    
    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);
    }
    

上述程式碼塊比較長,這裡做個簡單的總結:

1、add(E e):新增元素,首先會判斷 elementData 陣列的長度,然後設定值

2、ensureCapacityInternal(int minCapacity):判斷 element 是否為空,如果是,則設定預設陣列長度

3、ensureExplicitCapacity(int minCapacity):判斷預期增長陣列長度是否超過當前容量,如果過超過,則呼叫grow()

4、grow(int minCapacity):對陣列進行擴充套件

回到剛才的問題:為什麼說建立一個預設大小為10 的列表呢?或許你已經找到答案了~

1.2.2 ArrayList(int initialCapacity)

根據指定大小初始化 ArrayList 中的陣列大小,如果預設值大於0,根據引數進行初始化,如果等於0,指向EMPTY_ELEMENTDATA 記憶體地址(與上述預設構造器用法相似)。如果小於0,則丟擲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);
        }
    }

拓展思考:為什麼這裡是用 EMPTY_ELEMENTDATA 而不是跟預設構造器一樣使用 DEFAULTCAPACITY_EMPTY_ELEMENTDATA ?有興趣的童鞋可以自己縣思考,經過思考的知識,才是你的~

1.2.3 ArrayList(Collection c)

將Collection<T> c 中儲存的資料,首先轉換成陣列形式(toArray()方法),然後判斷當前陣列長度是否為0,為 0 則只想預設陣列(EMPTY_ELEMENTDATA);否則進行資料拷貝。

    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

1.2.4 總結

上述的三個構造方法可以看出,其實每個構造器內部做的事情都不一樣,特別是預設構造器與 ArrayList(int initialCapacity) 這兩個構造器直接的區別 ,我們是需要做一些區別的。

  • ArrayList():指向 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,當列表使用的時候,才會進行初始化,會通過判斷是不是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 這個物件而設定陣列預設大小。
  • ArrayList(int initialCapacity):當 initialCapacity >0 的時候,設定該長度。如果 initialCapacity =0,則指向 EMPTY_ELEMENTDATA 在使用的時候,並不會設定預設陣列長度 。

因此 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 與 EMPTY_ELEMENTDATA 的本質區別就在於,會不會設定預設的陣列長度。

1.3、新增方法(Add)

ArrayList 添加了四種新增方法:

  • add(E element)
  • add(int i , E element)
  • addAll(Collection<? extends E> c)
  • addAll(int index, Collection<? extends E> c)

1.3.1 add(E element)

首先看add(T t)的原始碼:

  public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // 元素個數加一,並且確認陣列長度是否足夠 
        elementData[size++] = e;		//在列表最後一個元素後新增資料。
        return true;
    }

結合預設構造器或其他構造器中,如果預設陣列為空,則會在 ensureCapacityInternal()方法呼叫的時候進行陣列初始化。這就是為什麼預設構造器呼叫的時候,我們建立的是一個空陣列,但是在註釋裡卻介紹為 長度為10的陣列。

1.3.2 add(int i , T t)

   public void add(int index, E element) {
    // 判斷index 是否有效
        rangeCheckForAdd(index);
    // 計數+1,並確認當前陣列長度是否足夠
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index); //將index 後面的資料都往後移一位
        elementData[index] = element; //設定目標資料
        size++;
    }

這個方法其實和上面的add類似,該方法可以按照元素的位置,指定位置插入元素,具體的執行邏輯如下:

1)確保數插入的位置小於等於當前陣列長度,並且不小於0,否則丟擲異常

2)確保陣列已使用長度(size)加1之後足夠存下 下一個資料

3)修改次數(modCount)標識自增1,如果當前陣列已使用長度(size)加1後的大於當前的陣列長度,則呼叫grow方法,增長陣列

4)grow方法會將當前陣列的長度變為原來容量的1.5倍。

5)確保有足夠的容量之後,使用System.arraycopy 將需要插入的位置(index)後面的元素統統往後移動一位。

6)將新的資料內容存放到陣列的指定位置(index)上

1.3.3 addAll(Collection<? extends E> c)

    public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }

addAll() 方法,通過將collection 中的資料轉換成 Array[] 然後新增到elementData 陣列,從而完成整個集合資料的新增。在整體上沒有什麼特別之初,這裡的collection 可能會丟擲控制異常 NullPointerException 需要注意一下。

1.3.4 addAll(int index,Collection<? extends E> c)

 public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount

        int numMoved = size - index;
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);

        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }

與上述方法相比,這裡主要多了兩個步驟,判斷新增資料的位置是不是在末尾,如果在中間,則需要先將資料向後移動 collection 長度 的位置。

1.4、刪除方法(Remove)

ArrayList 中提供了 五種刪除資料的方式:

  • remove(int i)
  • remove(E element)
  • removeRange(int start,int end)
  • clear()
  • removeAll(Collection c)

1.4.1、remove(int i):

刪除資料並不會更改陣列的長度,只會將資料重陣列種移除,如果目標沒有其他有效引用,則在GC 時會進行回收。

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; // 讓指標最後指向空,進行垃圾回收
        return oldValue;
    }

1.4.2、remove(E element):

這種方式,會在內部進行 AccessRandom 方式遍歷陣列,當匹配到資料跟 Object 相等,則呼叫 fastRemove() 進行刪除

public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }
    

fastRemove( ):
fastRemove 操作與上述的根據下標進行刪除其實是一致的。

   private void fastRemove(int index) {
        modCount++;
        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
    }

1.4.3、removeRange(int fromIndex, int toIndex)

該方法主要刪除了在範圍內的資料,通過System.arraycopy 對整部分的資料進行覆蓋即可。

    protected void removeRange(int fromIndex, int toIndex) {
        modCount++;
        int numMoved = size - toIndex;
        System.arraycopy(elementData, toIndex, elementData, fromIndex,
                         numMoved);

        // clear to let GC do its work
        int newSize = size - (toIndex-fromIndex);
        for (int i = newSize; i < size; i++) {
            elementData[i] = null;
        }
        size = newSize;
    }

1.4.4、clear()

直接將整個陣列設定為 null ,這裡不做細述。

1.4.5、removeAll(Collection c)

主要通過呼叫:

    private boolean batchRemove(Collection<?> c, boolean complement) {
        //獲取陣列指標
        final Object[] elementData = this.elementData;
        int r = 0, w = 0;
        boolean modified = false;
        try {
            for (; r < size; r++)
                //根據 complement 進行判斷刪除或留下
                if (c.contains(elementData[r]) == complement)
                    elementData[w++] = elementData[r];
        } finally {
            // 進行資料整理
            if (r != size) {
                System.arraycopy(elementData, r,
                                 elementData, w,
                                 size - r);
                w += size - r;
            }
            if (w != size) {
                // clear to let GC do its work
                for (int i = w; i < size; i++)
                    elementData[i] = null;
                modCount += size - w;
                size = w;
                modified = true;
            }
        }
        return modified;
    }

在retainAll(Collection c)也有呼叫,主要作用分別為,刪除這個集合中所包含的元素和留下這個集合中所包含的元素。

拓展思考

清楚ArrayList 的刪除方法後,再結合我們常用的刪除方式,進行思考,到底哪些步驟會出問題,我們通常會選擇變數列表,如果匹配,則刪除。我們遍歷的方式有以下幾種:

避免 ConcurrentModificationException 的有效辦法是使用 Concurrent包下面的 CopyOnWriteArrayList ,後續會進行詳細分析

1.5、toArray()

ArrayList提供了2個toArray()函式:

  • Object[] toArray()
  • T[] toArray(T[] contents)

呼叫 toArray() 函式會丟擲“java.lang.ClassCastException”異常,但是呼叫 toArray(T[] contents) 能正常返回 T[]。

toArray() 會丟擲異常是因為 toArray() 返回的是 Object[] 陣列,將 Object[] 轉換為其它型別(如如,將Object[]轉換為的Integer[])則會丟擲“java.lang.ClassCastException”異常,因為Java不支援向下轉型。

toArray() 原始碼:

    public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }
    

1.6、subList()

如果我們在開發過程中有需要獲取集合中的某一部分的資料進行操作,我們可以通過使用SubList() 方法來進行獲取,這裡會建立ArrayList 的一個內部類 SubList()。

SubList 繼承了 AbstractList,並且實現了大部分的 AbstractList 動作。

需要注意的是,SubList 返回的集合中的某一部分資料,是會與原集合相關聯。即當我們對Sublist 進行操作的時候,其實還是會影響到原始集合。
我們來看一下 Sublist 中的 add 方法:

  	public void add(int index, E e) {
        rangeCheckForAdd(index);
            checkForComodification();
            parent.add(parentOffset + index, e);
            this.modCount = parent.modCount;
            this.size++;
        }

可以看到,Sublist 中的 加操作,其實還是呼叫了 parent(也就是原集合) 中的加操作。所以在使用subList方法時,一定要想清楚,是否需要對子集合進行修改元素而不影響原有的list集合。

總結

ArrayList總體來說比較簡單,不過ArrayList還有以下一些特點:

  • ArrayList自己實現了序列化和反序列化的方法,因為它自己實現了 private void writeObject(java.io.ObjectOutputStream s)和 private void readObject(java.io.ObjectInputStream s) 方法

  • ArrayList基於陣列方式實現,無容量的限制(會擴容)

  • 新增元素時可能要擴容(所以最好預判一下),刪除元素時不會減少容量(若希望減少容量,trimToSize()),刪除元素時,將刪除掉的位置元素置為null,下次gc就會回收這些元素所佔的記憶體空間。

  • 執行緒不安全

  • add(int index, E element):新增元素到陣列中指定位置的時候,需要將該位置及其後邊所有的元素都整塊向後複製一位

  • get(int index):獲取指定位置上的元素時,可以通過索引直接獲取(O(1))

  • remove(Object o)需要遍歷陣列

  • remove(int index)不需要遍歷陣列,只需判斷index是否符合條件即可,效率比remove(Object o)高

  • contains(E)需要遍歷陣列

  • 使用iterator遍歷可能會引發多執行緒異常

拓展思考

  • 拓展思考1、RandomAccess 介面是如何實現快速定位資源的?
  • 拓展思考2、EMPTY_ELEMENTDATA 與 DEFAULTCAPACITY_EMPTY_ELEMENTDATA的作用?
  • 拓展思考3、remove 方法存在的坑?
  • 拓展思考4:、ArrayList為什麼不是執行緒安全?

二、Vector

在介紹Vector 的時候,人們常說:

底層實現與 ArrayList 類似,不過Vector 是執行緒安全的,而ArrayList 不是。

那麼這句話定義的到底對不對呢?我們接下來結合上一篇文章進行分析:

Java 集合系列1、細思極恐之ArrayList

public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable

Vector 是一個向量佇列,它的依賴關係跟 ArrayList 是一致的,因此它具有一下功能:

  • 1、Serializable:支援物件實現序列化,雖然成員變數沒有使用 transient 關鍵字修飾,Vector 還是實現了 writeObject() 方法進行序列化。
  • 2、Cloneable:重寫了 clone()方法,通過 Arrays.copyOf() 拷貝陣列。
  • 3、RandomAccess:提供了隨機訪問功能,我們可以通過元素的序號快速獲取元素物件。
  • 4、AbstractList:繼承了AbstractList ,說明它是一個列表,擁有相應的增,刪,查,改等功能。
  • 5、List:留一個疑問,為什麼繼承了 AbstractList 還需要 實現List 介面?

*拓展思考:為什麼 Vector 的序列化,只重寫了 writeObject()方法?

細心的朋友如果在檢視 vector 的原始碼後,可以發現,writeObject() 的註釋中有這樣的說法:

This method performs synchronization to ensure the consistency
    of the serialized data.

看完註釋,可能會有一種恍然大悟的感覺,Vector 的核心思想不就是 執行緒安全嗎?那麼序列化過程肯定也要加鎖進行操作,才能過說其是執行緒安全啊。因此,即使沒有 elementData 沒有使用 transient 進行修飾,還是需要重寫writeObject()。

*拓展思考:與ArrayLit,以及大部分集合類相同,為什麼繼承了 AbstractList 還需要 實現List 介面?

有兩種說法,大家可以參考一下:

1、在StackOverFlow 中:傳送門
得票最高的答案的回答者說他問了當初寫這段程式碼的 Josh Bloch,得知這就是一個寫法錯誤。

2、Class類的getInterfaces 可以獲取到實現的介面,卻不能獲取到父類實現介面,但是這種操作無意義。

2、Vector 成員變數

    /**
        與 ArrayList 中一致,elementData 是用於儲存資料的。
     */
    protected Object[] elementData;

    /**
     * The number of valid components in this {@code Vector} object.
      與ArrayList 中的size 一樣,儲存資料的個數
     */
    protected int elementCount;

    /**
     * 設定Vector 的增長係數,如果為空,預設每次擴容2倍。
     *
     * @serial
     */
    protected int capacityIncrement;
    
     // 陣列最大值
     private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

與ArrayList 中的成員變數相比,Vector 少了兩個空陣列物件:
EMPTY_ELEMENTDATADEFAULTCAPACITY_EMPTY_ELEMENTDATA

因此,Vector 與 ArrayList 中的第一個不同點就是,成員變數不一致

2.1、Vector建構函式

Vector 提供了四種建構函式:

  • Vector():預設建構函式
  • Vector(int initialCapacity):capacity是Vector的預設容量大小。當由於增加資料導致容量增加時,每次容量會增加一倍。
  • Vector(int initialCapacity, int capacityIncrement):capacity是Vector的預設容量大小,capacityIncrement是每次Vector容量增加時的增量值。
  • Vector(Collection<? extends E> c):建立一個包含collection的Vector

乍一眼看上去,Vector 中提供的建構函式,與ArrayList 中的一樣豐富。但是在上一節內容 中分析過 ArrayList 的建構函式後,再來看Vector 的建構函式,會覺得有一種索然無味的感覺。

    //預設建構函式
    public Vector() {
        this(10);
    }
    
    //帶初始容量建構函式
    public Vector(int initialCapacity) {
        this(initialCapacity, 0);
    }
    
    //帶初始容量和增長係數的建構函式
    public Vector(int initialCapacity, int capacityIncrement) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
        this.capacityIncrement = capacityIncrement;
    }

程式碼看上去沒有太多的問題,跟我們平時寫的程式碼一樣,只是與ArrayList 中的建構函式相比 缺少了一種韻味。有興趣的同學可以去看一下ArrayList 中的建構函式實現。

    public Vector(Collection<? extends E> c) {
        elementData = c.toArray();
        elementCount = elementData.length;
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, elementCount, Object[].class);
    }

JDK 1.2 之後提出了將Collection 轉換成 Vector 的建構函式,實際操作就是通過Arrays.copyOf() 拷貝一份Collection 陣列中的內容到Vector 物件中。這裡會有可能丟擲 NullPointerException

在建構函式上面的對比:Vector 的建構函式的設計上輸於 ArrayList。

2.2、新增方法(Add)

Vector 在新增元素的方法上面,比ArrayList 中多了一個方法。Vector 支援的add 方法有:

  • add(E)
  • addElement(E)
  • add(int i , E element)
  • addAll(Collection<? extends E> c)
  • addAll(int index, Collection<? extends E> c)

2.2.1 addElement(E)

我們看一下這個多出來的 addElement(E) 方法 有什麼特殊之處:

    /**
     * Adds the specified component to the end of this vector,
     * increasing its size by one. The capacity of this vector is
     * increased if its size becomes greater than its capacity.
     *
     * <p>This method is identical in functionality to the
     * {@link #add(Object) add(E)}
     * method (which is part of the {@link List} interface).
     *
     * @param   obj   the component to be added
     */
    public synchronized void addElement(E obj) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = obj;
    }

從註釋上面來看,這個方法就是 跟 add(E) 方法是有著一樣的功能的。因此除了返回資料不同外,也沒什麼特殊之處了。

我們順著上述程式碼來進行分析 Vector 中的新增方法。可以看到 Vector 對整個add 方法都上鎖了(添加了 synchronized 修飾),其實我們可以理解,在新增元素的過程主要包括以下幾個操作:

  • ensureCapacityHelper():確認容器大小
  • grow():如果有需要,進行容器擴充套件
  • elementData[elementCount++] = obj:設值

為了避免多執行緒情況下,在 ensureCapacityHelper 容量不需要拓展的情況下,其他執行緒剛好將陣列填滿了,這時候就會出現 ArrayIndexOutOfBoundsException ,因此對整個方法上鎖,就可以避免這種情況出現。

與ArrayList 中對比,確認容器大小這一步驟中,少了 ArrayList#ensureCapacityInternal 這一步驟,主要也是源於 Vector 在構造時,以及建立好預設陣列大小,不會出現陣列為空的情況。

其次 grow() 方法中:

    private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        //區別與ArrayList 中的位運算,這裡支援自定義增長係數
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

Vector 中支援自定義的增長係數,也是它在 add() 方法中為數不多的亮點了。

2.2.2 add(int index, E element)

這部分程式碼跟ArrayList 中沒有太多的差異,主要是丟擲的異常有所不同,ArrayList 中丟擲的是IndexOutOfBoundsException。這裡則是丟擲 ArrayIndexOutOfBoundsException。至於為什麼需要將操作抽取到 insertElementAt() 這個方法中呢?童鞋們可以進行相關思考。

    /**
     * @throws ArrayIndexOutOfBoundsException if the index is out of range
     *         ({@code index < 0 || index > size()})
     * @since 1.2
     */
    public void add(int index, E element) {
        insertElementAt(element, index);
    }
    
    public synchronized void insertElementAt(E obj, int index) {
        modCount++;
        if (index > elementCount) {
            throw new ArrayIndexOutOfBoundsException(index
                                                     + " > " + elementCount);
        }
        ensureCapacityHelper(elementCount + 1);
        System.arraycopy(elementData, index, elementData, index + 1, elementCount - index);
        elementData[index] = obj;
        elementCount++;
    }
    

在新增方法上面,Vector 與ArrayList 大同小異。Vector 多了一個奇怪的 addElement(E)。

2.3、刪除方法(Remove)

Vecotr 中提供了比較多的刪除方法,但是隻要檢視一下原始碼,就可以發現其實大部分都是相同的方法。

  • remove(int location)
  • remove(Object object)
  • removeAll(Collection<?> collection)
  • removeAllElements()
  • removeElement(Object object)
  • removeElementAt(int location)
  • removeRange(int fromIndex, int toIndex)
  • clear()

2.3.1、remove(int location) & removeElementAt(int location)

對比一下 remove(int location)removeElementAt(int location)

public synchronized void removeElementAt(int index) {
        modCount++;
        if (index >= elementCount) {
            throw new ArrayIndexOutOfBoundsException(index + " >= " +
                                                     elementCount);
        }
        else if (index < 0) {
            throw new ArrayIndexOutOfBoundsException(index);
        }
        int j = elementCount - index - 1;
        if (j > 0) {
            System.arraycopy(elementData, index + 1, elementData, index, j);
        }
        elementCount--;
        elementData[elementCount] = null; /* to let gc do its work */
    }


public synchronized E remove(int index) {
        modCount++;
        if (index >= elementCount)
            throw new ArrayIndexOutOfBoundsException(index);
        E oldValue = elementData(index);

        int numMoved = elementCount - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--elementCount] = null; // Let gc do its work

        return oldValue;
    }

除了返回的資料型別不同,其他內部操作其實是一致的。remove 是重寫了父類的操作,而removeElement 則是Vector 中自定義的方法。ArrayList 中提供了 fastRemove() 方法,與其有著同樣的效果,不過removeElement 作用範圍為public。

2.3.2、remove(Object object) & removeElement(Object object)

    public boolean remove(Object o) {
        return removeElement(o);
    }
    
    public synchronized boolean removeElement(Object obj) {
        modCount++;
        int i = indexOf(obj);
        if (i >= 0) {
            removeElementAt(i);
            return true;
        }
        return false;
    }
    
    

remove(Object object) 實際內部呼叫的就是 removeElement(Object object) 。刪除操作首先找到 物件的索引(與ArrayList 中的remmove(E)一樣),然後呼叫removeElementAt(i)(ArrayList 中呼叫 fastRemove()方法)進行刪除。

其餘刪除操作與ArrayList 類似,這裡不做詳細解析。總體來說,在刪除方法這一塊的話,Vector 與ArrayList 也是大同小異。

2.4、執行緒安全 Vector?

拓展思考,我們常說Vector 是執行緒安全的陣列列表,那麼它到底是不是無時無刻都是執行緒安全的呢?在StackOverFlow 中有這樣一個問題:

StackOverFlow 傳送門

Is there any danger, if im using one Vector(java.util.Vector) on my server program when im accessing it from multiple threads only for reading? (myvector .size() .get() ...) For writing im using synchronized methods. Thank you.

其中有一個答案解析的比較詳細的:

Vector 中的每一個獨立方法都是執行緒安全的,因為它有著 synchronized 進行修飾。但是如果遇到一些比較複雜的操作,並且多個執行緒需要依靠 vector 進行相關的判斷,那麼這種時候就不是執行緒安全的了。

if (vector.size() > 0) {
    System.out.println(vector.get(0));
}

如上述程式碼所示,Vector 判斷完 size()>0 之後,另一執行緒如果同時清空vector 物件,那麼這時候就會出現異常。因此,在複合操作的情況下,Vector 並不是執行緒安全的。

總結

本篇文章標題是:百密一疏之Vector,原因在於,如果我們沒有詳細去了解過Vector,或者在面試中,常常會認為Vector 是執行緒安全的。但是實際上 Vector 只是在每一個單一方法操作上是執行緒安全的。

總結一下與ArrayList 之間的差異:

  • 1、建構函式,ArrayList 比Vector 稍有深度,Vector 預設陣列長度為10,建立是設定。
  • 2、擴容方法 grow(),ArrayList 通過位運算進行擴容,而Vector 則通過增長係數(建立是設定,如果過為空,則增長一倍)
  • 3、Vector 方法呼叫是執行緒安全的。
  • 4、成員變數有所不同

三、LinkedList

我們在前面的文章中已經介紹過 List 大家族中的 ArrayListVector 這兩位猶如孿生兄弟一般,從底層實現,功能都有著相似之處,除了一些個人行為不同(成員變數,建構函式和方法執行緒安全)。接下來,我們將會認識一下他們的另一位功能強大的兄弟:LinkedList

LinkedList 的依賴關係:

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
  • 1、繼承於 AbstractSequentialList ,本質上面與繼承 AbstractList 沒有什麼區別,AbstractSequentialList 完善了 AbstractList 中沒有實現的方法。
  • 2、Serializable:成員變數 Node 使用 transient 修飾,通過重寫read/writeObject 方法實現序列化。
  • 3、Cloneable:重寫clone()方法,通過建立新的LinkedList 物件,遍歷拷貝資料進行物件拷貝。
  • 4、Deque:實現了Collection 大家庭中的佇列介面,說明他擁有作為雙端佇列的功能。

eng~從上述實現介面來看,LinkedList 與 ArrayList 之間在整體上面的區別在於,LinkedList 實現了 Collection 大家庭中的Queue(Deque)介面,擁有作為雙端佇列的功能。(就好比一個小孩子,他不僅僅有父母的特性,他們有些人還會有舅舅的一些特性,好比 外甥長得像舅舅一般)。

3.1、LinkedList 成員變數

    transient int size = 0;

    /**
     * Pointer to first node.
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     */
    transient Node<E> first;

    /**
     * Pointer to last node.
     * Invariant: (first == null && last == null) ||
     *            (last.next == null && last.item != null)
     */
    transient Node<E> last;
    
    
    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;
        }
    }

LinkedList 的成員變數主要由 size(資料量大小),first(頭節點)和last(尾節點)。結合資料結構中雙端連結串列的思想,每個節點需要擁有,儲存資料(E item),指向下一節點(Node next )和指向上一節點(Node prev)。

LinkedList 與ArrayLit、Vector 的成員變數對比中,明顯沒有提供 MAX_ARRAY_SIZE 這一個最大值的限定,這是由於連結串列沒有長度限制的原因,他的記憶體地址不需要分配固定長度進行儲存,只需要記錄下一個節點的儲存地址即可完成整個連結串列的連續。

拓展思考: LinkedList 中 JDK 1.8 與JDK 1.6 有哪些不同?

主要不同為,LinkedList 在1.6 版本以及之前,只通過一個 header 頭指標儲存佇列頭和尾。這種操作可以說很有深度,但是從程式碼閱讀性來說,卻加深了閱讀程式碼的難度。因此在後續的JDK 更新中,將頭節點和尾節點 區分開了。節點類也更名為 Node。

3.2、LinkedList 建構函式

LinkedList 只提供了兩個建構函式:

  • LinkedList()
  • LinkedList(Collection<? extends E> c)

在JDK1.8 中,LinkedList 的建構函式 LinkedList() 是一個空方法,並沒有提供什麼特殊操作。區別於 JDK1.6 中,會初始化 header 為一個空的指標物件。

3.2.1 LinkedList()

JDK 1.6

private transient Entry<E> header = new Entry<E>(null, null, null);
    public LinkedList() {
        header.next = header.previous = header;
    }

JDK 1.8
在使用的時候,才會建立第一個節點。

    public LinkedList() {
    }

3.2.2 LinkedList(Collection<? extends E> c)

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

這一構造方法主要通過 呼叫addAll 進行建立物件,在介紹LinkedList 新增方法的時候再進行細述。

3.2.3 小結

LinkedList 在新版本的實現中,除了區分了頭節點和尾節點外,更加註重在使用時進行記憶體分配,這裡跟ArrayList 類似(ArrayList 預設構造器是建立一個空的陣列物件)。

4、新增方法(Add)

LinkedList 繼承了 AbstractSequentialList(AbstractList),同時實現了Deque 介面,因此,他在新增方法 這一塊,包含了兩者的操作:

AbstractSequentialList:

  • add(E e)
  • add(int index,E e)
  • addAll(Collection<? extends E> c)
  • addAll(int index, Collection<? extends E> c)

Deque

  • addFirst(E e)
  • addLast(E e)
  • offer(E e)
  • offerFirst(E e)
  • offerLast(E e)

4.1 add(E e) & addLast(E e) & offer(E e) & offerLast(E e)

雖然 LinkedList 分別實現了List 和 Deque 的新增方法,但是在某種意義上,這些方法其實都是有共性的。例如,我們呼叫add(E e) 方法,不管是ArrayList 或 Vector 等列表,都是預設在陣列末尾進行新增,因此與 佇列中在末尾新增節點 addLast(E e) 是有著一樣的韻味的。所以,從LinkedList 的原始碼中,這幾個方法,底層操作其實是一致的。

    public boolean add(E e) {
        linkLast(e);
        return true;
    }
    
    public void addLast(E e) {
        linkLast(e);
    }
    
     public boolean offer(E e) {
        return add(e);
    }
    
    public boolean offerLast(E e) {
        addLast(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++;
    }

我們主要分析一下 linkLast 這個方法:

  • 獲取尾節點(last)
  • 建立插入節點,並且設定上一節點為 last,下一節點為 null。
  • 設定新節點為末尾節點(last)
  • 如果 l(初始末尾節點)==null,說明這是第一次操作,新加入的為頭節點
  • 否則,設定 l(初始末尾節點)的下一節點為新加入的節點
  • size + 1,操作計數 + 1

拓展思考:為什麼內部變數 Node l 需要使用 final 進行修飾?

4.2 addFirst(E e) & offerFirst(E e)

    public boolean offerFirst(E e) {
        addFirst(e);
        return true;
    }
    
    public void addFirst(E e) {
        linkFirst(e);
    }

    private void linkFirst(E e) {
        final Node<E> f = first;
        final Node<E> newNode = new Node<>(null, e, f);
        first = newNode;
        if (f == null)
            last = newNode;
        else
            f.prev = newNode;
        size++;
        modCount++;
    }

從上述程式碼可以看出,offerFirst 和addFirst 其實都是一樣的操作,只是返回的資料型別不同。而 linkFirst 方法,則與 linkLast 其實是一樣的思想,這裡也不做細述。

4.3 add(int index,E e)

這裡我們主要講一下,為什麼LinkedList 在新增、刪除元素這一方面優於 ArrayList。

    public void add(int index, E element) {
        checkPositionIndex(index);
        // 如果插入節點為末尾,直接插入
        if (index == size)
            linkLast(element);
        // 否則,找到該節點,進行插入
        else
            linkBefore(element, node(index));
    }
    
    Node<E> node(int index) {
        // 這裡順序查詢元素,通過二分查詢的方式,決定從頭或尾節點開始進行查詢,時間複雜度為 n/2
        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++;
    }

LinkedList 在 add(int index,Element e)方法的流程

  • 判斷下標有效性
  • 如果插入位置為末尾,直接插入
  • 否則,遍歷1/2的連結串列找到 index 下標的節點
  • 通過 succ 設定新節點的前,後節點

LinkedList 在插入資料之所以會優於ArrayList,主要是由於在插入資料這一環節(linkBefore),插入計算只需要設定節點的前,後節點即可,而ArrayList 則需要將整個陣列的資料進行後移(

System.arraycopy(elementData, index, elementData, index + 1,size - index);

4.4 addAll(Collection<? extends E> c)

LinkedList 中提供的兩個addAll 方法中,其實內部實現也是一樣的,主要通過:
addAll(int index, Collection<? extends E> c)進行實現:

    public boolean addAll(Collection<? extends E> c) {
        return addAll(size, c);
    }
    
      public boolean addAll(int index, Collection<? extends E> c) {
        checkPositionIndex(index);
        //將集合轉化為陣列
        Object[] a = c.toArray();
        int numNew = a.length;
        if (numNew == 0)
            return false;

        Node<E> pred, succ;
        //獲取插入節點的前節點(prev)和尾節點(next)
        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;
        }
        //將 Collection 的連結串列插入 LinkedList 中。
        if (succ == null) {
            last = pred;
        } else {
            pred.next = succ;
            succ.prev = pred;
        }

        size += numNew;
        modCount++;
        return true;
    }
    

4.5 小結

LinkedList 在插入資料優於ArrayList ,主要是因為他只需要修改指標的指向即可,而不需要將整個陣列的資料進行轉移。而LinkedList 優於沒有實現 RandomAccess,或者說 不支援索引搜尋的原因,他在查詢元素這一操作,需要消耗比較多的時間進行操作(n/2)。

5、刪除方法(Remove)

AbstractSequentialList

  • remove(int index)
  • remove(Object o)

Deque

  • remove()
  • removeFirst()
  • removeLast()
  • removeFirstOccurrence(Object o)
  • removeLastOccurrence(Object o)

5.1 remove(int index)&remove(Object o)

在 ArrayList 中,remove(Object o) 方法,是通過遍歷陣列,找到下標後,通過fastRemove(與 remove(int i) 類似的操作)進行刪除。而LinkedList,則是遍歷連結串列,找到目標節點(node),通過 unlink 進行刪除:
我們這裡主要來看看 unlink 方法:

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

整個過程為:

  • 獲取目標節點的 next、prev
  • 如果prev 為空,說明目標節點為頭節點
  • 設定first 為目標節點的下一節點(next)
  • 否則設定prev節點的下一節點為next(即將自己重連結串列中剔除)
  • 如果 next 為空,說明目標節點為尾節點
  • 設定last 為目標節點的上一節點
  • 否則,設定next節點的上一節點為prev
  • 將目標節點設定為null

可以看到,刪除方法與新增方法類似,只需要修改節點關係即可,避免了類似於ArrayList 的陣列平移情況,大大減少了時間損耗。

5.2 Deque 中的Remove

Deque 中的 removeFirstOccurrence 和 removeLastOccurrence 主要過程為,首先從first/last 節點開始遍歷,當發現第一個目標物件,則低哦啊用remove(Object o) 進行刪除物件。總體上沒有什麼特別之處。

稍有不同的是Deque 中的removeFirst()和removeLast()方法,在底層實現上面,由於明確知道刪除的物件為first/last物件,因此在刪除操作上面 會更加簡單:

    public E removeFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return unlinkFirst(f);
    }

    private E unlinkFirst(Node<E> f) {
        // assert f == first && f != null;
        final E element = f.item;
        final Node<E> next = f.next;
        f.item = null;
        f.next = null; // help GC
        first = next;
        if (next == null)
            last = null;
        else
            next.prev = null;
        size--;
        modCount++;
        return element;
    }

整體操作為,將first 節點的next 設定為新的頭節點,然後將 f 清空。 removeLast 操作也類似。

結合佇列的思想,removeFirst 和removeLast 都會返回 資料 E,相當於我們的出列操作(pollFirst/pollLast

6 LinkedList 雙端連結串列

我們之所以說LinkedList 為雙端連結串列,是因為他實現了Deque 介面,支援佇列的一些操作,我們來看一下有哪些方法實現:

  • pop()
  • poll()
  • push()
  • peek()
  • offer()

可以看到Deque 中提供的方法主要有上述的幾個方法,接下來我們來看看在LinkedList 中是如何實現這些方法的。

6.1 pop() & poll()

LinkedList#pop 的原始碼:

    public E pop() {
       return removeFirst();
   }
       public E removeFirst() {
       final Node<E> f = first;
       if (f == null)
           throw new NoSuchElementException();
       return unlinkFirst(f);
   }

從上述程式碼可以看出,Pop() 的操作為,佇列頭部元素出佇列,如果過first 為空 會丟擲異常。

LinkedList#poll 的原始碼:

    public E poll() {
       final Node<E> f = first;
       return (f == null) ? null : unlinkFirst(f);
   }

對比 pop 和poll 的原始碼可以看到,雖然同樣是 first 出列,不同的是,如果first 為null, pop()方法會丟擲異常

6.2 push()

push() 方法的底層實現,其實就是呼叫了 addFirst(Object o):

    public void push(E e) {
       addFirst(e);
   }

push()方法的操作,主要跟 棧(Stack) 中的入棧操作類似。

6.3 peek()

LinkedList#peek 操作主要為,將取佇列頭部元素的值(根據佇列的 FIFO,peek為取頭部資料)

    public E peek() {
       final Node<E> f = first;
       return (f == null) ? null : f.item;
   }
   

6.4 offer()

offer()方法為直接呼叫新增方法。

    public boolean offer(E e) {
       return add(e);
   }

7 LinkedList 遍歷

LinkedList 由於沒有實現 RandomAccess,因此,在以隨機訪問的形式進行遍歷時效果會非常低下。除此之外,LinkedList 提供了類似於通過Iterator 進行遍歷,節點的prev 或 next 進行遍歷,還有for迴圈遍歷,都有不錯的效果。

四、HashMap

在接下來主要為大家介紹一下Java 集合家庭中另一小分隊 Map ,我們先來看看 Map 家庭的整體架構:

我們主要介紹一下HashMap:

HashMap 的依賴關係:

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable 
  • 1、AbstractMap:表明它是一個散列表,基於Key-Value 的儲存方式
  • 2、Cloneable:支援拷貝功能
  • 3、Seriablizable:重寫了write/readObject,支援序列化

從依賴關係上面來看,HashMap 並沒有 List 集合 那麼的複雜,主要是因為在迭代上面,HashMap 區別 key-value 進行迭代,而他們的迭代又依賴與keySet-valueSet 進行,因此,雖然依賴關係上面HashMap 看似簡單,但是內部的依賴關係更為複雜。

4.1、HashMap 成員變數

預設 桶(陣列) 容量 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;

負載因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;

連結串列轉樹 大小
static final int TREEIFY_THRESHOLD = 8;

樹轉連結串列 大小
static final int UNTREEIFY_THRESHOLD = 6;

最小轉紅黑樹容量
static final int MIN_TREEIFY_CAPACITY = 64;

儲存資料節點
static class Node<K,V> implements Map.Entry<K,V> 

節點陣列
transient Node<K,V>[] table;

資料容量
transient int size;

操作次數
transient int modCount;

擴容大小
int threshold;

對比於JDK8之前的HashMap ,成員變數主要的區別在於多了紅黑樹的相關變數,用於標示我們在什麼時候進行 list -> Tree 的轉換。

附上Jdk8 中HashMap 的資料結構展示圖:

4.2、HashMap 建構函式

HashMap 提供了四種建構函式:

  • HashMap():預設建構函式,引數均使用預設大小
  • HashMap(int initialCapacity):指定初始陣列大小
  • HashMap(int initialCapacity, float loadFactor):指定初始陣列大小,載入因子
  • HashMap(Map<? extends K, ? extends V> m):建立新的HashMap,並將 m 中內容存入HashMap中

4.3、HashMap Put 過程

接下來我們主要講解一下,HashMap 在JDK8中的新增資料過程(引用):

4.3.1、put(K key, V value)

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

上述方法是我們在開發過程中最常使用到的方法,但是卻很少人知道,其實內部真正呼叫的方法是這個putVal(hash(key), key, value, false, true) 方法。這裡稍微介紹一下這幾個引數:

  • hash 值,用於確定儲存位置
  • key:存入鍵值
  • value:存入資料
  • onlyIfAbsent:是否覆蓋原本資料,如果為true 則不覆蓋
  • onlyIfAbsent:table 是否處於建立模式
4.3.1.1 hash(Object key)
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

這裡的Hash演算法本質上就是三步:取key的hashCode值、高位運算、取模運算。
這裡引用一張圖,易於大家瞭解相關機制


這裡可能會比較疑惑,為什麼需要對自身的hashCode 進行運算,這麼做可以在陣列table 比較小的時候,讓高位bit 也能參與到hash 運算中,同時不會又太大的開銷。

4.3.2、putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)

由於原始碼篇幅過長,這裡我進行分開講解,同學們可以對照原始碼進行閱讀

4.3.2.1 宣告成員變數(第一步)
Node<K,V>[] tab; Node<K,V> p; int n, i;

第一部分主要縣宣告幾個需要使用到的成員變數:

  • tab:對應table 用於儲存資料
  • p:我們需要儲存的資料,將轉化為該物件
  • n:陣列(table) 長度
  • i:陣列下標
4.3.2.2 Table 為 null,初始化Table(第二步)

table 為空說明當前操作為第一次操作,通過上面建構函式的閱讀,我們可以瞭解到,我們並沒有對table 進行初始化,因此在第一次put 操作的時候,我們需要先將table 進行初始化。

        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

從上述程式碼可以看到,table 的初始化和擴容,都依賴於 resize() 方法,在後面我們會對該方法進行詳細分析。

4.3.2.3 Hash碰撞確認下標(True)
 if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

在上一步我們以及確認當前table不為空,然後我們需要計算我們物件需要儲存的下標了。

如果該下標中並沒有資料,我們只需建立一個新的節點,然後將其存入 tab[] 即可。

4.3.2.4 Hash碰撞確認下標(False)

與上述過程相反,Hash碰撞結果後,發現該下標有儲存元素,將其儲存到變數 p = tab[i = (n - 1) & hash] ,現在 p 儲存的是目標陣列下標中的元素。如上圖所示(引用):

4.3.2.4.1 key 值相同覆蓋

在獲取到 p 後,我們首先判斷它的 key 是否與我們這次插入的key 相同,如果相同,我們將其引用傳遞給 e

if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
4.3.2.4.2 紅黑樹節點處理
else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

由於在JDK 8後,會對過長的連結串列進行處理,即 連結串列 -> 紅黑樹,因此對應的節點也會進行相關的處理。紅黑樹的節點則為TreeNode,因此在獲取到p後,如果他跟首位元素不匹配,那麼他就有可能為紅黑樹的內容。所以進行putTreeVal(this, tab, hash, key, value) 操作。該操作的原始碼,將會在後續進行細述。

4.3.2.4.3 連結串列節點處理
        else {
            //for 迴圈遍歷連結串列,binCount 用於記錄長度,如果過長則進行樹的轉化
                for (int binCount = 0; ; ++binCount) {
                // 如果發現p.next 為空,說明下一個節點為插入節點
                    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
                    p = e;
                }
            }

連結串列遍歷處理,整個過程就是,遍歷所有節點,當發現如果存在key 與插入的key 相同,那麼退出遍歷,否則在最後插入新的節點。判斷連結串列長度是否大於8,大於8的話把連結串列轉換為紅黑樹,在紅黑樹中執行插入操作,否則進行連結串列的插入操作;遍歷過程中若發現key已經存在直接覆蓋value即可;

4.3.2.4.3 判斷是否覆蓋
        if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }

如果 e 不為空,說明在校驗 key 的hash 值,發現存在相同的 key,那麼將會在這裡進行判斷是否對其進行覆蓋。

4.3.2.5 容量判斷
        if (++size > threshold)
            resize();

如果 size 大於 threshold 則進行擴容處理。

4.4、Resize()擴容

在上面的建構函式,和 put 過程都有呼叫過 resize() 方法,那麼,我們接下來將會分析一下 resize()過程。由於JDK 8引入了紅黑樹,我們先從JDK 7開始閱讀 resize() 過程。下面部分內容參考:傳送門

4.4.1 JDK 7 resize()

JDK 7 中,擴容主要分為了兩個步驟:

  • 容器擴充套件
  • 內容拷貝
4.4.1.1 容器擴充套件
 1 void resize(int newCapacity) {   //傳入新的容量
 2     Entry[] oldTable = table;    //引用擴容前的Entry陣列
 3     int oldCapacity = oldTable.length;         
 4     if (oldCapacity == MAXIMUM_CAPACITY) {  //擴容前的陣列大小如果已經達到最大(2^30)了
 5         threshold = Integer.MAX_VALUE; //修改閾值為int的最大值(2^31-1),這樣以後就不會擴容了
 6         return;
 7     }
 8  
 9     Entry[] newTable = new Entry[newCapacity];  //初始化一個新的Entry陣列
10     transfer(newTable);                         //!!將資料轉移到新的Entry數組裡
11     table = newTable;                           //HashMap的table屬性引用新的Entry陣列
12     threshold = (int)(newCapacity * loadFactor);//修改閾值
13 }
4.4.1.2 內容拷貝
 1 void transfer(Entry[] newTable) {
 2     Entry[] src = table;                   //src引用了舊的Entry陣列
 3     int newCapacity = newTable.length;
 4     for (int j = 0; j < src.length; j++) { //遍歷舊的Entry陣列
 5         Entry<K,V> e = src[j];             //取得舊Entry陣列的每個元素
 6         if (e != null) {
 7             src[j] = null;//釋放舊Entry陣列的物件引用(for迴圈後,舊的Entry陣列不再引用任何物件)
 8             do {
 9                 Entry<K,V> next = e.next;
10                 int i = indexFor(e.hash, newCapacity); //!!重新計算每個元素在陣列中的位置
11                 e.next = newTable[i]; //標記[1]
12                 newTable[i] = e;      //將元素放在陣列上
13                 e = next;             //訪問下一個Entry鏈上的元素
14             } while (e != null);
15         }
16     }
17 }
4.4.1.3 擴容過程展示(引用)

下面舉個例子說明下擴容過程。假設了我們的hash演算法就是簡單的用key mod 一下表的大小(也就是陣列的長度)。其中的雜湊桶陣列table的size=2, 所以key = 3、7、5,put順序依次為 5、7、3。在mod 2以後都衝突在table[1]這裡了。這裡假設負載因子 loadFactor=1,即當鍵值對的實際大小size 大於 table的實際大小時進行擴容。接下來的三個步驟是雜湊桶陣列 resize成4,然後所有的Node重新rehash的過程。

4.4.2 JDK 8 resize()

由於擴容部分程式碼篇幅比較長,童鞋們可以對比著部落格與原始碼進行閱讀。
與上述流程相似,JDK 8 中擴容過程主要分成兩個部分:

  • 容器擴充套件
  • 內容拷貝
4.4.2.1 容器擴充套件
        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) // initial capacity was placed in threshold
            //如果閥值不為空,那麼將容量設定為當前閥值
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            //如果陣列長度與閥值為空,建立一個預設長度的陣列長度
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        
        // 第二步,建立新陣列
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;

從上面的流程分析,我們可以看到在 JDK 8 HashMap 中,開始使用位運算進行擴容計算,主要優點將會在後續資料拷貝中具體表現。

4.4.2.2 內容拷貝

在上述容器擴容結束後,如果發現 oldTab 不為空,那麼接下來將會進行內容拷貝:

    if (oldTab != null) {
            //對舊陣列進行遍歷
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                //
                if ((e = oldTab[j]) != null) {
                    //將舊陣列中的內容清空
                    oldTab[j] = null;
                    //如果 e 沒有後續內容,只處理當前值即可
                    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 { // preserve order
                        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;
                            }
                            //高位與運算,確認索引為 願索引+ oldCap
                            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;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }

內容拷貝,在JDK 8 中優化,主要是:

  • 通過高位與運算確認儲存地址
  • 連結串列不會出現導致,JDK 8 通過建立新連結串列方式進行轉移

我們來看一下 JDK 8 是如何通過高位與運算確認儲存位置的:

4.5、小結

HashMap中,如果key經過hash演算法得出的陣列索引位置全部不相同,即Hash演算法非常好,那樣的話,getKey方法的時間複雜度就是O(1),如果Hash演算法技術的結果碰撞非常多,假如Hash算極其差,所有的Hash演算法結果得出的索引位置一樣,那樣所有的鍵值對都集中到一個桶中,或者在一個連結串列中,或者在一個紅黑樹中,時間複雜度分別為O(n)和O(lgn)。

(1) 擴容是一個特別耗效能的操作,所以當程式設計師在使用HashMap的時候,估算map的大小,初始化的時候給一個大致的數值,避免map進行頻繁的擴容。

(2) 負載因子是可以修改的,也可以大於1,但是建議不要輕易修改,除非情況非常特殊。

(3) HashMap是執行緒不安全的,不要在併發的環境中同時操作HashMap,建議使用ConcurrentHashMap。

(4) JDK1.8引入紅黑樹大程度優化了HashMap的效能。

(5) 還沒升級JDK1.8的,現在開始升級吧。HashMap的效能提升僅僅是JDK1.8的冰山一角。

參考

五、總結

沒有太多的拓展思考,腦子不夠清晰,總體來說,List 介面下面的小家庭的原始碼以及分析完了。對每一個成員都有了進一步的瞭解,面試的時候,也不會再簡單的回答,linkedList 插入刪除效能比較好,ArrayList 能過快速定位元素,Vector 是執行緒安全。只有在充分了解其實現,你才會發現,你回答的雖然沒錯,但是也就60分而已,如果你想要將每一個問題回答的完美,那麼請認真思考,認真去了解它。

最後貼一個新生的公眾號 (Java 補習課),歡迎各位關注,主要會分享一下面試的內容(參考之前博主的文章),阿里的開源技術之類和阿里生活相關。 想要交流面試經驗的,可以新增我的個人微信(Jayce-K)進群學習~