HashMap源码阅读

Posted 秋风Haut

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HashMap源码阅读相关的知识,希望对你有一定的参考价值。

本文内容来自于HashMap的源码内容,作为学习的记录。


HashMap是一种存储key-value对的对象。每个map不能包含重复的key,每个key至多映射一个value。我们可以看到HashMap实现自Map接口,继承自AbstractMap类。

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable
{
    //HashMap的默认容量为16、这个值通过移位来操作,而不是通过乘法运算符
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    //HashMap的最大容量。(为什么是2的30次方?参考String的最大长度,可以猜想其受限于Integer类型的最大值是2^32-1和真实物理内存大小)
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //默认加载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //HashMap存储内容的初始值。一个空的Entry数组
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;   
    //HashMap进行必要的调整时,table会进行变化。而且table的长度,必须,是2的整数次幂
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
    //HashMap中包含的key-value对的个数   
    transient int size;
    ···

如下图,HashMap的方法很多,选出最有特点的几个方法重点研究:
1. containsKey()

......
    //采用getEntry(key)的方式,直接拿指定key向HashMap中取value,取到了就是true,取不到就是false
    public boolean containsKey(Object key) {
        return getEntry(key) != null;
    }

    /**
     * 如果在HashMap中不存在指定key的映射就会返回null
     */
    final Entry<K,V> getEntry(Object key) {
        //防御式编程,如果hashMap中没有key-value映射对,则返回null
        if (size == 0) {
            return null;
        }
    //求出key对应的hashCode,直接使用hashCode向entry数组中拿值
    //编程风格:如果key==null,则hashCode为0
        int hash = (key == null) ? 0 : hash(key);
        //可以看到HashMap从table[hash]这个节点开始,以链表的方式一个个向后遍历,若一直没有找到符合条件的key,将一直遍历直到e=null才停止
        //若在该条链中找到指定key对应的映射(e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))),就返回e
        //for循环外部还有一个return null;就意味着如果整条链遍历结束,仍没有找到指定key,就说明hashMap中没有该key,就返回null,因此containsKey方法将返回false
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            //判断条件很长.e是当前节点(指针),判断条件认为有两种情况说明找到指定节点,1. e.hash=key.hash;
            //2. 将e的当前值与指定key使用==比较,或在保证key不是null的前提下,使用key.equals()方法来判断是否相等
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

2.put()

//使用指定key和指定value在HashMap中建立映射关系
//如果HashMap中已经有指定的key,则新value会替换旧value
//put方法其实会有返回值的,每次插入新值,会返回该key对应的旧value,如果key是新的key,返回null
public V put(K key, V value) {
    //如果当前的Entry数组为空,则将数组容量扩充为2的整数次幂
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }

    //如果key为空则使用另一种方式添加
    if (key == null)
        return putForNullKey(value);
    //key是不是null,其实区别并不大,只是如果key为null,就直接放在table的第一个位置
    //计算key的hash值并由hash计算该key对应的index,是跟null型key的区别
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    //遍历链表,找到key的位置,e.recordAccess(this)方法是无法理解的.本来这个方法是就是空的
    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;
}
    //如果key为null时,从Entry数组的第一个元素开始遍历,找到第一个key为null的
    //entry,将旧值替换为新值,并将旧值返回
    //e.recordAccess(this)是空方法,不知道是什么设计原则
    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;
            }
        }
        //HashMap结构化 修改次数    +1.如果有另一个进程进行了非同步的修改,就会抛出ConcurrentModificationException异常
        //问题:HashMap是怎样确定现在的modCount跟原本的modCount不一致?
        modCount++;
        //
        addEntry(0, null, value, 0);
        return null;
    }
......
    //实际插入Entry元素的操作
    //threshold是一个阈值,如果当前key-value个数大于等于threshold,说明HashMap需要扩容了
    //bucketIndex是桶索引,因为HashMap的数据结构是链表数组,每个数组元素都可能挂了一个链表
    //一种类似于门帘式的结构,因为这个bucketIndex是要插入的key对应的那个桶的索引,只有size个数达到了阈值,并且bucketIndex处的Entry已经有元素了,这样才需要扩容了....
    //扩容后,需要对hash和bucketIndex进行重新计算
    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);
        }
        //具体addEntry的操作,只是不用担心resie的问题
        createEntry(hash, key, value, bucketIndex);
    }
