Java 集合,你肯定會(huì)被問(wèn)到這些

「我是大廠面試官」—— Java 集合,你肯定會(huì)被問(wèn)到這些

文章收錄在 GitHub JavaKeeper ,N線互聯(lián)網(wǎng)開(kāi)發(fā)必備技能兵器譜

作為一位小菜 ”一面面試官“,面試過(guò)程中,我肯定會(huì)問(wèn) Java 集合的內(nèi)容,同時(shí)作為求職者,也肯定會(huì)被問(wèn)到集合,所以整理下 Java 集合面試題

image

說(shuō)說(shuō)常見(jiàn)的集合有哪些吧?

HashMap說(shuō)一下,其中的Key需要重寫hashCode()和equals()嗎?

HashMap中key和value可以為null嗎?允許幾個(gè)為null呀?

HashMap線程安全嗎?ConcurrentHashMap和hashTable有什么區(qū)別?

List和Set說(shuō)一下,現(xiàn)在有一個(gè)ArrayList,對(duì)其中的所有元素按照某一屬性大小排序,應(yīng)該怎么做?

ArrayList 和 Vector 的區(qū)別

list 可以刪除嗎,遍歷的時(shí)候可以刪除嗎,為什么

面向?qū)ο笳Z(yǔ)言對(duì)事物的體現(xiàn)都是以對(duì)象的形式,所以為了方便對(duì)多個(gè)對(duì)象的操作,需要將對(duì)象進(jìn)行存儲(chǔ),集合就是存儲(chǔ)對(duì)象最常用的一種方式,也叫容器。

img

從上面的集合框架圖可以看到,Java 集合框架主要包括兩種類型的容器

  • 一種是集合(Collection),存儲(chǔ)一個(gè)元素集合
  • 另一種是圖(Map),存儲(chǔ)鍵/值對(duì)映射。

Collection 接口又有 3 種子類型,List、Set 和 Queue,再下面是一些抽象類,最后是具體實(shí)現(xiàn)類,常用的有 ArrayList、LinkedList、HashSet、LinkedHashSet、HashMap、LinkedHashMap 等等。

集合框架是一個(gè)用來(lái)代表和操縱集合的統(tǒng)一架構(gòu)。所有的集合框架都包含如下內(nèi)容:

  • 接口:是代表集合的抽象數(shù)據(jù)類型。例如 Collection、List、Set、Map 等。之所以定義多個(gè)接口,是為了以不同的方式操作集合對(duì)象

  • 實(shí)現(xiàn)(類):是集合接口的具體實(shí)現(xiàn)。從本質(zhì)上講,它們是可重復(fù)使用的數(shù)據(jù)結(jié)構(gòu),例如:ArrayList、LinkedList、HashSet、HashMap。

  • 算法:是實(shí)現(xiàn)集合接口的對(duì)象里的方法執(zhí)行的一些有用的計(jì)算,例如:搜索和排序。這些算法被稱為多態(tài),那是因?yàn)橄嗤姆椒梢栽谙嗨频慕涌谏嫌兄煌膶?shí)現(xiàn)。


說(shuō)說(shuō)常用的集合有哪些吧?

Map 接口和 Collection 接口是所有集合框架的父接口:

  1. Collection接口的子接口包括:Set、List、Queue
  2. List是有序的允許有重復(fù)元素的Collection,實(shí)現(xiàn)類主要有:ArrayList、LinkedList、Stack以及Vector等
  3. Set是一種不包含重復(fù)元素且無(wú)序的Collection,實(shí)現(xiàn)類主要有:HashSet、TreeSet、LinkedHashSet等
  4. Map沒(méi)有繼承Collection接口,Map提供key到value的映射。實(shí)現(xiàn)類主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap 以及 Properties 等

ArrayList 和 Vector 的區(qū)別

相同點(diǎn):

  • ArrayList 和 Vector 都是繼承了相同的父類和實(shí)現(xiàn)了相同的接口(都實(shí)現(xiàn)了List,有序、允許重復(fù)和null)

    extends AbstractList<E>
            implements List<E>, RandomAccess, Cloneable, java.io.Serializable
    
  • 底層都是數(shù)組(Object[])實(shí)現(xiàn)的

  • 初始默認(rèn)長(zhǎng)度都為10

不同點(diǎn):

  • 同步性:Vector 中的 public 方法多數(shù)添加了 synchronized 關(guān)鍵字、以確保方法同步、也即是 Vector 線程安全、ArrayList 線程不安全

  • 性能:Vector 存在 synchronized 的鎖等待情況、需要等待釋放鎖這個(gè)過(guò)程、所以性能相對(duì)較差

  • 擴(kuò)容大?。篈rrayList在底層數(shù)組不夠用時(shí)在原來(lái)的基礎(chǔ)上擴(kuò)展 0.5 倍,Vector默認(rèn)是擴(kuò)展 1 倍

    擴(kuò)容機(jī)制,擴(kuò)容方法其實(shí)就是新創(chuàng)建一個(gè)數(shù)組,然后將舊數(shù)組的元素都復(fù)制到新數(shù)組里面。其底層的擴(kuò)容方法都在 grow() 中(基于JDK8)

    • ArrayList 的 grow(),在滿足擴(kuò)容條件時(shí)、ArrayList以1.5 倍的方式在擴(kuò)容(oldCapacity >> 1 ,右移運(yùn)算,相當(dāng)于除以 2,結(jié)果為二分之一的 oldCapacity)

      private void grow(int minCapacity) {
          // overflow-conscious code
          int oldCapacity = elementData.length;
          //newCapacity = oldCapacity + O.5*oldCapacity,此處擴(kuò)容0.5倍
          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);
      }
      
    • Vector 的 grow(),Vector 比 ArrayList多一個(gè)屬性,擴(kuò)展因子capacityIncrement,可以擴(kuò)容大小。當(dāng)擴(kuò)容容量增量大于0時(shí)、新數(shù)組長(zhǎng)度為原數(shù)組長(zhǎng)度+擴(kuò)容容量增量、否則新數(shù)組長(zhǎng)度為原數(shù)組長(zhǎng)度的2

      private void grow(int minCapacity) {
          // overflow-conscious code
          int oldCapacity = elementData.length;
          //
          int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                           capacityIncrement : oldCapacity);
          if (newCapacity - minCapacity < 0)
              newCapacity = minCapacity;
          if (newCapacity - MAX_ARRAY_SIZE > 0)
              newCapacity = hugeCapacity(minCapacity);
          elementData = Arrays.copyOf(elementData, newCapacity);
      }
      

ArrayList 與 LinkedList 區(qū)別

  • 是否保證線程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保證線程安全;
  • 底層數(shù)據(jù)結(jié)構(gòu): Arraylist 底層使用的是 Object 數(shù)組;LinkedList 底層使用的是雙向循環(huán)鏈表數(shù)據(jù)結(jié)構(gòu);
  • 插入和刪除是否受元素位置的影響:
    • ArrayList 采用數(shù)組存儲(chǔ),所以插入和刪除元素的時(shí)間復(fù)雜度受元素位置的影響。 比如:執(zhí)行 add(E e)方法的時(shí)候, ArrayList 會(huì)默認(rèn)在將指定的元素追加到此列表的末尾,這種情況時(shí)間復(fù)雜度就是O(1)。但是如果要在指定位置 i 插入和刪除元素的話( add(intindex,E element))時(shí)間復(fù)雜度就為 O(n-i)。因?yàn)樵谶M(jìn)行上述操作的時(shí)候集合中第 i 和第 i 個(gè)元素之后的(n-i)個(gè)元素都要執(zhí)行向后位/向前移一位的操作。
    • LinkedList 采用鏈表存儲(chǔ),所以插入,刪除元素時(shí)間復(fù)雜度不受元素位置的影響,都是近似 O(1),而數(shù)組為近似 O(n)。
    • ArrayList 一般應(yīng)用于查詢較多但插入以及刪除較少情況,如果插入以及刪除較多則建議使用 LinkedList
  • 是否支持快速隨機(jī)訪問(wèn): LinkedList 不支持高效的隨機(jī)元素訪問(wèn),而 ArrayList 實(shí)現(xiàn)了 RandomAccess 接口,所以有隨機(jī)訪問(wèn)功能。快速隨機(jī)訪問(wèn)就是通過(guò)元素的序號(hào)快速獲取元素對(duì)象(對(duì)應(yīng)于 get(intindex)方法)。
  • 內(nèi)存空間占用: ArrayList 的空間浪費(fèi)主要體現(xiàn)在在 list 列表的結(jié)尾會(huì)預(yù)留一定的容量空間,而 LinkedList 的空間花費(fèi)則體現(xiàn)在它的每一個(gè)元素都需要消耗比 ArrayList 更多的空間(因?yàn)橐娣胖苯雍罄^和直接前驅(qū)以及數(shù)據(jù))。

