亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

對(duì)HashMap的數(shù)據(jù)結(jié)構(gòu)的學(xué)習(xí)解讀

 更新時(shí)間:2025年05月13日 10:10:43   作者:找不到、了  
這篇文章主要介紹了對(duì)HashMap的數(shù)據(jù)結(jié)構(gòu)的學(xué)習(xí)心得,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教

1、介紹

JDK1.8之前HashMap由數(shù)組+鏈表組成。數(shù)組是HshMap的主體,鏈表則是主要為了解決哈希沖突。

JDK1.8及以后的版本在解決哈希沖突時(shí)有了較大的變化,當(dāng)鏈表長(zhǎng)度大于閾值(或者紅黑樹的邊界值,默認(rèn)為8,且數(shù)組中桶的數(shù)量大于64,此時(shí)此索引位置上的所有數(shù)據(jù)改為使用紅黑樹存儲(chǔ)。

1.1、數(shù)據(jù)結(jié)構(gòu)

如下圖所示:

桶數(shù)組是用來(lái)存儲(chǔ)數(shù)據(jù)元素,鏈表是用來(lái)解決沖突,紅黑樹是為了提高查詢的效率。

數(shù)據(jù)元素通過(guò)映射關(guān)系,也就是散列函數(shù),映射到桶數(shù)組對(duì)應(yīng)索引的位置。

1.如果發(fā)生沖突,從沖突的位置拉一個(gè)鏈表,插入沖突的元素。

  • 如果鏈表長(zhǎng)度>8&數(shù)組大小>=64,鏈表轉(zhuǎn)為紅黑樹。
  • 如果紅黑樹節(jié)點(diǎn)個(gè)數(shù)<6 ,轉(zhuǎn)為鏈表。

2.但是數(shù)組長(zhǎng)度小于64,此時(shí)并不會(huì)將鏈表變?yōu)榧t黑樹。而是選擇進(jìn)行數(shù)組擴(kuò)容。

  • 目的是因?yàn)閿?shù)組比較小,盡量避開紅黑樹結(jié)構(gòu),這種情況下變?yōu)榧t黑樹結(jié)構(gòu),反而會(huì)降低效率,因?yàn)榧t黑樹需要進(jìn)行左旋,右旋,變色這些操作來(lái)保持平衡 。同時(shí)數(shù)組長(zhǎng)度小于64時(shí),搜索時(shí)間相對(duì)要快些。

所以綜上所述為了提高性能和減少搜索時(shí)間,底層在閾值大于8并且數(shù)組長(zhǎng)度大于64時(shí),鏈表才轉(zhuǎn)換為紅黑樹。

至于為什么要轉(zhuǎn)變從鏈表轉(zhuǎn)到紅黑樹,以及紅黑樹轉(zhuǎn)鏈表:JDK8 HashMap紅黑樹退化為鏈表的機(jī)制解讀

1.2、紅黑樹

為什么要使用紅黑樹:

如果鏈表非常長(zhǎng)(特別是在某些不均勻分布的哈希情況下),查找、插入和刪除的性能會(huì)降到 O(n) 的水平,因?yàn)樾枰闅v整個(gè)鏈表。

在這里簡(jiǎn)單介紹下紅黑樹:

由上圖可知:

紅黑樹本質(zhì)上是一種二叉查找樹,為了保持平衡,它又在二叉查找樹的基礎(chǔ)上增加了一些規(guī)則:

1、每個(gè)節(jié)點(diǎn)要么是紅色,要么是黑色;

2、根節(jié)點(diǎn)永遠(yuǎn)是黑色的;

3、所有的葉子節(jié)點(diǎn)都是是黑色的(注意這里說(shuō)葉子節(jié)點(diǎn)其實(shí)是圖中的 NULL 節(jié)點(diǎn));

4、每個(gè)紅色節(jié)點(diǎn)的兩個(gè)子節(jié)點(diǎn)一定都是黑色;

從任一節(jié)點(diǎn)到其子樹中每個(gè)葉子節(jié)點(diǎn)的路徑都包含相同數(shù)量的黑色節(jié)點(diǎn);

這種方式可以在最壞情況下將查找、插入和刪除的時(shí)間復(fù)雜度降低到 O(log n)。

紅黑樹怎么保持平衡?

紅黑樹有兩種方式保持平衡:旋轉(zhuǎn)和染色。

