JAVA集合類(代碼手寫實現,全面梳理)

 

目錄

 

一、集合類關係圖

 二、Iterator

三、ListIterator

四、Collection

五、List

(1)ArrayList

1)Array和ArrayList區別

2)實現自己的ArrayList

(2)LinkedList

(3)Vector

六、Map

(1)HashMap

1)HashMap底層原理

2)實現自己的HashMap

3)爲什麼要加上紅黑樹,紅黑樹什麼時候纔會使用?

4)HashMap如何保證key值唯一

5)HashMap的線程安全問題

(2)LinkedHashMap

1)LinkedHashMap介紹

2)LinkedHashMap如何選擇排序方式

3)LinkedHashMap怎麼實現的排序

(3)ConcurrentHashMap

1)ConcurrentHashMap特點

2)內部結構

(4)Hashtable

(5)TreeMap

Map小結——如何選擇Map:

七、Set

(1)HashSet

(2)LinkedHashSet

(3)TreeSet


一、集合類關係圖

 

 二、Iterator

Iterator是一個接口,它是集合的迭代器。集合可以通過Iterator去遍歷集合中的元素。主要有以下方法:

  • forEachRemaining(Consumer<? super E> action):爲每個剩餘元素執行給定的操作,直到所有的元素都已經被處理或行動將拋出一個異常
  • hasNext():如果迭代器中還有元素,則返回true
  • next():返回迭代器中的下一個元素
  • remove():刪除迭代器新返回的元素
import java.util.ArrayList;
import java.util.Iterator;

public class Demo1 {
    public static void main(String[] args) {
        ArrayList<String> a = new ArrayList<String>();
        a.add("aaa");
        a.add("bbb");
        a.add("ccc");
        System.out.println("Before :" + a);
        Iterator<String> iterator = a.iterator();
        iterator.forEachRemaining(str -> System.out.println("沒有next()前 ->" + str));
        // 若果還有元素
        while(iterator.hasNext()){
            String str = iterator.next();
            if("ccc".equals(str)){
                // 刪除ccc
                iterator.remove();
            }
        }
        System.out.println("After :" + a);
        iterator.forEachRemaining(str -> System.out.println("next()後 ->" + str));
    }
}
Before :[aaa, bbb, ccc]
沒有next()前 ->aaa
沒有next()前 ->bbb
沒有next()前 ->ccc
After :[aaa, bbb, ccc]

可以看出,最後的一個打印沒有打印出來,這是因爲iterator使用next()方法跳轉到下一步,過程不可逆

 

三、ListIterator

ListIterator也是一個接口,繼承至Iterator接口,包含以下方法:

  • add(E e): 將指定的元素插入列表,插入位置爲迭代器當前位置之前
  • hasNext():以正向遍歷列表時,如果列表迭代器後面還有元素,則返回 true,否則返回false
  • hasPrevious():如果以逆向遍歷列表,列表迭代器前面還有元素,則返回 true,否則返回false
  • next():返回列表中ListIterator指向位置後面的元素
  • nextIndex():返回列表中ListIterator所需位置後面元素的索引
  • previous():返回列表中ListIterator指向位置前面的元素
  • previousIndex():返回列表中ListIterator所需位置前面元素的索引
  • remove():從列表中刪除next()或previous()返回的最後一個元素(有點拗口,意思就是對迭代器使用hasNext()方法時,刪除ListIterator指向位置後面的元素;當對迭代器使用hasPrevious()方法時,刪除ListIterator指向位置前面的元素)
  • set(E e):從列表中將next()或previous()返回的最後一個元素返回的最後一個元素更改爲指定元素e
     

Iterator和ListIterator之間區別?

  • 使用範圍不同,Iterator用於所有的集合(Set、List和Map和這些集合的子類型),而ListIterator只能用於List及其子類型;
  • ListIterator有add方法,可以向List中添加對象,而Iterator不能;
  • 都有hasNext()和next()方法,能實現向後遍歷,但ListIterator有hasPrevious()和previous()方法,能雙向遍歷,Iterator不能;
  • ListIterator可以定位當前索引的位置,nextIndex()和previousIndex()可以實現,Iterator沒有此功能。
  • 都可實現刪除操作,但是ListIterator可以實現對象的修改,set()方法可以實現,Iterator僅能遍歷,不能修改。
public class Demo2 {
    public static void main(String[] args) {
        ArrayList<String> a = new ArrayList<String>();
        a.add("aaa");
        a.add("bbb");
        a.add("ccc");
        System.out.println("Before :" + a);
        ListIterator iterator = a.listIterator();
        System.out.println(iterator.nextIndex());
        System.out.println(iterator.next());
        System.out.println(iterator.nextIndex());
        iterator.add("ddd");
    }
}
Before :[aaa, bbb, ccc]
0
aaa
1

 

四、Collection

Collection是集合的頂層接口,不能被直接實例化。Collection包含set/list/queue/deque/四種類型集合。

Collection主要方法如下:

  • boolean add(Object o):添加對象到集合
  • boolean remove(Object o):刪除指定的對象
  • int size():返回當前集合中元素的數量
  • boolean contains(Object o):查找集合中是否有指定的對象
  • boolean isEmpty():判斷集合是否爲空
  • Iterator iterator():返回一個迭代器
  • boolean containsAll(Collection c):查找集合中是否有集合c中的元素
  • boolean addAll(Collection c):將集合c中所有的元素添加給該集合
  • void clear():刪除集合中所有元素
  • void removeAll(Collection c):從集合中刪除c集合中也有的元素
  • void retainAll(Collection c):從集合中刪除集合c中不包含的元素

 

五、List

List也是接口,有三個實現類:ArrayList、LinkedList、Vector

5.1  ArrayList

ArrayList類基於數組,是一種動態數組。是List的實現子類。

5.1.1  Array和ArrayList區別:

  • 數組最高效;但是其容量固定且無法動態改變;ArrayList容量自動增長,但犧牲效率;
  • Array類型的變量在聲明的同時必須進行實例化(至少得初始化數組的大小),而ArrayList可以只是先聲明;
  • 數組僅提供一個length屬性,並且它長度是固定的;ArrayList提供了size()方法;
  • 基於效率和類型檢驗,應儘可能使用Array無法確定數組大小時才使用ArrayList

5.1.2  實現自己的ArrayList

那我們就需要研究一下ArrayList容量動態增長的原因了,可以自己實現ArrayList容器,實現它的幾個常見方法,這樣就能很深刻的理解這個容器:

public class MyArrayList {
    private Object[] elementData;
    private int size;

    public int getSize() {
        return size;
    }

    /**
     * 無參構造,初始化容量爲10
     */
    public MyArrayList() {
        elementData = new Object[10];
    }

    /**
     * 根據傳參進行初始化
     */
    public MyArrayList(int initialCapacity) {
        elementData = new Object[initialCapacity];
    }

    /**
     * 擴容:這也是ArrayList的容量可變的原因
     */
    private void ensureCapacity(){
        // 數組長度不夠時
        if(size >= elementData.length){
            // 兩倍擴容
            Object[] newArray = new Object[elementData.length*2];
            // System.arraycopy(原, 原起始位置, 目標, 目標起始位置, 要copy的數組的長度)
            System.arraycopy(elementData, 0, newArray, 0, elementData.length);
            elementData = newArray;
        }
    }

