从HashMap的常用方法来解析HashMap的内部实现(1)

HashMap 在我们的项目开发当中会经常用到,今天我们通过它常用方法的源码来解析它的内部实现机制

构造方法

HashMap hashMap = new HashMap();

private static final int MINIMUM_CAPACITY = 4;

transient HashMapEntry<K, V>[] table;

private static final Entry[] EMPTY_TABLE
            = new HashMapEntry[MINIMUM_CAPACITY >>> 1];

public HashMap() {
      table = (HashMapEntry<K, V>[]) EMPTY_TABLE;
      threshold = -1; // Forces first put invocation to replace EMPTY_TABLE
}

从构造方法可以看出它内部是一个HashMapEntry<K, V>对象的数组,(MINIMUM_CAPACITY >>> 1等于2),数组的初始化大小是2

从上面可以得出结论:HashMap内部是以一个HashMapEntry<K, V>对象的数组来存储数据的,并且数组的初始化大小是2。

HashMapEntry<K, V>是什么?

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

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

        public final K getKey() {
            return key;
        }

        public final V getValue() {
            return value;
        }

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

        @Override public final boolean equals(Object o) {
            if (!(o instanceof Entry)) {
                return false;
            }
            Entry<?, ?> e = (Entry<?, ?>) o;
            return Objects.equal(e.getKey(), key)
                    && Objects.equal(e.getValue(), value);
        }

        @Override public final int hashCode() {
            return (key == null ? 0 : key.hashCode()) ^
                    (value == null ? 0 : value.hashCode());
        }

        @Override public final String toString() {
            return key + "=" + value;
        }
    }

它实现了Entry接口,我们put的key,value就是存在这个对象当中,下面我们来看看它的put方法

put方法
  @Override 
  public V put(K key, V value) {
        if (key == null) {
            return putValueForNullKey(value);
        }

        int hash = Collections.secondaryHash(key);
        HashMapEntry<K, V>[] tab = table;
        int index = hash & (tab.length - 1);
        for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
            if (e.hash == hash && key.equals(e.key)) {
                preModify(e);
                V oldValue = e.value;
                e.value = value;
                return oldValue;
            }
        }

        // No entry for (non-null) key is present; create one
        modCount++;
        if (size++ > threshold) {
            tab = doubleCapacity();
            index = hash & (tab.length - 1);
        }
        addNewEntry(key, value, hash, index);
        return null;
    }

第一行代码就是判断如果key为Null 调用putValueForNullKey 方法

private V putValueForNullKey(V value) {
      HashMapEntry<K, V> entry = entryForNullKey;
      if (entry == null) {
            addNewEntryForNullKey(value);
            size++;
            modCount++;
            return null;
      } else {
            preModify(entry);
            V oldValue = entry.value;
            entry.value = value;
            return oldValue;
      }
  }

 void addNewEntryForNullKey(V value) {
        entryForNullKey = new HashMapEntry<K, V>(null, value, 0, null);
  }

首先把entryForNullKey赋值给entry, 如果entry为空,就创建一个HashMapEntry<K, V>对象,如果entry不为空首先执行preModify方法,这个方法是个空方法,没有具体的实现,然后下面就是把新添加的value值赋值给entryForNullKey对象的value

从上面可以得出结论:HashMap可以存放key为null的数据,但是只可以有一条,再添加key为null的数据会覆盖之前的数据。

如果key不为Null,继续往下看

int hash = Collections.secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
int index = hash & (tab.length - 1);

首先通过key计算出hash值,然后再计算出index,index就是数组的下标,我们先看下最后面的添加方法

void addNewEntry(K key, V value, int hash, int index) {
      table[index] = new HashMapEntry<K, V>(key, value, hash, table[index]);
}

因为不同的key计算出来的下标index有可能是相同的,也就是说我第一次put计算出来的index和第二次put计算出来的index是有可能相同的,遇到这种情况怎么处理呢?我们来看看HashMapEntry构造方法

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

首先实例化一个HashMapEntry对象,key,value分别赋值,如果现在要存储数据的index下标已经有数据了,就会把旧数据放到我现在新数据的next 属性上,然后再把新数据放到数组的index下标上,也就是说如果遇到了index相同的情况,它会以单链表的形式来存储数据,最先添加的数据会放在链表的最后面,最晚添加的数据会在链表的header位置;看完添加数据方法我们再返回来看它的其他代码逻辑。

for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
          if (e.hash == hash && key.equals(e.key)) {
              preModify(e);
              V oldValue = e.value;
              e.value = value;
              return oldValue;
          }
  }

这个循环是:如果我计算出的index下标已经存有数据,然后得到这个对象,如果key和hash值都相同,就说明添加了key值重复的数据,会把相同key值对象的value给覆盖掉;也就是说HashMap的key是唯一的,如果key值相同以最晚添加的数据为准。

我们知道数组的大小在创建的时候就固定了,HashMap用数组存储数据,为什么它可以无限制的添加数据呢?我们通过下面代码来分析

 modCount++;
 if (size++ > threshold) {
       tab = doubleCapacity();
       index = hash & (tab.length - 1);
  }
 addNewEntry(key, value, hash, index);

