Java容器(List、Set、Map)知識點快速複習手冊

前言

本文快速回顧了Java中容器的知識點,用做面試複習,事半功倍。php

其它知識點複習手冊

概覽

容器主要包括 Collection 和 Map 兩種,Collection 又包含了 List、Set 以及 Queue。html

Collection

在這裏插入圖片描述

在這裏插入圖片描述

數組和集合的區別:java

  • 長度
    • 數組的長度固定
    • 集合的長度可變
  • 內容
    • 數組存儲的是同一種類型的元素
    • 集合能夠存儲不一樣類型的元素(可是通常咱們不這樣幹..)
  • 元素的數據類型
    • 數組能夠存儲基本數據類型,也能夠存儲引用類型
    • 集合只能存儲引用類型(若存儲的是簡單的int,它會自動裝箱成Integer)

1. Set(元素不可重複)

  • HashSet:基於HashMap實現,支持快速查找,但不支持有序性操做git

  • TreeSet:基於紅黑樹實現,支持有序性操做,可是查找效率不如 HashSet,HashSet 查找時間複雜度爲 O(1),TreeSet 則爲 O(logN);github

  • LinkedHashSet:具備 HashSet 的查找效率,且內部使用鏈表維護元素的插入順序面試

2. List(有序(存儲順序和取出順序一致),可重複)

  • ArrayList:基於動態數組實現,支持隨機訪問;算法

  • Vector:和 ArrayList 相似,但它是線程安全的;編程

  • LinkedList:基於雙向鏈表實現,只能順序訪問,可是能夠快速地在鏈表中間插入和刪除元素。不只如此,LinkedList 還能夠用做棧、隊列和雙向隊列。segmentfault

3. Queue

  • LinkedList:能夠用它來支持雙向隊列;設計模式

  • PriorityQueue:基於堆結構實現,能夠用它來實現優先隊列。

Map

在這裏插入圖片描述

  • HashMap:基於哈希實現;

  • HashTable:和 HashMap 相似,但它是線程安全的,這意味着同一時刻多個線程能夠同時寫入 HashTable 而且不會致使數據不一致。它是遺留類,不該該去使用它

  • ConcurrentHashMap:支持線程安全,而且 ConcurrentHashMap 的效率會更高,由於 ConcurrentHashMap 引入了分段鎖。

  • LinkedHashMap:使用鏈表來維護元素的順序,順序爲插入順序或者最近最少使用(LRU)順序。

  • TreeMap:基於紅黑樹實現。

Fail-Fast 機制和 Fail-Safe 機制

blog.csdn.net/Kato_op/art…

Fail-Fast

Fail-fast 機制是 java 集合(Collection)中的一種錯誤機制。 當多個線程對同一個集合的內容進行操做時,就可能會產生 fail-fast 事件。

  • 迭代器在遍歷時直接訪問集合中的內容,而且在遍歷過程當中使用一個modCount變量,

  • 集合中在被遍歷期間若是內容發生變化(增刪改),就會改變modCount的值,

  • 每當迭代器使用 hashNext()/next()遍歷下一個元素以前,都會執行checkForComodification()方法檢測,modCount變量和expectedmodCount值是否相等,

  • 若是相等就返回遍歷,不然拋出異常,終止遍歷.

注意,若是集合發生變化時修改modCount值, 恰好有設置爲了expectedmodCount值, 則異常不會拋出.(好比刪除了數據,再添加一條數據)

因此,通常來講,存在非同步的併發修改時,不可能做出任何堅定的保證。

迭代器的快速失敗行爲應該僅用於檢測程序錯誤, 而不是用他來同步。

java.util包下的集合類都是Fail-Fast機制的,不能在多線程下發生併發修改(迭代過程當中被修改).

Fail-Safe

採用安全失敗(Fail-Safe)機制的集合容器,在遍歷時不是直接在集合內容上訪問的,而是先copy原有集合內容,在拷貝的集合上進行遍歷

原理:

因爲迭代時是對原集合的拷貝的值進行遍歷,因此在遍歷過程當中對原集合所做的修改並不能被迭代器檢測到,因此不會出發ConcurrentModificationException

缺點:

迭代器並不能訪問到修改後的內容(簡單來講就是, 迭代器遍歷的是開始遍歷那一刻拿到的集合拷貝,在遍歷期間原集合發生的修改迭代器是不知道的)

使用場景:

java.util.concurrent包下的容器都是Fail-Safe的,能夠在多線程下併發使用,併發修改

容器中使用的設計模式

迭代器模式

在這裏插入圖片描述

  • Iterator它是在ArrayList等集合的內部類的方式實現

Collection 實現了 Iterable 接口,其中的 iterator() 方法可以產生一個 Iterator 對象,經過這個對象就能夠迭代遍歷 Collection 中的元素。

從 JDK 1.5 以後可使用 foreach 方法來遍歷實現了 Iterable 接口的聚合對象。

List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
for (String item : list) {
    System.out.println(item);
}
複製代碼

適配器模式

適配器模式解釋:www.jianshu.com/p/93821721b…

java.util.Arrays#asList() 能夠把數組類型轉換爲 List 類型。

@SafeVarargs
public static <T> List<T> asList(T... a) 複製代碼

若是要將數組類型轉換爲 List 類型,應該注意的是 asList() 的參數爲泛型的變長參數,所以不能使用基本類型數組做爲參數,只能使用相應的包裝類型數組。