旋轉(zhuǎn)分為左旋和右旋。

1、左旋

2、右旋

1.3、參數(shù)

查看HashMap源碼,可以看到以下常用的變量,因此先在這里進(jìn)行歸類:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4:默認(rèn)的table初始容量
static final float DEFAULT_LOAD_FACTOR = 0.75f:默認(rèn)的負(fù)載因子

static final int TREEIFY_THRESHOLD = 8: 鏈表長(zhǎng)度大于等于該參數(shù)轉(zhuǎn)紅黑樹
static final int UNTREEIFY_THRESHOLD = 6: 當(dāng)樹的節(jié)點(diǎn)數(shù)小于等于該參數(shù)轉(zhuǎn)成鏈表

transient Node<K,V>[] table:這是一個(gè)Node類型的數(shù)組(也有稱作Hash桶)

可以從下面源碼中看到靜態(tài)內(nèi)部類Node在這邊可以看做就是一個(gè)節(jié)點(diǎn),多個(gè)Node節(jié)點(diǎn)構(gòu)成鏈表,當(dāng)鏈表長(zhǎng)度大于8的時(shí)候轉(zhuǎn)換為紅黑樹。

2、成員變量

Node 類

HashMap 內(nèi)部使用的一個(gè)重要結(jié)構(gòu)是 Node,通常定義為一個(gè)靜態(tài)內(nèi)部類,用于存儲(chǔ)鍵值對(duì)。

static class Node<K, V> {
    final int hash;       // 鍵的哈希值
    final K key;         // 鍵
    V value;             // 值
    Node<K, V> next;     // 指向下一個(gè)節(jié)點(diǎn),處理鏈表沖突
}

1. Node<K,V>[] table

  • 類型: Node<K,V>[]
  • 描述: 這是一個(gè)數(shù)組,存儲(chǔ)了 HashMap 的所有 "桶"(bucket)。
  • 作用: 每個(gè)桶對(duì)應(yīng)一個(gè)鏈表(或紅黑樹)以解決哈希沖突。桶的 index 通過(guò)取鍵的哈希值并與數(shù)組的長(zhǎng)度進(jìn)行模運(yùn)算得到。

2. int size

  • 類型: int
  • 描述: 當(dāng)前 HashMap 中鍵值對(duì)的數(shù)量。
  • 作用: 用于追蹤 HashMap 中的元素?cái)?shù)量,便于在執(zhí)行操作時(shí)(如添加或刪除元素)進(jìn)行容量檢查。

3. int threshold

  • 類型: int
  • 描述: 擴(kuò)容閾值,即當(dāng) HashMap 中的元素?cái)?shù)量達(dá)到該值時(shí),將觸發(fā)擴(kuò)容。
  • 作用: 閾值由 capacity * load factor 計(jì)算得出。它幫助管理 HashMap 的容量和負(fù)載因子,防止在高負(fù)載下性能下降。

4. float loadFactor

  • 類型: float
  • 描述: 負(fù)載因子是一個(gè)決定 HashMap 擴(kuò)容策略的參數(shù)。默認(rèn)值是 0.75。
  • 作用: 它用于衡量 HashMap 中使用的空間與實(shí)際容納的空間之間的比率。當(dāng) HashMap 中的元素?cái)?shù)量超過(guò)這個(gè)比率所計(jì)算的閾值時(shí),它將擴(kuò)容。

通過(guò)合理的負(fù)載因子設(shè)置:

不同的負(fù)載因子會(huì)影響 HashMap 的性能。較低的負(fù)載因子(例如 0.5)會(huì)使得 HashMap 更頻繁地?cái)U(kuò)容,從而減少?zèng)_突并提高快速訪問的性能,但會(huì)浪費(fèi)內(nèi)存。

高負(fù)載因子的影響:

較高的負(fù)載因子(例如 0.9)可以提高內(nèi)存使用效率,但同時(shí)也增加了哈希沖突的風(fēng)險(xiǎn),這可能導(dǎo)致訪問性能下降(例如,查詢、插入和刪除的時(shí)間復(fù)雜度從 O(1) 變?yōu)?O(n))。

以下代碼示例比較了使用不同負(fù)載因子的 HashMap 在大量插入元素時(shí)的性能差異:

import java.util.HashMap;