高級(jí)工程師的我,可不得看看源碼,具體分析下:

  • ArrayList工作原理其實(shí)很簡(jiǎn)單,底層是動(dòng)態(tài)數(shù)組,每次創(chuàng)建一個(gè) ArrayList 實(shí)例時(shí)會(huì)分配一個(gè)初始容量(沒(méi)有指定初始容量的話,默認(rèn)是 10),以add方法為例,如果沒(méi)有指定初始容量,當(dāng)執(zhí)行add方法,先判斷當(dāng)前數(shù)組是否為空,如果為空則給保存對(duì)象的數(shù)組分配一個(gè)最小容量,默認(rèn)為10。當(dāng)添加大容量元素時(shí),會(huì)先增加數(shù)組的大小,以提高添加的效率;

  • LinkedList 是有序并且支持元素重復(fù)的集合,底層是基于雙向鏈表的,即每個(gè)節(jié)點(diǎn)既包含指向其后繼的引用也包括指向其前驅(qū)的引用。鏈表無(wú)容量限制,但雙向鏈表本身使用了更多空間,也需要額外的鏈表指針操作。按下標(biāo)訪問(wèn)元素 get(i)/set(i,e) 要悲劇的遍歷鏈表將指針移動(dòng)到位(如果i>數(shù)組大小的一半,會(huì)從末尾移起)。插入、刪除元素時(shí)修改前后節(jié)點(diǎn)的指針即可,但還是要遍歷部分鏈表的指針才能移動(dòng)到下標(biāo)所指的位置,只有在鏈表兩頭的操作add(), addFirst(),removeLast()或用 iterator() 上的 remove() 能省掉指針的移動(dòng)。此外 LinkedList 還實(shí)現(xiàn)了 Deque(繼承自Queue接口)接口,可以當(dāng)做隊(duì)列使用。

不會(huì)囊括所有方法,只是為了學(xué)習(xí),記錄思想。

ArrayList 和 LinkedList 兩者都實(shí)現(xiàn)了 List 接口

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

構(gòu)造器

ArrayList 提供了 3 個(gè)構(gòu)造器,①無(wú)參構(gòu)造器 ②帶初始容量構(gòu)造器 ③參數(shù)為集合構(gòu)造器

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
   
public ArrayList(int initialCapacity) {
   if (initialCapacity > 0) {
       // 創(chuàng)建初始容量的數(shù)組
     this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
     this.elementData = EMPTY_ELEMENTDATA;
    } else {
   throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
  }
}
public ArrayList() {
  // 默認(rèn)為空數(shù)組
  this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
    
public ArrayList(Collection<? extends E> c) { //...}       
}

LinkedList 提供了 2 個(gè)構(gòu)造器,因?yàn)榛阪湵?,所以也就沒(méi)有初始化大小,也沒(méi)有擴(kuò)容的機(jī)制,就是一直在前面或者后面插插插~~

public LinkedList() {
}

public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}
// LinkedList 既然作為鏈表,那么肯定會(huì)有節(jié)點(diǎn)
private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

插入

ArrayList:

public boolean add(E e) {
    // 確保數(shù)組的容量,保證可以添加該元素
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 將該元素放入數(shù)組中
    elementData[size++] = e;
    return true;
}
private void ensureCapacityInternal(int minCapacity) {
    // 如果數(shù)組是空的,那么會(huì)初始化該數(shù)組
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // DEFAULT_CAPACITY 為 10,所以調(diào)用無(wú)參默認(rèn) ArrayList 構(gòu)造方法初始化的話,默認(rèn)的數(shù)組容量為 10
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // 確保數(shù)組的容量,如果不夠的話,調(diào)用 grow 方法擴(kuò)容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
//擴(kuò)容具體的方法
private void grow(int minCapacity) {
    // 當(dāng)前數(shù)組的容量
    int oldCapacity = elementData.length;
    // 新數(shù)組擴(kuò)容為原來(lái)容量的 1.5 倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 如果新數(shù)組擴(kuò)容容量還是比最少需要的容量還要小的話,就設(shè)置擴(kuò)充容量為最小需要的容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    //判斷新數(shù)組容量是否已經(jīng)超出最大數(shù)組范圍,MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    // 復(fù)制元素到新的數(shù)組中
    elementData = Arrays.copyOf(elementData, newCapacity);
}

當(dāng)然也可以插入指定位置,還有一個(gè)重載的方法 add(int index, E element)

public void add(int index, E element) {
    // 判斷 index 有沒(méi)有超出索引的范圍
    rangeCheckForAdd(index);
    // 和之前的操作是一樣的,都是保證數(shù)組的容量足夠
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 將指定位置及其后面數(shù)據(jù)向后移動(dòng)一位
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    // 將該元素添加到指定的數(shù)組位置
    elementData[index] = element;
    // ArrayList 的大小改變
    size++;
}

可以看到每次插入指定位置都要移動(dòng)元素,效率較低。

再來(lái)看 LinkedList 的插入,也有插入末尾,插入指定位置兩種,由于基于鏈表,肯定得先有個(gè) Node

private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}
public boolean add(E e) {
    // 直接往隊(duì)尾加元素
    linkLast(e);
    return true;
}

void linkLast(E e) {
    // 保存原來(lái)鏈表尾部節(jié)點(diǎn),last 是全局變量,用來(lái)表示隊(duì)尾元素
    final Node<E> l = last;
    // 為該元素 e 新建一個(gè)節(jié)點(diǎn)
    final Node<E> newNode = new Node<>(l, e, null);
    // 將新節(jié)點(diǎn)設(shè)為隊(duì)尾
    last = newNode;
    // 如果原來(lái)的隊(duì)尾元素為空,那么說(shuō)明原來(lái)的整個(gè)列表是空的,就把新節(jié)點(diǎn)賦值給頭結(jié)點(diǎn)
    if (l == null)
        first = newNode;
    else
    // 原來(lái)尾結(jié)點(diǎn)的后面為新生成的結(jié)點(diǎn)
        l.next = newNode;
    // 節(jié)點(diǎn)數(shù) +1
    size++;
    modCount++;
}

public void add(int index, E element) {
    // 檢查 index 有沒(méi)有超出索引范圍
    checkPositionIndex(index);
    // 如果追加到尾部,那么就跟 add(E e) 一樣了
    if (index == size)
        linkLast(element);
    else
    // 否則就是插在其他位置
     linkBefore(element, node(index));
}

//linkBefore方法中調(diào)用了這個(gè)node方法,類似二分查找的優(yōu)化
Node<E> node(int index) {
    // assert isElementIndex(index);
    // 如果 index 在前半段,從前往后遍歷獲取 node
    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        // 如果 index 在后半段,從后往前遍歷獲取 node
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    // 保存 index 節(jié)點(diǎn)的前節(jié)點(diǎn)
    final Node<E> pred = succ.prev;
    // 新建一個(gè)目標(biāo)節(jié)點(diǎn)
    final Node<E> newNode = new Node<>(pred, e, succ);
    succ.prev = newNode;
    // 如果是在開(kāi)頭處插入的話
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}

獲取

ArrayList 的 get() 方法很簡(jiǎn)單,就是在數(shù)組中返回指定位置的元素即可,所以效率很高

public E get(int index) {
    // 檢查 index 有沒(méi)有超出索引的范圍
    rangeCheck(index);
    // 返回指定位置的元素
    return elementData(index);
}

LinkedList 的 get() 方法,就是在內(nèi)部調(diào)用了上邊看到的 node() 方法,判斷在前半段還是在后半段,然后遍歷得到即可。

public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

HashMap的底層實(shí)現(xiàn)

什么時(shí)候會(huì)使用HashMap?他有什么特點(diǎn)?

你知道HashMap的工作原理嗎?

你知道get和put的原理嗎?equals()和hashCode()的都有什么作用?

你知道hash的實(shí)現(xiàn)嗎?為什么要這樣實(shí)現(xiàn)?

如果HashMap的大小超過(guò)了負(fù)載因子(load factor)定義的容量,怎么辦?

HashMap 在 JDK 7 和 JDK8 中的實(shí)現(xiàn)方式略有不同。分開(kāi)記錄。

深入 HahsMap 之前,先要了解的概念

  1. initialCapacity:初始容量。指的是 HashMap 集合初始化的時(shí)候自身的容量。可以在構(gòu)造方法中指定;如果不指定的話,總?cè)萘磕J(rèn)值是 16 。需要注意的是初始容量必須是 2 的冪次方。(1.7中,已知HashMap中將要存放的KV個(gè)數(shù)的時(shí)候,設(shè)置一個(gè)合理的初始化容量可以有效的提高性能

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    
  2. size:當(dāng)前 HashMap 中已經(jīng)存儲(chǔ)著的鍵值對(duì)數(shù)量,即 HashMap.size()

  3. loadFactor:加載因子。所謂的加載因子就是 HashMap (當(dāng)前的容量/總?cè)萘? 到達(dá)一定值的時(shí)候,HashMap 會(huì)實(shí)施擴(kuò)容。加載因子也可以通過(guò)構(gòu)造方法中指定,默認(rèn)的值是 0.75 。舉個(gè)例子,假設(shè)有一個(gè) HashMap 的初始容量為 16 ,那么擴(kuò)容的閥值就是 0.75 * 16 = 12 。也就是說(shuō),在你打算存入第 13 個(gè)值的時(shí)候,HashMap 會(huì)先執(zhí)行擴(kuò)容。

  4. threshold:擴(kuò)容閥值。即 擴(kuò)容閥值 = HashMap 總?cè)萘?* 加載因子。當(dāng)前 HashMap 的容量大于或等于擴(kuò)容閥值的時(shí)候就會(huì)去執(zhí)行擴(kuò)容。擴(kuò)容的容量為當(dāng)前 HashMap 總?cè)萘康膬杀丁1热?,?dāng)前 HashMap 的總?cè)萘繛?16 ,那么擴(kuò)容之后為 32 。

  5. table:Entry 數(shù)組。我們都知道 HashMap 內(nèi)部存儲(chǔ) key/value 是通過(guò) Entry 這個(gè)介質(zhì)來(lái)實(shí)現(xiàn)的。而 table 就是 Entry 數(shù)組。