    /**
     * 邊界檢測
     */
    private void rangeCheck(int index){
        if(index < 0 || index >= size){
            try {
                throw new Exception();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 實現add()方法
     */
    public void add(Object o){
        ensureCapacity();
        elementData[size] = o;
        size ++;
    }


    /**
     * 實現add(int index, Object o)方法
     */
    public void add(int index, Object o){
        rangeCheck(index);
        ensureCapacity();
        System.arraycopy(elementData, index, elementData, index + 1,size - index);
        elementData[index] = o;
        size ++;
    }

    /**
     * 實現remove(int index)方法
     */
    public void remove (int index){
        rangeCheck(index);
        if(size - index - 1 > 0){
            System.arraycopy(elementData, index + 1, elementData, index, size - index - 1);
        }
        elementData[size] = null;
        size -- ;
    }

    /**
     * 實現get(int index)方法
     */
    public Object get(int index){
        rangeCheck(index);
        return elementData[index];
    }

    /**
     * 實現isEmpty()方法
     */
    public boolean isEmpty(){
        return size == 0;
    }

    public static void main(String[] args) {
        MyArrayList myArrayList = new MyArrayList();
        myArrayList.add("111");
        myArrayList.add("222");
        myArrayList.add("333");
        myArrayList.add("444");
        myArrayList.add("555");
        myArrayList.remove(0);
        System.out.println(myArrayList.elementData[0]);
        System.out.println(myArrayList.get(0));
        myArrayList.add(3, "666");
        System.out.println(myArrayList.get(3));
        System.out.println(myArrayList.isEmpty());
        System.out.println(myArrayList.getSize());
    }
}
222
222
666
false
5

 

5.2  LinkedList

LinkedList也是List接口的實現類,基於雙向鏈表實現。

鏈表數據結構的特點是每個元素分配的空間不必連續、插入和刪除元素時速度非常快、但訪問元素的速度較慢。LinkedList是一個雙向鏈表, 當數據量很大或者操作很頻繁的情況下,添加和刪除元素時具有比ArrayList更好的性能。但在元素的查詢和修改方面要弱於ArrayList。

LinkedList類每個結點用內部類Node表示,Node節點結構如下圖:

        

LinkedList通過first和last引用分別指向鏈表的第一個和最後一個元素,當鏈表爲空時,first和last都爲NULL值,鏈表結構如下:

可以自己實現下LinkedList,探究一下它的增刪改查:

public class MyLinkedList {
    private Node first;
    private Node last;
    private int size;

    public int getSize() {
        return size;
    }

    public MyLinkedList() {
    }

    public MyLinkedList(Node first, Node last, int size) {
        this.first = first;
        this.last = last;
        this.size = size;
    }

    /**
     * 邊界檢測
     */
    private void rangeCheck(int index){
        if(index < 0 || index >= size){
            try {
                throw new Exception();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public void add(Object o){
        Node node = new Node();
        if (o != null){
            if (first == null){
                node.setPrevious(null);
                node.setData(o);
                node.setNext(null);
                first = node;
                last = node;
            }else {
                node.setPrevious(last);
                node.setData(o);
                node.setNext(null);
                last.setNext(node);
                last = node;
            }
        }
        size ++;
    }

    public void add(int index, Object o){
        rangeCheck(index);
        if(o != null){
            Node temp = new Node();
            Node newNode = new Node();
            if(first == null){
                temp = first;
                // 定位要插入的節點位置
                for (int i = 0; i <index ; i++) {
                    temp = temp.getNext();
                }
                newNode.setPrevious(temp.getPrevious());
                newNode.setData(o);
                newNode.setNext(temp);

                temp.getPrevious().setNext(newNode);
                temp.getNext().setPrevious(newNode);
                size ++;
            }
        }
    }

    public Object get(int index){
        rangeCheck(index);
        Node temp = new Node();
        if (first != null){
            temp = first;
            for (int i = 0; i < index; i++) {
                temp = temp.getNext();
            }
        }
        return temp.getData();
    }

    public Object remove(int index){
        rangeCheck(index);
        Node temp = new Node();
        if(first != null){
            temp = first;
            for (int i = 0; i < index; i++) {
                temp = temp.getNext();
            }
            temp.getPrevious().setNext(temp.getNext());
            temp.getNext().setPrevious(temp.getPrevious());
            size --;
        }
        return temp.getData();
    }

    public static void main(String[] args) {
        MyLinkedList myLinkedList = new MyLinkedList();
        myLinkedList.add("0");
        myLinkedList.add("1");
        myLinkedList.add("2");
        myLinkedList.add("3");
        myLinkedList.add("4");
        myLinkedList.add("5");
        myLinkedList.add(5,"6");
        System.out.println(myLinkedList.remove(1));
        System.out.println(myLinkedList.size);
        System.out.println(myLinkedList.get(4));
    }
}

class Node{
    private Node previous;
    private Node next;
    private Object data;

    public Node getPrevious() {
        return previous;
    }
    public void setPrevious(Node previous) {
        this.previous = previous;
    }
    public Node getNext() {
        return next;
    }
    public void setNext(Node next) {
        this.next = next;
    }
    public Object getData() {
        return data;
    }
    public void setData(Object data) {
        this.data = data;
    }
}
1
5
5

需要注意鏈表遍歷的方式,是新建臨時的節點,並且將first節點賦值給它,再移動到index的位置。

5.3  Vector

Vector與ArrayList一樣,底層是數組,可實現自動增長,不同的是它線程安全,即某一時刻只有一個線程能夠寫Vector,避免多線程同時寫而引起的不一致性,但實現同步需要很高的花費,因此,訪問它比訪問ArrayList慢。

 

六、Map

Map是一個頂層接口(需要注意的是Map並不是Iterator接口的子類,要想用Iterator進行遍歷輸出Map,需要將Map類型先轉換成Set類型),它提供了一種映射關係,

  • 其中的元素是以鍵值對(key-value)的形式存儲的,能夠實現根據key快速查找value
  • Map中的鍵值對以Entry類型的對象實例形式存在
  • 建(key值)不可重複,value值可以重複

Map借鑑了散列思想,散列將鍵的信息通過散列函數映射到數組的索引中,以便能夠實現快速的查找。首先我們通過鍵對象生成一個整數,將其作爲數組的下標,這個數字就是散列碼。散列碼不需要是唯一的,但是通過hashCode()和equals()必須能夠完全確定對象身份。散列密度關係其效率,可通過負載因子進行調整。

Map接口常見實現類有:HashMap、HashTable、TreeMap、ConcurrentHashMap、LinkedHashMap、weakHashMap。主要Map類特點對比如下: 

Map集合 Key Value 多線程 底層實現 說明
HashMap 允許null 允許null

不安全

get安全

數組+單向鏈表

數組+單向鏈表+紅黑樹

 
LinkedHashMap 允許null 允許null 不安全 數組+單向鏈表+雙向鏈表 在HashMap基礎上增加雙向鏈表
ConcurrentHashMap 不允許null 不允許null 安全

JDK7數組+Segment+分段鎖

JDK8數組+單向鏈表+紅黑樹

CAS + synchronized
Hashtable 不允許null 不允許null 安全

數組+單向鏈表

數組+單向鏈表+紅黑樹

就是在HashMap基礎上每個方法加上sychronized 

 

TreeMap 不允許null 允許null 不安全 紅黑樹  


 

 

 

 

 

 

 

 

 

 

 

注意: 

線程安全的只有Hashtable和ConcurrentHashMap。

但是Hashtable 線程安全很好理解,因爲它每個方法中都加入了Synchronize,在線程競爭激烈的情況下Hashtable的效率非常低下。因爲當一個線程訪問Hashtable的同步方法時,其他線程訪問Hashtable的同步方法時,可能會進入阻塞或輪詢狀態。因此Hashtable不建議使用,不做重點介紹。

ConcurrentHashMap採用鎖分段技術,Hashtable表現出效率低下的原因是因爲所有訪問HashTable的線程都必須競爭同一把鎖,假如容器裏有多把鎖,每一把鎖用於鎖容器其中一部分數據,那麼當多線程訪問容器裏不同數據段的數據時,線程間就不會存在鎖競爭,從而可以有效的提高併發訪問效率,這就是ConcurrentHashMap所使用的鎖分段技術。

6.1  HashMap

6.1.1  HashMap底層原理

JDK7前,是由數組+單向鏈表實現。

JDK8後增加了紅黑樹,即數組+單向鏈表+紅黑樹

HashMap的主幹是一個Entry數組。Entry是HashMap的基本組成單元,每一個Entry包含一個key-value鍵值對(實際上還有隱含的hash值),HashMap的數據結構如下:

6.1.2  實現自己的HashMap

(HashMap源碼中是單向鏈表實現的,這裏爲了順便理解LinkedHashMap爲啥支持排序,使用雙向鏈表實現) 

class MyEntry<K,V> implements Map.Entry<K,V> {
    int hash;
    final K key;
    V value;
    MyEntry<K,V> next;

    MyEntry(int hash, K key, V value, MyEntry<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public MyEntry(K key, V value) {
        this.key = key;
        this.value = value;
    }

    @Override
    public final K getKey()        { return key; }
    @Override
    public final V getValue()      { return value; }
    @Override
    public final String toString() { return key + "=" + value; }
    @Override
    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }
    @Override
    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }
    @Override
    public final boolean equals(Object o) {
        if (o == this){
            return true;
        }
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue())){
                return true;
            }
        }
        return false;
    }
}

public class MyHashMap {
    LinkedList[] array = new LinkedList[999];
    int size;

    public int getSize() {
        return size;
    }
    public MyHashMap(int size) {
        this.size = size;
    }
    public MyHashMap() {
    }

    /**
     * 計算key的hash值
     */
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    public void put(Object key, Object value){
        // 先把key和value放入Entry對象中
        MyEntry myEntry = new MyEntry(key, value);
        // 根據key值計算其hash值,定位數組中可以插入的數組位置
        int hashValue = hash(key);
        // 若該位置爲空直接添加新的鏈表
        if(array[hashValue] == null){
            LinkedList linkedList = new LinkedList();
            linkedList.add(myEntry);
            array[hashValue] = linkedList;
        } else {
            // 若數組該位置已經有鏈表,就需要循環判斷是否有重複的key值,若有
            // 則替換該處的value值,若無則直接在尾部新增
            for (int i = 0; i < array[hashValue].size(); i++) {
                MyEntry entry = (MyEntry)array[hashValue].get(i);
                if(key.equals(entry.key)){
                    entry.value = value;
                }else {
                    array[hashValue].add(myEntry);
                }
            }
        }
        size ++;
    }

    public Object get(Object key){
        int hashValue = hash(key);
        Object obj = null;
        if(array[hashValue] != null){
            for (int i = 0; i < array[hashValue].size(); i++) {
                MyEntry entry = (MyEntry)array[hashValue].get(i);
                if (key.equals(entry.key)){
                    obj = entry.value;
                }
            }
            return obj;
        }else {
            return null;
        }
    }


    public static void main(String[] args) {
        MyHashMap myHashMap = new MyHashMap();
        myHashMap.put("1", "a");
        myHashMap.put("2", "b");
        myHashMap.put("3", "c");
        System.out.println(myHashMap.get("2"));
        System.out.println(myHashMap.size);
    }
}
b
3

6.1.3  爲什麼要加上紅黑樹,紅黑樹什麼時候纔會使用?

因爲鏈表查詢效率不像數組那樣高,當鏈表長度很長時會降低效率,因此JDK8後加上了紅黑樹,當鏈表長度超過閾值8時,會進行紅黑樹篩選查詢(類似二分法,不再全部遍歷,而是不斷折中遍歷,有個紅黑樹模擬的網站:https://www.cs.usfca.edu/~galles/visualization/RedBlack.html)。紅黑樹的遍歷過程會在本文後面的TreeMap中介紹。

HashMap源碼如下:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //如果table爲空或者未分配空間,則resize,放入第一個K-V時總是先resize
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //(n-1)&hash計算K-V存的table位置,如果首節點爲null,代表該位置還沒放入過結點
        if ((p = tab[i = (n - 1) & hash]) == null)
            //調用newNode新建一個結點放入該位置
            tab[i] = newNode(hash, key, value, null);
        // 到這裏,表示有衝突,開始處理衝突
        else {
            Node<K,V> e; K k;
            //這時p指向首個Node,判斷table[i]是否與待插入節點有相同的hash,key值,如果是則e=p
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //到這裏說明table[i]的第一個Node與待插入Node的hash或key不同,那麼要在
       //這個節點之後的鏈表節點或者樹節點中查找
            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);
                        // 如果衝突的節點數已經達到8個,看是否需要改變衝突節點的存儲結構
                        // treeifyBin首先判斷當前hashMap的長度,如果不足64,只進行
                        // resize,擴容table,如果達到64,那麼將衝突的存儲結構爲紅黑樹
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果找到同hash或同key的節點,那麼直接退出循環,
           //此時e等於衝突Node
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    //調整p節點,以繼續查找
                    p = e;
                }
            }
            //退出循環後,先判斷e是否爲null,爲null表示已經插入成功,不爲null表示有衝突
            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;
    }
    /**
     * Replaces all linked nodes in bin at index for given hash unless
     * table is too small, in which case resizes instead.
     */
    /*樹形化*/
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        // 定義n:節點數組長度、index:hash對應的數組下標、e:用於循環的迭代變量,代表當前節
        int n, index; Node<K,V> e;
        // 若數組尚未初始化或者數組長度小於64,則直接擴容而不進行樹形化
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        // 獲取指定數組下標的頭結點e
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            // 定義head節點hd、尾節點tl
            TreeNode<K,V> hd = null, tl = null;
            // 該循環主要是將原單向鏈表轉化爲雙向鏈表
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                // 若尾節點爲null表明首次循環,此時e爲頭結點、p爲根節點,將p賦值給表示頭結點的hd
                if (tl == null)
                    hd = p;
                // 負責根節點已經產生過了此時tl尾節點指向上次循環創建的樹形節點
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                // 將tl指向當前節點
                tl = p;
            } while ((e = e.next) != null);
            // 若指定的位置頭結點不爲空則進行樹形化
            if ((tab[index] = hd) != null)
                // 根據鏈表創建紅黑樹結構
                hd.treeify(tab);
        }
    }

 

        /**
         * Forms tree of the nodes linked from this node.
         * @return root of tree
         */
        final void treeify(Node<K,V>[] tab) {
            // 定義樹的根節點
            TreeNode<K,V> root = null;
            // 遍歷鏈表,x指向當前節點、next指向下一個節點
            for (TreeNode<K,V> x = this, next; x != null; x = next) {
                next = (TreeNode<K,V>)x.next;
                x.left = x.right = null;
                // 如果還沒有根節點
                if (root == null) {
                    x.parent = null;
                    // 當前節點的紅色屬性設爲false(把當前節點設爲黑色)
                    x.red = false;
                    // 根節點指向到當前節點
                    root = x;
                }
                else {
                    // 如果已經存在根節點了  
                    K k = x.key;
                    int h = x.hash;
                    Class<?> kc = null;
                    // 從根節點開始遍歷,此遍歷沒有設置邊界,只能從內部跳出
                    for (TreeNode<K,V> p = root;;) {
                        // dir 標識方向(左右)、ph標識當前樹節點的hash值
                        int dir, ph;
                        // 當前樹節點的key
                        K pk = p.key;
                        // 如果當前樹節點hash值 大於 當前鏈表節點的hash值
                        if ((ph = p.hash) > h)
                            // 標識當前鏈表節點會放到當前樹節點的左側
                            dir = -1;
                        else if (ph < h)
                            // 右側
                            dir = 1;
                         /*
                 * 如果兩個節點的key的hash值相等,那麼還要通過其他方式再進行比較
                 * 如果當前鏈表節點的key實現了comparable接口,並且當前樹節點和鏈表節點是相同            
                 * Class的實例,那麼通過comparable的方式再比較兩者。
                 * 如果還是相等,最後再通過tieBreakOrder比較一次
                 */
                        else if ((kc == null &&
                                  (kc = comparableClassFor(k)) == null) ||
                                 (dir = compareComparables(kc, k, pk)) == 0)
                            dir = tieBreakOrder(k, pk);

                        TreeNode<K,V> xp = p;
                        if ((p = (dir <= 0) ? p.left : p.right) == null) {
                            // 當前鏈表節點 作爲 當前樹節點的子節點
                            x.parent = xp;
                            if (dir <= 0)
                                xp.left = x;
                            else
                                xp.right = x;
                            // 重新平衡
                            root = balanceInsertion(root, x);
                            break;
                        }
                    }
                }
            }
            // 把所有的鏈表節點都遍歷完之後,最終構造出來的樹可能經歷多個平衡操作,根節點目前到 
            // 底是鏈表的哪一個節點是不確定的
            // 因爲我們要基於樹來做查找,所以就應該把 tab[N] 得到的對象一定根節點對象,而目前只 
            // 是鏈表的第一個節點對象,所以要做相應的處理。
            moveRootToFront(tab, root);
        }

 