public class HashMapPerformanceTest {
    public static void main(String[] args) {
        int numberOfElements = 1_000_000; // 要插入的元素?cái)?shù)量

        // 測(cè)試負(fù)載因子為默認(rèn)值 (0.75)
        long timeWithDefaultLoadFactor = testHashMapPerformance(0.75f, numberOfElements);
        System.out.println("Time taken with default load factor (0.75): " + timeWithDefaultLoadFactor + " ms");

        // 測(cè)試負(fù)載因子為 0.5
        long timeWithLowLoadFactor = testHashMapPerformance(0.5f, numberOfElements);
        System.out.println("Time taken with low load factor (0.5): " + timeWithLowLoadFactor + " ms");

        // 測(cè)試負(fù)載因子為 0.9
        long timeWithHighLoadFactor = testHashMapPerformance(0.9f, numberOfElements);
        System.out.println("Time taken with high load factor (0.9): " + timeWithHighLoadFactor + " ms");
    }

    private static long testHashMapPerformance(float loadFactor, int numberOfElements) {
        long startTime = System.currentTimeMillis();

        HashMap<Integer, String> map = new HashMap<>(16, loadFactor); // 初始化 HashMap
        
        // 插入元素
        for (int i = 0; i < numberOfElements; i++) {
            map.put(i, "Value " + i);
        }

        long endTime = System.currentTimeMillis();
        return endTime - startTime; // 返回耗時(shí)
    }
}

輸出結(jié)果:

Time taken with default load factor (0.75): 197 ms
Time taken with low load factor (0.5): 716 ms
Time taken with high load factor (0.9): 552 ms

5. int modCount

  • 類型: int
  • 描述: 記錄 HashMap 進(jìn)行結(jié)構(gòu)性修改的次數(shù)。
  • 作用: 用于實(shí)現(xiàn) fail-fast 機(jī)制,能夠檢測(cè)到在迭代 HashMap 時(shí)是否發(fā)生了并發(fā)修改,通過(guò)與迭代器中的記錄值進(jìn)行比較來(lái)判斷。

關(guān)于fail-fast機(jī)制:可參考JDK8 HashMap紅黑樹退化為鏈表的機(jī)制解析

3、構(gòu)造方法

以下有四種不同的初始化方式。

1、無(wú)參構(gòu)造

    //構(gòu)造一個(gè)空的HashMap,默認(rèn)初始容量16和負(fù)載因子0.75
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
 

2、有初始化容量構(gòu)造

//構(gòu)造一個(gè)指定初始容量的HashMap,默認(rèn)負(fù)載因子0.75。
   public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

3、指定容量和負(fù)載因子

//構(gòu)造一個(gè)指定的HashMap,指定默認(rèn)初始容量和負(fù)載因子
   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);
    }

4、映射Map

   //構(gòu)造一個(gè)映射的map 
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

HashMap和HashSet都允許你指定負(fù)載因子的構(gòu)造器,表示當(dāng)負(fù)載情況達(dá)到負(fù)載因子水平的時(shí)候,容器會(huì)自動(dòng)擴(kuò)容,HashMap默認(rèn)使用的負(fù)載因子值為0.75f(當(dāng)容量達(dá)到四分之三進(jìn)行再散列(擴(kuò)容))。當(dāng)負(fù)載因子越大的時(shí)候能夠容納的鍵值對(duì)就越多但是查找的代價(jià)也會(huì)越高。

4、常用方法

4.1、put方法

在使用默認(rèn)構(gòu)造器初始化一個(gè)HashMap對(duì)象的時(shí)候,首次Put鍵值對(duì)的時(shí)候會(huì)先計(jì)算對(duì)應(yīng)Key的hash值通過(guò)hash值來(lái)確定存放的地址。

原理:

計(jì)算哈希值:

  • 首先,根據(jù)鍵的 hashCode() 方法計(jì)算出鍵的哈希值,并通過(guò)某種哈希算法對(duì)其進(jìn)行整合,以找到適合當(dāng)前 HashMap 容量的索引。

關(guān)于更多的哈希值的介紹可參考:HashMap中哈希值與數(shù)組坐標(biāo)的關(guān)聯(lián)

檢查桶(bucket):

  • 使用計(jì)算出的索引在內(nèi)部數(shù)組 table 中查找對(duì)應(yīng)的桶。如果該桶已經(jīng)有元素,則需要檢查是否存在哈希沖突(即多個(gè)鍵經(jīng)過(guò)哈希計(jì)算后映射到了同一個(gè)桶)。