如果size++ > threshold, size是添加数据的大小,threshold表示数组总容量的3/4,也就是说如果添加数据的大小大于数组总容量的3/4会执行下面的代码,我们来看看doubleCapacity方法

    private HashMapEntry<K, V>[] doubleCapacity() {
        HashMapEntry<K, V>[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            return oldTable;
        }
        int newCapacity = oldCapacity * 2;
        HashMapEntry<K, V>[] newTable = makeTable(newCapacity);
        if (size == 0) {
            return newTable;
        }

        for (int j = 0; j < oldCapacity; j++) {
            /*
             * Rehash the bucket using the minimum number of field writes.
             * This is the most subtle and delicate code in the class.
             */
            HashMapEntry<K, V> e = oldTable[j];
            if (e == null) {
                continue;
            }
            int highBit = e.hash & oldCapacity;
            HashMapEntry<K, V> broken = null;
            newTable[j | highBit] = e;
            for (HashMapEntry<K, V> n = e.next; n != null; e = n, n = n.next) {
                int nextHighBit = n.hash & oldCapacity;
                if (nextHighBit != highBit) {
                    if (broken == null)
                        newTable[j | nextHighBit] = n;
                    else
                        broken.next = n;
                    broken = e;
                    highBit = nextHighBit;
                }
            }
            if (broken != null)
                broken.next = null;
        }
        return newTable;
    }

先从方面的第一行开始看

HashMapEntry<K, V>[] oldTable = table;
      int oldCapacity = oldTable.length;
      if (oldCapacity == MAXIMUM_CAPACITY) {
         return oldTable;
}

如果当前数组的长度大于最大值MAXIMUM_CAPACITY,就返回当前的数组,
(MAXIMUM_CAPACITY == 1 << 30 == 1073741824),这是HashMap的最大容量,继续往下看

 int newCapacity = oldCapacity * 2;
 HashMapEntry<K, V>[] newTable = makeTable(newCapacity);
 if (size == 0) {
    return newTable;
 }

首先把原来数组长度乘以2,传给makeTable方法

 private HashMapEntry<K, V>[] makeTable(int newCapacity) {
        @SuppressWarnings("unchecked") HashMapEntry<K, V>[] newTable
                = (HashMapEntry<K, V>[]) new HashMapEntry[newCapacity];
        table = newTable;
        threshold = (newCapacity >> 1) + (newCapacity >> 2); // 3/4 capacity
        return newTable;
}

首先new一个newCapacity大小的数组替换原来的数组,然后设置threshold 值,这行代码的后面有注释: 3/4 capacity,大家可以自己计算计算,方法完成以后,如果size为0就直接返回这个新数组,不为0就往下走

     for (int j = 0; j < oldCapacity; j++) {
            /*
             * Rehash the bucket using the minimum number of field writes.
             * This is the most subtle and delicate code in the class.
             */
            HashMapEntry<K, V> e = oldTable[j];
            if (e == null) {
                continue;
            }
            int highBit = e.hash & oldCapacity;
            HashMapEntry<K, V> broken = null;
            newTable[j | highBit] = e;
            for (HashMapEntry<K, V> n = e.next; n != null; e = n, n = n.next) {
                int nextHighBit = n.hash & oldCapacity;
                if (nextHighBit != highBit) {
                    if (broken == null)
                        newTable[j | nextHighBit] = n;
                    else
                        broken.next = n;
                    broken = e;
                    highBit = nextHighBit;
                }
            }
            if (broken != null)
                broken.next = null;
        }

上面代码就是把旧数组中的数据移动到了新的数组当中

 if (nextHighBit != highBit) {
          if (broken == null)
                newTable[j | nextHighBit] = n;
             else
                broken.next = n;
          broken = e;
          highBit = nextHighBit;
}

这段代码看不太懂,有大神懂的话麻烦告诉一下,谢谢了。
下面就是获得新数组,算出index位置,然后添加进去;到此put方法就解析完了。

总结

从put方法可以看出HashMap内部维护的是一个HashMapEntry对象的数组,这个对象包括我们添加的key,value,hash,next等属性,put方法通过key得到hash值。然后通过hash值来计算存放在数组中的index下标,如果index相同,就会以一个单向链表的形式存放数据,最先添加的数据放在链接的最尾部;因为数组创建的时候是要固定大小的,所有在添加数据的同时,添加的数据量大于数组总量的3/4时,就会重新创建一个原来数组大小2位的新数组来存储数据。

HashMap其他方法分析:http://www.jianshu.com/p/336b1b45d140

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

推荐阅读更多精彩内容

  • 实际上,HashSet 和 HashMap 之间有很多相似之处,对于 HashSet 而言,系统采用 Hash 算...
    曹振华阅读 2,507评论 1 37
  • HashMap 是 Java 面试必考的知识点,面试官从这个小知识点就可以了解我们对 Java 基础的掌握程度。网...
    野狗子嗷嗷嗷阅读 6,641评论 9 107
  • 上篇文章(http://www.jianshu.com/p/a122c79ee60c)我们分析了HashMap的构...
    伪文艺大叔阅读 547评论 0 1
  • 宝贝儿,今天是你出生的第五十一天,已经七周啦,很快你就两个月了,时间就是这样过去的,生活有了你之后变化很大,但是也...
    3237f5d4b965阅读 81评论 0 0
  • 人生道路很长 我只顾一味的向前走 不知何时 猛然回首 突然发现有些人的名字都早已忘却 有些人的名字,虽记得, 却早...
    skevon阅读 217评论 0 0