JDK1.7 實(shí)現(xiàn)

JDK1.7 中 HashMap 由 數(shù)組+鏈表 組成(“鏈表散列” 即數(shù)組和鏈表的結(jié)合體),數(shù)組是 HashMap 的主體,鏈表則是主要為了解決哈希沖突而存在的(HashMap 采用 “拉鏈法也就是鏈地址法” 解決沖突),如果定位到的數(shù)組位置不含鏈表(當(dāng)前 entry 的 next 指向 null ),那么對(duì)于查找,添加等操作很快,僅需一次尋址即可;如果定位到的數(shù)組包含鏈表,對(duì)于添加操作,其時(shí)間復(fù)雜度依然為 O(1),因?yàn)樽钚碌?Entry 會(huì)插入鏈表頭部,即需要簡(jiǎn)單改變引用鏈即可,而對(duì)于查找操作來(lái)講,此時(shí)就需要遍歷鏈表,然后通過(guò) key 對(duì)象的 equals 方法逐一比對(duì)查找。

所謂 “拉鏈法” 就是將鏈表和數(shù)組相結(jié)合。也就是說(shuō)創(chuàng)建一個(gè)鏈表數(shù)組,數(shù)組中每一格就是一個(gè)鏈表。若遇到哈希沖突,則將沖突的值加到鏈表中即可。

image

源碼解析

構(gòu)造方法

《阿里巴巴 Java 開(kāi)發(fā)手冊(cè)》推薦集合初始化時(shí),指定集合初始值大小。(說(shuō)明:HashMap 使用HashMap(int initialCapacity) 初始化)建議原因: https://www.zhihu.com/question/314006228/answer/611170521

// 默認(rèn)的構(gòu)造方法使用的都是默認(rèn)的初始容量和加載因子
// DEFAULT_INITIAL_CAPACITY = 16,DEFAULT_LOAD_FACTOR = 0.75f
public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

// 可以指定初始容量,并且使用默認(rèn)的加載因子
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

public HashMap(int initialCapacity, float loadFactor) {
    // 對(duì)初始容量的值判斷
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    // 設(shè)置加載因子
    this.loadFactor = loadFactor;
    threshold = initialCapacity;
    // 空方法
    init();
}

public HashMap(Map<? extends K, ? extends V> m) {
  this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
  inflateTable(threshold);
  putAllForCreate(m);
}

HashMap 的前 3 個(gè)構(gòu)造方法最后都會(huì)去調(diào)用 HashMap(int initialCapacity, float loadFactor) 。在其內(nèi)部去設(shè)置初始容量和加載因子。而最后的 init() 是空方法,主要給子類實(shí)現(xiàn),比如LinkedHashMap。

put() 方法
public V put(K key, V value) {
    // 如果 table 數(shù)組為空時(shí)先創(chuàng)建數(shù)組,并且設(shè)置擴(kuò)容閥值
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    // 如果 key 為空時(shí),調(diào)用 putForNullKey 方法特殊處理
    if (key == null)
        return putForNullKey(value);
    // 計(jì)算 key 的哈希值
    int hash = hash(key);
    // 根據(jù)計(jì)算出來(lái)的哈希值和當(dāng)前數(shù)組的長(zhǎng)度計(jì)算在數(shù)組中的索引
    int i = indexFor(hash, table.length);
    // 先遍歷該數(shù)組索引下的整條鏈表
    // 如果該 key 之前已經(jīng)在 HashMap 中存儲(chǔ)了的話,直接替換對(duì)應(yīng)的 value 值即可
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
       //先判斷hash值是否一樣,如果一樣,再判斷key是否一樣,不同對(duì)象的hash值可能一樣
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    modCount++;
    // 如果該 key 之前沒(méi)有被存儲(chǔ)過(guò),那么就進(jìn)入 addEntry 方法
    addEntry(hash, key, value, i);
    return null;
}

void addEntry(int hash, K key, V value, int bucketIndex) {
    // 當(dāng)前容量大于或等于擴(kuò)容閥值的時(shí)候,會(huì)執(zhí)行擴(kuò)容
    if ((size >= threshold) && (null != table[bucketIndex])) {
        // 擴(kuò)容為原來(lái)容量的兩倍
        resize(2 * table.length);
        // 重新計(jì)算哈希值
        hash = (null != key) ? hash(key) : 0;
        // 重新得到在新數(shù)組中的索引
        bucketIndex = indexFor(hash, table.length);
    }
    // 創(chuàng)建節(jié)點(diǎn)
    createEntry(hash, key, value, bucketIndex);
}

//擴(kuò)容,創(chuàng)建了一個(gè)新的數(shù)組,然后把數(shù)據(jù)全部復(fù)制過(guò)去,再把新數(shù)組的引用賦給 table
void resize(int newCapacity) {
    Entry[] oldTable = table;  //引用擴(kuò)容前的Entry數(shù)組
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) { //擴(kuò)容前的數(shù)組大小如果已經(jīng)達(dá)到最大(2^30)了
        threshold = Integer.MAX_VALUE;  //修改閾值為int的最大值(2^31-1),這樣以后就不會(huì)擴(kuò)容了
        return;
    }
    // 創(chuàng)建新的 entry 數(shù)組
    Entry[] newTable = new Entry[newCapacity];
    // 將舊 entry 數(shù)組中的數(shù)據(jù)復(fù)制到新 entry 數(shù)組中
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    // 將新數(shù)組的引用賦給 table
    table = newTable;
    // 計(jì)算新的擴(kuò)容閥值
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

void transfer(Entry[] newTable) {
     Entry[] src = table;                   //src引用了舊的Entry數(shù)組
     int newCapacity = newTable.length;
     for (int j = 0; j < src.length; j++) { //遍歷舊的Entry數(shù)組
         Entry<K,V> e = src[j];             //取得舊Entry數(shù)組的每個(gè)元素
         if (e != null) {
             src[j] = null;//釋放舊Entry數(shù)組的對(duì)象引用(for循環(huán)后,舊的Entry數(shù)組不再引用任何對(duì)象)
             do {
                 Entry<K,V> next = e.next;
                 int i = indexFor(e.hash, newCapacity); //??!重新計(jì)算每個(gè)元素在數(shù)組中的位置
                 e.next = newTable[i]; //標(biāo)記[1]
                 newTable[i] = e;      //將元素放在數(shù)組上
                 e = next;             //訪問(wèn)下一個(gè)Entry鏈上的元素
             } while (e != null);
         }
     }
} 

void createEntry(int hash, K key, V value, int bucketIndex) {
   // 取出table中下標(biāo)為bucketIndex的Entry
    Entry<K,V> e = table[bucketIndex];
   // 利用key、value來(lái)構(gòu)建新的Entry
   // 并且之前存放在table[bucketIndex]處的Entry作為新Entry的next
   // 把新創(chuàng)建的Entry放到table[bucketIndex]位置
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    // 當(dāng)前 HashMap 的容量加 1
    size++;
}

最后的 createEntry() 方法就說(shuō)明了當(dāng)hash沖突時(shí),采用的拉鏈法來(lái)解決hash沖突的,并且是把新元素是插入到單邊表的表頭。

image
get() 方法
public V get(Object key) {
    // 如果 key 是空的,就調(diào)用 getForNullKey 方法特殊處理
    if (key == null)
        return getForNullKey();
    // 獲取 key 相對(duì)應(yīng)的 entry 
    Entry<K,V> entry = getEntry(key);

    return null == entry ? null : entry.getValue();
}

//找到對(duì)應(yīng) key 的數(shù)組索引,然后遍歷鏈表查找即可
final Entry<K,V> getEntry(Object key) {
    if (size == 0) {
        return null;
    }
    // 計(jì)算 key 的哈希值
    int hash = (key == null) ? 0 : hash(key);
    // 得到數(shù)組的索引,然后遍歷鏈表,查看是否有相同 key 的 Entry
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
    }
    // 沒(méi)有的話,返回 null
    return null;
}

JDK1.8 實(shí)現(xiàn)

JDK 1.7 中,如果哈希碰撞過(guò)多,拉鏈過(guò)長(zhǎng),極端情況下,所有值都落入了同一個(gè)桶內(nèi),這就退化成了一個(gè)鏈表。通過(guò) key 值查找要遍歷鏈表,效率較低。 JDK1.8在解決哈希沖突時(shí)有了較大的變化,當(dāng)鏈表長(zhǎng)度大于閾值(默認(rèn)為8)時(shí),將鏈表轉(zhuǎn)化為紅黑樹(shù),以減少搜索時(shí)間。

image