處理沖突:

  • 如果找到了沖突,HashMap 將采用鏈表(Java 7及之前)或紅黑樹(Java 8引入,當(dāng)鏈表長(zhǎng)度超過(guò)一定閾值時(shí))來(lái)存儲(chǔ)沖突的元素。
  • 遍歷桶中的元素,看是否已有相同的鍵。如果找到了,更新其對(duì)應(yīng)的值;如果沒找到,就將新鍵值對(duì)添加到鏈表或樹的末尾。

更新大小和閾值:

  • 在插入后更新當(dāng)前的元素?cái)?shù)量(size),并檢查是否需要擴(kuò)容(即判斷當(dāng)前數(shù)量與負(fù)載因子乘積的關(guān)系)

每當(dāng)元素?cái)?shù)量增加到閾值時(shí)(即 size >= threshold),HashMap 會(huì)調(diào)用 resize() 方法來(lái)擴(kuò)大底層數(shù)組的大小。

以下是put的簡(jiǎn)化版代碼示例:

import java.util.Arrays;

class MyHashMap<K, V> {
    static class Node<K, V> {
        final int hash; // 存儲(chǔ)哈希值
        final K key;    // 存儲(chǔ)鍵
        V value;        // 存儲(chǔ)值
        Node<K, V> next; // 指向下一個(gè)節(jié)點(diǎn)
        
        Node(int hash, K key, V value, Node<K, V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }

    private Node<K, V>[] table; // 存儲(chǔ)節(jié)點(diǎn)的數(shù)組
    private int size;            // 當(dāng)前大小
    private int threshold;       // 擴(kuò)容閾值
    private float loadFactor;    // 負(fù)載因子

    public MyHashMap(int initialCapacity, float loadFactor) {
        this.loadFactor = loadFactor;
        this.table = new Node[initialCapacity];
        this.threshold = (int)(initialCapacity * loadFactor);
    }

    public void put(K key, V value) {
        int hash = key == null ? 0 : key.hashCode();
        int index = (hash & (table.length - 1)); // 計(jì)算桶的索引
        Node<K, V> existingNode = table[index];

        // 檢查桶是否為空
        if (existingNode == null) {
            // 如果桶為空,創(chuàng)建一個(gè)新節(jié)點(diǎn)
            table[index] = new Node<>(hash, key, value, null);
            size++;
        } else {
            // 遍歷鏈表,檢查是否已有相同的鍵
            Node<K, V> currentNode = existingNode;
            while (true) {
                if (currentNode.hash == hash && (currentNode.key == key || (key != null && key.equals(currentNode.key)))) {
                    // 找到相同的鍵,更新值
                    currentNode.value = value;
                    return;
                }
                if (currentNode.next == null) {
                    // 沒有找到,添加新節(jié)點(diǎn)
                    currentNode.next = new Node<>(hash, key, value, null);
                    size++;
                    break;
                }
                currentNode = currentNode.next;
            }
        }

        // 檢查是否需要擴(kuò)容
        if (size >= threshold) {
            resize();
        }
    }

    private void resize() {
        // 擴(kuò)容邏輯,目前省略具體實(shí)現(xiàn)
    }
}

4.2、get方法

擴(kuò)展:

在 Java 中,hashCode() 函數(shù)的返回值是一個(gè) int 類型,意味著哈希碼的值占用 32 位(4 字節(jié))。hashCode() 函數(shù)計(jì)算出的值可以在范圍 [-2,147,483,648, 2,147,483,647] 之間。

高位與低位的解釋

  • 低位(Least Significant Bits, LSB):在這個(gè)上下文中,低位是指哈希碼的最右邊的位。對(duì)于一個(gè) int 類型,32 位中最右邊的 16 位即為低位部分(即得到的結(jié)果是 0x0000FFFF)。
  • 高位(Most Significant Bits, MSB):這是指哈希碼的最左邊的位。對(duì)于一個(gè) int 類型,最左邊的 16 位即為高位部分(即得到的結(jié)果是 0xFFFF0000)。

先右移異或

首先會(huì)根據(jù)hashcode函數(shù)的到hash值,然后右移16位,然后將原始值和右移后的值進(jìn)行異或(相同為0,不同為1)。

int hash = key.hashCode();
// 分兩次右移,并進(jìn)行異或,減少碰撞
hash ^= (hash >>> 16);
  • 為什么右移16位和異或

