HashMap 源码分析(JDK1.7)

HashMap 简介

HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。

HashMap 实现了 Map 接口,根据键的 HashCode 值存储数据,具有很快的访问速度,最多允许一条记录的键为 null,不支持线程同步。

HashMap 是无序的,即不会记录插入的顺序。

HashMap 继承于AbstractMap,实现了 Map、Cloneable、java.io.Serializable 接口。

HashMap 的继承关系

首先看下集合框架图,Map 和 Collection 属于两个不同的结构,这里容易弄混淆。

结构框架图

HashMap 的继承关系

HashMap继承关系.png

HashMap 简单使用

HashMap 的 key 与 value 类型可以相同也可以不同,可以是字符串(String)类型的 key 和 value,也可以是整型(Integer)的 key 和字符串(String)类型的 value。

    HashMap<String, Integer> hhs = new HashMap<>();
    hhs.put("aaa", 1);
    hhs.put("bbb", 2);
    hhs.put("aaa", 1);
    hhs.get("aaa");
    hhs.remove("aaa");

HashMap 的存储结构

在了解 HashMap 源码之前,先来看下 HashMap 的存储结构图。

HashMap存储结构.png

可以看到左方为数组存储方式,数组的方式特点是查询快 [o(1)] ,但是增加和删除比较慢 [o(n)] 。右方为链表的数据结构,链表数据结构查询慢 [o(n)] ,但是增加和删除 [(o(1))] 比较快。而 HashMap 采用两种方式的结合。相当于折中方案,提高效率。

HashMap 的基本原理

1、首先判断Key是否为 Null,如果为 null,直接查找 Enrty[0],如果不是 Null,先计算 Key 的 HashCode,然后经过二次 Hash。得到 Hash 值,这里的 Hash 特征值是一个 int 值。

2、根据Hash值,找到对应的数组,所以对 Entry[] 的长度 length 求余,得到的就是 Entry 数组的 index。

3、找到对应的数组,就是找到了所在的链表,然后按照链表的操作对 Value 进行插入、删除和查询操作。

HashMap 的构造方法

首先来看 HashMap 的构造函数

public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, 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;
    threshold = initialCapacity;
    init();
}

构造函数中初始化了一个 loadFactor 变量,默认值 DEFAULT_LOAD_FACTOR 为 0.75。 threshold 变量默认值为 16(数 1 左移四位得到) 表示为 Entry[] 数组的临界长度。 loadFactor 和 threshold 决定是否要对 Entry[] 的容量进行调整。 threshold 由 HashMap 数组长度和 loadFactor 相乘计算得出,例如数组长度为 16 ,loadFactor 为 0.75,则 threshold 应该为 12 。即当 HashMap 中的容量数据大于等于 12 的时候,则应该对 table 数组进行扩容处理。 此时初始化 threshold 默认为数组长度 16(后续操作会对其更改)。 init() 为空调用方法。

可以看到构造函数只是初始化了一些变量。

接下来 HashMap 的 put 操作则是 HashMap 原理的主要研究点。

public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
     // table 数组为空则根据 threshold 初始化数组,即为容量为 16 的数组,初始化完成后同时 threshold 更改为 数组长度 length * loadFactor
        inflateTable(threshold);
    }
    if (key == null) // key 为空 
        return putForNullKey(value);
    int hash = hash(key); // 对key 进行 hash 处理
    int i = indexFor(hash, table.length); // 通过 hash 值拿到 key 对应数组的位置
    for (Entry<K,V> e = table[i]; e != null; e = e.next) { // 对应 Entry
        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;
}

这里可以看到,table[] 数组中存入的为 Entry,默认初始容量为 16 。根据上述数据和 Entry 结构图知道 Entry 数据保存了数据的 key 、value、hash 、i(数组下标)的值。

传入 key 为 null 的时候,会将数据 Entry 数据存入 table[0] 的位置;

private V putForNullKey(V value) {
    for (Entry<K,V> e = table[0]; e != null; e = e.next) { 
        if (e.key == null) {// 替换 key 为 null 的value 
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(0, null, value, 0);// 插入数据
    return null;
}

HashMap put方法的 hash 处理

如果key 不为空,则对 key 进行 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);
}

针对 key 获取 hash 值,这里会再次进行一系列的逻辑操作, 这里注释也对其进行了说明,对 hash 值进行二次操作是为了不同的 key 算出来的 hash 值尽可能不一样,避免出现 hash 冲突的情况

接下来,拿到二次处理过的 hash 值生成对应 table 的下标 i;

static int indexFor(int h, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return h & (length-1);
}

length 为 table 数组的长度, h & (length-1) 实际为对数组长度取模运算(h % table.length()),例如,现在数组长度为 16,算出来的 hash 值为 15,与运算之后即为 15。这里其实 length - 1 后对相应低位二进制全部改变,而再和 hash 值的低位进行 & 运算,对应拿到的就是 hash 对应低位的值。即为对 length 的取模。