TreeMap、TreeSet以及 JDK1.8 之后的 HashMap 底層都用到了紅黑樹(shù)。紅黑樹(shù)就是為了解決二叉查找樹(shù)的缺陷,因?yàn)槎娌檎覙?shù)在某些情況下會(huì)退化成一個(gè)線性結(jié)構(gòu)。

源碼解析

構(gòu)造方法

JDK8 構(gòu)造方法改動(dòng)不是很大

public HashMap() {
  this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

public HashMap(int initialCapacity) {
  this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

public HashMap(int initialCapacity, float loadFactor) {
  if (initialCapacity < 0)
    throw new IllegalArgumentException("Illegal initial capacity: " +
                                       initialCapacity);
  if (initialCapacity > MAXIMUM_CAPACITY)
    initialCapacity = MAXIMUM_CAPACITY;
  if (loadFactor <= 0 || Float.isNaN(loadFactor))
    throw new IllegalArgumentException("Illegal load factor: " +
                                       loadFactor);
  this.loadFactor = loadFactor;
  this.threshold = tableSizeFor(initialCapacity);
}

public HashMap(Map<? extends K, ? extends V> m) {
  this.loadFactor = DEFAULT_LOAD_FACTOR;
  putMapEntries(m, false);
}
確定哈希桶數(shù)組索引位置(hash 函數(shù)的實(shí)現(xiàn))
//方法一:
static final int hash(Object key) { //jdk1.8 & jdk1.7
 int h;
 // h = key.hashCode() 為第一步 取hashCode值
 // h ^ (h >>> 16)  為第二步 高位參與運(yùn)算
 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//方法二:
static int indexFor(int h, int length) { //jdk1.7的源碼,jdk1.8沒(méi)有提取這個(gè)方法,而是放在了其他方法中,比如 put 的p = tab[i = (n - 1) & hash]
 return h & (length-1); //第三步 取模運(yùn)算
}

HashMap定位數(shù)組索引位置,直接決定了hash方法的離散性能。Hash算法本質(zhì)上就是三步:取key的hashCode值、高位運(yùn)算、取模運(yùn)算。

hash

為什么要這樣呢?

HashMap 的長(zhǎng)度為什么是2的冪次方?

目的當(dāng)然是為了減少哈希碰撞,使 table 里的數(shù)據(jù)分布的更均勻。

  1. HashMap 中桶數(shù)組的大小 length 總是2的冪,此時(shí),h & (table.length -1) 等價(jià)于對(duì) length 取模 h%length。但取模的計(jì)算效率沒(méi)有位運(yùn)算高,所以這是是一個(gè)優(yōu)化。假設(shè) h = 185,table.length-1 = 15(0x1111),其實(shí)散列真正生效的只是低 4bit 的有效位,所以很容易碰撞。

    img
  2. 圖中的 hash 是由鍵的 hashCode 產(chǎn)生。計(jì)算余數(shù)時(shí),由于 n 比較小,hash 只有低4位參與了計(jì)算,高位的計(jì)算可以認(rèn)為是無(wú)效的。這樣導(dǎo)致了計(jì)算結(jié)果只與低位信息有關(guān),高位數(shù)據(jù)沒(méi)發(fā)揮作用。為了處理這個(gè)缺陷,我們可以上圖中的 hash 高4位數(shù)據(jù)與低4位數(shù)據(jù)進(jìn)行異或運(yùn)算,即 hash ^ (hash >>> 4)。通過(guò)這種方式,讓高位數(shù)據(jù)與低位數(shù)據(jù)進(jìn)行異或,以此加大低位信息的隨機(jī)性,變相的讓高位數(shù)據(jù)參與到計(jì)算中。此時(shí)的計(jì)算過(guò)程如下:

    img

    在 Java 中,hashCode 方法產(chǎn)生的 hash 是 int 類型,32 位寬。前16位為高位,后16位為低位,所以要右移16位,即 hash ^ (hash >>> 16) 。這樣還增加了hash 的復(fù)雜度,進(jìn)而影響 hash 的分布性。

HashMap 的長(zhǎng)度為什么是2的冪次方?

為了能讓HashMap存取高效,盡量減少碰撞,也就是要盡量把數(shù)據(jù)分配均勻,Hash值的范圍是-2147483648到2147483647,前后加起來(lái)有40億的映射空間,只要哈希函數(shù)映射的比較均勻松散,一般應(yīng)用是很難出現(xiàn)碰撞的,但一個(gè)問(wèn)題是40億的數(shù)組內(nèi)存是放不下的。所以這個(gè)散列值是不能直接拿來(lái)用的。用之前需要先對(duì)數(shù)組長(zhǎng)度取模運(yùn)算,得到余數(shù)才能用來(lái)存放位置也就是對(duì)應(yīng)的數(shù)組小標(biāo)。這個(gè)數(shù)組下標(biāo)的計(jì)算方法是(n-1)&hash,n代表數(shù)組長(zhǎng)度

這個(gè)算法應(yīng)該如何設(shè)計(jì)呢?

我們首先可能會(huì)想到采用%取余的操作來(lái)實(shí)現(xiàn)。但是,重點(diǎn)來(lái)了。

取余操作中如果除數(shù)是2的冪次則等價(jià)于其除數(shù)減一的與操作,也就是說(shuō)hash%length=hash&(length-1),但前提是length是2的n次方,并且采用&運(yùn)算比%運(yùn)算效率高,這也就解釋了HashMap的長(zhǎng)度為什么是2的冪次方。

put() 方法
image
public V put(K key, V value) {
  // 對(duì)key的hashCode()做hash
  return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
  Node<K,V>[] tab; Node<K,V> p; int n, i;
  // tab為空則創(chuàng)建
  if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;
  // 計(jì)算index,并對(duì)null做處理
  if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
  else {
    Node<K,V> e; K k;
    // 節(jié)點(diǎn)key存在,直接覆蓋value
    if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
      e = p;
    // 判斷該鏈為紅黑樹(shù)
    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);
          //鏈表長(zhǎng)度大于8轉(zhuǎn)換為紅黑樹(shù)進(jìn)行處理
          if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
            treeifyBin(tab, hash);
          break;
        }
        //key已經(jīng)存在直接覆蓋value
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
          break;
        p = e;
      }
    }
    if (e != null) { // existing mapping for key
      V oldValue = e.value;
      if (!onlyIfAbsent || oldValue == null)
        e.value = value;
      afterNodeAccess(e);
      return oldValue;
    }
  }
  ++modCount;
  // 超過(guò)最大容量 就擴(kuò)容
  if (++size > threshold)
    resize();
  afterNodeInsertion(evict);
  return null;
}
resize() 擴(kuò)容
final Node<K,V>[] resize() {
  Node<K,V>[] oldTab = table;
  int oldCap = (oldTab == null) ? 0 : oldTab.length;
  int oldThr = threshold;
  int newCap, newThr = 0;
  if (oldCap > 0) {
    // 超過(guò)最大值就不再擴(kuò)充了,就只好隨你碰撞了
    if (oldCap >= MAXIMUM_CAPACITY) {
      //修改閾值為int的最大值(2^31-1),這樣以后就不會(huì)擴(kuò)容了
      threshold = Integer.MAX_VALUE;
      return oldTab;
    }
     // 沒(méi)超過(guò)最大值,就擴(kuò)充為原來(lái)的2倍
    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
             oldCap >= DEFAULT_INITIAL_CAPACITY)
      newThr = oldThr << 1; // double threshold
  }
  else if (oldThr > 0) // initial capacity was placed in threshold
    newCap = oldThr;
  else {               // zero initial threshold signifies using defaults
    newCap = DEFAULT_INITIAL_CAPACITY;
    newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
  }
  // 計(jì)算新的resize上限
  if (newThr == 0) {
    float ft = (float)newCap * loadFactor;
    newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
              (int)ft : Integer.MAX_VALUE);
  }
  threshold = newThr;
  @SuppressWarnings({"rawtypes","unchecked"})
  Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  table = newTab;
  // 把每個(gè)bucket都移動(dòng)到新的buckets中
  if (oldTab != null) {
    for (int j = 0; j < oldCap; ++j) {
      Node<K,V> e;
      if ((e = oldTab[j]) != null) {
        oldTab[j] = null;
        if (e.next == null)
          newTab[e.hash & (newCap - 1)] = e;
        else if (e instanceof TreeNode)
          ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
        else { // preserve order  鏈表優(yōu)化重hash的代碼塊
          Node<K,V> loHead = null, loTail = null;
          Node<K,V> hiHead = null, hiTail = null;
          Node<K,V> next;
          do {
            // 原索引
            next = e.next;
            if ((e.hash & oldCap) == 0) {
              if (loTail == null)
                loHead = e;
              else
                loTail.next = e;
              loTail = e;
            }
            // 原索引+oldCap
            else {
              if (hiTail == null)
                hiHead = e;
              else
                hiTail.next = e;
              hiTail = e;
            }
          } while ((e = next) != null);
          // 原索引放到bucket里
          if (loTail != null) {
            loTail.next = null;
            newTab[j] = loHead;
          }
          // 原索引+oldCap放到bucket里
          if (hiTail != null) {
            hiTail.next = null;
            newTab[j + oldCap] = hiHead;
          }
        }
      }
    }
  }
  return newTab;
}
get() 方法
public V get(Object key) {
  Node<K,V> e;
  return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
  Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
  //定位鍵值對(duì)所在桶的位置
  if ((tab = table) != null && (n = tab.length) > 0 &&
      (first = tab[(n - 1) & hash]) != null) {
    // 直接命中
    if (first.hash == hash && // always check first node
        ((k = first.key) == key || (key != null && key.equals(k))))
      return first;
    // 未命中
    if ((e = first.next) != null) {
      // 如果 first 是 TreeNode 類型,則調(diào)用黑紅樹(shù)查找方法
      if (first instanceof TreeNode)
        return ((TreeNode<K,V>)first).getTreeNode(hash, key);
      do {
        // 在鏈表中查找
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
          return e;
      } while ((e = e.next) != null);
    }
  }
  return null;
}

