1. 程式人生 > >關於Java中的集合你應該知道的一切

關於Java中的集合你應該知道的一切

1.集合的分類

  我們可以從一張類圖來了解集合整個的情況,圖中虛線框為介面,實線框為類,加重的實線框為比較重要的類。
集合的類圖

2.集合相關概念

2.1集合和陣列的區別

  集合:集合類存放於java.util包中,集合中存放的是物件的引用,長度可以發生改變,可在多數情況下使用。
  陣列:可以儲存有限個型別相同的變數的集合,把具有相同型別的若干元素按無序的形式組織起來的一種形式,陣列的長度是固定的,不適合在元素的數量不確定的情況下使用。

2.2集合中各個集合的簡介

  從圖中可以看出集合中重要且常用的集合就那幾種,List、Set、Map是這個集合體系中最主要的三個介面。 List和Set繼承自Collection介面。 Map也屬於集合系統,但和Collection介面不同,他和Collection是依賴關係。

介面名稱 用法 功能特點
List ArrayList、LinkedList和Vector是三個主要的實現類,使用時可直接建立ArrayList、LinkedList和Vector的物件。 有序且允許元素重複
Set HashSet和TreeSet是兩個主要的實現類,使用方法同上 Set 只能通過遊標來取值,並且集合中的元素無序但是不能重複的。
Map HashMap、TreeMap和Hashtable是Map的三個主要的實現類,使用方法同上 Map 是鍵值對集合。其中key列就是一個集合,key不能重複,但是value可以重複
實現List介面的實體類說明
1. ArrayList
  • ArrayList底層的資料結構
    ArrayList的繼承關係
    java.lang.Object
   ↳     java.util.AbstractCollection<E>
         ↳     java.util.AbstractList<E>
               ↳     java.util.ArrayList<E>

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

  ArrayList 是一個數組佇列,相當於 動態陣列。與Java中的陣列相比,它的容量能動態增長。它繼承於AbstractList,實現了List, RandomAccess, Cloneable, java.io.Serializable這些介面.
  RandomAccess:是一個空介面,提供了隨機訪問的功能,當我們的List實現了此介面後,就會為該List提供提供快速訪問功能,我們可以通過元素的序號快速獲得元素物件
  Cloneable:實現了Cloneable介面後,即覆蓋了clone這個函式,能被克隆
  Serializable:實現了Serializable這些介面後即表示ArrayList支援序列化,能夠通過序列化操作去傳輸。
ArrayList相信大家都使用過,ArrayList中的操作是執行緒不安全的,執行緒不安全就是不提供資料訪問保護,有可能出現多個執行緒先後更改資料造成所得到的資料是髒資料。所以建議在單執行緒中使用ArrayList,多執行緒中我們可以使用Vector。看一下ArrayList的建構函式

 /**
    initialCapacity是ArrayList的預設容量大小。
    初始化elementData陣列大小
     */
    public ArrayList(int initialCapacity) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
    }

    /**
     預設建構函式
     */
    public ArrayList() {
        super();
        //未指定陣列大小,則elementData為空陣列EMPTY_ELEMENTDATA
        this.elementData = EMPTY_ELEMENTDATA;
    }

    /**
     建一個包含collection的ArrayList
     */
    public ArrayList(Collection<? extends E> c) {
        //將collection轉化為陣列,賦值給elementData
        elementData = c.toArray();
        //將elementData陣列的長度賦值給size
        size = elementData.length;
        //返回若不是Object[]將呼叫Arrays.copyOf方法將其轉為Object[]
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    }

  從ArrayList的原始碼中可以看出,ArrayList包含了兩個重要的物件:elementData 和 size。也就是說

(1) elementData 是”Object[]型別的陣列”,它儲存了新增到ArrayList中的元素。實際上,elementData是個動態陣列,我們能通過建構函式 ArrayList(int initialCapacity)來執行它的初始容量為initialCapacity;如果通過不含引數的建構函式ArrayList()來建立ArrayList,則elementData預設建立一個空陣列。elementData陣列的大小會根據ArrayList容量的增長而動態的增長,具體的增長方式,請參考原始碼分析中的ensureCapacity()函式。
(2) size 則是動態陣列的實際大小。

  也就是說ArrayList的底層資料結構是動態陣列,他是先確定ArrayList的容量,若當前容量不足以容納當前的元素個數時,然後通過Arrays.copyOf()重新建立一個數組,將原來的陣列copy進去,設定新的容量,然後賦值給elementData。