Integer[] arr = {1, 2, 3};
List list = Arrays.asList(arr);
複製代碼

也可使用如下方式生成 List。

List list = Arrays.asList(1,2,3);
複製代碼

源碼分析

ArrayList

關鍵詞

  • 默認大小爲 10
  • 擴容 1.5 倍,加載因子爲 0.5
  • 基於動態數組實現
  • 刪除元素時不會減小容量,若但願減小容量則調用trimToSize()
  • 它不是線程安全的
  • 它能存放null值。
  • 擴容操做須要調用 Arrays.copyOf() 把原數組整個複製到新數組
  • 刪除須要調用 System.arraycopy() 將 index+1 後面的元素都複製到 index 位置上,複製的代價很高。 -序列化:只序列化數組中有元素填充那部份內容

概覽

在這裏插入圖片描述

實現了 RandomAccess 接口,所以支持隨機訪問。這是理所固然的,由於 ArrayList 是基於數組實現的。

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

擴容

若是不夠時,須要使用 grow() 方法進行擴容,新容量的大小爲 oldCapacity + (oldCapacity >> 1),也就是舊容量的 1.5 倍。

擴容操做須要調用 Arrays.copyOf() 把原數組整個複製到新數組

所以最好在建立 ArrayList 對象時就指定大概的容量大小,減小擴容操做的次數。

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

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

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    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);
}
複製代碼

加入元素:add

add(E e)

首先去檢查一下數組的容量是否足夠

  • 足夠:直接添加
  • 不足夠:擴容

擴容到原來的1.5倍,第一次擴容後,若是容量仍是小於minCapacity,就將容量擴充爲minCapacity。

add(int index, E element)

步驟:

  • 檢查角標
  • 空間檢查,若是有須要進行擴容
  • 插入元素

刪除元素:remove

步驟:

  • 檢查角標
  • 刪除元素
  • 計算出須要移動的個數,並移動
  • 設置爲null,讓GC回收(因此說不是馬上回收,而是等待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; // clear to let GC do its work
    return oldValue;
}
複製代碼

須要調用 System.arraycopy() 將 index+1 後面的元素都複製到 index 位置上,複製的代價很高。

複製數組:System.arraycopy()

看到arraycopy(),咱們能夠發現:該方法是由C/C++來編寫的

在這裏插入圖片描述

Fail-Fast

modCount 用來記錄 ArrayList 結構發生變化的次數。結構發生變化是指添加或者刪除至少一個元素的全部操做,或者是調整內部數組的大小,僅僅只是設置元素的值不算結構發生變化。

在進行序列化或者迭代等操做時,須要比較操做先後 modCount 是否改變,若是改變了須要拋出 ConcurrentModificationException。

private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount;
    s.defaultWriteObject();

    // Write out size as capacity for behavioural compatibility with clone()
    s.writeInt(size);

    // Write out all elements in the proper order.
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }

    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}
複製代碼

構造器

ArrayList 提供了三種方式的構造器:

  • public ArrayList()能夠構造一個默認初始容量爲10的空列表;
  • public ArrayList(int initialCapacity)構造一個指定初始容量的空列表;
  • public ArrayList(Collection<? extends E> c)構造一個包含指定 collection 的元素的列表,這些元素按照該collection的迭代器返回它們的順序排列的。

序列化

補充:transient講解

www.importnew.com/21517.html

你只須要實現Serilizable接口,將不須要序列化的屬性前添加關鍵字transient,序列化對象的時候,這個屬性就不會序列化到指定的目的地中。

ArrayList 基於數組實現,而且具備動態擴容特性,所以保存元素的數組不必定都會被使用,那麼就不必所有進行序列化。

保存元素的數組 elementData 使用 transient 修飾,該關鍵字聲明數組默認不會被序列化

transient Object[] elementData; // non-private to simplify nested class access
複製代碼

ArrayList 實現了 writeObject() 和 readObject() 來控制只序列化數組中有元素填充那部份內容

private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
    elementData = EMPTY_ELEMENTDATA;

    // Read in size, and any hidden stuff
    s.defaultReadObject();

    // Read in capacity
    s.readInt(); // ignored

    if (size > 0) {
        // be like clone(), allocate array based upon size not capacity
        ensureCapacityInternal(size);

        Object[] a = elementData;
        // Read in all elements in the proper order.
        for (int i=0; i<size; i++) {
            a[i] = s.readObject();
        }
    }
}
複製代碼
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount;
    s.defaultWriteObject();

    // Write out size as capacity for behavioural compatibility with clone()
    s.writeInt(size);

    // Write out all elements in the proper order.
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }

    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}
複製代碼

序列化時須要使用 ObjectOutputStream 的 writeObject() 將對象轉換爲字節流並輸出。而 writeObject() 方法在傳入的對象存在 writeObject() 的時候會去反射調用該對象的 writeObject() 來實現序列化。反序列化使用的是 ObjectInputStream 的 readObject() 方法,原理相似。

ArrayList list = new ArrayList();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeObject(list);
複製代碼

Vector

關鍵詞

  • 默認大小爲 10(與Arraylist相同)
  • 擴容 2 倍,加載因子是 1(Arraylist是擴容 1.5 倍,加載因子爲 0.5)
  • 其它幾乎與ArrayList徹底相同,惟一的區別在於 Vector 是同步的,所以開銷就比 ArrayList 要大,訪問速度更慢。
  • 使用了 synchronized 進行同步
  • Vector是jdk1.2的類了,比較老舊的一個集合類。應使用JUC的CopyOnWriteArrayList代替