Hashtable

Hashtable 和 HashMap 都是散列表,也是用”拉鏈法“實(shí)現(xiàn)的哈希表。保存數(shù)據(jù)和 JDK7 中的 HashMap 一樣,是 Entity 對(duì)象,只是 Hashtable 中的幾乎所有的 public 方法都是 synchronized 的,而有些方法也是在內(nèi)部通過(guò) synchronized 代碼塊來(lái)實(shí)現(xiàn),效率肯定會(huì)降低。且 put() 方法不允許空值。

HashMap 和 Hashtable 的區(qū)別

  1. 線程是否安全: HashMap 是非線程安全的,HashTable 是線程安全的;HashTable 內(nèi)部的方法基本都經(jīng)過(guò) synchronized 修飾。(如果你要保證線程安全的話就使用 ConcurrentHashMap 吧?。?;

  2. 效率: 因?yàn)榫€程安全的問(wèn)題,HashMap 要比 HashTable 效率高一點(diǎn)。另外,HashTable 基本被淘汰,不要在代碼中使用它;

  3. 對(duì)Null key 和Null value的支持: HashMap 中,null 可以作為鍵,這樣的鍵只有一個(gè),可以有一個(gè)或多個(gè)鍵所對(duì)應(yīng)的值為 null。。但是在 HashTable 中 put 進(jìn)的鍵值只要有一個(gè) null,直接拋出 NullPointerException。

  4. 初始容量大小和每次擴(kuò)充容量大小的不同 :

    ① 創(chuàng)建時(shí)如果不指定容量初始值,Hashtable 默認(rèn)的初始大小為11,之后每次擴(kuò)充,容量變?yōu)樵瓉?lái)的2n+1。HashMap 默認(rèn)的初始化大小為16。之后每次擴(kuò)充,容量變?yōu)樵瓉?lái)的2倍。

    ② 創(chuàng)建時(shí)如果給定了容量初始值,那么 Hashtable 會(huì)直接使用你給定的大小,而 HashMap 會(huì)將其擴(kuò)充為2的冪次方大小。也就是說(shuō) HashMap 總是使用2的冪次方作為哈希表的大小,后面會(huì)介紹到為什么是2的冪次方。

  5. 底層數(shù)據(jù)結(jié)構(gòu): JDK1.8 以后的 HashMap 在解決哈希沖突時(shí)有了較大的變化,當(dāng)鏈表長(zhǎng)度大于閾值(默認(rèn)為8)時(shí),將鏈表轉(zhuǎn)化為紅黑樹(shù),以減少搜索時(shí)間。Hashtable 沒(méi)有這樣的機(jī)制。

  6. HashMap的迭代器(Iterator)是fail-fast迭代器,但是 Hashtable的迭代器(enumerator)不是 fail-fast的。如果有其它線程對(duì)HashMap進(jìn)行的添加/刪除元素,將會(huì)拋出ConcurrentModificationException,但迭代器本身的remove方法移除元素則不會(huì)拋出異常。這條同樣也是 Enumeration 和 Iterator 的區(qū)別。

ConcurrentHashMap

HashMap在多線程情況下,在put的時(shí)候,插入的元素超過(guò)了容量(由負(fù)載因子決定)的范圍就會(huì)觸發(fā)擴(kuò)容操作,就是rehash,這個(gè)會(huì)重新將原數(shù)組的內(nèi)容重新hash到新的擴(kuò)容數(shù)組中,在多線程的環(huán)境下,存在同時(shí)其他的元素也在進(jìn)行put操作,如果hash值相同,可能出現(xiàn)同時(shí)在同一數(shù)組下用鏈表表示,造成閉環(huán),導(dǎo)致在get時(shí)會(huì)出現(xiàn)死循環(huán),所以HashMap是線程不安全的。

Hashtable,是線程安全的,它在所有涉及到多線程操作的都加上了synchronized關(guān)鍵字來(lái)鎖住整個(gè)table,這就意味著所有的線程都在競(jìng)爭(zhēng)一把鎖,在多線程的環(huán)境下,它是安全的,但是無(wú)疑是效率低下的。

JDK1.7 實(shí)現(xiàn)

Hashtable 容器在競(jìng)爭(zhēng)激烈的并發(fā)環(huán)境下表現(xiàn)出效率低下的原因,是因?yàn)樗性L問(wèn) Hashtable 的線程都必須競(jìng)爭(zhēng)同一把鎖,那假如容器里有多把鎖,每一把鎖用于鎖容器其中一部分?jǐn)?shù)據(jù),那么當(dāng)多線程訪問(wèn)容器里不同數(shù)據(jù)段的數(shù)據(jù)時(shí),線程間就不會(huì)存在鎖競(jìng)爭(zhēng),,這就是ConcurrentHashMap所使用的鎖分段技術(shù)。

在 JDK1.7版本中,ConcurrentHashMap 的數(shù)據(jù)結(jié)構(gòu)是由一個(gè) Segment 數(shù)組和多個(gè) HashEntry 組成。Segment 數(shù)組的意義就是將一個(gè)大的 table 分割成多個(gè)小的 table 來(lái)進(jìn)行加鎖。每一個(gè) Segment 元素存儲(chǔ)的是 HashEntry數(shù)組+鏈表,這個(gè)和 HashMap 的數(shù)據(jù)存儲(chǔ)結(jié)構(gòu)一樣。

image

ConcurrentHashMap 類中包含兩個(gè)靜態(tài)內(nèi)部類 HashEntry 和 Segment。
HashEntry 用來(lái)封裝映射表的鍵值對(duì),Segment 用來(lái)充當(dāng)鎖的角色,每個(gè) Segment 對(duì)象守護(hù)整個(gè)散列映射表的若干個(gè)桶。每個(gè)桶是由若干個(gè) HashEntry 對(duì)象鏈接起來(lái)的鏈表。一個(gè) ConcurrentHashMap 實(shí)例中包含由若干個(gè) Segment 對(duì)象組成的數(shù)組。每個(gè) Segment 守護(hù)著一個(gè) HashEntry 數(shù)組里的元素,當(dāng)對(duì) HashEntry 數(shù)組的數(shù)據(jù)進(jìn)行修改時(shí),必須首先獲得它對(duì)應(yīng)的 Segment 鎖。

Segment 類

Segment 類繼承于 ReentrantLock 類,從而使得 Segment 對(duì)象能充當(dāng)可重入鎖的角色。一個(gè) Segment 就是一個(gè)子哈希表,Segment 里維護(hù)了一個(gè) HashEntry 數(shù)組,并發(fā)環(huán)境下,對(duì)于不同 Segment 的數(shù)據(jù)進(jìn)行操作是不用考慮鎖競(jìng)爭(zhēng)的。

從源碼可以看到,Segment 內(nèi)部類和我們上邊看到的 HashMap 很相似。也有負(fù)載因子,閾值等各種屬性。

static final class Segment<K,V> extends ReentrantLock implements Serializable {
  
  static final int MAX_SCAN_RETRIES =
    Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
  transient volatile HashEntry<K,V>[] table;
  transient int count;
  transient int modCount;  //記錄修改次數(shù)
  transient int threshold;
  final float loadFactor;

  Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
    this.loadFactor = lf;
    this.threshold = threshold;
    this.table = tab;
  }

  //put 方法會(huì)有加鎖操作,
  final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);
    // ...
  }

  @SuppressWarnings("unchecked")
  private void rehash(HashEntry<K,V> node) {
    // ...
  }

  private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
        //...
  }

  private void scanAndLock(Object key, int hash) {
        //...
  }

  final V remove(Object key, int hash, Object value) {
    //...
  }

  final boolean replace(K key, int hash, V oldValue, V newValue) {
    //...
  }

  final V replace(K key, int hash, V value) {
    //...
  }

  final void clear() {
        //...
  }
}

HashEntry 類

HashEntry 是目前我們最小的邏輯處理單元。一個(gè)ConcurrentHashMap 維護(hù)一個(gè) Segment 數(shù)組,一個(gè)Segment維護(hù)一個(gè) HashEntry 數(shù)組。

static final class HashEntry<K,V> {
  final int hash;
  final K key;
  volatile V value;   // value 為 volatie 類型,保證可見(jiàn)
  volatile HashEntry<K,V> next;
    //...
}

ConcurrentHashMap 類