- ArrayList內部主要原始碼分析

首先來看類中的宣告

    // 序列版本號      
    private static final long serialVersionUID = 8683452581122892189L;
    //預設集合的長度
    private static final int DEFAULT_CAPACITY = 10;
    // 預設空陣列
    private static final Object[] EMPTY_ELEMENTDATA = {};
    // 儲存ArrayList中資料的陣列
    transient Object[] elementData;
    // ArrayList中實際元素的數量
    private int size;

接著看add的方法:

  public boolean add(E e) {
         // 擴容檢查
        ensureCapacityInternal(size + 1); 
        //新增單個元素
        elementData[size++] = e;
        return true;
    }

    public void add(int index, E element) {
        //倘若指定的index大於陣列的長度,報出陣列下標越界的異常
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        //  擴容檢查
        ensureCapacityInternal(size + 1); 
        //將index位置後面的陣列元素統一後移一位,把index位置空出來
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }
    //擴容
    private void ensureCapacityInternal(int minCapacity) {
        //判斷elementData是否為預設空陣列
        if (elementData == EMPTY_ELEMENTDATA) {
            //倘若elementData為空陣列,找出預設集合的長度和minCapacity中最大的一個賦值給minCapacity
            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) {
        //記錄下原來的陣列容量
        int oldCapacity = elementData.length;
        //獲得新的擴容後的容量:原來的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
            //當陣列的長度過大後會呼叫hugeCapacity
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        //拷貝到新的陣列,指定大小,返回後賦值給elementData,完成擴容
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    ArrayList類中還有很多其他方法,比較簡單,這裡就不贅述了。
  • ArrayList的應用場景
型別 內部結構 順序遍歷速度 隨機遍歷速度 追加代價 插入代價 刪除代價 佔用記憶體
ArrayList 動態陣列

所以很明顯,當我們的實際需求中需要查詢或者遍歷的時候,使用ArrayList最好,如果有大量的插入刪除操作儘量避免使用它。

2. LinkedList
  • LinkedList底層的資料結構
    LinkedList的繼承關係
    java.lang.Object
   ↳     java.util.AbstractCollection<E>
         ↳     java.util.AbstractList<E>
               ↳     java.util.AbstractSequentialList<E>
                    ↳     java.util.LinkedList<E>

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable{}

從繼承關係上我們可以看到不實現RandomAccess介面,不支援隨機訪問。LinkedList繼承自AbstractSequentialList,這個抽象類實現了最基本的順序訪問功能
Deque介面:可以充當一般的雙端佇列或者棧
自jdk1.7之後,LinkedList底層使用的是不帶頭結點的普通的雙向連結串列,增加了兩個節點指標first和last分別指向首尾節點。如圖:
LinkedList底層雙向連結串列示意圖

注意:倘若size=0,則first和last指向同一空元素

  • LinkedList內部原始碼分析
     /**
     * 頭部新增
     */
    private void linkFirst(E e) {
        //獲取頭結點
        final Node<E> f = first;
        //新建一個節點,尾部指向之前的頭元素的first
        final Node<E> newNode = new Node<>(null, e, f);
        //first指向新建的節點
        first = newNode;
        //如果之前連結串列為null、新建的節點也就是最後一個節點
        if (f == null)
            last = newNode;
        else
        //如果不為null,原來的頭節點的頭部指向現在新建的頭節點
            f.prev = newNode;
        //連結串列的長度++
        size++;
        //連結串列修改的次數++
        modCount++;
    }

    /**
     *尾部新增
     */
    void linkLast(E e) {
        //獲取尾節點
        final Node<E> l = last;
        //新建一個節點,頭部指向之前的尾節點last
        final Node<E> newNode = new Node<>(l, e, null);
        //last指向新建的節點
        last = newNode;
        //假如之前的last指向null,新建的節點也是頭節點
        if (l == null)
            first = newNode;
        else
        //如果不為null,原來的尾節點的尾部指向新建的尾節點
            l.next = newNode;
        //連結串列的長度++
        size++;
        //連結串列修改次數++
        modCount++;
    }
    /**
     * 在指定節點之前插入某個元素,這裡假定指定節點不為null
     */
    void linkBefore(E e, Node<E> succ) {
        //獲取指定節點 succ 前面的一個節點
        final Node<E> pred = succ.prev;
        //新建一個節點,頭部指向succ節點前面的節點,尾部指向succ,資料為e
        final Node<E> newNode = new Node<>(pred, e, succ);
        //succ的節點頭部指向新建節點
        succ.prev = newNode;
        //假如succ的前一個節點為null,讓first指向新建的節點
        if (pred == null)
            first = newNode;
        else
            //否則讓原先succ前面的節點的尾部指向新建節點
            pred.next = newNode;
        size++;
        modCount++;
    }
    /**
     * 刪除頭結點,返回頭結點上的資料,既定first不為null
     */
    private E unlinkFirst(Node<E> f) {
        // 獲取頭結點的資料
        final E element = f.item;
        //獲取頭結點的下一個節點
        final Node<E> next = f.next;
        //將頭結點置null
        f.item = null;
        //尾部也指向null
        f.next = null; // help GC
        //讓first指向頭結點的下一個節點
        first = next;
        //頭節點後面的節點為 null,說明移除這個節點後,連結串列裡沒節點了
        if (next == null)
            last = null;
        else
            next.prev = null;
        size--;
        modCount++;
        return element;
    }

    /**
     * 刪除尾部節點並返回資料,假設不為空
     */
    private E unlinkLast(Node<E> l) {
        //獲取尾節點的資料
        final E element = l.item;
        //獲取尾節點的前一個節點
        final Node<E> prev = l.prev;
        //值置null
        l.item = null;
        l.prev = null; // help GC
        //讓last指向尾節點的前一個節點
        last = prev;
        if (prev == null)
            first = null;
        else
            prev.next = null;
        size--;
        modCount++;
        return element;
    }

    /**
     *刪除某個指定節點
     */
    E unlink(Node<E> x) {
        //假設 x 不為空
        final E element = x.item;
        //獲取指定節點前面、後面的節點
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;
        //如果前面沒有節點,說明 x 是第一個
        if (prev == null) {
            first = next;
        } else {
          //前面有節點,讓前面節點跨過 x 直接指向 x 後面的節點
            prev.next = next;
            x.prev = null;
        }
        //如果後面沒有節點,說 x 是最後一個節點
        if (next == null) {
            last = prev;
        } else {
            //後面有節點,讓後面的節點指向 x 前面的
            next.prev = prev;
            x.next = null;
        }

        x.item = null;
        size--;
        modCount++;
        return element;
    }

展示一下新增的過程,紅色代表已經做出修改,是無效的
連結串列的新增流程圖
展示一下刪除的過程,紅色代表已經做出修改,是無效的
連結串列的刪除流程圖
ListedList還有很多方法,但是都較為簡單,只要理解了上面的幾個重要的方法,其他的你都可以融會貫通。

  • LinkedList的應用場景
型別 內部結構 順序遍歷速度 隨機遍歷速度 追加代價 插入代價 刪除代價 佔用記憶體
LikedList 雙端連結串列 不支援

所以很明顯,ListedList基於雙端連結串列,新增/刪除元素只會影響周圍的兩個節點,開銷很低;只能順序遍歷,無法按照索引獲得元素,因此查詢效率不高;沒有固定容量,不需要擴容;需要更多的記憶體, 每個節點中需要多儲存前後節點的資訊,佔用空間更多些。

注意: linkedList 和 ArrayList 一樣,不是同步容器。所以需要外部做同步操作,或者直接用 Collections.synchronizedList 方法包一下,最好在建立時就包一下:
List l = Collections.synchronizedList(new LinkedList(…));
LinkedList的迭代器都是 fail-fast 的: 如果在併發環境下,其他執行緒使用迭代器以外的方法修改資料,會導致 ConcurrentModificationException異常,所以遍歷是最好使用迭代器進行。

3. Vector
  • Vectort底層的資料結構
    Vector的繼承關係
    java.lang.Object
   ↳     java.util.AbstractCollection<E>
         ↳     java.util.AbstractList<E>
               ↳     java.util.Vector<E>

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

  看過前面的ArrayList和LinkedList之後,相信大家已經對RandomAccess,和Cloneable,還有Serializable介面很熟悉了,這裡不再贅述。
接著來看構造方法和宣告。

    //儲存元素的陣列
    protected Object[] elementData;
    //元素的個數
    protected int elementCount;
    //擴容增量
    protected int capacityIncrement;
    //序列化標識
    private static final long serialVersionUID = -2767605614048989439L;
    //capacityIncrement這個變數,需要在構造器中指定這個值(預設為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;
    }
    //手動指定Vector的大小
    public Vector(int initialCapacity) {
        this(initialCapacity, 0);
    }
    //預設大小為10  
    public Vector() {
        this(10);
    }
    //初始化一個集合進來
    public Vector(Collection<? extends E> c) {
        //將集合轉化為陣列賦值給elementData
        elementData = c.toArray();
        //獲取陣列的長度賦值給elementCount
        elementCount = elementData.length;
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, elementCount, Object[].class);
    }

  通過常量和構造方法我們可以看出Vector的底層也是個動態陣列,並且它的結構程式碼和ArrayList相似。我們這裡主要看擴容和保證執行緒同步的原始碼。

  • Vector內部原始碼分析
    //為了保證同步,只有一個執行緒操作,方法前面加了synchronized來修飾
    public synchronized void ensureCapacity(int minCapacity) {
        if (minCapacity > 0) {
            //修改陣列的次數
            modCount++;
            //判斷是否擴容
            ensureCapacityHelper(minCapacity);
        }
    }

    /**
     *
     */
    private void ensureCapacityHelper(int minCapacity) {
        // 當傳進來的長度減去原先elementData大於0時,開始擴容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

    /**
     * 
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    //擴容的方法
    private void grow(int minCapacity) {
        // 將elementData原先的長度賦值給oldCapacity
        int oldCapacity = elementData.length;
        //新的長度等於原先的長度加上(擴容增量>0的話就是擴容增量,否則原先的長度)
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
            //當陣列的長度過大後會呼叫hugeCapacity
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        //拷貝到新的陣列,指定大小,返回後賦值給elementData,完成擴容
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

從方法命名上來看,Vector跟ArrayList還是很類似的,但是兩者的grow方法有點小區別:
Vector的擴容是基於capacityIncrement的,也就是所謂的擴容增量,如果該值不為0,那麼每次擴容後的大小就是在原始容量加上擴容增量。如果未設定capacityIncrement,那麼直接擴容為原來的兩倍。
當然,前提是擴容後大小得大於等於所需要的最小容量minCapacity且不能超過MAX_ARRAY_SIZE,同時還要防止溢位(會丟擲異常)
接下來看一下幾個方法

    public synchronized void trimToSize() {
        modCount++;
        int oldCapacity = elementData.length;
        if (elementCount < oldCapacity) {
            elementData = Arrays.copyOf(elementData, elementCount);
        }
    }
    //獲得陣列的長度
    public synchronized int capacity() {
        return elementData.length;
    }
    //獲得集合的長度
    public synchronized int size() {
        return elementCount;
    }

    //判null的方法
    public synchronized boolean isEmpty() {
        return elementCount == 0;
    }

可以發現,這些方法都加上了synchronized關鍵字,也就是說Vector是一個執行緒安全的類。

Vector除了ListIterator和iterator兩種迭代方式之外,還有獨特的迭代方式,那就是elements方法,這個方法通過匿名內部類的方式構造一個Enumeration物件,並實現了hasMoreElements和nextElement方法,類似迭代器的hasNext和next方法

 public Enumeration<E> elements() {
        //匿名內部類方式構造一個Enumeration物件
        return new Enumeration<E>() {
            int count = 0;

            public boolean hasMoreElements() {
                return count < elementCount;
            }
            public E nextElement() {
                synchronized (Vector.this) {
                    if (count < elementCount) {
                        return elementData(count++);
                    }
                }
                throw new NoSuchElementException("Vector Enumeration");
            }
        };
    }
  • Vectort的應用場景

  Vector在實際的開發中使用較少,Vector所有方法都是同步,有效能損失。並且Vector會在你不需要進行執行緒安全的時候,強制給你加鎖,導致了額外開銷,所以慢慢被棄用了。

  • #### 實現Set介面的實現類說明
1. HashSet
  • HashSet 底層的資料結構
    首先來看HashSet的繼承關係
    java.lang.Object
   ↳     java.util.AbstractCollection<E>
         ↳     java.util.AbstractSet<E>
               ↳     java.util.HashSet<E>

    public class HashSet<E>
        extends AbstractSet<E>
            implements Set<E>, Cloneable, java.io.Serializable

可以看到HashSet實現了Set介面和Cloneable,Serializable介面,關於Cloneable,Serializable介面不再多說,這裡HashSet繼承AbstactSet這個中間抽象類,並且這個抽象類又繼承自AbstractCollection,AbstractCollection其實更像是實現List,Set的共同的方法,而AbstactSet和AbstactList更像是提供給Set、List各自特有方法的實現。接著來看:

    //序列化標識
    static final long serialVersionUID = -5024744406713321676L;
    //底層使用HashMap來儲存HashSet中所有元素。 
    private transient HashMap<E,Object> map;
    //定義一個虛擬的Object物件作為HashMap的value
    private static final Object PRESENT = new Object();

可以看到HashSet的底層實現是基於HasMap的,它不保證set 的迭代順序,特別是它不保證該順序恆久不變。且允許使用null元素,HashSet的實現較為的簡單,其相關的操作都是通過直接呼叫底層HashMap的相關方法來完成

    HashSet 內部原始碼分析

1.建構函式

    /*
    預設建構函式,實際底層會初始化一個空的HashMap,並使用預設初始容量為16和載入因子0.75。 
   */
    public HashSet() {
        map = new HashMap<>();
    }
    /**
     構造一個包含指定collection中的元素的新set。 
     * 實際底層使用預設的載入因子0.75和足以包含指定 
     * collection中所有元素的初始容量來建立一個HashMap。 
     */
    public HashSet(Collection<? extends E> c) {
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }
    /**
      以指定的initialCapacity和loadFactor構造一個空的HashSet。     
     */
    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }

    /**
       以指定的initialCapacity構造一個空的HashSet。       
     */
    public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }

    /**
     以指定的initialCapacity和loadFactor構造一個新的空連結雜湊集合。 
     此建構函式為包訪問許可權,不對外公開,實際只是是對LinkedHashSet的支援
     */
    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }
從建構函式中可以看到,基本都是為了構造一個HashMap來儲存資料
2.常用方法
    //如果set中尚未包含指定元素,則呼叫map的put方法,其中value是一個靜態的Object物件
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
    //如果指定元素存在於此 set 中,則將其移除
     public boolean remove(Object o) {
        return map.remove(o)==PRESENT;
    }
    //從此 set 中移除所有元素
    public void clear() {
        map.clear();
    }
    //判斷set中是否含有指定元素,如果有,返回true
    public boolean contains(Object o) {
        return map.containsKey(o);
    }
    //實際呼叫HashMap的clone()方法,獲取HashMap的淺表副本,並設定到  HashSet中
     public Object clone() {
        try {
            HashSet<E> newSet = (HashSet<E>) super.clone();
            newSet.map = (HashMap<E, Object>) map.clone();
            return newSet;
        } catch (CloneNotSupportedException e) {
            throw new InternalError();
        }
    }
    //返回對此set中元素進行迭代的迭代器。返回元素的順序並不是特定的
    public Iterator<E> iterator() {
        return map.keySet().iterator();
    }
可見HashSet中的元素,只是存放在了底層HashMap的key上, value使用一個static final的Object物件標識。
3.保證儲存物件的唯一性
  Set是一個不包含重複物件的集合,且最多隻有null元素,如何保證其唯一且不重複呢,看一下建構函式的addAll(c); 追原始碼發現最後進入AbstractCollection中addAll中
