HashMap源码分析

HashMap具有以下特点:

  1. Hashmap是基于Map的非同步实现,如果多线程修改,必须在外部保持同步
  2. 允许使用null值和null键
  3. 不保证映射顺序
  4. Hashmap实际上是一个链表散列的数据结构,即数组和链表的结合体
  5. Hashmap底层是一个数组,数组中每一项又是一个链表
  6. 默认容量 = 4(必须是2的n次方), 默认加载因子 = 0.75
  7. 只有对于HashMap中的Entry有新增或删除的操作,都会更新modCount

假定Hash函数将元素适当的分布在各桶之间,可为基本操作getput提供稳定的性能,迭代collection视图所需的时间与HashMap实例的容量(桶的数量)及其大小(键值映射关系数)成比例,所以如果迭代性能很重要,则不要将出示容量设置的太高或将加载因子设置的太低

成员变量

    //默认初始容量为4
    staic final int DEFAULT_INITIAL_CAPACITY = 4;

    static final int MAXIMUM_CAPACITY = 1 << 30;

    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    static final HashMapEntry<?,?>[] EMPTY_TABLE = {};

    transient HashMapEntry<K,V>[] table = (HashMapEntry<K,V>[]) EMPTY_TABLE;

    transient int size;

    int threshold;

    final float loadFactor = DEFAULT_LOAD_FACTOR;

    transient int modCount;

构造函数

    public HashMap() {
        //默认容量是4, 加载因子是0.75
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

    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;
        } else if (initialCapacity < DEFAULT_INITIAL_CAPACITY) {
            initialCapacity = DEFAULT_INITIAL_CAPACITY;
        }

        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                    loadFactor);

        threshold = initialCapacity;
        init();
    }

一般情况下,我们使用HashMap时都会调用空参构造函数,可以看到,空参构造函数的初始容量默认为4

方法

1. put

    public V put(K key, V value) {
        //如果table是空,重新初始化table,容量是threshold,如果是空构造函数,threshold就是4
        if (table == EMPTY_TABLE) {
            //【1.1】
            inflateTable(threshold);
        }
        //如果key是null, 更新相应的Entry, key = null, 则Entry放在table[0]
        if (key == null)
            //【1.2】
            return putForNullKey(value);

        //计算hash值, 【1.3】
        int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
        //获取hash值对应的index位置, 【1.4】
        int i = indexFor(hash, table.length);
        //如果对应的index位置已经有链表,证明hash冲突,遍历链表
        for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //如果链表中某一个entry的key值与传入的key值相等,更新entry对应的value值
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        //如果没有hash冲突或者在hash冲突的链表中没有相等的key值,更新modCount
        modCount++;
        //添加新的Entry, 【1.5】
        addEntry(hash, key, value, i);
        return null;
    }
  1. key = null时,对应的Entry放置在索引0处
  2. 如果key值存在了,新的value会替换旧的value,put方法会返回旧value
  3. 计算对应的索引值的方法是, 获取key值hash值,然后用hash & table.length - 1, table.length总是2的n次方

对于任意给定对象,只要其hashCode()返回值相同,那么HashMap计算出来的hash值也总相同

1.1 inflateTable

    private void inflateTable(int toSize) {
        // 保证HashMap的容量是2的n次方
        int capacity = roundUpToPowerOf2(toSize);

        //计算threshold值
        float thresholdFloat = capacity * loadFactor;
        if (thresholdFloat > MAXIMUM_CAPACITY + 1) {
            thresholdFloat = MAXIMUM_CAPACITY + 1;
        }

        threshold = (int) thresholdFloat;
        table = new HashMapEntry[capacity];
    }
    
    private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        int rounded = number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (rounded = Integer.highestOneBit(number)) != 0
                //Integer.bitCount返回number的二进制补码中1的总数量
                ? (Integer.bitCount(number) > 1) ? rounded << 1 : rounded
                : 1;

        return rounded;
    }

HashMap的容量会强制性的圆整到2的n次方,即使在初始化时传入的初始容量不是2的n次方,这样做是为了减少hash碰撞率从而提升空间利用效率

1.2 Hashing.singleWordWangJenkinsHash

    public static int singleWordWangJenkinsHash(Object k) {
        int h = k.hashCode();

        h += (h <<  15) ^ 0xffffcd7d;
        h ^= (h >>> 10); //无符号右移6位
        h += (h <<   3);
        h ^= (h >>>  6);//无符号右移6位,空位以0补齐, 带符号右移是根据最左边的符号位来确定空位补什么,如果符号位是1,则空位补1,符号位是0,空位补0
        h += (h <<   2) + (h << 14);
        return h ^ (h >>> 16);
    }