默認(rèn)的情況下,每個(gè)ConcurrentHashMap 類會(huì)創(chuàng)建16個(gè)并發(fā)的 segment,每個(gè) segment 里面包含多個(gè) Hash表,每個(gè) Hash 鏈都是由 HashEntry 節(jié)點(diǎn)組成的。

public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
        implements ConcurrentMap<K, V>, Serializable {
    //默認(rèn)初始容量為 16,即初始默認(rèn)為 16 個(gè)桶
  static final int DEFAULT_INITIAL_CAPACITY = 16;
  static final float DEFAULT_LOAD_FACTOR = 0.75f;
  //默認(rèn)并發(fā)級(jí)別為 16。該值表示當(dāng)前更新線程的估計(jì)數(shù)
  static final int DEFAULT_CONCURRENCY_LEVEL = 16;
  
  static final int MAXIMUM_CAPACITY = 1 << 30;
  static final int MIN_SEGMENT_TABLE_CAPACITY = 2;  
  static final int MAX_SEGMENTS = 1 << 16; // slightly conservative  
  static final int RETRIES_BEFORE_LOCK = 2;
  final int segmentMask;  //段掩碼,主要為了定位Segment
  final int segmentShift;
  final Segment<K,V>[] segments;   //主干就是這個(gè)分段鎖數(shù)組
  
  //構(gòu)造器
  public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
      //MAX_SEGMENTS 為1<<16=65536,也就是最大并發(fā)數(shù)為65536
        if (concurrencyLevel > MAX_SEGMENTS)
            concurrencyLevel = MAX_SEGMENTS;
        // 2的sshif次方等于ssize,例:ssize=16,sshift=4;ssize=32,sshif=5
        int sshift = 0;
        // ssize 為segments數(shù)組長(zhǎng)度,根據(jù)concurrentLevel計(jì)算得出
        int ssize = 1;
        while (ssize < concurrencyLevel) {
            ++sshift;
            ssize <<= 1;
        }
        this.segmentShift = 32 - sshift;
        this.segmentMask = ssize - 1;
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        int c = initialCapacity / ssize;
        if (c * ssize < initialCapacity)
            ++c;
        int cap = MIN_SEGMENT_TABLE_CAPACITY;
        while (cap < c)
            cap <<= 1;
        // 創(chuàng)建segments數(shù)組并初始化第一個(gè)Segment,其余的Segment延遲初始化
        Segment<K,V> s0 =
            new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                             (HashEntry<K,V>[])new HashEntry[cap]);
        Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
        UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
        this.segments = ss;
    }
}

put() 方法

  1. **定位segment并確保定位的Segment已初始化 **
  2. 調(diào)用 Segment的 put 方法。
public V put(K key, V value) {
  Segment<K,V> s;
  //concurrentHashMap不允許key/value為空
  if (value == null)
    throw new NullPointerException();
  //hash函數(shù)對(duì)key的hashCode重新散列,避免差勁的不合理的hashcode,保證散列均勻
  int hash = hash(key);
  //返回的hash值無(wú)符號(hào)右移segmentShift位與段掩碼進(jìn)行位運(yùn)算,定位segment
  int j = (hash >>> segmentShift) & segmentMask;
  if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
       (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
    s = ensureSegment(j);
  return s.put(key, hash, value, false);
}

get() 方法

get方法無(wú)需加鎖,由于其中涉及到的共享變量都使用volatile修飾,volatile可以保證內(nèi)存可見(jiàn)性,所以不會(huì)讀取到過(guò)期數(shù)據(jù)

public V get(Object key) {
  Segment<K,V> s; // manually integrate access methods to reduce overhead
  HashEntry<K,V>[] tab;
  int h = hash(key);
  long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
  if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
      (tab = s.table) != null) {
    for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
         (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
         e != null; e = e.next) {
      K k;
      if ((k = e.key) == key || (e.hash == h && key.equals(k)))
        return e.value;
    }
  }
  return null;
}

JDK1.8 實(shí)現(xiàn)

img

ConcurrentHashMap 在 JDK8 中進(jìn)行了巨大改動(dòng),光是代碼量就從1000多行增加到6000行!1.8摒棄了Segment(鎖段)的概念,采用了 CAS + synchronized 來(lái)保證并發(fā)的安全性。

可以看到,和HashMap 1.8的數(shù)據(jù)結(jié)構(gòu)很像。底層數(shù)據(jù)結(jié)構(gòu)改變?yōu)椴捎脭?shù)組+鏈表+紅黑樹(shù)的數(shù)據(jù)形式。

和HashMap1.8相同的一些地方

  • 底層數(shù)據(jù)結(jié)構(gòu)一致
  • HashMap初始化是在第一次put元素的時(shí)候進(jìn)行的,而不是init
  • HashMap的底層數(shù)組長(zhǎng)度總是為2的整次冪
  • 默認(rèn)樹(shù)化的閾值為 8,而鏈表化的閾值為 6
  • hash算法也很類似,但多了一步& HASH_BITS,該步是為了消除最高位上的負(fù)符號(hào),hash的負(fù)在ConcurrentHashMap中有特殊意義表示在擴(kuò)容或者是樹(shù)節(jié)點(diǎn)
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash

static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}

一些關(guān)鍵屬性

private static final int MAXIMUM_CAPACITY = 1 << 30; //數(shù)組最大大小 同HashMap

private static final int DEFAULT_CAPACITY = 16;//數(shù)組默認(rèn)大小

static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; //數(shù)組可能最大值,需要與toArray()相關(guān)方法關(guān)聯(lián)

private static final int DEFAULT_CONCURRENCY_LEVEL = 16; //兼容舊版保留的值,默認(rèn)線程并發(fā)度,類似信號(hào)量

private static final float LOAD_FACTOR = 0.75f;//默認(rèn)map擴(kuò)容比例,實(shí)際用(n << 1) - (n >>> 1)代替了更高效

static final int TREEIFY_THRESHOLD = 8; // 鏈表轉(zhuǎn)樹(shù)閥值,大于8時(shí)

static final int UNTREEIFY_THRESHOLD = 6; //樹(shù)轉(zhuǎn)鏈表閥值,小于等于6(tranfer時(shí),lc、hc=0兩個(gè)計(jì)數(shù)器分別++記錄原bin、新binTreeNode數(shù)量,<=UNTREEIFY_THRESHOLD 則untreeify(lo))。【僅在擴(kuò)容tranfer時(shí)才可能樹(shù)轉(zhuǎn)鏈表】

static final int MIN_TREEIFY_CAPACITY = 64;

private static final int MIN_TRANSFER_STRIDE = 16;//擴(kuò)容轉(zhuǎn)移時(shí)的最小數(shù)組分組大小

private static int RESIZE_STAMP_BITS = 16;//本類中沒(méi)提供修改的方法 用來(lái)根據(jù)n生成位置一個(gè)類似時(shí)間戳的功能

private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1; // 2^15-1,help resize的最大線程數(shù)

private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS; // 32-16=16,sizeCtl中記錄size大小的偏移量

static final int MOVED = -1; // hash for forwarding nodes(forwarding nodes的hash值)、標(biāo)示位

static final int TREEBIN = -2; // hash for roots of trees(樹(shù)根節(jié)點(diǎn)的hash值)

static final int RESERVED = -3; // ReservationNode的hash值

static final int HASH_BITS = 0x7fffffff; // 用在計(jì)算hash時(shí)進(jìn)行安位與計(jì)算消除負(fù)hash

static final int NCPU = Runtime.getRuntime().availableProcessors(); // 可用處理器數(shù)量

/* ---------------- Fields -------------- */

transient volatile Node<K,V>[] table; //裝載Node的數(shù)組,作為ConcurrentHashMap的數(shù)據(jù)容器,采用懶加載的方式,直到第一次插入數(shù)據(jù)的時(shí)候才會(huì)進(jìn)行初始化操作,數(shù)組的大小總是為2的冪次方。

private transient volatile Node<K,V>[] nextTable; //擴(kuò)容時(shí)使用,平時(shí)為null,只有在擴(kuò)容的時(shí)候才為非null

/**
* 實(shí)際上保存的是hashmap中的元素個(gè)數(shù)  利用CAS鎖進(jìn)行更新但它并不用返回當(dāng)前hashmap的元素個(gè)數(shù) 
*/
private transient volatile long baseCount;

/**
*該屬性用來(lái)控制table數(shù)組的大小,根據(jù)是否初始化和是否正在擴(kuò)容有幾種情況:
*當(dāng)值為負(fù)數(shù)時(shí):如果為-1表示正在初始化,如果為-N則表示當(dāng)前正有N-1個(gè)線程進(jìn)行擴(kuò)容操作;
*當(dāng)值為正數(shù)時(shí):如果當(dāng)前數(shù)組為null的話表示table在初始化過(guò)程中,sizeCtl表示為需要新建數(shù)組的長(zhǎng)度;若已經(jīng)初始化了,表示當(dāng)前數(shù)據(jù)容器(table數(shù)組)可用容量也可以理解成臨界值(插入節(jié)點(diǎn)數(shù)超過(guò)了該臨界值就需要擴(kuò)容),具體指為數(shù)組的長(zhǎng)度n 乘以 加載因子loadFactor;當(dāng)值為0時(shí),即數(shù)組長(zhǎng)度為默認(rèn)初始值。
*/
private transient volatile int sizeCtl;