替代方案

可使用 Collections.synchronizedList(); 獲得一個線程安全的 ArrayList。

List<String> list = new ArrayList<>();
List<String> synList = Collections.synchronizedList(list);
複製代碼

也可使用 concurrent 併發包下的 CopyOnWriteArrayList 類。

List<String> list = new CopyOnWriteArrayList<>();
複製代碼

CopyOnWriteArrayList

關鍵詞

  • 寫操做在一個複製的數組上進行,讀操做仍是在原始數組中進行,讀寫分離,互不影響。
  • 寫操做須要加鎖,防止併發寫入時致使寫入數據丟失。
  • 寫操做結束以後須要把原始數組指向新的複製數組。
  • 適用於讀操做遠大於寫操做的場景。

讀寫分離

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

final void setArray(Object[] a) {
    array = a;
}
複製代碼
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
    return (E) a[index];
}
複製代碼

適用場景

CopyOnWriteArrayList 在寫操做的同時容許讀操做,大大提升了讀操做的性能,所以很適合讀多寫少的應用場景。

缺陷

  • 內存佔用:在寫操做時須要複製一個新的數組,使得內存佔用爲原來的兩倍左右;
  • 數據不一致:讀操做不能讀取實時性的數據,由於部分寫操做的數據還未同步到讀數組中

因此 CopyOnWriteArrayList 不適合內存敏感以及對實時性要求很高的場景。

LinkedList

關鍵詞

  • 雙向鏈表
  • 默認大小爲 10
  • 帶 Head 和 Tail 指針
  • Node 存儲節點信息

概覽

在這裏插入圖片描述

基於雙向鏈表實現,內部使用 Node 來存儲鏈表節點信息。

private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;
}
複製代碼

每一個鏈表存儲了 Head 和 Tail 指針:

transient Node<E> first;
transient Node<E> last;
複製代碼

在這裏插入圖片描述

ArrayList 與 LinkedList 比較

  • ArrayList 基於動態數組實現,LinkedList 基於雙向鏈表實現;
  • ArrayList 支持隨機訪問,LinkedList 不支持;
  • LinkedList 在任意位置添加刪除元素更快。

刪除元素:remove

在這裏插入圖片描述

獲取元素:get

  • 下標小於長度的一半,從頭遍歷
  • 反之,從尾部遍歷

替換元素:set

set方法和get方法其實差很少,根據下標來判斷是從頭遍歷仍是從尾遍歷

其餘方法

LinkedList實現了Deque接口,所以,咱們能夠操做LinkedList像操做隊列和棧同樣

LinkedList的方法比ArrayList的方法多太多了,這裏我就不一一說明了。具體可參考:

HashMap

wiki.jikexueyuan.com/project/jav…

源碼分析:segmentfault.com/a/119000001…

關鍵詞

  • 初始容量16
  • 擴容是2倍,加載因子0.75
  • 頭插法
  • 0桶存放null
  • 從 JDK 1.8 開始,一個桶存儲的鏈表長度大於 8 時會將鏈表轉換爲紅黑樹(前提:鍵值對要超過64個)
  • 自動地將傳入的容量轉換爲2的冪次方
    • 保證運算速度:確保用位運算代替模運算來計算桶下標。hash& (length-1)運算等價於對 length 取模。
    • hash均勻分佈:數據在數組上分佈就比較均勻,而且可以利用所有二進制位,也就是說碰撞的概率小
  • table數組+Entry<K,V>[]鏈表(散列表),紅黑樹
  • 擴容操做須要把鍵值對從新插入新的 table 中,從新計算全部key有特殊機制(JDK1.8後)

存儲結構

hashMap的一個內部類Node:

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next; //鏈表結構,存儲下一個元素
複製代碼

在這裏插入圖片描述

Node內部包含了一個 Entry 類型的數組table,數組中的每一個位置被當成一個桶。

transient Entry[] table;
複製代碼

Entry 存儲着鍵值對。它包含了四個字段,從 next 字段咱們能夠看出 Entry 是一個鏈表。即數組中的每一個位置被當成一個桶,一個桶存放一個鏈表。

HashMap 使用拉鍊法來解決衝突,同一個鏈表中存放哈希值相同的 Entry。

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

    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }

    public final K getKey() {
        return key;
    }

    public final V getValue() {
        return value;
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry e = (Map.Entry)o;
        Object k1 = getKey();
        Object k2 = e.getKey();
        if (k1 == k2 || (k1 != null && k1.equals(k2))) {
            Object v1 = getValue();
            Object v2 = e.getValue();
            if (v1 == v2 || (v1 != null && v1.equals(v2)))
                return true;
        }
        return false;
    }

    public final int hashCode() {
        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
    }

    public final String toString() {
        return getKey() + "=" + getValue();
    }
}
複製代碼

構造器

在這裏插入圖片描述

構造時就會調用tableSizeFor():返回一個大於輸入參數且最近的2的整數次冪。

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
複製代碼

拉鍊法

應該注意到鏈表的插入是以頭插法方式進行的