public boolean addAll(Collection<? extends E> c) {
        boolean modified = false;
        for (E e : c)
            if (add(e))
                modified = true;
        return modified;
    }

從上面程式碼中可以看到,原始碼中也是通過迴圈一個一個add進去的,那我們看一下HashSet的add方法。

public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

發現呼叫了map的put方法,進入HashMap的put方法看看:

public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
        int i = indexFor(hash, table.length);
        for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

  可以看到for迴圈中,遍歷table中的元素,如果hash碼值不相同,說明是一個新元素,存;如果沒有元素和傳入物件(也就是add的元素)的hash值相等,那麼就認為這個元素在table中不存在,將其新增進table;如果hash碼值相同,且equles判斷相等,說明元素已經存在,不存;如果hash碼值相同,且equles判斷不相等,說明元素不存在,存;如果有元素和傳入物件的hash值相等,那麼,繼續進行equles()判斷,如果仍然相等,那麼就認為傳入元素已經存在,不再新增,結束,否則仍然新增;
  從上面我們可以看到通過雜湊演算法,對key產生雜湊碼,通過雜湊碼和equals方法保證其唯一,也就是說,要想保證物件在Set中的唯一,需要重寫hashCode和equals方法。
- HashSet 小結
HashSet是Set介面典型實現,它按照Hash演算法來儲存集合中的元素,具有很好的存取和查詢效能。且主要具有以下特點:
(1)不保證set的迭代順序
(2)HashSet不是同步的,如果多個執行緒同時訪問一個HashSet,要通過程式碼來保證其同步
(3)集合元素值可以是null,且只能有一個
(4)當向HashSet集合中存入一個元素時,HashSet會呼叫該物件的hashCode()方法來得到該物件的hashCode值,然後根據該值確定物件在HashSet中的儲存位置
(5)在Hash集合中,不能同時存放兩個相等的元素,而判斷兩個元素相等的標準是兩個物件通過equals方法比較相等並且兩個物件的HashCode方法返回值也相等。