put() 方法

  1. 首先會(huì)判斷 key、value是否為空,如果為空就拋異常!
  2. spread()方法獲取hash,減小hash沖突
  3. 判斷是否初始化table數(shù)組,沒(méi)有的話調(diào)用initTable()方法進(jìn)行初始化
  4. 判斷是否能直接將新值插入到table數(shù)組中
  5. 判斷當(dāng)前是否在擴(kuò)容,MOVED為-1說(shuō)明當(dāng)前ConcurrentHashMap正在進(jìn)行擴(kuò)容操作,正在擴(kuò)容的話就進(jìn)行協(xié)助擴(kuò)容
  6. 當(dāng)table[i]為鏈表的頭結(jié)點(diǎn),在鏈表中插入新值,通過(guò)synchronized (f)的方式進(jìn)行加鎖以實(shí)現(xiàn)線程安全性。
    1. 在鏈表中如果找到了與待插入的鍵值對(duì)的key相同的節(jié)點(diǎn),就直接覆蓋
    2. 如果沒(méi)有找到的話,就直接將待插入的鍵值對(duì)追加到鏈表的末尾
  7. 當(dāng)table[i]為紅黑樹(shù)的根節(jié)點(diǎn),在紅黑樹(shù)中插入新值/覆蓋舊值
  8. 根據(jù)當(dāng)前節(jié)點(diǎn)個(gè)數(shù)進(jìn)行調(diào)整,否需要轉(zhuǎn)換成紅黑樹(shù)(個(gè)數(shù)大于等于8,就會(huì)調(diào)用treeifyBin方法將tabel[i]第i個(gè)散列桶拉鏈轉(zhuǎn)換成紅黑樹(shù))
  9. 對(duì)當(dāng)前容量大小進(jìn)行檢查,如果超過(guò)了臨界值(實(shí)際大小*加載因子)就進(jìn)行擴(kuò)容
final V putVal(K key, V value, boolean onlyIfAbsent) {
    // key 和 value 均不允許為 null
    if (key == null || value == null) throw new NullPointerException();
    // 根據(jù) key 計(jì)算出 hash 值
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
         // 判斷是否需要進(jìn)行初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        // f 即為當(dāng)前 key 定位出的 Node,如果為空表示當(dāng)前位置可以寫入數(shù)據(jù),利用 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
        }
        // 如果當(dāng)前位置的 hashcode == MOVED == -1,則需要進(jìn)行擴(kuò)容
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        // 如果都不滿足,則利用 synchronized 鎖寫入數(shù)據(jù)
        else {
          // 剩下情況又分兩種,插入鏈表、插入紅黑樹(shù)
            V oldVal = null;
          //采用同步內(nèi)置鎖實(shí)現(xiàn)并發(fā)控制
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    // 如果 fh=f.hash >=0,當(dāng)前為鏈表,在鏈表中插入新的鍵值對(duì)
                    if (fh >= 0) {
                        binCount = 1;
                      //遍歷鏈表,如果找到對(duì)應(yīng)的 node 節(jié)點(diǎn),修改 value,否則直接在鏈表尾部加入節(jié)點(diǎn)
                        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;
                            }
                        }
                    }
                    // 當(dāng)前為紅黑樹(shù),將新的鍵值對(duì)插入到紅黑樹(shù)中
                    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;
                        }
                    }
                }
            }
            // 插入完鍵值對(duì)后再根據(jù)實(shí)際大小看是否需要轉(zhuǎn)換成紅黑樹(shù)
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    // 對(duì)當(dāng)前容量大小進(jìn)行檢查,如果超過(guò)了臨界值(實(shí)際大小*加載因子)就需要擴(kuò)容 
    addCount(1L, binCount);
    return null;
}

我們可以發(fā)現(xiàn)JDK8中的實(shí)現(xiàn)也是鎖分離的思想,只是鎖住的是一個(gè)Node,而不是JDK7中的Segment,而鎖住Node之前的操作是無(wú)鎖的并且也是線程安全的,建立在之前提到的原子操作上。

get() 方法

get方法無(wú)需加鎖,由于其中涉及到的共享變量都使用volatile修飾,volatile可以保證內(nèi)存可見(jiàn)性,所以不會(huì)讀取到過(guò)期數(shù)據(jù)

public V get(Object key) {
  Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
  int h = spread(key.hashCode());
  // 判斷數(shù)組是否為空
  if ((tab = table) != null && (n = tab.length) > 0 &&
      (e = tabAt(tab, (n - 1) & h)) != null) {
    // 判斷node 節(jié)點(diǎn)第一個(gè)元素是不是要找的,如果是直接返回
    if ((eh = e.hash) == h) {
      if ((ek = e.key) == key || (ek != null && key.equals(ek)))
        return e.val;
    }
    // // hash小于0,說(shuō)明是特殊節(jié)點(diǎn)(TreeBin或ForwardingNode)調(diào)用find
    else if (eh < 0)
      return (p = e.find(h, key)) != null ? p.val : null;
    // 不是上面的情況,那就是鏈表了,遍歷鏈表
    while ((e = e.next) != null) {
      if (e.hash == h &&
          ((ek = e.key) == key || (ek != null && key.equals(ek))))
        return e.val;
    }
  }
  return null;
}

Hashtable 和 ConcurrentHashMap 的區(qū)別

ConcurrentHashMap 和 Hashtable 的區(qū)別主要體現(xiàn)在實(shí)現(xiàn)線程安全的方式上不同。

  • 底層數(shù)據(jù)結(jié)構(gòu): JDK1.7的 ConcurrentHashMap 底層采用 分段的數(shù)組+鏈表 實(shí)現(xiàn),JDK1.8 采用的數(shù)據(jù)結(jié)構(gòu)和HashMap1.8的結(jié)構(gòu)類似,數(shù)組+鏈表/紅黑二叉樹(shù)。Hashtable 和 JDK1.8 之前的 HashMap 的底層數(shù)據(jù)結(jié)構(gòu)類似都是采用 數(shù)組+鏈表 的形式,數(shù)組是 HashMap 的主體,鏈表則是主要為了解決哈希沖突而存在的;
  • 實(shí)現(xiàn)線程安全的方式(重要):
    • 在JDK1.7的時(shí)候,ConcurrentHashMap(分段鎖) 對(duì)整個(gè)桶數(shù)組進(jìn)行了分割分段(Segment),每一把鎖只鎖容器其中一部分?jǐn)?shù)據(jù),多線程訪問(wèn)容器里不同數(shù)據(jù)段的數(shù)據(jù),就不會(huì)存在鎖競(jìng)爭(zhēng),提高并發(fā)訪問(wèn)率。(默認(rèn)分配16個(gè)Segment,比Hashtable效率提高16倍。) 到了 JDK1.8 的時(shí)候已經(jīng)摒棄了Segment的概念,而是直接用 Node 數(shù)組+鏈表/紅黑樹(shù)的數(shù)據(jù)結(jié)構(gòu)來(lái)實(shí)現(xiàn),并發(fā)控制使用 synchronized 和 CAS 來(lái)操作。(JDK1.6以后 對(duì) synchronized鎖做了很多優(yōu)化) 整個(gè)看起來(lái)就像是優(yōu)化過(guò)且線程安全的 HashMap,雖然在 JDK1.8 中還能看到 Segment 的數(shù)據(jù)結(jié)構(gòu),但是已經(jīng)簡(jiǎn)化了屬性,只是為了兼容舊版本;
    • Hashtable(同一把鎖) :使用 synchronized 來(lái)保證線程安全,效率非常低下。當(dāng)一個(gè)線程訪問(wèn)同步方法時(shí),其他線程也訪問(wèn)同步方法,可能會(huì)進(jìn)入阻塞或輪詢狀態(tài),如使用 put 添加元素,另一個(gè)線程不能使用 put 添加元素,也不能使用 get,競(jìng)爭(zhēng)越激烈效率越低。

Java快速失?。╢ail-fast)和安全失敗(fail-safe)區(qū)別

快速失?。╢ail—fast)

在用迭代器遍歷一個(gè)集合對(duì)象時(shí),如果遍歷過(guò)程中對(duì)集合對(duì)象的內(nèi)容進(jìn)行了修改(增加、刪除、修改),則會(huì)拋出ConcurrentModificationException。

原理:迭代器在遍歷時(shí)直接訪問(wèn)集合中的內(nèi)容,并且在遍歷過(guò)程中使用一個(gè) modCount 變量。集合在被遍歷期間如果內(nèi)容發(fā)生變化,就會(huì)改變 modCount 的值。每當(dāng)?shù)魇褂?hashNext()/next() 遍歷下一個(gè)元素之前,都會(huì)檢測(cè) modCount 變量是否為 expectedmodCount 值,是的話就返回遍歷;否則拋出異常,終止遍歷。

注意:這里異常的拋出條件是檢測(cè)到 modCount!=expectedmodCount 這個(gè)條件。如果集合發(fā)生變化時(shí)修改modCount 值剛好又設(shè)置為了 expectedmodCount 值,則異常不會(huì)拋出。因此,不能依賴于這個(gè)異常是否拋出而進(jìn)行并發(fā)操作的編程,這個(gè)異常只建議用于檢測(cè)并發(fā)修改的bug。