HashMap<String, String> map = new HashMap<>();
map.put("K1", "V1");
map.put("K2", "V2");
map.put("K3", "V3");
複製代碼
  • 新建一個 HashMap,默認大小爲 16;
  • 插入 <K1,V1> 鍵值對,先計算 K1 的 hashCode 爲 115,使用除留餘數法獲得所在的桶下標 115%16=3。
  • 插入 <K2,V2> 鍵值對,先計算 K2 的 hashCode 爲 118,使用除留餘數法獲得所在的桶下標 118%16=6。
  • 插入 <K3,V3> 鍵值對,先計算 K3 的 hashCode 爲 118,使用除留餘數法獲得所在的桶下標 118%16=6,插在 <K2,V2> 前面。

查找須要分紅兩步進行:

  • 計算鍵值對所在的桶;
  • 在鏈表上順序查找,時間複雜度顯然和鏈表的長度成正比。

put 操做

  • 當咱們 put 的時候,若是 key 存在了,那麼新的 value 會代替舊的 value
  • 若是 key 存在的狀況下,該方法返回的是舊的 value,
  • 若是 key 不存在,那麼返回 null。
public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    // 鍵爲 null 單獨處理
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    // 肯定桶下標
    int i = indexFor(hash, table.length);
    // 先找出是否已經存在鍵爲 key 的鍵值對,若是存在的話就更新這個鍵值對的值爲 value
    for (Entry<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;
}
複製代碼

HashMap 容許插入鍵爲 null 的鍵值對。可是由於沒法調用 null 的 hashCode() 方法,也就沒法肯定該鍵值對的桶下標,只能經過強制指定一個桶下標來存放。HashMap 使用第 0 個桶存放鍵爲 null 的鍵值對。

private V putForNullKey(V value) {
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(0, null, value, 0);
    return null;
}
複製代碼

使用鏈表的頭插法,也就是新的鍵值對插在鏈表的頭部,而不是鏈表的尾部。

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}

void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    // 頭插法,鏈表頭部指向新的鍵值對
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}
複製代碼
Entry(int h, K k, V v, Entry<K,V> n) {
    value = v;
    next = n;
    key = k;
    hash = h;
}
複製代碼

補充:hashmap裏hash方法的高位優化:

www.cnblogs.com/liujinhong/…

note.youdao.com/yws/res/187…

設計者將key的哈希值的高位也作了運算(與高16位作異或運算,使得在作&運算時,此時的低位其實是高位與低位的結合),這就增長了隨機性,減小了碰撞衝突的可能性!

爲什麼要這麼作?

table的長度都是2的冪,所以index僅與hash值的低n位有關,hash值的高位都被與操做置爲0了。

這樣作很容易產生碰撞。設計者權衡了speed, utility, and quality,將高16位與低16位異或來減小這種影響。設計者考慮到如今的hashCode分佈的已經很不錯了,並且當發生較大碰撞時也用樹形存儲下降了衝突。僅僅異或一下,既減小了系統的開銷,也不會形成的由於高位沒有參與下標的計算(table長度比較小時),從而引發的碰撞。

肯定桶下標

不少操做都須要先肯定一個鍵值對所在的桶下標。

int hash = hash(key);
int i = indexFor(hash, table.length);
複製代碼

4.1 計算 hash 值

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();

    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
複製代碼
public final int hashCode() {
    return Objects.hashCode(key) ^ Objects.hashCode(value);
}
複製代碼

4.2 取模

令 x = 1<<\4,即 \x 爲 2 的 4 次方,它具備如下性質:

x   : 00010000
x-1 : 00001111
複製代碼

令一個數 y 與 x-1 作與運算,能夠去除 y 位級表示的第 4 位以上數:

y       : 10110010
x-1     : 00001111
y&(x-1) : 00000010
複製代碼

這個性質和 y 對 x 取模效果是同樣的:

y   : 10110010
x   : 00010000
y%x : 00000010
複製代碼

咱們知道,位運算的代價比求模運算小的多,所以在進行這種計算時用位運算的話能帶來更高的性能。

肯定桶下標的最後一步是將 key 的 hash 值對桶個數取模:hash%capacity,若是能保證 capacity 爲 2 的 n 次方,那麼就能夠將這個操做轉換爲位運算。

static int indexFor(int h, int length) {
    return h & (length-1);
}
複製代碼

當 length 老是 2 的 n 次方時,h& (length-1)運算等價於對 length 取模,也就是 h%length,可是 & 比 % 具備更高的效率。這看上去很簡單,其實比較有玄機的,咱們舉個例子來講明:

h & (table.length-1) hash table.length-1
8 & (15-1): 0100 & 1110 = 0100
9 & (15-1): 0101 & 1110 = 0100
8 & (16-1): 0100 & 1111 = 0100
9 & (16-1): 0101 & 1111 = 0101
  • 從上面的例子中能夠看出:當它們和 15-1(1110)「與」的時候,8 和 9產生了相同的結果,也就是說它們會定位到數組中的同一個位置上去,這就產生了碰撞,8 和 9 會被放到數組中的同一個位置上造成鏈表,那麼查詢的時候就須要遍歷這個鏈 表,獲得8或者9,這樣就下降了查詢的效率。

  • 同時,咱們也能夠發現,當數組長度爲 15 的時候,hash 值會與 15-1(1110)進行「與」,那麼最後一位永遠是 0,而 0001,0011,0101,1001,1011,0111,1101 這幾個位置永遠都不能存放元素了空間浪費至關大,數組可使用的位置比數組長度小了不少,這意味着進一步增長了碰撞的概率。

  • 而當數組長度爲16時,即爲2的n次方時,2n-1 獲得的二進制數的每一個位上的值都爲 1,這使得在低位上&時,獲得的和原 hash 的低位相同,加之 hash(int h)方法對 key 的 hashCode 的進一步優化,加入了高位計算,就使得只有相同的 hash 值的兩個值纔會被放到數組中的同一個位置上造成鏈表。