    • 右移 16 位的操作是為了將高位(通常低位 bits 可能更有可能重復(fù))與低位進(jìn)行混合。這是一種減小碰撞的方式,通過(guò)與高位進(jìn)行異或運(yùn)算,使得哈希值更加復(fù)雜,概率上減少哈希沖突。
    • 右移之后的結(jié)果與原始哈希碼進(jìn)行異或,進(jìn)一步打散哈希值。這樣,無(wú)論哈希碼的某部分和另外一部分如何重復(fù),通過(guò)這一步都會(huì)產(chǎn)生不同的結(jié)果,減少哈希沖突的概率。

再計(jì)算哈希值:

  • 使用鍵的 hashCode() 方法計(jì)算哈希值,然后通過(guò)與當(dāng)前數(shù)組長(zhǎng)度的模運(yùn)算(通常為數(shù)組長(zhǎng)度減去 1)來(lái)找到該鍵應(yīng)存儲(chǔ)在數(shù)組中的桶(bucket)位置。
int index = (hash & (table.length - 1));

查找桶(bucket):

根據(jù)計(jì)算出的索引,從內(nèi)部數(shù)組 table 中獲取相應(yīng)的桶(Node[])。如果桶是空的,直接返回 null,表示所查詢的鍵不存在。

遍歷鏈表或紅黑樹 (處理哈希沖突):

如果桶中有多個(gè)元素(即形成了鏈表或紅黑樹),則需要遍歷鏈表或樹來(lái)查找對(duì)應(yīng)的鍵。

使用 equals() 方法比較每個(gè)節(jié)點(diǎn)的鍵,找到匹配的鍵時(shí)返回對(duì)應(yīng)的值。

返回值:

如果找到了匹配的鍵,返回其對(duì)應(yīng)的值;如果未找到,則返回 null。

以下是get方法的代碼示例圖:

class Node<K, V> {
    final int hash;
    final K key;
    V value;
    Node<K, V> next;

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

public class MyHashMap<K, V> {
    private Node<K, V>[] table; // 存儲(chǔ)節(jié)點(diǎn)的數(shù)組
    private int size;

    public MyHashMap(int initialCapacity) {
        this.table = new Node[initialCapacity];
    }

    public V get(K key) {
        if (key == null) {
            return null; // 處理空鍵
        }
        
        int hash = key.hashCode(); // 計(jì)算哈希值
        int index = (hash & (table.length - 1)); // 計(jì)算索引

        Node<K, V> node = table[index]; // 獲取對(duì)應(yīng)的桶

        // 遍歷鏈表或紅黑樹
        while (node != null) {
            if (node.hash == hash && (node.key.equals(key))) {
                return node.value; // 找到鍵,返回對(duì)應(yīng)的值
            }
            node = node.next; // 移動(dòng)到下一個(gè)節(jié)點(diǎn)
        }

        return null; // 未找到
    }
}

小結(jié):

時(shí)間復(fù)雜度:

  • get 操作的平均時(shí)間復(fù)雜度為 O(1),在理想情況下,若沒有發(fā)生沖突。
  • 在最壞的情況下(所有元素都發(fā)生沖突,組成鏈表或紅黑樹),時(shí)間復(fù)雜度為 O(n) 或 O(log n),分別取決于鏈表或樹的實(shí)現(xiàn)方式。

處理哈希沖突:

  • 若兩個(gè)或多個(gè)鍵的哈希值相同,會(huì)發(fā)生哈希沖突,使用鏈表或紅黑樹來(lái)解決沖突。

5、擴(kuò)容

關(guān)于HashMap的原理圖。

主要用于存儲(chǔ)鍵值對(duì)。其內(nèi)部實(shí)現(xiàn)包含了數(shù)組和鏈表(或紅黑樹)以處理哈希沖突。HashMap 在使用過(guò)程中會(huì)發(fā)生擴(kuò)容,以確保其存儲(chǔ)性能及均勻分布。

下面是 HashMap 擴(kuò)容的詳細(xì)原理。

5.1、擴(kuò)容介紹

HashMap 的擴(kuò)容是指當(dāng) HashMap 的當(dāng)前容量不足以存儲(chǔ)新的鍵值對(duì)時(shí),自動(dòng)調(diào)整其容量和結(jié)構(gòu),以提高其性能。

擴(kuò)容通常會(huì)在 put() 方法中觸發(fā),這個(gè)方法用于將鍵值對(duì)添加到哈希表中。

5.2、擴(kuò)容的觸發(fā)條件

擴(kuò)容會(huì)在以下情況下觸發(fā):