1.3 putForNullKey

    private V putForNullKey(V value) {
        //如果null已经有对应的value,则替换旧value为新value
        for (HashMapEntry<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;
    }

null可以做键值,但是null所对应的value每次只能存一个,如果已存在以null为键值的Entry, 会有新的value替换旧value

1.4 indexFor

    
    static int indexFor(int h, int length) {
        //length总是2的n次方时, h & (length - 1)等价于对length取模,h % length
        return h & (length-1);
    }

由于pow(2, n)是2的n次方,所以pow(2,n) - 1的二进制必然每一位都是1,这样用hash值同pow(2,n)-1进行位与操作,得到的值得低位与hash值得低位一致

1.5 addEntry

    void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            //如果key-value键值对数量达到threshold且对应index处有Entry即存在链表, 扩容
            resize(2 * table.length);
            //计算hash值,null键值的hash是0
            hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        //创建新的Entry,并放入对应的位置
        createEntry(hash, key, value, bucketIndex);
    }
    
        void createEntry(int hash, K key, V value, int bucketIndex) {
        HashMapEntry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);
        size++;
    }
  • 如果key-value键值对的总和达到threshold且对应的索引处存在链表,进行扩容
  • 创建新的Entry,并放入table

2. get

    public V get(Object key) {
        //如果key为null, 获取null对应的value
        if (key == null)
            //【2.1】
            return getForNullKey();
        //【2.2】
        Entry<K,V> entry = getEntry(key);

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

2.1 getForNullKey

    private V getForNullKey() {
        if (size == 0) {
            return null;
        }
        for (HashMapEntry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }

2.2 getEntry


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

        int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
        for (HashMapEntry<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;
    }

这里吐槽一下:曾经遇到一个面试官问我如果哈希冲突的时候HashMap是通过什么方式获取value的,我很确定的说会遍历链表,然后判断entry的哈希值是否跟key值计算出来的hash值相等同时会判断entry的key值和传入的key是否会相等,然后那个面试官告诉我不对!!!弄得我一脸懵逼,我说就是这样的啊,结果他可能觉得我知错不改,脸色不太好的告诉我回去再好好看看。。。好吧,我现在再一次好好看了,依然不知道我哪里说错了,也许这位大哥可能有自己独特的理解

3. remove

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

        while (e != null) {
            HashMapEntry<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;
    }

删除元素其实也是首先计算has值,之后会根据hash值回去元素的索引值,之后遍历索引位置处的链表,之后再进行entry的比对,如果想等,就会删除这个元素,这里要注意的是之前提到过的一旦HashMap中的元素发生删除或增加的改变时,会增加modCount

4. 遍历HashMap

一般遍历HashMap都是通过迭代器来遍历:


    Iterator iter = map.entrySet().iterator();
 while (iter.hasNext()) {
       Map.Entry entry = (Map.Entry) iter.next();
      Object key = entry.getKey();
      Object val = entry.getValue();
  }

用迭代器来遍历Hashmap时,不可以调用put或remove这样会更改HashMap中Entry数量的操作,如果调用了,会抛出ConcurrentModificationException,这是因为fast-fail机制,至于具体原因,从下面的代码分析中可以知道

4.1 entrySet

    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()以后会返回一个EntrySet对象

4.2 EntrySet.iterator

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

EntryIterator继承自HashIterator, EntryIterator.next实际调用的是其父类HashIterator.nextEntry()

4.3 HashIterator构造函数

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

        HashIterator() {
            expectedModCount = modCount;
            if (size > 0) { // advance to first entry
                HashMapEntry[] t = table;
                while (index < t.length && (next = t[index++]) == null)
                    ;
            }
        }
        ...
        
   }

HashIterator中有一个很重要的变量expectedModCount, 可以看到在构造函数中将它的值摄为了HashMap.modCount,这就意味着HashIterator对象一旦创建,expectedModCount值就是固定的,如果在用迭代器遍历HashMap时进行putremove,那么modCount值必然会和expectedModCount不相等,一旦不相等,就会抛出ConcurrentModificationException

4.4 HashIterator.nextEntry

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

          HashMapEntry<K,V> e = next;
          if (e == null)
              throw new NoSuchElementException();

          if ((next = e.next) == null) {
              HashMapEntry[] t = table;
              while (index < t.length && (next = t[index++]) == null)
                    ;
          }
          current = e;
          return e;
    }

可以看到,正如之前所说,当expectedModCount != modCount时,会抛出ConcurrentModificationException, 那如果想在遍历的时候也可以进行删除操作,应该通过什么方法呢?答案是通过Iterator.remove

4.5 HashIterator.remove

    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是HashMap的内部类,所以可以调用HashMap的内部方法removeEntryForKey,当删除完entry后,会更新expectedModCount,这样在进行下一次的next()调用时, expectedModCount就会与modCount保持一致

5. 遍历key和遍历value

如果通过迭代器遍历key和遍历value时的注意事项跟用迭代器遍历HashMap的注意事项一样,也是不可以在遍历时调用remove方法,原理也是一样的,内部有一个expectedModCount,如果要删除要掉用迭代器的remove方法

6. Java8新特性

在Java8中新增了forEach方法,接收一个BiConsumere<? super K, ? super V>参数,这个参数是一个函数式接口,所谓函数式接口是指接口只有一个待实现的方法(Java8中,接口可以有默认的实现), 函数式接口可以用lambda表达式来代替, BiConsumer<K, V>lambda表达式原型是(K, V) - Void, 即传入K,V, 返回Void

参考

HashMap的实现原理

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

推荐阅读更多精彩内容