//对这个map的内容进行rehash成一个新的容量更大的数组,当map中key的数量达到这个数组的threshold阈值时会自动调用
//PS:得亏我看了源码,现在知道了,不是说桶的多少达到了数组长度的threshold,而是key的个数,而且不管这些key有多少映射到了同一个桶中,
//也是,这种情况可以有效避免出现哈希表退化为链表的问题.
//如果当前容量是MAXIMUM_CAPACITY,就会将Integer.MAXVALUE设为threshold,这样可以防止将来再调用
    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);
    }
    //将当前entries中的所有元素transfer到新的table中,新table的规格已经在resize中设置好了,具体操作由transfer来完成
    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;
                //看不懂,把null赋给e.hash然后再与e.key相比,相等的话返回0,否则返回e.key的hash值
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                //根据e.hash和newCapacity新的容量来计算e元素的桶索引

                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }
  1. remove(Object key)
    //删除节点操作,并且会返回删除节点的value.
    public V remove(Object key) {
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.value);
    }
    //移除HashMap中指定的key,若该hashMap不包含该key的映射,则返回为空
   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;
    }
  1. keySet()
   /**
     * Returns a {@link Set} view of the keys contained in this map.
     * The set is backed by the map, so changes to the map are
     * reflected in the set, and vice-versa.  If the map is modified
     * while an iteration over the set is in progress (except through
     * the iterator's own <tt>remove</tt> operation), the results of
     * the iteration are undefined.  The set supports element removal,
     * which removes the corresponding mapping from the map, via the
     * <tt>Iterator.remove</tt>, <tt>Set.remove</tt>,
     * <tt>removeAll</tt>, <tt>retainAll</tt>, and <tt>clear</tt>
     * operations.  It does not support the <tt>add</tt> or <tt>addAll</tt>
     * operations.
     * 返回map中的所有key 的set视图,此set是由map支持的,因此对map的修改,都将对set产生影响,反之亦然。
     如果当set上的iterator正在执行中时,map被修改了,那么iterator的结果就是未 定义的(除非是对iterator进行remove操作。该set支持对元素的remove操作,可以使用remove/removeAll/Iterator.remove/retainAll/clear等操作,都将会同时移除元素在map中的映射。但不支持add/addAll操作
     *
     */
    public Set<K> keySet() {
        Set<K> ks = keySet;
        return (ks != null ? ks : (keySet = new KeySet()));
    }

    private final class KeySet extends AbstractSet<K> {
        public Iterator<K> iterator() {
            return newKeyIterator();
        }
        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();
        }
    }

从上可以了解到:
1、HashMap的数据结构一个Entry数组,而数组中的元素又是一个链表
2、HashMap的get操作都会先按key的hash得出该key对应的Entry数组的元素索引,然后以该元素为起点,以链表的方式往后遍历查找
3、HashMap中的Entry数组的size,原始长度是16,当元素个数达到了size * loadFactor=threshold时,将进行一次resize操作,就会声明一个新的长度为原来2倍的Entry数组,并将旧的Entry数组内容拷贝进去。而loadFactor加载因子是一个空间和时间效率上的平衡,如果loadFactor太高,空间利用率较高,但查找效率会降低,loadFactor过低,查找很快,但空间利用率不高
4、进行resize操作的目的有两个,一、扩展Entry数组。二、可以在一定程度上避免哈希表退货成链表的问题

5、关于put操作有个问题,在源码中可以看到,如果key是null,hashMap依旧可以插入到Entry数组的第一个元素后,然后返回旧key(null)的旧值,如果key已经存在,则put操作会找到key对应的桶,然后将key的旧值替换为新值,并返回该key对应的旧值,但对于key是新值,或key的hash为新值的情况,先调用addEntry(),进行必要的resize,重新计算hash和bucketIndex,然后调用createEntry(),为table[bucketIndex]申请空间,为new Entry()。

问题是:为什么可以确定不同的key,如果具有相同的hash,就一定具有相同的内容?因为以上并没有对按照key.hash找到了桶,但没有相同的key,需要在Entry后新增加节点的情况进行处理。对于新的要put的key,有两种情况,一、key为null,这种将直接插入到Entry[0]中,二、key!=null,若key!=null,也有两种情况,一、key.hash不存在,则将在进行的resize之后,新建一个Entry元素并申请空间,二、key.hash存在,我认为key.hash存在,也会有两种情况,一、key.hash存在,并且key已存在,将替换旧值为新值,二、这是key.hash存在,但key不存在的情况。HashMap没有处理。即,为什么在HashMap中没有向链表添加节点的操作,但是遍历链表的操作??
这里写图片描述

看完源码,还是有很多收获,其它收获有java的位运算、严谨的编程风格等,当然也限于本人现在的水平,有些操作是无法理解的,比较每次添加节点,都会执行一次e.recordAccess(this);而此方法的方法体是空的,暂时无法理解。还有其他比如keySet方法来获取HashMap的所有key的set视图,但是这个方法也看不懂,还有HashIterator、ValueIterator、KeyIterator内部数的设计,都不好理解。总之,这些都留待以后水平提高以后再去理解。

以上是关于HashMap源码阅读的主要内容,如果未能解决你的问题,请参考以下文章

HashMap 源码阅读

HashMap与HashTable的哈希算法——JDK1.9源码阅读总结

源码阅读(16):Java中主要的Map结构——HashMap容器(上)

HashMap源码阅读

HashMap源码阅读

HashMap源码阅读