JDK1.7-HashMap源码分析

如果我在介绍集合的时候不介绍HashMap我相信一定会有人觉得我是个奇葩。毕竟是这么这么重要的类嘛。本篇开始进入Map阶段,相应地会提到HashMap,ConcurrentHashMap,TreeMap等。作为实习以及平时日常使用的出现频率最高的集合工具类,他被使用的场景我已经无须再描述了。因为其的重要性,在本篇我们将关注它的任何一个细枝末节。

==因为HashMap在JDK1.7和1.8的实现中变化比较大,所以我们先介绍1.7中的HashMap==

源码分析

注释导读

  • 允许null的key和null的value
  • 不保证遍历顺序,多次遍历顺序可能不一致
  • 提供常量的时间复杂度,希望在合适时候设置初始容量(不宜过大或者过小)
  • 0.75的factor一般情况不希望改变
  • 能避免就尽量避免rehash
  • 不是线程安全,如果要求线程安全可以使用Collections.synchronizedMap()
  • 遍历过程一旦改变元素就立马抛出错误

类定义

public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable

对于AbstractMap如果现在介绍的话到后面可能会忘记,所以就在HashMap引用父类相关方法的时候在分别介绍。

map.JPG

Map接口的所有内容都在上图,上层接口提供的都是一些通用的方法,重点是这个Entry类。
Entry接口其实作为一个<K,V>的对出现,代表了这一对pair的组合,它有定义自己的getKey()和getValue()方法,同时也有自己的equals和hashCode方法的复写。在Map类内部对于数据的操作都要借助于Entry来进行,它才是Map类内部真正核心的局部容器。

基本属性

    /**
     * 初始容量,一定是2的N次方
     */
    static final int DEFAULT_INITIAL_CAPACITY = 16;
    /**
     * 最大容量
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;
    /**
     * 默认的因子.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    /**
     * 承载Entry数据的核心数组
     */
    transient Entry<K,V>[] table;
    /**
     * Map中元素的数量
     */
    transient int size;
    /**
     * 需要进行扩容的阈值
     */
    int threshold;
    /**
     * 实际容器的因子(不提供的话就用DEFAULT_LOAD_FACTOR)
     */
    final float loadFactor;
    /**
     * 结构内容更新的次数(遍历时候使用)
     */
    transient int modCount;
    /**
     * 系统默认的扩容阈值
     */
    static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
    /**
     * 如果这个值为true,就使用另外一个hash算法来减少碰撞
     */
    transient boolean useAltHashing;
    /**
     * 为hash算法提供一个种子
     */
    transient final int hashSeed = sun.misc.Hashing.randomHashSeed(this);
    /**
     * 为获得Entry提供一种方便的操作结构
     */
    private transient Set<Map.Entry<K,V>> entrySet = null;