  • 當(dāng) HashMap 中的元素?cái)?shù)量超過(guò)了負(fù)載因子(load factor)與當(dāng)前容量的乘積,即:threshold=capacity×load 。

默認(rèn)為 0.75 的負(fù)載因子。

舉個(gè)例子:

  1. 初始容量: 默認(rèn) HashMap 初始容量為 16。
  2. 負(fù)載因子: 默認(rèn)負(fù)載因子為 0.75。
  3. 閾值: 計(jì)算得出的閾值為 16×0.75=12。

所以,當(dāng)插入的元素?cái)?shù)量達(dá)到 12 時(shí),HashMap 會(huì)觸發(fā)擴(kuò)容。

5.3、擴(kuò)容過(guò)程

當(dāng)需要進(jìn)行擴(kuò)容時(shí),HashMap 的擴(kuò)容過(guò)程如下:

當(dāng)前容量翻倍:

  • 新容量是當(dāng)前容量的 2 倍。

重新計(jì)算哈希:

  • 每個(gè)鍵的哈希值會(huì)基于新的容量重新計(jì)算,并通過(guò)新的桶數(shù)組插入到相應(yīng)的桶中。
  • 為了保持元素的分布處理,HashMap 使用“鏈地址法”或拉鏈法處理哈希沖突。

元素遷移:

  • 在擴(kuò)容過(guò)程中,所有的鍵值對(duì)會(huì)被重新計(jì)算位置并拷貝到新的桶數(shù)組。
  • 這需要遍歷當(dāng)前所有的鏈表或紅黑樹并重新插入到新位置。

示例:

public class HashMapDemo {
    public static void main(String[] args) {
        MyHashMap<String, Integer> map = new MyHashMap<>(4, 0.75f); // 初始化容量為4,負(fù)載因子為0.75
        
       map.put("one", 1); → 當(dāng)前大小為 1(沒有觸發(fā)擴(kuò)容)。
       map.put("two", 2); → 當(dāng)前大小為 2(沒有觸發(fā)擴(kuò)容)。
       map.put("three", 3); → 當(dāng)前大小為 3(這時(shí)已達(dá)到閾值,因此將觸發(fā)擴(kuò)容)。
       map.put("four", 4); → 由于已經(jīng)擴(kuò)容,實(shí)際中會(huì)發(fā)生元素的重新分配,現(xiàn)在當(dāng)前數(shù)組的大小為 4。
       map.put("five", 5); → 當(dāng)前大小為 5 (但是不會(huì)再觸發(fā)擴(kuò)容,因?yàn)閿?shù)組容量已增加到 8)。
        
        // 你可以擴(kuò)展上面代碼來(lái)實(shí)現(xiàn) get 方法等
    }
}

HashMap是先插入還是先擴(kuò)容:HashMap是先插入數(shù)據(jù)再進(jìn)行擴(kuò)容的,但是如果是剛剛初始化容器的時(shí)候是先擴(kuò)容再插入數(shù)據(jù)。

5.4、擴(kuò)容失敗

通常會(huì)導(dǎo)致 OutOfMemoryError 異常,當(dāng)前的操作(例如添加元素)將會(huì)失敗。之后,HashMap 仍然保持不變,已經(jīng)加入的元素也不會(huì)被丟失。

擴(kuò)容屬于原子操作,不會(huì)重試,因?yàn)閿U(kuò)容是同步操作,失敗意味著資源不足,無(wú)法繼續(xù)。

當(dāng)擴(kuò)容成功后的代碼示例如下:

package com.example.test;

import java.lang.reflect.Field;
import java.util.HashMap;

public class HashMapTest {
    public static void main(String[] args) {
        HashMap<String, Object> map = new HashMap<>(4,0.75f);
        map.put("one",1);
        map.put("two",2);
        map.put("three",3);
        map.put("four",4);
        try {
            // 獲取底層數(shù)組長(zhǎng)度
            Field tableField = HashMap.class.getDeclaredField("table");
            tableField.setAccessible(true);
            Object[] table = (Object[]) tableField.get(map);

            System.out.println("鍵值對(duì)數(shù)量(map.size): " + map.size());
            System.out.println("底層數(shù)組長(zhǎng)度: " + (table != null ? table.length : 0));
        } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
        System.out.println("map.size="+map.size());
    }
}