6.1.4  HashMap如何保證key值唯一

這裏還有個需要關注的地方,就是HashMap的key值是唯一的,這也是爲什麼Set集合可以保存不重複元素的基礎。那麼怎麼實現key值唯一呢?注意看上面的put()方法中有一行代碼:

// 根據key值計算其hash值,定位數組中可以插入的數組位置
int hashValue = hash(key);

實際上,我們往HashMap中存儲數據時,會先檢查要存的數據的key值,通過hash()方法得到一個hashCode值,這個值用於定位數組中的位置,若該位置爲空則表示這是一個全新的元素,在該數組位置直接新建鏈表把這個元素保存起來,此時該key值在整個HashMap中只有一個,是唯一的;若該位置不爲空,已經有鏈表了,說明有其他元素的key的hashCode值和現在要保存元素的key的HashCode值一樣,那麼極有可能key值是一樣的,就需要循環判斷是否有重複的key值,判斷的方法是equals()(默認不重寫equals方法時,“equals”和“==”一樣,判斷對象的引用(地址)是否相同,即判斷是否是同一個對象),若有則替換該處的value值,若無則直接在尾部新增,因此key值也是唯一的。

總結起來就是,先用hash()方法判斷key的HashCode值是否一樣,若一樣再用equals()方法判斷是否key已經存在,若存在就覆蓋該key對應的value值,始終保持key值唯一。

還要注意一個辯證原則:

  1. 如果兩個對象equals,他們的hashcode一定相等,反過來不一定。
  2. 如果兩個對象不equals,他們的hashcode有可能相等

簡單的說就是:“如果兩個對象相同,那麼他們的hashcode應該相等。

若重寫了equals方法就必須重寫hashCode方法,不能違反辯證原則一,即確保通過equals(Object obj)方法判斷結果爲true的兩個對象具備相等的hashcode()返回值,若只重寫了equals,不重寫hashcode可能導致equals(Object obj)方法判斷結果爲true而hashcode()返回值不相等。

6.1.5  HashMap的線程安全問題

HashMap都說是線程不安全的,其實有些太絕對,準確的說它的get()方法是線程安全的,但是put()方法是線程不安全的,我們分別看下這兩個方法的源碼(put()方法源碼在本文上面已列):

get()方法不會改變數據存儲結構,無論哪個線程訪問都是一樣的結果,因此線程安全。

put()方法會改變原有的存儲結構,可能會進行擴容,a線程訪問比如A[0]這個位置值爲1,等b線程進來後可能會擴容,這時A[0]這個位置的值在擴容後就不一定還在原來A[0]的位置,這時再訪問A[0]可能是空或者別的值,因此線程不安全。

注意:

HashMap中resize()方法用於擴容:當HashMap中的元素越來越多的時候,碰撞的機率也就越來越高(因爲數組的長度是固定的),所以爲了提高查詢的效率,就要對HashMap的數組進行擴容,數組擴容這個操作也會出現在ArrayList中,所以這是一個通用的操作,很多人對它的性能表示過懷疑,不過想想我們的“均攤”原理,就釋然了,而在hashmap數組擴容之後,最消耗性能的點就出現了:原數組中的數據必須重新計算其在新數組中的位置,並放進去,System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length);核心方法。