場(chǎng)景:java.util包下的集合類都是快速失敗的,不能在多線程下發(fā)生并發(fā)修改(迭代過(guò)程中被修改)。

安全失?。╢ail—safe)

采用安全失敗機(jī)制的集合容器,在遍歷時(shí)不是直接在集合內(nèi)容上訪問(wèn)的,而是先復(fù)制原有集合內(nèi)容,在拷貝的集合上進(jìn)行遍歷。

原理:由于迭代時(shí)是對(duì)原集合的拷貝進(jìn)行遍歷,所以在遍歷過(guò)程中對(duì)原集合所作的修改并不能被迭代器檢測(cè)到,所以不會(huì)觸發(fā) Concurrent Modification Exception。

缺點(diǎn):基于拷貝內(nèi)容的優(yōu)點(diǎn)是避免了Concurrent Modification Exception,但同樣地,迭代器并不能訪問(wèn)到修改后的內(nèi)容,即:迭代器遍歷的是開(kāi)始遍歷那一刻拿到的集合拷貝,在遍歷期間原集合發(fā)生的修改迭代器是不知道的。

場(chǎng)景:java.util.concurrent包下的容器都是安全失敗,可以在多線程下并發(fā)使用,并發(fā)修改。

快速失敗和安全失敗是對(duì)迭代器而言的。 快速失?。寒?dāng)在迭代一個(gè)集合的時(shí)候,如果有另外一個(gè)線程在修改這個(gè)集合,就會(huì)拋出ConcurrentModification異常,java.util下都是快速失敗。 安全失?。涸诘鷷r(shí)候會(huì)在集合二層做一個(gè)拷貝,所以在修改集合上層元素不會(huì)影響下層。在java.util.concurrent下都是安全失敗

如何避免fail-fast

  • 在單線程的遍歷過(guò)程中,如果要進(jìn)行remove操作,可以調(diào)用迭代器 ListIterator 的 remove 方法而不是集合類的 remove方法。看看 ArrayList中迭代器的 remove方法的源碼,該方法不能指定元素刪除,只能remove當(dāng)前遍歷元素。
public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();

    try {
        SubList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = ArrayList.this.modCount;  //
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}
  • 使用并發(fā)包(java.util.concurrent)中的類來(lái)代替 ArrayList 和 hashMap
    • CopyOnWriterArrayList 代替 ArrayList
    • ConcurrentHashMap 代替 HashMap

Iterator 和 Enumeration 區(qū)別

在Java集合中,我們通常都通過(guò) “Iterator(迭代器)” 或 “Enumeration(枚舉類)” 去遍歷集合。

public interface Enumeration<E> {
    boolean hasMoreElements();
    E nextElement();
}
public interface Iterator<E> {
    boolean hasNext();
    E next();
    void remove();
}
  • 函數(shù)接口不同,Enumeration只有2個(gè)函數(shù)接口。通過(guò)Enumeration,我們只能讀取集合的數(shù)據(jù),而不能對(duì)數(shù)據(jù)進(jìn)行修改。Iterator只有3個(gè)函數(shù)接口。Iterator除了能讀取集合的數(shù)據(jù)之外,也能數(shù)據(jù)進(jìn)行刪除操作。
  • Iterator支持 fail-fast機(jī)制,而Enumeration不支持。Enumeration 是JDK 1.0添加的接口。使用到它的函數(shù)包括Vector、Hashtable等類,這些類都是JDK 1.0中加入的,Enumeration存在的目的就是為它們提供遍歷接口。Enumeration本身并沒(méi)有支持同步,而在Vector、Hashtable實(shí)現(xiàn)Enumeration時(shí),添加了同步。
    而Iterator 是JDK 1.2才添加的接口,它也是為了HashMap、ArrayList等集合提供遍歷接口。Iterator是支持fail-fast機(jī)制的:當(dāng)多個(gè)線程對(duì)同一個(gè)集合的內(nèi)容進(jìn)行操作時(shí),就可能會(huì)產(chǎn)生fail-fast事件

Comparable 和 Comparator接口有何區(qū)別?

Java中對(duì)集合對(duì)象或者數(shù)組對(duì)象排序,有兩種實(shí)現(xiàn)方式:

  • 對(duì)象實(shí)現(xiàn)Comparable 接口

    • Comparable 在 java.lang 包下,是一個(gè)接口,內(nèi)部只有一個(gè)方法 compareTo()

      public interface Comparable<T> {
          public int compareTo(T o);
      }
      
    • Comparable 可以讓實(shí)現(xiàn)它的類的對(duì)象進(jìn)行比較,具體的比較規(guī)則是按照 compareTo 方法中的規(guī)則進(jìn)行。這種順序稱為 自然順序。

    • 實(shí)現(xiàn)了 Comparable 接口的 List 或則數(shù)組可以使用 Collections.sort() 或者 Arrays.sort() 方法進(jìn)行排序

  • 定義比較器,實(shí)現(xiàn) Comparator接口

    • Comparator 在 java.util 包下,也是一個(gè)接口,JDK 1.8 以前只有兩個(gè)方法:

      public interface Comparator<T> {
          public int compare(T lhs, T rhs);
          public boolean equals(Object object);
      }
      

comparable相當(dāng)于內(nèi)部比較器。comparator相當(dāng)于外部比較器

區(qū)別:

  • Comparator 位于 java.util 包下,而 Comparable 位于 java.lang 包下

  • Comparable 接口的實(shí)現(xiàn)是在類的內(nèi)部(如 String、Integer已經(jīng)實(shí)現(xiàn)了 Comparable 接口,自己就可以完成比較大小操作),Comparator 接口的實(shí)現(xiàn)是在類的外部(可以理解為一個(gè)是自已完成比較,一個(gè)是外部程序?qū)崿F(xiàn)比較)

  • 實(shí)現(xiàn) Comparable 接口要重寫 compareTo 方法, 在 compareTo 方法里面實(shí)現(xiàn)比較。一個(gè)已經(jīng)實(shí)現(xiàn)Comparable 的類的對(duì)象或數(shù)據(jù),可以通過(guò) Collections.sort(list) 或者 Arrays.sort(arr)實(shí)現(xiàn)排序。通過(guò) Collections.sort(list,Collections.reverseOrder()) 對(duì)list進(jìn)行倒序排列。

    image
  • 實(shí)現(xiàn)Comparator需要重寫 compare 方法

    image

HashSet

HashSet是用來(lái)存儲(chǔ)沒(méi)有重復(fù)元素的集合類,并且它是無(wú)序的。HashSet 內(nèi)部實(shí)現(xiàn)是基于 HashMap ,實(shí)現(xiàn)了 Set 接口。

從 HahSet 提供的構(gòu)造器可以看出,除了最后一個(gè) HashSet 的構(gòu)造方法外,其他所有內(nèi)部就是去創(chuàng)建一個(gè) Hashap 。沒(méi)有其他的操作。而最后一個(gè)構(gòu)造方法不是 public 的,所以不對(duì)外公開(kāi)。

image

HashSet如何檢查重復(fù)

HashSet的底層其實(shí)就是HashMap,只不過(guò)我們HashSet是實(shí)現(xiàn)了Set接口并且把數(shù)據(jù)作為K值,而V值一直使用一個(gè)相同的虛值來(lái)保存,HashMap的K值本身就不允許重復(fù),并且在HashMap中如果K/V相同時(shí),會(huì)用新的V覆蓋掉舊的V,然后返回舊的V。

image

Iterater 和 ListIterator 之間有什么區(qū)別?

  • 我們可以使用Iterator來(lái)遍歷Set和List集合,而ListIterator只能遍歷List
  • ListIterator有add方法,可以向List中添加對(duì)象,而Iterator不能
  • ListIterator和Iterator都有hasNext()和next()方法,可以實(shí)現(xiàn)順序向后遍歷,但是ListIterator有hasPrevious()和previous()方法,可以實(shí)現(xiàn)逆向(順序向前)遍歷。Iterator不可以
  • ListIterator可以定位當(dāng)前索引的位置,nextIndex()和previousIndex()可以實(shí)現(xiàn)。Iterator沒(méi)有此功能
  • 都可實(shí)現(xiàn)刪除操作,但是 ListIterator可以實(shí)現(xiàn)對(duì)象的修改,set()方法可以實(shí)現(xiàn)。Iterator僅能遍歷,不能修改
image

參考與感謝

所有內(nèi)容都是基于源碼閱讀和各種大佬之前總結(jié)的知識(shí)整理而來(lái),輸入并輸出,奧利給。

https://www.javatpoint.com/java-arraylist

https://www.runoob.com/java/java-collections.html

https://www.javazhiyin.com/21717.html

https://yuqirong.me/2018/01/31/LinkedList內(nèi)部原理解析/

https://youzhixueyuan.com/the-underlying-structure-and-principle-of-hashmap.html

《HashMap源碼詳細(xì)分析》http://www.tianxiaobo.com/2018/01/18/HashMap-源碼詳細(xì)分析-JDK1-8/

《ConcurrentHashMap1.7源碼分析》https://www.cnblogs.com/chengxiao/p/6842045.html

http://www.justdojava.com/2019/12/18/java-collection-15.1/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容