結(jié)果輸出:

鍵值對(duì)數(shù)量(map.size): 4
底層數(shù)組長(zhǎng)度: 8
map.size=4

6、迭代異常

迭代器的remove()可以安全使用,而直接操作Map會(huì)觸發(fā)異常。通過(guò)fast-fail機(jī)制可以有效的進(jìn)行檢測(cè)。

代碼示例如下:

// 正確方式(使用迭代器的remove)
Iterator<String> it = map.keySet().iterator();
while (it.hasNext()) {
    if (it.next().equals("remove")) {
        it.remove();  // 不會(huì)增加modCount
    }
} 這個(gè)是正常的,  Map<String, Integer> map = new HashMap<>();
map.put("A", 1);
map.put("B", 2);
Iterator<String> it = map.keySet().iterator();
while (it.hasNext()) {
    String key = it.next();
    map.put("C", 3);  // 這里會(huì)觸發(fā)fail-fast
} 而這個(gè)會(huì)觸發(fā)fail-fast

6.1、區(qū)別對(duì)比

6.2、源碼解析

1. 迭代器remove實(shí)現(xiàn)

// HashMap.HashIterator.remove()
public final void remove() {
    Node<K,V> p = current;
    if (p == null)
        throw new IllegalStateException();
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    
    current = null;
    K key = p.key;
    removeNode(hash(key), key, null, false, false);
    expectedModCount = modCount; // 關(guān)鍵點(diǎn):同步更新!
}

關(guān)鍵步驟

  • 檢查合法性
  • 實(shí)際刪除節(jié)點(diǎn)
  • 同步更新expectedModCount

2. 直接put操作

// HashMap.put()
public V put(K key, V value) {
    // ...省略其他代碼...
    ++modCount; // 修改計(jì)數(shù)器增加
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

此時(shí)expectedModCount不會(huì)更新,導(dǎo)致下次迭代檢查時(shí):

// HashIterator.nextNode()
final Node<K,V> nextNode() {
    if (modCount != expectedModCount) // 此時(shí)不相等!
        throw new ConcurrentModificationException();
    // ...
}

6.3、結(jié)構(gòu)圖

比較expectedModCount和ModCount值是否相等。通常在hashmap的clear(),remove(),put()方法里面都會(huì)進(jìn)行modcount++;

如下圖所示:

6.4、線程安全

即使單線程環(huán)境,兩種操作也有本質(zhì)區(qū)別:

迭代器remove

  • 操作路徑:迭代器→Map
  • 迭代器全程掌握修改控制權(quán)

直接修改Map

  • 操作路徑:直接訪問Map
  • 迭代器無(wú)法感知外部修改

6.5、開發(fā)建議

1. 安全刪除模式

// 安全刪除示例
List<String> toRemove = new ArrayList<>();
for (Entry<String, Integer> entry : map.entrySet()) {
    if (entry.getValue() < 0) {
        toRemove.add(entry.getKey());
    }
}
toRemove.forEach(map::remove); // 迭代完成后再批量刪除

2. 多線程替代方案

// 使用ConcurrentHashMap
ConcurrentHashMap<String, Integer> safeMap = new ConcurrentHashMap<>();
// 迭代時(shí)修改不會(huì)拋異常
for (String key : safeMap.keySet()) {
    if (key.startsWith("test")) {
        safeMap.remove(key); // 安全操作
    }
}

6.6、底層設(shè)計(jì)

契約式設(shè)計(jì)

  • 迭代器與Map約定:只有通過(guò)迭代器的修改才被認(rèn)可

防御式編程

  • 假設(shè)外部修改都可能破壞一致性
  • 通過(guò)fail-fast快速暴露問題

最小驚訝原則

  • 直接修改集合產(chǎn)生異常符合開發(fā)者直覺
  • 迭代器提供專用修改方法更安全

這種設(shè)計(jì)確保了集合在迭代過(guò)程中的狀態(tài)可控性,雖然限制了靈活性,但大幅提高了代碼的可靠性。理解這一機(jī)制可以幫助開發(fā)者避免常見的并發(fā)修改錯(cuò)誤。

總結(jié)

以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。

相關(guān)文章

最新評(píng)論