6.1.6  負載因子

HashMap中有一個靜態屬性(其他Map子類都有,可能命名和形式有些差別,但作用一樣),就是負載因子。 

HashMap有一個構造方法爲:

HashMap(int initialCapacity, float loadFactor)

這兩個參數,一個是 int initCapacity(初始化數組大小,默認值是16),一個是float loadFactor(負載因子,默認值是0.75),首先會根據initCapacity計算出一個大於或者等於initCapacity且爲2的冪的值capacity,例如initCapacity爲15,那麼capacity就會爲16,還會算出一個臨界值threshold,也就是capacity * loadFactor,initailCapacity,loadFactor會影響到HashMap擴容。

負載因子表示一個散列表的空間的使用程度,有這樣一個公式:initailCapacity*loadFactor=HashMap的容量。
所以負載因子越大則散列表的裝填程度越高,也就是能容納更多的元素,元素多了,鏈表大了,所以此時索引效率就會降低。
反之,負載因子越小則鏈表中的數據量就越稀疏,此時會對空間造成爛費,但是此時索引效率高。負載因子的大小決定了HashMap的數據密度。負載因子越大密度越大,發生碰撞的機率越高,數組中的鏈表越容易長,造成查詢或插入時的比較次數增多,性能會下降。負載因子越小,就越容易觸發擴容,數據密度也越小,意味着發生碰撞的機率越小,數組中的鏈表也就越短,查詢和插入時比較的次數也越小,性能會更高。但是會浪費一定的內容空間。而且經常擴容也會影響性能,建議初始化預設大一點的空間。

 

6.2  LinkedHashMap

6.2.1  LinkedHashMap介紹

LinkedHashMap是HashMap的子類,很多方法都是繼承自父類,總得來說,LinkedHashMap底層是數組+單項鍊表+雙向鏈表。數組加單向鏈表就是HashMap的結構,記錄數據用,雙向鏈表,存儲插入順序用。

因此LinkedHashMap是有序的,HashMap是無序的,當我們希望有順序地去存儲key-value時,就需要使用LinkedHashMap了。例如:

public class LinkedHashMapDemo {
    public static void main(String[] args) {
        Map<String, String> hashMap = new HashMap<String, String>();
        hashMap.put("knameiuk", "josan1");
        hashMap.put("ku", "josan3");
        hashMap.put("uk", "josan3");
        hashMap.put("nakume4", "josan4");
        hashMap.put("nateme5", "josan5");
        System.out.println(hashMap);
    }
}
{nakume4=josan4, uk=josan3, knameiuk=josan1, ku=josan3, nateme5=josan5}

以上HashMap並未按照插入順序保存和輸出,也就是說是無序的。再看LinkedHashMap:

public class LinkedHashMapDemo {
    public static void main(String[] args) {
        Map<String, String> hashMap = new LinkedHashMap<>();
        hashMap.put("knameiuk", "josan1");
        hashMap.put("ku", "josan3");
        hashMap.put("uk", "josan3");
        hashMap.put("nakume4", "josan4");
        hashMap.put("nateme5", "josan5");
        System.out.println(hashMap);
    }
}
{knameiuk=josan1, ku=josan3, uk=josan3, nakume4=josan4, nateme5=josan5}

以上,LinkedHashMap是按插入順序保存和輸出。

6.2.2  LinkedHashMap如何選擇排序方式

LinkedHashMap就是HashMap+雙向鏈表,雙向鏈表使得它支持兩種排序:插入順序和訪問順序,且默認爲插入順序,就像上面的示例代碼。那麼,LinkedHashMap怎麼控制這兩種順序的呢?先看下LinkedHashMap的構造方法:

  • LinkedHashMap()
  • LinkedHashMap(int initialCapacity, float loadFactor)
  • LinkedHashMap(int initialCapacity)
  • LinkedHashMap(Map<? extends K, ? extends V> m)
  • LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)

這些構造方法中出鏡率最高的就是無參構造,其源碼如下:

public LinkedHashMap() {
    super();
    accessOrder = false;
}

這裏的accessOrder是個boolean類型的變量,它就是用來控制生成的LinkedHashMap實例是按插入順序還是按訪問順序的關鍵,除了最後一個LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)構造方法傳參可以控制accessOrder的值以外,其他構造方法都默認accessOrder = false

public class LinkedHashMapDemo {
    public static void main(String[] args) {
        Map<String, String> linkMap = new LinkedHashMap<>(16, 0.16f, true);
        linkMap.put("1", "josan1");
        linkMap.put("2", "josan3");
        linkMap.put("3", "josan3");
        linkMap.put("4", "josan4");
        linkMap.put("5", "josan5");
        System.out.println("沒有get之前: " + linkMap);
        linkMap.get("3");
        System.out.println("使用get之後: " + linkMap);
    }
}
沒有get之前: {1=josan1, 2=josan3, 3=josan3, 4=josan4, 5=josan5}
使用get之後: {1=josan1, 2=josan3, 4=josan4, 5=josan5, 3=josan3}

可以看到,初始化時把accessOrder的值賦爲true,即設置該LinkedHashMap實例化對象按照訪問順序排序。這裏linkMap.get("3");導致了“3”對應的Entry移動到了最後,get()方法的源碼如下:

public V get(Object key) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        return null;
    if (accessOrder)
        afterNodeAccess(e); // move node to last
    return e.value;
}

可以看出get()方法的邏輯,accessOrder若爲true(按訪問順序),則調用內部afterNodeAccess()方法,將被訪問的值(key-value)放到鏈表尾部。若accessOrder爲false(按插入順序),則不會出現上述變化。afterNodeAccess()方法源碼如下:

6.2.3  LinkedHashMap怎麼實現的排序

看到這你可能明白了怎麼控制LinkedHashMap選擇按訪問順序或者按插入順序,通過布爾類型的accessOrder變量可以切換排序模式,但是你可能有個疑問,LinkedHashMap怎麼實現的排序?

與HashMap的單向鏈表相比,LinkedHashMap增加了雙向鏈表。

從上面自己實現的HashMap來看,有first和last兩個節點分別表示該鏈表的頭和尾,first頭不會改變,last尾隨着插入數據向後移動,由first遍歷到last就是按照插入順序獲取,實現了按插入順序排序(first頭始終是遍歷的入口,在源碼中,first即head,他的hash值是-1,也就是說head是不在數組table中的)。

 

6.3  ConcurrentHashMap

6.3.1  ConcurrentHashMap特點

ConcurrentHashMap可以看成是併發的HashMap,默認併發級別爲16線程。

ConcurrentHashMap的特點:Hashtable的線程安全性 + HashMap的高性能。

原因:
(1)數據更新的時候只鎖對應區域(桶),而其他區域的訪問不受影響;
(2)在鎖的區域使用讀寫鎖,讀異步而寫同步,即便在同一個桶中,數據讀取依然不受影響。

ConcurrentHashMap當鏈表節點數超過指定閾值的話,也是會轉換成紅黑樹的,大體結構是一樣的。源碼入下(主要看註釋):

/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    //1. 計算key的hash值
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        //2. 如果當前table還沒有初始化先調用initTable方法將tab進行初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        //3. tab中索引爲i的位置的元素爲null,則直接使用CAS將值插入即可
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        //4. 當前正在擴容
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    //5. 當前爲鏈表,在鏈表中插入新的鍵值對
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    // 6.當前爲紅黑樹,將新的鍵值對插入到紅黑樹中
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            // 7.插入完鍵值對後再根據實際大小看是否需要轉換成紅黑樹
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    //8.對當前容量大小進行檢查,如果超過了臨界值(實際大小*加載因子)就需要擴容 
    addCount(1L, binCount);
    return null;
}

6.3.2  內部結構

ConcurrentHashMap是由Segment數組結構和HashEntry數組結構組成。Segment是一種可重入鎖ReentrantLock,在ConcurrentHashMap裏扮演鎖的角色,HashEntry則用於存儲鍵值對數據。一個ConcurrentHashMap裏包含一個Segment數組,Segment的結構和HashMap類似,是一種數組和鏈表結構, 一個Segment裏包含一個HashEntry數組,每個HashEntry是一個鏈表結構的元素, 每個Segment守護者一個HashEntry數組裏的元素,當對HashEntry數組的數據進行修改時,必須首先獲得它對應的Segment鎖。

ConcurrentHashMap定位一個元素的過程需要進行兩次Hash操作,第一次Hash定位到Segment,第二次Hash定位到元素所在的鏈表的頭部,因此,這一種結構的帶來的副作用是Hash的過程要比普通的HashMap要長,但是帶來的好處是寫操作的時候可以只對元素所在的Segment進行加鎖即可,不會影響到其他的Segment,這樣,在最理想的情況下,ConcurrentHashMap可以最高同時支持Segment數量大小的寫操作(剛好這些寫操作都非常平均地分佈在所有的Segment上),所以,通過這一種結構,ConcurrentHashMap的併發能力可以大大的提高。

1.Segment(分段鎖)

ConcurrentHashMap中的分段鎖稱爲Segment,它即類似於HashMap的結構,即內部擁有一個Entry數組,數組中的每個元素又是一個鏈表,同時又是一個ReentrantLock鎖(Segment繼承了ReentrantLock實現鎖功能)。

