集合包系列七 —— HashMap

本系列文章所描述的所有类或接口都是基于 JDK 1.7的源码,在其它 JDK 的实现方式中可能会有所不同。

一、实现方式

HashMap 是 Map 实现中最常用的,具体实现方式如下。

二、创建 HashMap

将 loadFactor 设为默认的 0.75,threshold 设置为 16。HashMap 还提供了另外三个构造函数:HashMap(int initialCapacity)、HashMap(int initialCapacity, float loadFactor)、HashMap(Map<? extends K, ? extends V> m)。

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    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();
    }

    void init() {
    }

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

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

三、添加元素 put(K key, V value)

首先判断 Entry<K,V>[] 数组是否为空数组,如果是空数组,则初始化数组。在初始化数组的过程中首先计算 capacity 的值和 threshold 的值,注意:这里才是真正初始化 Entry 数组的大小。创建的 Entry 对象数组的大小,并非传入的初始容量值,而是采用如下方式来决定:

        // Find a power of 2 >= toSize
        int capacity = roundUpToPowerOf2(toSize);

    private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }

比如默认传进去的 toSize=16,则计算出来的 capacity=16,其中 Integer.highestOneBit 方法是计算最高的1位,其余位设置为0。如果我们在构造函数中指定初始化容量为5,那么计算出来的 capacity=8。如果我们在构造函数中指定初始化容量为10,那么计算出来的 capacity=16。这也是如果我们在初始化时要指定 HashMap 的容量时设置的值最好设置为2的指数方的值的原因。默认情况下创建的 Entry 对象数组的大小为 16,threshold 为 12。
  对于 key 为 null 的情况,HashMap 的做法为获取 Entry 数组中的第一个 Entry 对象,并基于 Entry 对象的 next 属性遍历。当找到了其中 Entry 对象的 key 属性为 null 时,则将其 value 赋值为新的 value,然后返回。如没有 key 属性为 null 的 Entry,则增加一个 Entry 对象,增加时为先获取当前数组的第一个 Entry 对象:e,并创建 Entry 对象,key 为 null,value 为新传入的对象,next 为之前获取的 e,如此时 Entry 数组中已有的大小 ≥ threshold,则将 Entry 数组扩大为当前大小的两倍,扩大时对当前 Entry 对象数组中的元素重新 hash,并填充数组,最后重新设置 threshold 值。
  对于 key 不为 null 的情况,首先获取 key 对象本身的 hashCode,然后再对此 hashCode 做 hash 操作。
  hash 完毕后,将 hash 出来的值与 Entry 对象数组的大小减 1 的值进行按位与操作,从而得出当前 key 要存储到数组的位置。从这个过程可以看出,可能会出现不同的 key 找到相同的存储位置的问题,也就是数据结构中经典的 hash 冲突的问题了,来看看 HashMap 的解决方法。
  在找到要存储的目标数组的位置后,获取该数组对应的 Entry 对象,通过调用 Entry 对象的 next 来进行遍历,寻找 hash 值和计算出来的 hash 值相等,且 key 相等或 equals 的 Entry 对象,如寻找到,则替换此 Entry 对象的值,完成 put 操作,并返回旧的值;如未找到,则往对应的数组位置增加新的 Entry 对象,增加时采取的方法和 key 为 null 的情况基本相同,只是它是替换指定位置的 Entry 对象,可以看出,HashMap 解决冲突采用的是链表的方式,而不是开放地址的方法。

    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            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;
    }

    private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize
        int capacity = roundUpToPowerOf2(toSize);

        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }

    private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }

    final boolean initHashSeedAsNeeded(int capacity) {
        boolean currentAltHashing = hashSeed != 0;
        boolean useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean switching = currentAltHashing ^ useAltHashing;
        if (switching) {
            hashSeed = useAltHashing
                ? sun.misc.Hashing.randomHashSeed(this)
                : 0;
        }
        return switching;
    }

    private V putForNullKey(V value) {
        for (Entry<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;
    }

    void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

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

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

        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        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;
            }
        }
    }

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

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

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

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

        public final K getKey() {
            return key;
        }

        public final V getValue() {
            return value;
        }

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

        public final boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry e = (Map.Entry)o;
            Object k1 = getKey();
            Object k2 = e.getKey();
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                Object v1 = getValue();
                Object v2 = e.getValue();
                if (v1 == v2 || (v1 != null && v1.equals(v2)))
                    return true;
            }
            return false;
        }

        public final int hashCode() {
            return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
        }

        public final String toString() {
            return getKey() + "=" + getValue();
        }

        /**
         * This method is invoked whenever the value in an entry is
         * overwritten by an invocation of put(k,v) for a key k that's already
         * in the HashMap.
         */
        void recordAccess(HashMap<K,V> m) {
        }

        /**
         * This method is invoked whenever the entry is
         * removed from the table.
         */
        void recordRemoval(HashMap<K,V> m) {
        }
    }

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

四、获取元素 get(Object key)

get 的过程和 put 一样,也是根据 key 是否为 null来分别处理的。对于 key 为 null 的情况下,则直接获取数组中第一个 Entry 对象,并基于 next 属性进行遍历,寻找 key 为null 的 Entry 对象,如找到则返回 Entry 对象的 value,如未找到,则返回 null;对于 key 为非 null 的情况下,则对 key 进行 hash 和按位与操作,找到其对应的存储位置,然后获取此位置对应的 Entry 对象,基于 next 属性遍历,寻找到 hash 值相等,且 key 相等或 equals 的 Entry 对象,返回其 value,如未找到,则返回 null。

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

五、删除元素 remove(Object key)

remove 的过程和 get 类似,只是在找到匹配的 key 后,如数组上的元素等于 key ,则将数组上的元素的值置为其 next 元素的值;如数据上的元素不等于 key,则对链表遍历,一直到找到匹配的 key 或链表的末尾。

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

七、判断元素是否存在 containsKey(Object key)

通过调用 getEntry 方法来完成,getEntry 方法和 get 过程基本相同。只是在找到匹配的 key 后,直接返回 Entry 对象,而 containsKey 判断返回的 Entry 对象是否为 null,为 null 则返回 false,不为 null 则返回 true。

    public boolean containsKey(Object key) {
        return getEntry(key) != null;
    }

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

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

八、遍历元素 keySet()

在使用 Map 时,经常会通过 keySet 来遍历 Map 对象,调用 keySet 方法后会返回一个 HashMap 中的 KeySet对象实例,此 KeySet 对象继承了 AbstractSet。当调用 iterator 方法时,返回一个 KeyIterator 对象实例,调用 next 方法时,遍历整个数组及 Entry 对象的链表,如在遍历过程中有新的元素加入或删除了元素,则会抛出 ConcurrentModificationException。
  HashMap 在遍历时是无法保证顺序的,如果要保证 Map 中的对象是按顺序排列的,最好是使用 TreeMap。
  HashMap 是非线程安全的,在并发场景中如果不保持足够的同步,就有可能在执行 HashMap.get时进入死循环,将 CPU 消耗到 100%,在并发场景中可通过 Collections.synchronizedMap、Collections.unmodifiableMap 或自行同步来保障线程安全,但这些实现方式通常性能会在高并发时下降迅速,最好的方法仍然是使用 ConcurrentHashMap。

六、注意要点

对于 HashMap 而言,最要注意的有以下几点:

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

推荐阅读更多精彩内容