核心内部类

    //核心静态内部类Entry
    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        //因为HashMap是采用链表法处理哈希冲突的,所以Entry需要有一个指向下一个节点的指针
        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;
        }
        //final修饰主要为了防止子类覆盖
        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; //这个步骤上面一定要有instanceof判断,否则出现ClassCastException
            Object k1 = getKey();
            Object k2 = e.getKey();
            //这里值得学习的地方就是时刻保持对Null的警惕
            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;
        }
        //hashCode的计算就是要保证key和value全部相等(要考虑null的情况)
        public final int hashCode() {
            return (key==null   ? 0 : key.hashCode()) ^
                   (value==null ? 0 : value.hashCode());
        }

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

        /**
         * 在put操作调用的时候如果存在相同的key就触发下面的操作
         */
        void recordAccess(HashMap<K,V> m) {
        }

        /**
         * 当entry被移除的时候触发下面操作
         */
        void recordRemoval(HashMap<K,V> m) {
        }
    }
    
    //内部抽象类HashIterator
    private abstract class HashIterator<E> implements Iterator<E> {
        Entry<K,V> next;        // 下一个返回的entry
        int expectedModCount;   // 为了快速失败
        int index;              // 当前的slot位置
        Entry<K,V> current;     // 当前的entry

        HashIterator() {
            expectedModCount = modCount;
            if (size > 0) { // advance to first entry
                Entry[] t = table;
                
                //定位到第一个位置不为null的slot,我们知道HashMap内部的容器是一个数组,数组的实体为Entry,而每一个Entry又相当于LinkedList 的Node节点一样,
                //它后面可能跟了很多Entry(链表法),所以在遍历的时候肯定需要定位到第一个不为空的slot
                //图解见下
                while (index < t.length && (next = t[index++]) == null)
                    ;
            }
        }

        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) {
                Entry[] t = table;
                //继续将index定位到下一个不为null的slot
                while (index < t.length && (next = t[index++]) == null)
                    ;
            }
            current = e;
            return e;
        }

        public void remove() {
            if (current == null) //多次remove同一对象就报错
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            Object k = current.key;
            current = null; //help GC
            //这个remove操作我们后面讲
            HashMap.this.removeEntryForKey(k);
            expectedModCount = modCount;
        }
    }
    //这三个方法完全是基于上面的抽象类
    private final class ValueIterator extends HashIterator<V> {
        public V next() {
            return nextEntry().value;
        }
    }

    private final class KeyIterator extends HashIterator<K> {
        public K next() {
            return nextEntry().getKey();
        }
    }

    private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {
        public Map.Entry<K,V> next() {
            return nextEntry();
        }
    }
    
    private final class KeySet extends AbstractSet<K> {
        public Iterator<K> iterator() {
            return newKeyIterator();//KeySet->KeyIterator->HashIterator
        }
        public int size() {
            return size;
        }
        public boolean contains(Object o) {
            return containsKey(o);
        }
        public boolean remove(Object o) {
            return HashMap.this.removeEntryForKey(o) != null;
        }
        public void clear() {
            HashMap.this.clear();
        }
    }
    
    private final class Values extends AbstractCollection<V> {
        public Iterator<V> iterator() {
            return newValueIterator();
        }
        public int size() {
            return size;
        }
        public boolean contains(Object o) {
            return containsValue(o);
        }
        public void clear() {
            HashMap.this.clear();
        }
    }
    
    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();
        }
    }
    无论是key还是value的单独操作,都是依赖于Entry接口的,提供这些其他的接口就是为了方便我们单独处理key或者value,因为我们再使用map的时候一般不会直接使用Entry
HashMap.JPG

所以通过Iterator进行遍历的时候顺序就是先遍历slot:0里面的每一个entry,然后遍历slot:2里面的对象然后依次进行。

构造函数

    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        //loadFactory要合法
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +   loadFactor);

        // 找到一个最小的2的N次幂使其大于等于 initialCapacity
        // 例如: initialCapacity = 2 -> capacity = 2
        // 例如: initialCapacity = 3 -> capacity = 4
        // 例如: initialCapacity = 15 -> capacity = 16
        
        int capacity = 1; 
        while (capacity < initialCapacity)
            capacity <<= 1;

        this.loadFactor = loadFactor;
        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        //当现在的容量超过了ALTERNATIVE_HASHING_THRESHOLD值后就采用不同的Hash策略
        useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        init(); //空方法,供子类扩展
    }
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    /**
     * 默认就是16,0.75f的组合
     */
    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }
    public HashMap(Map<? extends K, ? extends V> m) {
        //m.size() / DEFAULT_LOAD_FACTOR) + 1:在不进行扩容的前提下的最小的阈值
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
            DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        putAllForCreate(m); //后面统一看这些插入操作
    }