2. TreeSet
  • TreeSet底層的資料結構
    首先來看TreeSet的繼承關係
java.lang.Object
   ↳     java.util.AbstractCollection<E>
         ↳     java.util.AbstractSet<E>
               ↳     java.util.TreeSet<E>

    public class TreeSet<E> extends AbstractSet<E>
        implements NavigableSet<E>, Cloneable, java.io.Serializable

TreeSet繼承自AbstractSet,所以他是一個Set的集合,具有Set的屬性和方法
TreeSet實現了NavigaableSet介面,意味著它支援一系列的導航方法,比如查詢與指定目標最匹配項

    //NavigableMap物件,TreeMap實現了NavigableMap介面
    private transient NavigableMap<E,Object> m;

    //靜態的PRESENT物件,代表TreeMap中的value
    private static final Object PRESENT = new Object();

從上述繼承關係以及常量宣告中看到,TreeSet的底層是基於TreeMap的key來儲存的,而Value值全部為預設值PRESENT。

  • TreeSet內部原始碼分析
    首先來看TreeSet的建構函式
    //內部私有建構函式不對外公開,初始化NavigableMap物件m
    TreeSet(NavigableMap<E,Object> m) {
        this.m = m;
    }

    /**
        預設建構函式,建立空的TreeMap的物件
     */
    public TreeSet() {
        this(new TreeMap<E,Object>());
    }

    /**
      帶比較器的建構函式。
     */
    public TreeSet(Comparator<? super E> comparator) {
        this(new TreeMap<>(comparator));
    }

    /**
     建立TreeSet
     */
    public TreeSet(Collection<? extends E> c) {
        // 呼叫預設構造器建立一個TreeSet,底層以 TreeMap 儲存集合元素
        this();
        //將集合c中的全部元素都新增到TreeSet中
        addAll(c);
    }

    /**
      建立TreeSet,並將s中的全部元素都新增到TreeSet中
     */
    public TreeSet(SortedSet<E> s) {
        this(s.comparator());
        addAll(s);
    }