而这里对二次处理的 hash 值进行取模运算,是为了数据能够尽可能均匀分布在 table 数组中。提高 table 的使用效率。

到这里,已经找到可以找到 key 值对应在 table 数组的位置,前面的处理总结下有两个目的:

  1. 二次 hash 处理,避免出现 hash 碰撞的情况。
  2. 对数组长度取模处理,使数据均匀分布在数组中。

拿到了数组 table 的对应下标 index,取出对应的 Entry 数据。针对 Entey 链表进行遍历,如果查找到对应的 key 则进行替换,否则,调用 addEntry 插入数据。

接下来再看看 addEntry 方法;

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) { // 当 size 达到 HashMap 临界值了,调整 table 的长度
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

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

先看 createEntry 方法去创建数据;

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++;
}

将对应数据放在 table 数组引用的节点处,假如原来 bucketIndex 处有相应的数据,存入的数据的下一个数据指向的即时 e (原存储数据,即在第一位插入数据);

针对上述操作,下图图示展示:

HashMap_put_01.png

现在 HashMap 的存储的数据如上图所示,假设现在有个(k9,v9)的值需要插入到 HashMap 中。

当对 k9 进行 hash 等一系列运算之后,发现此时数据的下标为 1,即和 (k3,v3)、(k8,v8)的处理下标一致,进行如下存储;

HashMap_put_02.png

此时再次存储 (k3,v10)的数据,k3 通过 hash 之后算出来的下标肯定也是在 table[1] 处,替换进行如下存储;

HashMap_put_03.png

最后再插入一条(k10,v10)的数据,算出下标为 5 ,如下进行存储;

HashMap_put_04.png

HashMap 的扩容处理

当 HashMap 里面存储的数据到了一定的临界值的时候,应当对 table 进行扩容处理;

刚刚上述 addEntry 方法中提到扩容处理情况;

    if ((size >= threshold) && (null != table[bucketIndex])) { // 当 size 达到 HashMap 临界值了,调整 table 的长度
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

可以看到当 size 大于等于 threshold 临界值的时候,且当前的数据不为空的情况下,我们就要进行 table 扩容了。threshold 我们知道由 loadFactor(加载因子)和 table 的容量共同决定,loadFactor 为大于 0 小于 1 的值,即此时已存数据 size 到达 table 容量的 loadFactor 倍数的时候,就必须进行扩容了。那么如何扩容的呢?

void resize(int newCapacity) {
    Entry[] oldTable = table;// 旧的table
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }

    Entry[] newTable = new Entry[newCapacity];// 新table 容量翻倍
    transfer(newTable, initHashSeedAsNeeded(newCapacity));// 数据的转移
    table = newTable;// newtable 赋值给 table
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); // 重新计算临界值
}

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

看到扩容处理不仅仅把 table 容量增长两倍就完事了,原 table 中的数据该怎么存呢?

如果不进行更换的话,假如对上图所示数据进行(k9,v9)的数据查找,因为 table 扩容后,二次 hash 结果和 table.length 取模后获取的下标数据大概率是变化了,那样就找不到数据了。

于是 transfer 方法拿到旧的 table 数据,然后根据 newTable 的容量对原数据所有的 key 重新计算对应下标,存入到新的 table 中,这样保证了后续查找的正确性。

到这里,HashMap put 相关的分析已经完成,其中涉及到对数组和对应链表数据的插入。以及相应的数组进行扩容方案。

HashMap 的 其他方法

分析完成了 put 方法之后,再看其他方法就会很容易。

get 方法:

public V get(Object key) {
    if (key == null)
        return getForNullKey();
    Entry<K,V> entry = getEntry(key);

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

final Entry<K,V> getEntry(Object key) {
    if (size == 0) {
        return null;
    }

    int hash = (key == null) ? 0 : hash(key);// hash 处理
    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;
    }
    return null;
}

get 方法需要传入相应的 key ,如果为空 key ,肯定就是去找 table 数组第 0 位的数据。否则,key 的 hash 值获取以及二次处理,找到对应的数组下标,比对对应 Entry 一致就返回,否则就返回 null。

remove 方法:

public V remove(Object key) {
    Entry<K,V> e = removeEntryForKey(key);
    return (e == null ? null : e.value);
}

final Entry<K,V> removeEntryForKey(Object key) {
    if (size == 0) {
        return null;
    }
    int hash = (key == null) ? 0 : hash(key);
    int i = indexFor(hash, table.length);
    Entry<K,V> prev = table[i];
    Entry<K,V> e = prev;

    while (e != null) {
        Entry<K,V> next = e.next;
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k)))) {
            modCount++;
            size--;
            if (prev == e) // 删除的是链表第一个数据
                table[i] = next;
            else
                prev.next = next;
            e.recordRemoval(this);
            return e;
        }
        prev = e;
        e = next;
    }

    return e;
}