2.內部結構

ConcurrentHashMap使用分段鎖技術,將數據分成一段一段的存儲,然後給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據的時候,其他段的數據也能被其他線程訪問,能夠實現真正的併發訪問。

從上面的結構我們可以瞭解到,ConcurrentHashMap定位一個元素的過程需要進行兩次Hash操作。

第一次Hash定位到Segment,第二次Hash定位到元素所在的鏈表的頭部。

3.該結構的優劣勢

壞處

這一種結構的帶來的副作用是Hash的過程要比普通的HashMap要長

好處

寫操作的時候可以只對元素所在的Segment進行加鎖即可,不會影響到其他的Segment,這樣,在最理想的情況下,ConcurrentHashMap可以最高同時支持Segment數量大小的寫操作(剛好這些寫操作都非常平均地分佈在所有的Segment上)。所以,通過這一種結構,ConcurrentHashMap的併發能力可以大大的提高。

ConcurrentHashMap通過將完整的表分成若干個segment的方式實現鎖分離,每個segment都是一個獨立的線程安全的Hash表,當需要操作數據時,HashMap通過Key的hash值和segment數量來路由到某個segment。這裏segment繼承了ReentrantLock,ReentrantLock可通過構造參數設置時公平鎖還是非公平鎖,需要明文釋放鎖,而synchronized是自動釋放的。

static final class Segment<K,V> extends ReentrantLock implements Serializable {
    /*
    1.segment的讀操作不需要加鎖,但需要volatile讀
    2.當進行擴容時(調用reHash方法),需要拷貝原始數據,在拷貝數據上操作,保證在擴容完成前讀操作仍可以在原始數據上進行。
    3.只有引起數據變化的操作需要加鎖。
    4.scanAndLock(刪除、替換)/scanAndLockForPut(新增)兩個方法提供了獲取鎖的途徑,是通過自旋鎖實現的。
    5.在等待獲取鎖的過程中,兩個方法都會對目標數據進行查找,每次查找都會與上次查找的結果對比,雖然查找結果不會被調用它的方法使用,但是這樣做可以減少後續操作可能的cache miss。
         */

        private static final long serialVersionUID = 2249069246763182397L;
         
         /*
         自旋鎖的等待次數上限,多處理器時64次,單處理器時1次。
         每次等待都會進行查詢操作,當等待次數超過上限時,不再自旋,調用lock方法等待獲取鎖。
         */
        static final int MAX_SCAN_RETRIES =
            Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
         /*
         segment中的hash表,與hashMap結構相同,表中每個元素都是一個鏈表。
         */
        transient volatile HashEntry<K,V>[] table;

        /*
         表中元素個數
         */
        transient int count;

        /*
        記錄數據變化操作的次數。
        這一數值主要爲Map的isEmpty和size方法提供同步操作檢查,這兩個方法沒有爲全表加鎖。
        在統計segment.count前後,都會統計segment.modCount,如果前後兩次值發生變化,可以判斷在統計count期間有segment發生了其它操作。
         */
        transient int modCount;

        /*
        容量閾值,超過這一數值後segment將進行擴容,容量變爲原來的兩倍。
        threshold = loadFactor*table.length
         */
        transient int threshold;