從原始碼中也可以看到TreeSet中的其他方法也比較簡單,和HashSet中的有點類似,我們主要來看重點的方法:

public  boolean addAll(Collection<? extends E> c) {
        // 判斷是否傳入的集合引數c是否為SortedSet或其子類且c不為空(c.size()>0),
        //如果是則會呼叫addAllForTreeSet方法,否則會直接返回addAll方法的結果
        if (m.size()==0 && c.size() > 0 &&
            c instanceof SortedSet &&
            m instanceof TreeMap) {
            SortedSet<? extends E> set = (SortedSet<? extends E>) c;
            TreeMap<E,Object> map = (TreeMap<E, Object>) m;
            Comparator<?> cc = set.comparator();
            Comparator<? super E> mc = map.comparator();
            if (cc==mc || (cc != null && cc.equals(mc))) {
                map.addAllForTreeSet(set, PRESENT);
                return true;
            }
        }
        return super.addAll(c);
    }
    //從TreeMap中找到了該方法
     void addAllForTreeSet(SortedSet<? extends K> set, V defaultVal) {
        try {
            buildFromSorted(set.size(), set.iterator(), null, defaultVal);
        } catch (java.io.IOException cannotHappen) {
        } catch (ClassNotFoundException cannotHappen) {
        }
        //該方法的作用即是線上性時間內對資料進行排序
      private void buildFromSorted(int size, Iterator<?> it,
                                 java.io.ObjectInputStream str,
                                 V defaultVal)
        throws  java.io.IOException, ClassNotFoundException {
        this.size = size;
        root = buildFromSorted(0, 0, size-1, computeRedLevel(size),
                               it, str, defaultVal);
    }

  當使用一個TreeMap集合作為引數構造一個TreeSet的時候,TreeSet會將Map中的元素先排序,然後將排序後的元素add到TreeSet中。也就是說TreeSet中的元素都是排過序的,另外正因為存在排序過程,所以TreeSet不允許插入null值,因為null值不能排序

  • TreeSet小結
    1、TreeSet不能有重複的元素;
    2、TreeSet具有排序功能;
    3、TreeSet中的元素必須實現Comparable介面並重寫compareTo()方法,TreeSet判斷元素是否重複 、以及確定元素的順序 靠的都是這個方法;
    對於java類庫中定義的類,TreeSet可以直接對其進行儲存,如String,Integer等,因為這些類已經實現了Comparable介面);
    對於自定義類,如果不做適當的處理,TreeSet中只能儲存一個該型別的物件例項,否則無法判斷是否重複。
    4、TreeSet依賴TreeMap。
    5、TreeSet相對HashSet,TreeSet的優勢是有序,劣勢是相對讀取慢。根據不同的場景選擇不同的集合。