remove 方法相对 get 方法,在拿到相应数据删除之后,同时要将链表对应的数据重新进行指向。即将当前要删除数据的前一项指向后一项。

HashMap 的遍历方法

// 方式1
for(Map.Entry<String, Integer> entry : hhs.entrySet()){
    System.out.println("key = " + entry.getKey() + ", value = " + entry.getValue());
}

//方式2
Iterator<Map.Entry<String, Integer>> entries = hhs.entrySet().iterator();
while (entries.hasNext()) {
    Map.Entry<String, Integer> entry = entries.next();
    System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
}

由上述 HashMap 的操作可以看到 HashMap 中存储数据是无序的,所以遍历得到的数据也将会是无序的;通常 HashMap 使用到的两种遍历方式(其实两种方法最终都会走向方式 2 ),entrySet 和 entrySet 的迭代器 iterator;

第一种遍历方式;

private transient Set<Map.Entry<K,V>> entrySet = null;

public Set<Map.Entry<K,V>> entrySet() {
    return entrySet0();
}

private Set<Map.Entry<K,V>> entrySet0() {
    Set<Map.Entry<K,V>> es = entrySet;
    return es != null ? es : (entrySet = new EntrySet());
}

这里 entrySet 存储 Entry 的集合,在之前 put 方法中,并没有发现往其中存入数据。所以,这里去实例化 EntrySet 方法中查看是否有相关获取数据的方法?

private final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
    public Iterator<Map.Entry<K,V>> iterator() {
        return newEntryIterator();
    }
    public boolean contains(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<K,V> e = (Map.Entry<K,V>) o;
        Entry<K,V> candidate = getEntry(e.getKey());
        return candidate != null && candidate.equals(e);
    }
    public boolean remove(Object o) {
        return removeMapping(o) != null;
    }
    public int size() {
        return size;
    }
    public void clear() {
        HashMap.this.clear();
    }
}

EntrySet 的实例化又去创建了一个 EntryIterator;

Iterator<Map.Entry<K,V>> newEntryIterator()   {
    return new EntryIterator();
}

private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {
    public Map.Entry<K,V> next() {
        return nextEntry();
    }
}

EntryIterator 实则就是一个 HashIterator,到这里可以发现,似乎走到了第二种方式了,继续看 HashIterator;

private abstract class HashIterator<E> implements Iterator<E> {
    Entry<K,V> next;        // next entry to return
    int expectedModCount;   // For fast-fail
    int index;              // current slot
    Entry<K,V> current;     // current entry

    HashIterator() {
        expectedModCount = modCount;
        if (size > 0) { // advance to first entry
            Entry[] t = table;
            while (index < t.length && (next = t[index++]) == null) // 自增数组下标找到第一个不为null结束循环 即为HashMap要遍历的第一个值
                ;
        }
    }

    public final boolean hasNext() {
        return next != null;
    }

    final Entry<K,V> nextEntry() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        Entry<K,V> e = next;
        if (e == null)
            throw new NoSuchElementException();

        if ((next = e.next) == null) {// index 处链表数据没有下一位了
            Entry[] t = table;
            while (index < t.length && (next = t[index++]) == null)
                ;
        }
        current = e;
        return e;
    }

    public void remove() {
        if (current == null)
            throw new IllegalStateException();
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        Object k = current.key;
        current = null;
        HashMap.this.removeEntryForKey(k);
        expectedModCount = modCount;
    }
}

HashIterator 实现了 Iterator,而增强 for 循环使用的就是 Iterator ,所以这里 Entry 覆盖了 Iterator 方法进行遍历数据。

HashIterator 构造方法回去遍历数组找到第一个不为 null 的数据结束循环,此数据作为遍历 HashMap 的起始数据;

这里主要看下 nextEntry 方法,因为 EntryIterator 的 next 方法其实调用的就是 nextEntry 方法;

可以看到 nextEntry 会将数据按照 table 的下标顺序进行查找,如果某个下标对应的链表数据查找完毕,继续向下遍历 table 数组。直到全部查找完毕。

HashMap_put_04.png

以上图 HashMap 数据来看,最终遍历出数据结果应该是(null,vaule)、(k9,v9)、(k3,v8)、(k8,v8)、(k10,v10),当然,存储顺序大概率不是按照这个顺序来的。所以我们说 HashMap 是无序的。

最后,可以看到 HashMap 也不支持多线程的操作 ,因为对 HashMap 的操作每次都会改变 modCount 值,而遍历的过程会比对相关值,所以遍历过程也不可对 HashMap 进行增删操作。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,165评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,503评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,295评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,589评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,439评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,342评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,749评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,397评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,700评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,740评论 2 313
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,523评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,364评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,755评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,024评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,297评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,721评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,918评论 2 336

推荐阅读更多精彩内容