        final float loadFactor;

        Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
            this.loadFactor = lf;
            this.threshold = threshold;
            this.table = tab;
        }
         /*
         onlyIfAbsent:若爲true,當key已經有對應的value時,不進行替換;
         若爲false,即使key已經有對應的value,仍進行替換。
         
         關於put方法,很重要的一點是segment最大長度的問題:
         代碼 c > threshold && tab.length < MAXIMUM_CAPACITY 作爲是否需要擴容的判斷條件。
         擴容條件是node總數超過閾值且table長度小於MAXIMUM_CAPACITY也就是2的30次冪。
         由於擴容都是容量翻倍,所以tab.length最大值就是2的30次冪。此後,即使node總數超過了閾值,也不會擴容了。
         由於table[n]對應的是一個鏈表,鏈表內元素個數理論上是無限的,所以segment的node總數理論上也是無上限的。
         ConcurrentHashMap的size()方法考慮到了這個問題,當計算結果超過Integer.MAX_VALUE時,直接返回Integer.MAX_VALUE.
         
         */
        final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            //tryLock判斷是否已經獲得鎖.
            //如果沒有獲得,調用scanAndLockForPut方法自旋等待獲得鎖。
            HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
                HashEntry<K,V>[] tab = table;
                //計算key在表中的下標
                int index = (tab.length - 1) & hash;
                //獲取鏈表的第一個node
                HashEntry<K,V> first = entryAt(tab, index);
                for (HashEntry<K,V> e = first;;) {
                //鏈表下一個node不爲空,比較key值是否相同。
                //相同的,根據onlyIfAbsent決定是否替換已有的值
                    if (e != null) {
                        K k;
                        if ((k = e.key) == key ||
                            (e.hash == hash && key.equals(k))) {
                            oldValue = e.value;
                            if (!onlyIfAbsent) {
                                e.value = value;
                                ++modCount;
                            }
                            break;
                        }
                        e = e.next;
                    }
                    else {
                    //鏈表遍歷到最後一個node,仍沒有找到key值相同的.
                    //此時應當生成新的node,將node的next指向鏈表表頭,這樣新的node將處於鏈表的【表頭】位置
                        if (node != null)
                        //scanAndLockForPut當且僅當hash表中沒有該key值時
                        //纔會返回新的node,此時node不爲null
                            node.setNext(first);
                        else
                        //node爲null,表明scanAndLockForPut過程中找到了key值相同的node
                        //可以斷定在等待獲取鎖的過程中,這個node被刪除了,此時需要新建一個node
                            node = new HashEntry<K,V>(hash, key, value, first);
                        //添加新的node涉及到擴容,當node數量超過閾值時,調用rehash方法進行擴容,並將新的node加入對應鏈表表頭;
                        //沒有超過閾值,直接加入鏈表表頭。
                        int c = count + 1;
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            rehash(node);
                        else
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
                unlock();
            }
            return oldValue;
        }

        /*
         hash表容量翻倍,將需要添加的node添加到擴容後的表中。
         hash表默認初始長度爲16,實際長度總是2的n次冪。
         設當前table長度爲S,根據key的hash值計算table中下標index的公式:
         擴容前:oldIndex = (S-1)&hash
         擴容後:newIndex = (S<<1-1)&hash
         擴容前後下標變化:newIndex-oldIndex = S&hash
         所以,擴容前後node所在鏈表在table中的下標要麼不變,要麼右移2的冪次。
         根據本方法官方註釋說明,大約六分之一的node需要複製操作。
         
         對於每個鏈表,處理方法如下:
         
         步驟一:對於鏈表中的每個node,計算node和node.next的新下標,如果它們不相等,記錄最後一次出現這種情況時的node.next,記爲nodeSpecial。
         這一部分什麼意思呢,假設table[n]所在的鏈表共有6個node,計算它們的新下標:
         情況1:若計算結果爲0:n,1:n+S,2:n,3:n+2,4:n,5:n,那麼我們記錄的特殊node編號爲4;
         情況2:若計算結果爲0:n,1:n+S,2:n,3:n+2,4:n+4,5:n+8,那麼我們記錄的特殊node編號爲5;
         情況3:若計算結果爲0:n,1:n,2:n,3:n,4:n,5:n,特殊node爲0;
         情況4:若計算結果爲0:n+S,1:n+S,2:n+S,3:n+S,4:n+S,5:n+S,特殊node爲0。
         很重要的一點,由於新下標只可能是n或n+S,因此這兩個位置的鏈表中不會出現來自其它鏈表的node。
         對於情況3,令table[n]=node0,進入步驟三;
         對於情況4,令table[n+S]=node0,進入步驟三;
         對於情況1,令table[n]=node4,進入步驟二;
         對於情況2,令table[n+S]=node3,進入步驟二。
         
         步驟二:從node0遍歷至nodeSpecial的前一個node,對於每一個node,調用HashEntry構造方法複製這個node,放入對應的鏈表。
         
         步驟三:計算需要新插入的node的下標index,同樣令node.next=table[index],table[index]=node,將node插入鏈表表頭。
         
         通過三步完成了鏈表的擴容和新node的插入。
         
         在理解這一部分代碼的過程中,牢記三點:
         1.調用rehash方法的前提是已經獲得了鎖,所以擴容過程中不存在其他線程修改數據;
         2.新的下標只有兩種情況,原始下標n或者新下標n+S;
         3.通過2可以推出,原表中不在同一鏈表的node,在新表中仍不會出現在同一鏈表中。
         */
        @SuppressWarnings("unchecked")
        private void rehash(HashEntry<K,V> node) {
            //拷貝table,所有操作都在oldTable上進行,不會影響無需獲得鎖的讀操作
            HashEntry<K,V>[] oldTable = table;
            int oldCapacity = oldTable.length;
            int newCapacity = oldCapacity << 1;//容量翻倍
            threshold = (int)(newCapacity * loadFactor);//更新閾值
            HashEntry<K,V>[] newTable =
                (HashEntry<K,V>[]) new HashEntry[newCapacity];
            int sizeMask = newCapacity - 1;
            for (int i = 0; i < oldCapacity ; i++) {
                HashEntry<K,V> e = oldTable[i];
                if (e != null) {
                    HashEntry<K,V> next = e.next;
                    int idx = e.hash & sizeMask;//新的table下標,定位鏈表
                    if (next == null)
                        //鏈表只有一個node,直接賦值
                        newTable[idx] = e;
                    else {
                        HashEntry<K,V> lastRun = e;
                        int lastIdx = idx;
                        //這裏獲取特殊node
                        for (HashEntry<K,V> last = next;
                             last != null;
                             last = last.next) {
                            int k = last.hash & sizeMask;
                            if (k != lastIdx) {
                                lastIdx = k;
                                lastRun = last;
                            }
                        }
                        //步驟一中的table[n]賦值過程
                        newTable[lastIdx] = lastRun;
                        // 步驟二,遍歷剩餘node,插入對應表頭
                        for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                            V v = p.value;
                            int h = p.hash;
                            int k = h & sizeMask;
                            HashEntry<K,V> n = newTable[k];
                            newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
                        }
                    }
                }
            }
            //步驟三,處理需要插入的node
            int nodeIndex = node.hash & sizeMask;
            node.setNext(newTable[nodeIndex]);
            newTable[nodeIndex] = node;
            //將擴容後的hashTable賦予table
            table = newTable;
        }

        /*
        put方法調用本方法獲取鎖,通過自旋鎖等待其他線程釋放鎖。
        變量retries記錄自旋鎖循環次數,當retries超過MAX_SCAN_RETRIES時,不再自旋,調用lock方法等待鎖釋放。
        變量first記錄hash計算出的所在鏈表的表頭node,每次循環結束,重新獲取表頭node,與first比較,如果發生變化,說明在自旋期間,有新的node插入了鏈表,retries計數重置。
        自旋過程中,會遍歷鏈表,如果發現不存在對應key值的node,創建一個,這個新node可以作爲返回值返回。
         根據官方註釋,自旋過程中遍歷鏈表是爲了緩存預熱,減少hash表經常出現的cache miss
         */
        private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
            HashEntry<K,V> first = entryForHash(this, hash);
            HashEntry<K,V> e = first;
            HashEntry<K,V> node = null;
            int retries = -1; //自旋次數計數器
            while (!tryLock()) {
                HashEntry<K,V> f;
                if (retries < 0) {
                    if (e == null) {
                    //鏈表爲空或者遍歷至鏈表最後一個node仍沒有找到匹配
                        if (node == null)
                            node = new HashEntry<K,V>(hash, key, value, null);
                        retries = 0;
                    }
                    else if (key.equals(e.key))
                        retries = 0;
                    else
                        e = e.next;
                }
                else if (++retries > MAX_SCAN_RETRIES) {
                    lock();
                    break;
                }
                else if ((retries & 1) == 0 &&
                         (f = entryForHash(this, hash)) != first) {
                //比較first與新獲得的鏈表表頭node是否一致,如果不一致,說明該鏈表別修改過,自旋計數重置
                    e = first = f;
                    retries = -1;
                }
            }
            return node;
        }

        /*
        remove,replace方法會調用本方法獲取鎖,通過自旋鎖等待其他線程釋放鎖。
        與scanAndLockForPut機制相似。
         */
        private void scanAndLock(Object key, int hash) {
            // similar to but simpler than scanAndLockForPut
            HashEntry<K,V> first = entryForHash(this, hash);
            HashEntry<K,V> e = first;
            int retries = -1;
            while (!tryLock()) {
                HashEntry<K,V> f;
                if (retries < 0) {
                    if (e == null || key.equals(e.key))
                        retries = 0;
                    else
                        e = e.next;
                }
                else if (++retries > MAX_SCAN_RETRIES) {
                    lock();
                    break;
                }
                else if ((retries & 1) == 0 &&
                         (f = entryForHash(this, hash)) != first) {
                    e = first = f;
                    retries = -1;
                }
            }
        }

        /*
        刪除key-value都匹配的node,刪除過程很簡單:
        1.根據hash計算table下標index。
        2.根據index定位鏈表,遍歷鏈表node,如果存在node的key值和value值都匹配,刪除該node。
        3.令node的前一個節點pred的pred.next = node.next。
         */
        final V remove(Object key, int hash, Object value) {
            //獲得鎖
            if (!tryLock())
                scanAndLock(key, hash);
            V oldValue = null;
            try {
                HashEntry<K,V>[] tab = table;
                int index = (tab.length - 1) & hash;
                HashEntry<K,V> e = entryAt(tab, index);
                HashEntry<K,V> pred = null;
                while (e != null) {
                    K k;
                    HashEntry<K,V> next = e.next;
                    if ((k = e.key) == key ||
                        (e.hash == hash && key.equals(k))) {
                        V v = e.value;
                        if (value == null || value == v || value.equals(v)) {
                            if (pred == null)
                                setEntryAt(tab, index, next);
                            else
                                pred.setNext(next);
                            ++modCount;
                            --count;
                            oldValue = v;
                        }
                        break;
                    }
                    pred = e;
                    e = next;
                }
            } finally {
                unlock();
            }
            return oldValue;
        }
        /*
        找到hash表中key-oldValue匹配的node,替換爲newValue,替換過程與replace方法類似,不再贅述了。
        */
        final boolean replace(K key, int hash, V oldValue, V newValue) {
            if (!tryLock())
                scanAndLock(key, hash);
            boolean replaced = false;
            try {
                HashEntry<K,V> e;
                for (e = entryForHash(this, hash); e != null; e = e.next) {
                    K k;
                    if ((k = e.key) == key ||
                        (e.hash == hash && key.equals(k))) {
                        if (oldValue.equals(e.value)) {
                            e.value = newValue;
                            ++modCount;
                            replaced = true;
                        }
                        break;
                    }
                }
            } finally {
                unlock();
            }
            return replaced;
        }

        final V replace(K key, int hash, V value) {
            if (!tryLock())
                scanAndLock(key, hash);
            V oldValue = null;
            try {
                HashEntry<K,V> e;
                for (e = entryForHash(this, hash); e != null; e = e.next) {
                    K k;
                    if ((k = e.key) == key ||
                        (e.hash == hash && key.equals(k))) {
                        oldValue = e.value;
                        e.value = value;
                        ++modCount;
                        break;
                    }
                }
            } finally {
                unlock();
            }
            return oldValue;
        }
         /*
         清空segment,將每個鏈表置爲空,count置爲0,剩下的工作交給GC。
         */
        final void clear() {
            lock();
            try {
                HashEntry<K,V>[] tab = table;
                for (int i = 0; i < tab.length ; i++)
                    setEntryAt(tab, i, null);
                ++modCount;
                count = 0;
            } finally {
                unlock();
            }
        }
    }

 

 

6.4  Hashtable

先要了解HashTable和HashMap的區別與聯繫:

  • HashMap是基於哈希表實現的,每一個元素是一個key-value對,其內部通過單鏈表解決衝突問題,容量不足(超過了閥值)時,同樣會自動增長
  • HashMap是非線程安全的,只是用於單線程環境下,多線程環境下可以採用concurrent併發包下的concurrentHashMap
  • Hashtable同樣是基於哈希表實現的,同樣每個元素是一個key-value對,其內部也是通過單鏈表解決衝突問題,容量不足(超過了閥值)時,同樣會自動增長
  • Hashtable,是線程安全的,能用於多線程環境中
  • HashMap、Hashtable都實現了Serializable接口,它支持序列化,也都實現了Cloneable接口,能被克隆
  • Hashtable繼承自Dictionary類,而HashMap繼承自AbstractMap類。但二者都實現了Map接口
  • HashMap結構中,是允許保存null的,key和Entry.value均可以爲null。但是HashTable中是不允許保存null
  • HashMap的初始容量爲16,Hashtable初始容量爲11,兩者的填充因子默認都是0.75
  • HashMap擴容時是當前容量翻倍即:capacity2,Hashtable擴容時是容量翻倍+1即:capacity2+1
  • ConcurrentHashMap不允許key和value爲null。

HashTable的主要方法的源碼實現邏輯,與HashMap中非常相似,有一點重大區別就是所有的操作都是通過synchronized鎖保護的。只有獲得了對應的鎖,才能進行後續的讀寫等操作。

put(K key, V value)源碼:

public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
        throw new NullPointerException();
    }

    // Makes sure the key is not already in the hashtable.
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) && entry.key.equals(key)) {
             V old = entry.value;
             entry.value = value;
             return old;
        }
     }

     addEntry(hash, key, value, index);
     return null;
}

get()源碼: 

public synchronized V get(Object key) {
   Entry<?,?> tab[] = table;
   int hash = key.hashCode();
   int index = (hash & 0x7FFFFFFF) % tab.length;
   for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
       if ((e.hash == hash) && e.key.equals(key)) {
           return (V)e.value;
       }
   }
   return null;
}

rehash()擴容方法源碼:

protected void rehash() {
  int oldCapacity = table.length;
  Entry<?,?>[] oldMap = table;

  // overflow-conscious code
  int newCapacity = (oldCapacity << 1) + 1;
  if (newCapacity - MAX_ARRAY_SIZE > 0) {
     if (oldCapacity == MAX_ARRAY_SIZE)
         // Keep running with MAX_ARRAY_SIZE buckets
         return;
         newCapacity = MAX_ARRAY_SIZE;
     }
   Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];

   modCount++;
   threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
   table = newMap;

   for (int i = oldCapacity ; i-- > 0 ;) {
       for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
           Entry<K,V> e = old;
           old = old.next;

           int index = (e.hash & 0x7FFFFFFF) % newCapacity;
           e.next = (Entry<K,V>)newMap[index];
           newMap[index] = e;
        }
   }
}

那麼既然ConcurrentHashMap那麼優秀,爲什麼還要有Hashtable的存在呢?ConcurrentHashMap能完全替代HashTable嗎?

HashTable雖然性能上不如ConcurrentHashMap,但並不能完全被取代,兩者的迭代器的一致性不同的,HashTable的迭代器是強一致性的,而ConcurrentHashMap是弱一致的。可能你期望往ConcurrentHashMap底層數據結構中加入一個元素後,立馬能對get可見,但ConcurrentHashMap並不能如你所願。換句話說,put操作將一個元素加入到底層數據結構後,get可能在某段時間內還看不到這個元素,若不考慮內存模型,單從代碼邏輯上來看,卻是應該可以看得到的。

ConcurrentHashMap的弱一致性主要是爲了提升效率,是一致性與效率之間的一種權衡。要成爲強一致性,就得到處使用鎖,甚至是全局鎖,這就與Hashtable和同步的HashMap一樣了。

6.5  TreeMap

前面介紹了Map接口的實現類LinkedHashMap,LinkedHashMap存儲的元素是有序的,可以保持元素的插入順序,但不能對元素進行自動排序。假如你遇到這樣的場景,插入數據後想按照插入數據的大小來排序,該怎麼辦?當然你可以自己循環排序一下,但是開發效率就降低了,這時用TreeMap就事半功倍了。

TreeMap中的元素默認按照keys的自然排序排列。(對Integer來說,其自然排序就是數字的升序;對String來說,其自然排序就是按照字母表排序)它是通過紅黑樹(也叫平衡二叉樹)實現的,這裏簡單介紹,後面會用單獨的篇幅介紹紅黑樹,這裏簡單描述一下:

紅黑樹首先是一棵二叉樹,具有二叉樹所有的特性,即樹中的任何節點的值大於它的左子節點,且小於它的右子節點,如果是一棵左右完全均衡的二叉樹,元素的查找效率將獲得極大提高。最壞的情況就是一邊倒,只有左子樹或只有右子樹,這樣勢必會導致二叉樹的檢索效率大大降低。爲了維持二叉樹的平衡,人提出了各種實現的算法,其中平衡二叉樹就是其中的一種算法。平衡二叉樹的數據結構如下圖所示(可以到上面的紅黑樹模擬網站試試 https://www.cs.usfca.edu/~galles/visualization/RedBlack.html):

6.5.1  TreeMap的構造函數

  1. TreeMap() : TreeMap中的key按照自然排序進行排列
  2. TreeMap(Map<? extends K, ? extends V> copyFrom) :用指定的Map填充TreeMap,TreeMap中key按照自然排序排列
  3. TreeMap(Comparator<? super K> comparator) : 指定元素排序所用的比較器,key排列順序由比較器指定

6.5.2  TreeMap的使用

public class Demo {
    public static void main(String[] args) {
        Map treeMap = new TreeMap();
        treeMap.put(6, "6");
        treeMap.put(5, "5");
        treeMap.put(3, "3");
        treeMap.put(10, "10");
        treeMap.put(9, "9");
        treeMap.put(8, "8");
        System.out.println("1.輸出: " + treeMap);
        System.out.println("2.get:  " + treeMap.get(3));
        treeMap.put(3, "666");
        System.out.println("3.輸出:  " + treeMap);
    }
}
1.輸出: {3=3, 5=5, 6=6, 8=8, 9=9, 10=10}
2.get:  3
3.輸出:  {3=666, 5=5, 6=6, 8=8, 9=9, 10=10}

 按照key的自然順序排序。

6.5.3  TreeMap的遍歷

前面已經說過,Map沒有繼承Iterator接口,可以先轉換成Set再循環遍歷。

/**
 * Feng, Ge 2020/3/7 17:20
 */
public class Demo {
    public static void main(String[] args) {
        Map treeMap = new TreeMap();
        treeMap.put(6, "6");
        treeMap.put(5, "5");
        treeMap.put(3, "3");
        treeMap.put(10, "10");
        treeMap.put(9, "9");
        treeMap.put(8, "8");

        Set set = treeMap.entrySet();
        Iterator iterator = set.iterator();
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
}
3=666
5=5
6=6
8=8
9=9
10=10

6.5.4  TreeMap源碼

TreeMap的成員變量:

/**
 * Map的自動排序按照我們自己的規則,這個時候你就需要傳遞Comparator的實現類
 */
private final Comparator<? super K> comparator;

/**
 *紅黑樹的根節點。
 */
private transient Entry<K,V> root;

/**
 * 紅黑樹中節點Entry的數量
 */
private transient int size = 0;

/**
 * 紅黑樹結構的調整次數
 */
private transient int modCount = 0;

Entry:

put()方法:

public V put(K key, V value) {
    Entry<K,V> t = root;
    /**
     * 如果根節點都爲null,還沒建立起來紅黑樹,先new Entry並賦值給root把紅黑樹建立起來,這個時候紅
     * 黑樹中已經有一個節點了,同時修改操作+1。
     */
    if (t == null) {
        compare(key, key); 
        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    /**
     * 如果節點不爲null,定義一個cmp,這個變量用來進行二分查找時的比較;定義parent,是new Entry時必須
     * 要的參數
     */
    int cmp;
    Entry<K,V> parent;
    // 有無自己定義的排序規則,分兩種情況遍歷執行
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        /**
         * 有自定義的排序規則:
         * 從root節點開始遍歷,通過二分查找逐步向下找
         * 第一次循環:從根節點開始,這個時候parent就是根節點,然後通過自定義的排序算法
         * cpr.compare(key, t.key)比較傳入的key和根節點的key值,如果傳入的key<root.key,那麼
         * 繼續在root的左子樹中找,從root的左孩子節點(root.left)開始:如果傳入的key>root.key,
         * 那麼繼續在root的右子樹中找,從root的右孩子節點(root.right)開始;如果恰好key==root.key,
         * 那麼直接根據root節點的value值即可。
         * 後面的循環規則一樣,當遍歷到的當前節點作爲起始節點,逐步往下找
         *
         * 需要注意的是:這裏並沒有對key是否爲null進行判斷,建議自己的實現Comparator時應該要考慮在內
         */
        do {
            parent = t;
            cmp = cpr.compare(key, t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    else {
        //從這裏看出,當默認排序時,key值是不能爲null的
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
        Comparable<? super K> k = (Comparable<? super K>) key;
        //這裏的實現邏輯和上面一樣,都是通過二分查找,就不再多說了
        do {
            parent = t;
            cmp = k.compareTo(t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    /**
     * 能執行到這裏,說明前面並沒有找到相同的key,節點已經遍歷到最後了,我們只需要new一個Entry放到
     * parent下面即可,但放到左子節點上還是右子節點上,就需要按照紅黑樹的規則來。
     */
    Entry<K,V> e = new Entry<>(key, value, parent);
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    /**
     * 節點加進去了,並不算完,我們在前面紅黑樹原理章節提到過,一般情況下加入節點都會對紅黑樹的結構造成
     * 破壞,我們需要通過一些操作來進行自動平衡處置,如【變色】【左旋】【右旋】
     */
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}
private void fixAfterInsertion(Entry<K,V> x) {
    //新插入的節點爲紅色節點
    x.color = RED;
    //我們知道父節點爲黑色時,並不需要進行樹結構調整,只有當父節點爲紅色時,才需要調整
    while (x != null && x != root && x.parent.color == RED) {
        //如果父節點是左節點,對應上表中情況1和情況2
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            Entry<K,V> y = rightOf(parentOf(parentOf(x)));
            //如果叔父節點爲紅色,對應於“父節點和叔父節點都爲紅色”,此時通過變色即可實現平衡
            //此時父節點和叔父節點都設置爲黑色,祖父節點設置爲紅色
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                //如果插入節點是黑色,插入的是右子節點,通過【左右節點旋轉】(這裏先進行父節點左旋)
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateLeft(x);
                }
                //設置父節點和祖父節點顏色
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                //進行祖父節點右旋(這裏【變色】和【旋轉】並沒有嚴格的先後順序,達成目的就行)
                rotateRight(parentOf(parentOf(x)));
            }
        } else {
            //父節點是右節點的情況
            Entry<K,V> y = leftOf(parentOf(parentOf(x)));
            //對應於“父節點和叔父節點都爲紅色”,此時通過變色即可實現平衡
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                //如果插入節點是黑色,插入的是左子節點,通過【右左節點旋轉】(這裏先進行父節點右旋)
                if (x == leftOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateRight(x);
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                //進行祖父節點左旋(這裏【變色】和【旋轉】並沒有嚴格的先後順序,達成目的就行)
                rotateLeft(parentOf(parentOf(x)));
            }
        }
    }
    //根節點必須爲黑色
    root.color = BLACK;
}
private void rotateLeft(Entry<K,V> p) {
    if (p != null) {
        /**
         * 斷開當前節點p與其右子節點的關聯,重新將節點p的右子節點的地址指向節點p的右子節點的左子節點
         * 這個時候節點r沒有父節點
         */
        Entry<K,V> r = p.right;
        p.right = r.left;
        //將節點p作爲節點r的父節點
        if (r.left != null)
            r.left.parent = p;
        //將節點p的父節點和r的父節點指向同一處
        r.parent = p.parent;
        //p的父節點爲null,則將節點r設置爲root
        if (p.parent == null)
            root = r;
        //如果節點p是左子節點,則將該左子節點替換爲節點r
        else if (p.parent.left == p)
            p.parent.left = r;
        //如果節點p爲右子節點,則將該右子節點替換爲節點r
        else
            p.parent.right = r;
        //重新建立p與r的關係
        r.left = p;
        p.parent = r;
    }
}

 

get()方法:

public V get(Object key) {
    Entry<K,V> p = getEntry(key);
    return (p==null ? null : p.value);
}
/**
 * 從root節點開始遍歷,通過二分查找逐步向下找
 * 第一次循環:從根節點開始,這個時候parent就是根節點,然後通過k.compareTo(p.key)比較傳入的key和
 * 根節點的key值;
 * 如果傳入的key<root.key, 那麼繼續在root的左子樹中找,從root的左孩子節點(root.left)開始;
 * 如果傳入的key>root.key, 那麼繼續在root的右子樹中找,從root的右孩子節點(root.right)開始;
 * 如果恰好key==root.key,那麼直接根據root節點的value值即可。
 * 後面的循環規則一樣,當遍歷到的當前節點作爲起始節點,逐步往下找
 */
//默認排序情況下的查找
final Entry<K,V> getEntry(Object key) {
    
    if (comparator != null)
        return getEntryUsingComparator(key);
    if (key == null)
        throw new NullPointerException();
    @SuppressWarnings("unchecked")
    Comparable<? super K> k = (Comparable<? super K>) key;
    Entry<K,V> p = root;
    while (p != null) {
        int cmp = k.compareTo(p.key);
        if (cmp < 0)
            p = p.left;
        else if (cmp > 0)
            p = p.right;
        else
            return p;
    }
    return null;
}
/**
 * 從root節點開始遍歷,通過二分查找逐步向下找
 * 第一次循環:從根節點開始,這個時候parent就是根節點,然後通過自定義的排序算法
 * cpr.compare(key, t.key)比較傳入的key和根節點的key值,如果傳入的key<root.key,那麼
 * 繼續在root的左子樹中找,從root的左孩子節點(root.left)開始:如果傳入的key>root.key,
 * 那麼繼續在root的右子樹中找,從root的右孩子節點(root.right)開始;如果恰好key==root.key,
 * 那麼直接根據root節點的value值即可。
 * 後面的循環規則一樣,當遍歷到的當前節點作爲起始節點,逐步往下找
 */
//自定義排序規則下的查找
final Entry<K,V> getEntryUsingComparator(Object key) {
    @SuppressWarnings("unchecked")
    K k = (K) key;
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        Entry<K,V> p = root;
        while (p != null) {
            int cmp = cpr.compare(k, p.key);
            if (cmp < 0)
                p = p.left;
            else if (cmp > 0)
                p = p.right;
            else
                return p;
        }
    }
    return null;
}

 

Map小結——如何選擇Map:

  • HashMap可實現快速存儲和檢索,但其缺點是其包含的元素是無序的,這導致它在存在大量迭代的情況下表現不佳。
  • LinkedHashMap保留了HashMap的優勢,且其包含的元素是有序的。它在有大量迭代的情況下表現更好。
  • TreeMap能便捷的實現對其內部元素的各種排序,但其一般性能比前兩種map差。 

LinkedHashMap映射減少了HashMap排序中的混亂,且不會導致TreeMap的性能損失。

七、Set

提起set集合,Set集合與List集合的區別就是,Set集合的元素不能重複,且是無序的(LinkedHashSet有序),List集合的元素是可以重複的。如下:

public class SetTest {
    public static void main(String[] args) {
        Set set = new HashSet();
        // Set set = new TreeSet();
        // Set set = new LinkedHashSet();    
        set.add("a1");
        set.add("a2");
        System.out.println("未添加重複元素之前的set集合: " + set);
        System.out.println("添加【不重複】元素返回值是: " + set.add("a3"));
        System.out.println("添加【重複】元素返回值是: " + set.add("a1"));
        System.out.println("未添加重複元素之後的set集合: " + set);
    }
}

結果:

未添加重複元素之前的set集合: [a1, a2]
添加【不重複】元素返回值是: true
添加【重複】元素返回值是: false
未添加重複元素之後的set集合: [a1, a2, a3]

可以看出我們想在set集合中添加重複元素是添加不上的,這裏無論是HashSet還是LinkedHashSet或者TreeSet都是一樣的,都無法保存重複元素。

Set集合的繼承關係可以參考本文一開始的繼承關係圖,set的實現類有很多,包含:AbstractSet , ConcurrentHashMap.KeySetView , ConcurrentSkipListSet , CopyOnWriteArraySet , EnumSet , HashSet , JobStateReasons , LinkedHashSet , TreeSet,這裏重點看HashSet 、 LinkedHashSet 和 TreeSet

實際上set主要基於各種map進行實現,具體的:

  • HashSet:內部採用HashMap實現的
  • LinkedHashSet:採用LinkedHashMap實現
  • TreeSet:TreeMap

7.1  HashSet

HashSet有以下Tips:

  • 不允許有重複元素
  • 無序,即添加順序和遍歷出來的順序是不同的
  • 基於HashMap實現

HashMap的key值通過hashCode和equals實現了不重複,HashSet正是運用了這個特性實現了不重複保存元素。HashSet的構造方法返回的是一個HashMap對象,把要保存的數據全部保存到該HashMap對象的key中,而該HashMap對象的所有value值則統一用一個Object類型的對象來填充。下面自己簡單寫個HashSet類:

public class MySet {
    private transient HashMap<Object,Object> map;
    // 模擬一個value值
    private static final Object PRESENT = new Object();

    // 構造方法返回一個HashMap對象
    public MySet() {
        map = new HashMap<>();
    }

    // 直接調用的是HashMap的put()方法, value值是固定的Object對象 
    public boolean add(Object e) {
        return map.put(e, PRESENT)==null;
    }

    public int size() {
        return map.size();
    }

    public static void main(String[] args) {
        MySet set = new MySet();
        set.add("hahaha");
        set.add("ooooo");
        set.add("ooooo");
        System.out.println(set.size());
    }
}
2

PRESENT對象是模擬所有的value值,源碼是這樣註釋的:“// Dummy value to associate with an Object in the backing Map”

7.2  LinkedHashSet

明白了HashSet再去理解LinkedHashSet就簡單了,與HashSet不同的是LinkedHashSet除了不允許重複外,可以支持排序(所以那些直接說Set集合是無序的說法不夠嚴謹,LinkedHashSet是支持排序的,即添加順序和遍歷順序一致)。

爲啥LinkedHashSet能支持排序呢?沒錯,你猜的沒錯,它的底層是一個LinkedHashMap,自己簡單寫個LinkedHashSet類如下:

public class MySet {
    private transient HashMap<Object,Object> map;
    // Dummy value to associate with an Object in the backing Map
    // 模擬一個value值
    private static final Object PRESENT = new Object();

    // 構造方法返回一個LinkedHashMap對象
    public MySet() {
        map = new LinkedHashMap<>();
    }

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

    public int size() {
        return map.size();
    }

    public static void main(String[] args) {
        MySet set = new MySet();
        set.add("hahaha");
        set.add("ooooo");
        set.add("66666");
        System.out.println(set.size());
    }
}

當然源碼中LinkedHashSet繼承了HashSet,實例化LinkedHashMap是在父類HashSet中完成的。

7.3  TreeSet

TreeSet和LinkedHashSet類似,提供有序的Set集合。

TreeSet的構造函數都是通過新建一個TreeMap作爲實際存儲Set元素的容器。對於TreeMap而言,它採用一種被稱爲”紅黑樹”的排序二叉樹來保存Map中每個Entry。每個Entry被當成”紅黑樹”的一個節點來對待。

所以TreeMap添加元素,取出元素的性能都比HashMap低。當TreeMap添加元素時,需要通過循環找到新增的Entry的插入位置,因爲比較耗性能。當取出元素時,也需要通過循環才能找到合適的Entry一樣比較耗性能。但並不是說TreeMap性能低於HashMap就一無是處,TreeMap中的所有Entry總是按key根據指定的排序規則保持有序狀態。從本質上來說TreeMap就是一棵”紅黑樹”,每個Entry就是一個節點。

備註:紅黑樹是一種自平衡二叉查找樹 , 它們當中每一個節點的比較值都必須大於或等於在它的左子樹中的所有節點,並且小於或等於在它的右子樹中的所有節點。這確保紅黑樹運作時能夠快速的在樹中查找給定的值。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章