因此說,當數組長度爲 2 的 n 次冪的時候,不一樣的 key 算得得 index 相同的概率較小,那麼數據在數組上分佈就比較均勻,也就是說碰撞的概率小

擴容-基本原理

設 HashMap 的 table 長度爲 M,須要存儲的鍵值對數量爲 N,若是哈希函數知足均勻性的要求,那麼每條鏈表的長度大約爲 N/M,所以平均查找次數的複雜度爲 O(N/M)。

爲了讓查找的成本下降,應該儘量使得 N/M 儘量小,所以須要保證 M 儘量大,也就是說 table 要儘量大。HashMap 採用動態擴容來根據當前的 N 值來調整 M 值,使得空間效率和時間效率都能獲得保證。

和擴容相關的參數主要有:capacity、size、threshold 和 load_factor。

參數 含義
capacity table 的容量大小,默認爲 16。須要注意的是 capacity 必須保證爲 2 的 n 次方。
size 鍵值對數量。
threshold size 的臨界值,當 size 大於等於 threshold 就必須進行擴容操做。
loadFactor 裝載因子,table 可以使用的比例,threshold = capacity * loadFactor。
static final int DEFAULT_INITIAL_CAPACITY = 16;

static final int MAXIMUM_CAPACITY = 1 << 30;

static final float DEFAULT_LOAD_FACTOR = 0.75f;

transient Entry[] table;

transient int size;

int threshold;

final float loadFactor;

transient int modCount;
複製代碼

從下面的添加元素代碼中能夠看出,當須要擴容時,令 capacity 爲原來的兩倍。

void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    if (size++ >= threshold)
        resize(2 * table.length);
}
複製代碼

擴容使用 resize() 實現,須要注意的是,擴容操做一樣須要把 oldTable 的全部鍵值對從新插入 newTable 中,所以這一步是很費時的。

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    Entry[] newTable = new Entry[newCapacity];
    transfer(newTable);
    table = newTable;
    threshold = (int)(newCapacity * loadFactor);
}

void transfer(Entry[] newTable) {
    Entry[] src = table;
    int newCapacity = newTable.length;
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}
複製代碼

擴容-從新計算桶下標

Rehash優化:my.oschina.net/u/3568600/b…

在進行擴容時,須要把鍵值對從新放到對應的桶上。HashMap 使用了一個特殊的機制,能夠下降從新計算桶下標的操做。

假設原數組長度 capacity 爲 16,擴容以後 new capacity 爲 32:

capacity     : 00010000
new capacity : 00100000
複製代碼

對於一個 Key,

  • 它的哈希值若是在第 5 位上爲 0,那麼取模獲得的結果和以前同樣;
  • 若是爲 1,那麼獲得的結果爲原來的結果 +16。

總結:

通過rehash以後,元素的位置要麼是在原位置,要麼是在原位置再移動2次冪的位置

所以,咱們在擴充HashMap的時候,不須要像JDK1.7的實現那樣從新計算hash,只須要看看原來的hash值新增的那個bit是1仍是0就行了,是0的話索引沒變,是1的話索引變成「原索引+oldCap」,能夠看看下圖爲16擴充爲32的resize示意圖:

在這裏插入圖片描述

計算數組容量

HashMap 構造函數容許用戶傳入的容量不是 2 的 n 次方,由於它能夠自動地將傳入的容量轉換爲 2 的 n 次方。

先考慮如何求一個數的掩碼,對於 10010000,它的掩碼爲 11111111,可使用如下方法獲得:

mask |= mask >> 1    11011000
mask |= mask >> 2    11111110
mask |= mask >> 4    11111111
複製代碼

mask+1 是大於原始數字的最小的 2 的 n 次方。

num     10010000
mask+1 100000000
複製代碼

如下是 HashMap 中計算數組容量的代碼:

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
複製代碼

鏈表轉紅黑樹

並非桶子上有8位元素的時候它就能變成紅黑樹,它得同時知足咱們的鍵值對大於64才行的

這是爲了不在哈希表創建初期,多個鍵值對剛好被放入了同一個鏈表中而致使沒必要要的轉化。

HashTable

關鍵詞:

  • Hashtable的迭代器不是 fail-fast,HashMap 的迭代器是 fail-fast 迭代器。
  • Hashtable 的 key 和 value 都不容許爲 null,HashMap 能夠插入鍵爲 null 的 Entry。
  • HashTable 使用 synchronized 來進行同步。
  • 基於 Dictionary 類(遺留類)
  • HashMap 不能保證隨着時間的推移 Map 中的元素次序是不變的。

HashMap 與 HashTable