3. LinkedHashSet
  • LinkedHashSet底層的資料結構
    LinkedHashSet集合同樣是根據元素的hashCode值來決定元素的儲存位置,但是它同時使用連結串列維護元素的次序。這樣使得元素看起 來像是以插入順 序儲存的,也就是說,當遍歷該集合時候,LinkedHashSet將會以元素的新增順序訪問集合的元素。LinkedHashSet在迭代訪問Set中的全部元素時,效能比HashSet好,但是插入時效能稍微遜色於HashSet。
java.lang.Object
   ↳     java.util.AbstractCollection<E>
         ↳     java.util.AbstractSet<E>
               ↳     java.util.HashSet<E>
                        ↳     java.util.LinkedHashSet<E>
public class LinkedHashSet<E>
    extends HashSet<E>
    implements Set<E>, Cloneable, java.io.Serializable

可以看到LinkedHashSet繼承自HashSet,那麼HashSet的屬性和方法他都有,他比較簡單,我們主要看一下他的建構函式

  • LinkedHashSet內部原始碼分析
    public LinkedHashSet(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor, true);
    }
    public LinkedHashSet(int initialCapacity) {
        super(initialCapacity, .75f, true);
    }
    public LinkedHashSet() {
        super(16, .75f, true);
    }
    public LinkedHashSet(Collection<? extends E> c) {
        super(Math.max(2*c.size(), 11), .75f, true);
        addAll(c);
    }

從上面的建構函式看到,都是呼叫HashSet的構造器,而HashSet中有一個重要的方法:

HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }

也就是說在父類 HashSet 中,專為 LinkedHashSet 提供的構造方法如下,該方法為包訪問許可權,並未對外公開。由上述原始碼可見,LinkedHashSet 通過繼承HashSet,底層使用LinkedHashMap,以很簡單明瞭的方式來實現了其自身的所有功能。

實現Map介面的實現類說明
1. TreeMap
  • TreeMap底層的資料結構
    首先來看TreeMap的繼承關係
java.lang.Object
         ↳     java.util.AbstractMap<E>
               ↳     java.util.AbstractMap<E>
                        ↳     java.util.TreeMap<E>
public class TreeMap<K,V>
    extends AbstractMap<K,V>
        implements NavigableMap<K,V>, Cloneable, java.io.Serializable
TreeMap的繼承和實現關係圖
可以看到
TreeMap繼承自AbstractMap,所以它是一個Map,即是一個key-value的集合
TreeMap實現了NavigableMap,表示其支援一系列的導航方法,比如返回有序的key集合
TreeMap實現了Cloneable和Serializable介面,即表示它能被克隆也支援序列化
//比較器,通過comparator介面我們可以對TreeMap的內部排序進行精密的控制 
    private final Comparator<? super K> comparator;
    //TreeMap紅-黑節點,為TreeMap的內部類
    private transient TreeMapEntry<K,V> root = null;

    /**
     * 樹中的條目數
     */
    private transient int size = 0;

    /**
     *對樹進行結構修改的次數
     */
    private transient int modCount = 0;
    //TreeMap的靜態內部類,“紅黑樹的節點”對應的類。
    static final class TreeMapEntry<K,V> implements Map.Entry<K,V> {
        K key;//鍵
        V value;//值
        TreeMapEntry<K,V> left =