常用方法

    说道常用方法我就忍不住要先说put和get这两个高频出现的方法了。哈哈哈哈
    
    public V put(K key, V value) {
        if (key == null) //允许null的key和null的value
            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;
            //保证hash值和key值全部相同才会进行覆盖
            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;
    }
    //统一把所有的key为Null的值放在slot:0的位置
    private V putForNullKey(V value) {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) { //初始化的时候已经初始化过table了所以不用担心e = null导致NPE
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);//没有实现,留给子类实现
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }
    //bucketIndex及我所说的slot
    void addEntry(int hash, K key, V value, int bucketIndex) {
        //如果超过了阈值并且要插入的位置已经有元素存在就扩容(我们在日常使用中应在进行避免扩容,能指定尽量指定HashMap的初始容量)
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length); //扩容为原来的2倍
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length); //重新定位到目标bucketIndex
        }
        //前面都是定位扩容操作,最后就是将节点连接到table数组中
        createEntry(hash, key, value, bucketIndex);
    }
    //在增加Entry的时候直接进行连接
    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++;
    }
    
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        //原容量已经最大了就不进行扩容了,但是阈值调整为Integer的最大值
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        boolean oldAltHashing = useAltHashing;
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        //如果两个值一样rehash=false, 不一样的话rehash=true
        //rehash是否进行根据现阶段的调整策略
        boolean rehash = oldAltHashing ^ useAltHashing;
        transfer(newTable, rehash); //将原来的元素移到新的table中
        table = 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) { //在每一个slot上进行遍历取到所有元素
                Entry<K,V> next = e.next;
                if (rehash) { //根据参数重新计算hash值
                    e.hash = null == e.key ? 0 : hash(e.key);
                }//算出要进行添加的slot位置
                int i = indexFor(e.hash, newCapacity);
                //将新元素作为头结点进行添加,不作为尾节点添加的原因是这样简单,而且不用考虑空元素
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }
    //哈希散列算法(有人可能会问,Object对象都有自己的hashCode()方法了问什么还再实现一个,这个Hash算法是为了是元素分布更在均衡,减少碰撞。)
    final int hash(Object k) {
        int h = 0;
        if (useAltHashing) { //加入JVM自己的实现
            if (k instanceof String) {
                return sun.misc.Hashing.stringHash32((String) k);
            }
            h = hashSeed;
        }

        h ^= k.hashCode();
        //对以下具体hash算法的原理实现不太了解
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
    /**
     * 根据Hash值取得其在table中应该插入的位置,这里实现极为巧妙,也解释了为什么要求table的长度一定是2的n次方
     * 初始化capacity = 16,转换为二级制就是10000,假设目前一个元素的hash值为80,也就是1010000. 
     * 现在 16 & 80 = 0001111 & 1010000 = 0000000 = 0(十进制) 所以插入位置就是0 这种巧妙就是利用了数组长度-1后的所有二级制为全是1的特性,巧妙实现了取余的算法。
     * 因为硬件内部底层是支持&运算的(数字电路),所以这样操作效率比%高。
     */
    static int indexFor(int h, int length) {
        return h & (length-1);
    }
    
    =======================================================
    put操作相关的方法都在上面进行了介绍,下面让我们看看另外一个高频使用方法get吧。
    public V get(Object key) {
        if (key == null)
            return getForNullKey(); // null key
        Entry<K,V> entry = getEntry(key);
    
        return null == entry ? null : entry.getValue();
    }
    
    private V getForNullKey() {
        //在上面的介绍中我们知道null key全部放在table[0]的位置
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }
    
    final Entry<K,V> getEntry(Object key) {
        int hash = (key == null) ? 0 : hash(key);
        //第一步定位slot
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            //注意这里的判断,hash相等是必要条件(Entry的hash已经被复写过,我们在插入元素的使用,Entry的hash值即为其key的hash值),key相等或者满足equals都可以
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }
    因为get方法不包括扩容等操作,也不会改变现在的结构,所以比较简单。
    我们再来看看最后一个高频方法contains
    public boolean containsKey(Object key) {
        return getEntry(key) != null;
    }
    
    public boolean containsValue(Object value) {
        if (value == null)
            return containsNullValue();
        //这里没有什么技巧,只不过我觉得tab应该用final修饰一下会更好
        Entry[] tab = table;
        for (int i = 0; i < tab.length ; i++)
            for (Entry e = tab[i] ; e != null ; e = e.next)
                if (value.equals(e.value))
                    return true;
        return false;
    }
    //把上面的方法最后的比较从equals换成了 == null 而已
    private boolean containsNullValue() {
        Entry[] tab = table;
        for (int i = 0; i < tab.length ; i++)
            for (Entry e = tab[i] ; e != null ; e = e.next)
                if (e.value == null)
                    return true;
        return false;
    }
    
    public V remove(Object key) {
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.value);
    }
    
    final Entry<K,V> removeEntryForKey(Object key) {
        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; //这里e仍然可能保留了指向后面元素的指针,我觉得应该加e.next = null
            }
            prev = e;
            e = next;
        }

        return e;
    }
    
    public void clear() {
        modCount++; //这里可不是modCount变了很多噢
        Entry[] tab = table;
        for (int i = 0; i < tab.length; i++)
            tab[i] = null;
        size = 0;
    }
    
    //这个克隆是浅克隆(Entry还是原来的Entry,指针是一样的)
    public Object clone() {
        HashMap<K,V> result = null;
        try { 
            result = (HashMap<K,V>)super.clone();
        } catch (CloneNotSupportedException e) {
            // assert false;
        }
        result.table = new Entry[table.length];
        result.entrySet = null;
        result.modCount = 0;
        result.size = 0;
        result.init();
        //上面有介绍过这个方法
        result.putAllForCreate(this);

        return result;
    }
    

到这里,JDK1.7的HashMap已经全部都介绍完了。我们从源码中看不到任何关于同步的操作,所以多线程环境下使用的话还是需要多点注意的。下一篇我会讲讲JDK1.8中的HashMap,敬请期待.

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

推荐阅读更多精彩内容