在這裏插入圖片描述

  • HashTable 基於 Dictionary 類(遺留類),而 HashMap 是基於 AbstractMap。
    • Dictionary 是任何可將鍵映射到相應值的類的抽象父類
    • 而AbstractMap是基於Map接口的實現,它以最大限度地減小實現此接口所需的工做。
  • HashMap 的 key 和 value 都容許爲 null,而 Hashtable 的 key 和 value 都不容許爲 null
  • HashMap 的迭代器是 fail-fast 迭代器,而 Hashtable 的 enumerator 迭代器不是 fail-fast 的。
  • 因爲 Hashtable 是線程安全的也是 synchronized,因此在單線程環境下它比 HashMap 要慢。
  • Hashtable 中的幾乎全部的 public 的方法都是synchronized的,而有些方法也是在內部經過 synchronized 代碼塊來實現。
    • 可是在 Collections 類中存在一個靜態方法:synchronizedMap(),該方法建立了一個線程安全的 Map 對象,並把它做爲一個封裝的對象來返回。
    • 也可使用 ConcurrentHashMap,它是 HashTable 的替代,並且比 HashTable 可擴展性更好

ConcurrentHashMap

談談ConcurrentHashMap1.7和1.8的不一樣實現:

www.importnew.com/23610.html

詳細源碼分析(還未細看):

blog.csdn.net/yan_wenlian…

segmentfault.com/a/119000001…

主要針對jdk1.7的實現來介紹

關鍵詞

  • key和value都不容許爲null
  • Hashtable是將全部的方法進行同步,效率低下。而ConcurrentHashMap經過部分鎖定+CAS算法來進行實現線程安全的
  • get方法是非阻塞,無鎖的。重寫Node類,經過volatile修飾next來實現每次獲取都是最新設置的值
  • 在高併發環境下,統計數據(計算size...等等)實際上是無心義的,由於在下一時刻size值就變化了。
  • 實現形式不一樣:
    • 1.7:Segment + HashEntry的方式進行實現
    • 1.8:與HashMap相同(散列表(數組+鏈表)+紅黑樹)採用Node數組 + CAS + Synchronized來保證併發安全進行實現

存儲結構

jdk1.7

jdk1.7中採用Segment + HashEntry的方式進行實現

在這裏插入圖片描述

Segment:其繼承於 ReentrantLock 類,從而使得 Segment 對象能夠充當鎖的角色。

Segment 中包含HashBucket的數組,其能夠守護其包含的若干個桶。

static final class HashEntry<K,V> {
    final int hash;
    final K key;
    volatile V value;
    volatile HashEntry<K,V> next;
}
複製代碼

ConcurrentHashMap採用了分段鎖,每一個分段鎖維護着幾個桶,多個線程能夠同時訪問不一樣分段鎖上的桶,從而使其併發度更高(併發度就是 Segment 的個數)。

jdk1.8

在這裏插入圖片描述

  • JDK 1.7 使用分段鎖機制來實現併發更新操做,核心類爲 Segment,它繼承自重入鎖 ReentrantLock,併發程度與 Segment 數量相等。

  • JDK 1.8 使用了 CAS 操做來支持更高的併發度,在 CAS 操做失敗時使用內置鎖 synchronized。

  • 而且 JDK 1.8 的實現也在鏈表過長時會轉換爲紅黑樹。

1.8中放棄了Segment臃腫的設計,取而代之的是採用Node數組 + CAS + Synchronized來保證併發安全進行實現

添加元素:put

在這裏插入圖片描述

只讓一個線程對散列表進行初始化!

獲取元素:get

從頂部註釋咱們能夠讀到,get方法是不用加鎖的,是非阻塞的。

Node節點是重寫的,設置了volatile關鍵字修飾,導致它每次獲取的都是最新設置的值

獲取大小:size

每一個 Segment 維護了一個 count 變量來統計該 Segment 中的鍵值對個數。

在執行 size 操做時,須要遍歷全部 Segment 而後把 count 累計起來。

ConcurrentHashMap 在執行 size操做時先嚐試不加鎖,若是連續兩次不加鎖操做獲得的結果一致,那麼能夠認爲這個結果是正確的。

嘗試次數使用 RETRIES_BEFORE_LOCK 定義,該值爲 2,retries 初始值爲 -1,所以嘗試次數爲 3。

若是嘗試的次數超過 3 次,就須要對每一個 Segment 加鎖。

刪除元素:remove

在這裏插入圖片描述

爲何用這麼方式刪除呢,細心的同窗會發現上面定義的HashEntry的key和next都是final類型的,因此不能改變next的指向,因此又複製了一份指向刪除的結點的next。

Collections.synchronizedMap()與ConcurrentHashMap的區別

參考:blog.csdn.net/lanxiangru/…

  • Collections.synchronizedMap()和Hashtable同樣,實現上在調用map全部方法時,都對整個map進行同步,而ConcurrentHashMap的實現卻更加精細,它對map中的全部桶加了鎖同步操做精確控制到桶,因此,即便在遍歷map時,其餘線程試圖對map進行數據修改,也不會拋出ConcurrentModificationException。
  • ConcurrentHashMap從類的命名就能看出,它是個HashMap。而Collections.synchronizedMap()能夠接收任意Map實例,實現Map的同步。好比TreeMap。

總結

ConcurrentHashMap 的高併發性主要來自於三個方面:

  • 分離鎖實現多個線程間的更深層次的共享訪問。
  • HashEntery對象的不變性來下降執行讀操做的線程在遍歷鏈表期間對加鎖的需求。
  • 經過對同一個 Volatile 變量的寫 / 讀訪問,協調不一樣線程間讀 / 寫操做的內存可見性。

LinkedHashMap

wiki.jikexueyuan.com/project/jav…

segmentfault.com/a/119000001…

關鍵詞

  • 容許使用 null 值和 null 鍵
  • 此實現不是同步的(linkedlist,lilnkedhashset也不是同步的)
  • 維護着一個運行於全部條目的雙向鏈表。定義了迭代順序,該迭代順序能夠是插入順序或者是訪問順序
  • 初始容量對遍歷沒有影響:遍歷的雙向鏈表,而不是散列表
  • 在訪問順序的狀況下,使用get方法也是結構性的修改(會致使Fail-Fast)

概論

在這裏插入圖片描述

在這裏插入圖片描述

成員變量

該 Entry 除了保存當前對象的引用外,還保存了其上一個元素 before 和下一個元素 after的引用,從而在哈希表的基礎上又構成了雙向連接列表。

/**
* LinkedHashMap的Entry元素。
* 繼承HashMap的Entry元素,又保存了其上一個元素before和下一個元素after的引用。
 */
static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }
複製代碼

構造器

在這裏插入圖片描述

  • 經過源代碼能夠看出,在 LinkedHashMap 的構造方法中,實際調用了父類 HashMap 的相關構造方法來構造一個底層存放的 table 數組,但額外能夠增長 accessOrder 這個參數,若是不設置

    • 默認爲 false,表明按照插入順序進行迭代;
    • 固然能夠顯式設置爲 true,表明以訪問順序進行迭代。
  • 在構建新節點時,構建的是LinkedHashMap.Entry 再也不是Node.

獲取元素:get

LinkedHashMap 重寫了父類 HashMap 的 get 方法,實際在調用父類 getEntry() 方法取得查找的元素後,再判斷當排序模式 accessOrder 爲 true 時,記錄訪問順序,將最新訪問的元素添加到雙向鏈表的表頭,並從原來的位置刪除。

因爲的鏈表的增長、刪除操做是常量級的,故並不會帶來性能的損失。

遍歷元素

爲啥註釋說:初始容量對遍歷沒有影響?

由於它遍歷的是LinkedHashMap內部維護的一個雙向鏈表,而不是散列表(固然了,鏈表雙向鏈表的元素都來源於散列表)

LinkedHashMap應用

wiki.jikexueyuan.com/project/jav…

LRU最近最少使用(訪問順序)

用這個類有兩大好處:

  • 它自己已經實現了按照訪問順序或插入順序的存儲
  • LinkedHashMap 自己有removeEldestEntry方法用於判斷是否須要移除最不常讀取的數,可是,原始方法默認不須要移除,咱們須要override這樣一個方法。

Java裏面實現LRU緩存一般有兩種選擇:

  • 使用LinkedHashMap
  • 本身設計數據結構,使用鏈表+HashMap

如下是使用 LinkedHashMap 實現的一個 LRU 緩存:

  • 設定最大緩存空間 MAX_ENTRIES 爲 3;
  • 使用 LinkedHashMap 的構造函數將 accessOrder 設置爲 true,開啓 LRU 順序;
  • 覆蓋 removeEldestEntry() 方法實現,在節點多於 MAX_ENTRIES 就會將最近最久未使用的數據移除。
class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private static final int MAX_ENTRIES = 3;

    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > MAX_ENTRIES;
    }

    LRUCache() {
        super(MAX_ENTRIES, 0.75f, true);
    }
}
複製代碼
public static void main(String[] args) {
    LRUCache<Integer, String> cache = new LRUCache<>();
    cache.put(1, "a");
    cache.put(2, "b");
    cache.put(3, "c");
    cache.get(1);
    cache.put(4, "d");
    System.out.println(cache.keySet());
}
複製代碼
[3, 1, 4]
複製代碼

實現詳細代碼請參考文章:補充知識點-緩存

FIFO(插入順序)

還能夠在插入順序的LinkedHashMap直接重寫下removeEldestEntry方法便可輕鬆實現一個FIFO緩存

TreeMap

關鍵詞

  • 紅黑樹
  • 非同步
  • key不能爲null
  • 實現了NavigableMap接口,而NavigableMap接口繼承着SortedMap接口,是有序的(HahMap是Key無序的)
  • TreeMap的基本操做 containsKey、get、put 和 remove 的時間複雜度是 log(n) 。
  • 適用於查找性能要求不那麼高,反而對有序性要求比較高的應用場景
  • 使用Comparator或者Comparable來比較key是否相等與排序的問題

概覽

在這裏插入圖片描述

獲取元素:get

詳細看:

segmentfault.com/a/119000001…

總結:

  • 若是在構造方法中傳遞了Comparator對象,那麼就會以Comparator對象的方法進行比較。不然,則使用Comparable的compareTo(T o)方法來比較。
  • 值得說明的是:若是使用的是compareTo(T o)方法來比較,key必定是不能爲null,而且得實現了Comparable接口的。
  • 即便是傳入了Comparator對象,不用compareTo(T o)方法來比較,key也是不能爲null的

刪除元素:remove

刪除節點而且平衡紅黑樹

HashSet

wiki.jikexueyuan.com/project/jav…

segmentfault.com/a/119000001…

關鍵詞:

  • 默認容量16,擴容兩倍,加載因子0.75

  • 容許元素爲null

  • 實現Set接口

  • 不保證迭代順序

  • 非同步

  • 初始容量很是影響迭代性能

  • 底層其實是一個HashMap實例

    public HashSet() {map = new HashMap<>();}

若是添加的是在 HashSet 中不存在的,則返回 true;若是添加的元素已經存在,返回 false。

對於 HashSet 中保存的對象,請注意正確重寫其 equals 和 hashCode 方法,以保證放入的對象的惟一性。

HashSet 和 HashMap 的區別

重要:

1. HashMap中使用鍵對象來計算hashcode值

2. HashSet使用成員對象來計算hashcode值,對於兩個對象來講hashcode可能相同,因此equals()方法用來判斷對象的相等性,若是兩個對象不一樣的話,那麼返回false

在這裏插入圖片描述

TreeSet

關鍵詞

  • 實現NavigableSet接口
  • 能夠實現排序功能
  • 底層其實是一個TreeMap實例
  • 非同步
  • 不容許爲null

LinkedHashSet

關鍵詞

  • 迭代是有序的
  • 容許爲null
  • 底層其實是一個HashMap+雙向鏈表實例(其實就是LinkedHashMap)
  • 非同步
  • 性能比HashSet差一丟丟,由於要維護一個雙向鏈表
  • 初始容量與迭代無關(與LinkedHashMap相同),由於LinkedHashSet迭代的是雙向鏈表

總結Set

HashSet:

  • 無序,容許爲null,底層是HashMap(散列表+紅黑樹),非線程同步

TreeSet:

  • 有序,不容許爲null,底層是TreeMap(紅黑樹),非線程同步

LinkedHashSet:

  • 迭代有序,容許爲null,底層是HashMap+雙向鏈表,非線程同步

WeekHashMap

存儲結構

WeakHashMap 的 Entry 繼承自 WeakReference,被 WeakReference 關聯的對象在下一次垃圾回收時會被回收

WeakHashMap 主要用來實現緩存,經過使用 WeakHashMap 來引用緩存對象,由 JVM 對這部分緩存進行回收。

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> 複製代碼

ConcurrentCache

Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 來實現緩存功能。

ConcurrentCache 採起的是分代緩存:

  • 常用的對象放入 eden 中,eden 使用 ConcurrentHashMap 實現,不用擔憂會被回收(伊甸園);
  • 不經常使用的對象放入 longterm,longterm 使用 WeakHashMap 實現,這些老對象會被垃圾收集器回收。
  • 當調用 get() 方法時,會先從 eden 區獲取,若是沒有找到的話再到 longterm 獲取,當從 longterm 獲取到就把對象放入 eden 中,從而保證常常被訪問的節點不容易被回收。
  • 當調用 put() 方法時,若是 eden 的大小超過了 size,那麼就將 eden 中的全部對象都放入 longterm 中,利用虛擬機回收掉一部分不常用的對象。
public final class ConcurrentCache<K, V> {

    private final int size;

    private final Map<K, V> eden;

    private final Map<K, V> longterm;

    public ConcurrentCache(int size) {
        this.size = size;
        this.eden = new ConcurrentHashMap<>(size);
        this.longterm = new WeakHashMap<>(size);
    }

    public V get(K k) {
        V v = this.eden.get(k);
        if (v == null) {
            v = this.longterm.get(k);
            if (v != null)
                this.eden.put(k, v);
        }
        return v;
    }

    public void put(K k, V v) {
        if (this.eden.size() >= size) {
            this.longterm.putAll(this.eden);
            this.eden.clear();
        }
        this.eden.put(k, v);
    }
}
複製代碼

常見問題總結

Enumeration和Iterator接口的區別

Iterator替代了Enumeration,Enumeration是一箇舊的迭代器了。

與Enumeration相比,Iterator更加安全,由於當一個集合正在被遍歷的時候,它會阻止其它線程去修改集合。

區別有三點:

  • Iterator的方法名比Enumeration更科學
  • Iterator有fail-fast機制,比Enumeration更安全
  • Iterator可以刪除元素,Enumeration並不能刪除元素

ListIterator有什麼特色

  • ListIterator繼承了Iterator接口,它用於遍歷List集合的元素。
  • ListIterator能夠實現雙向遍歷,添加元素,設置元素

在這裏插入圖片描述

與Java集合框架相關的有哪些最好的實踐

若是是單列的集合,咱們考慮用Collection下的子接口ArrayList和Set。

若是是映射,咱們就考慮使用Map

  • 是否須要同步:去找線程安全的集合類使用

  • 迭代時是否須要有序(插入順序有序):去找Linked雙向列表結構的

  • 是否須要排序(天然順序或者手動排序):去找Tree紅黑樹類型的(JDK1.8)

  • 估算存放集合的數據量有多大,不管是List仍是Map,它們實現動態增加,都是有性能消耗的。在初始集合的時候給出一個合理的容量會減小動態增加時的消耗

  • 使用泛型,避免在運行時出現ClassCastException

  • 儘量使用Collections工具類,或者獲取只讀、同步或空的集合,而非編寫本身的實現。它將會提供代碼重用性,它有着更好的穩定性和可維護性

參考

關注我

本人目前爲後臺開發工程師,主要關注Python爬蟲,後臺開發等相關技術。

原創博客主要內容:

  • 筆試面試複習知識點手冊
  • Leetcode算法題解析(前150題)
  • 劍指offer算法題解析
  • Python爬蟲相關實戰
  • 後臺開發相關實戰

同步更新如下幾大博客:

相關文章
相關標籤/搜索