HashMap阅读笔记

简介

HashMap是一个比较常用的键值对集合,在开发中常用于映射关系。以下分析是基于Android中API25下的源码

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

HashMap属于map族类,并且默认实现Serializable,可用于网络传输和本地序列化。

基础属性

    /**
     * HashMap的默认初始容量,必须是2的倍数
     */
    static final int DEFAULT_INITIAL_CAPACITY = 4;

    /**
     * HashMap的最大容量,必须为2的倍数
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 默认的扩容因子
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * HashMap为空时候共享的空数组
     */
    static final HashMapEntry<?,?>[] EMPTY_TABLE = {};

    /**
     * HashMap里面的存储数组
     * 每一个数据都是一个HashMapEntry,而HashMapEntry本质上是一个单项链表
     */
    transient HashMapEntry<K,V>[] table = (HashMapEntry<K,V>[]) EMPTY_TABLE;

    /**
     * HashMap中键值对的数量
     */
    transient int size;

    /**
     * 临界值决定什么时候进行扩容(当前容量 * 扩容因子)
     */
    int threshold;

    /**
     * 扩容因子,一个比例,相对于当前容量来说
     * Android中只会使用0.75,而会忽视其它的数值
     */
    final float loadFactor = DEFAULT_LOAD_FACTOR;

这里可以看到HashMap的基础存储结构:

存储结构

后面会详细看到具体的意义。

构造

    /**
     * 创建一个空的HashMap
     *
     * @param  initialCapacity 初始化容量
     * @param  loadFactor      扩容因子,在Android中实际上并没有用,还是采用默认的0.75f
     */
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                    initialCapacity);
        //容量不能小于默认容量和大于最大容量
        if (initialCapacity > MAXIMUM_CAPACITY) {
            initialCapacity = MAXIMUM_CAPACITY;//1 << 30
        } else if (initialCapacity < DEFAULT_INITIAL_CAPACITY) {
            initialCapacity = DEFAULT_INITIAL_CAPACITY;//4
        }

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

        // 实际上每次扩容的时候threshold都会重新计算
        threshold = initialCapacity;
        init();//默认空实现,子类可以尝试实现这个方法做一些插入之类的操作
    }

    /**
     * 创建一个空的HashMap,容量为4,扩容因子0.75
     */
    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

    /**
     * 从其它的HashMap上面复制一份
     *
     * @param   m 想要复制的HashMap
     */
    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);
    }

    /**
     * 当HashMap为空的时候初始化当前HashMap
     */
    private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize
        int capacity = roundUpToPowerOf2(toSize);
        //下一次扩容的大小为当前容量*扩容因子
        float thresholdFloat = capacity * loadFactor;
        if (thresholdFloat > MAXIMUM_CAPACITY + 1) {
            thresholdFloat = MAXIMUM_CAPACITY + 1;
        }

        threshold = (int) thresholdFloat;
        table = new HashMapEntry[capacity];//构建一个容量大小的数组
    }

    /**
     * 找到一个比number大的2的倍数
     */
    private static int roundUpToPowerOf2(int number) {
        int rounded = number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (rounded = Integer.highestOneBit(number)) != 0
                ? (Integer.bitCount(number) > 1) ? rounded << 1 : rounded
                : 1;
        /*稍微拆解一下方便理解
        if(number >= MAXIMUM_CAPACITY){
            rounded = MAXIMUM_CAPACITY;//不允许超过最大容量
        }else{
            //首先将number转为2进制表现形式,比方说00100101
            //通过highestOneBit则rounded为00100000
            //找出最高位的1,其余位朴0
            rounded = Integer.highestOneBit(number);
            if(rounded != 0){
                //如果number的2进制中不止1个1,比方说00100101
                //因为highestOneBit其余位朴0,得到rounded为00100000
                //所以获得要大于number的2的倍数,需要再乘以2,则rounded左移一位即可
                rounded = ((Integer.bitCount(number) > 1) ? rounded << 1 : rounded);
            }else{//如果number为0,容量取1即可
                rounded = 1;
            }
        }*/
        return rounded;
    }

    /**
     * 将指定map的数据放入当前HashMap中
     * @param m 需要添加的数据
     */
    private void putAllForCreate(Map<? extends K, ? extends V> m) {
        //实际上就是遍历一遍指定map,然后一个个添加到当前HashMap中
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
            putForCreate(e.getKey(), e.getValue());
    }

    /**
     * 该方法不会重新进行扩容操作
     * 只有在构造的时候复制map之类的场景下才会使用
     */
    private void putForCreate(K key, V value) {
        //计算key的hash值
        int hash = null == key ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
        //计算出当前key在table的下标
        int i = indexFor(hash, table.length);
        //下标已经计算成功
        //在当前table的链表中先查找是否有相同key的元素,有的话直接设置,不新建
        for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {//遍历单向链表
            Object k;
            //key的hash值相同
            //key是同一个引用或者key的equals相同
            //此时直接复写value即可
            if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k)))) {
                e.value = value;
                return;
            }
        }
        //当前table的链表中没有当前key,在链表当中新建一个节点
        createEntry(hash, key, value, i);
    }

    /**
     * 通过key的hash值和当前HashMap的容量
     * 计算出当前node在table中的下标
     */
    static int indexFor(int h, int length) {
        //假设h:0000 1110 length:0000 1000
        //0000 1110 h
        //0000 0111 length - 1,刚好将前面的所有位朴1
        //0000 0110 结果
        //结果必然是小于length
        //而且从这个计算也知道,length必须是2的倍数
        return h & (length-1);
    }

    /**
     * 在指定的链表末尾添加一个节点
     * @param hash 当前要添加节点的key的hash值
     * @param key 当前要添加的节点的key
     * @param value 当前要添加的节点的value
     * @param bucketIndex 当前链表在table[]中的下标位置
     */
    void createEntry(int hash, K key, V value, int bucketIndex) {
        HashMapEntry<K,V> e = table[bucketIndex];//从table中获取链表
        table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);//新建一个HashMapEntry并且添加到链表中
        size++;//HashMap的大小+1
    }

从HashMap的拷贝型构造函数的实现可以看出,为了更快的计算table[]的下标,HashMap中的容量都是2的倍数,每一个table[i]都维持着一个链表。

链表结构

先看一下table[i]中的链表结构

    /** 
     * Android添加的
     * 单向链表结构,每一次新建的时候会把节点放在链表的头部
     * @hide 
     * */
    static class HashMapEntry<K,V> implements Map.Entry<K,V> {
        final K key;//当前节点的key
        V value;//当前节点的value
        HashMapEntry<K,V> next;//当前节点的下一个节点
        int hash;//当前节点的key的hash值

        /**
         * 创建一个新的节点,并且将当前节点放到指定链表的头部
         */
        HashMapEntry(int h, K k, V v, HashMapEntry<K,V> n) {
            value = v;
            next = n;//这样实现的效率极高
            key = k;
            hash = h;
        }
        
        //...省略一些
        
    }

HashMapEntry一个单向链表结构,每一次新建的时候直接把当前节点放到指定链表的头部,操作效率极高。

基本操作

    /**
     * 将指定键值对添加到当前HashMap中
     * @param key 键值
     * @param value 键值
     * @return 如果当前是覆盖操作,返回被覆盖的原来的value,否则为null
     */
    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {//当前HashMap为空
            inflateTable(threshold);//初始化HashMap,此时会构建好table[]、容量和扩容值
        }
        if (key == null)//HashMap支持null作为key
            return putForNullKey(value);
        //计算key的hash值
        int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
        //通过hash值计算出对应table[]下标
        int i = indexFor(hash, table.length);
        //先尝试复写当前table[]的链表中“相同”的key的value
        for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //这里可以看到如果hash值直接相等是效率最高的
            //否则走equals
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        //当前链表中没有指定key的节点,新建一个节点并且添加到链表当中
        addEntry(hash, key, value, i);
        return null;
    }

    /**
     * 当添加到HashMap中的键值对的key为null的时候
     * @param value key为null对应的value
     * @return 如果当前是覆盖操作,返回被覆盖的原来的value,否则为null
     */
    private V putForNullKey(V value) {
        //可以看到key为null的时候,默认hash为0
        //先遍历table[0]的链表,看是否需要覆盖
        for (HashMapEntry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {//如果当前节点key为null,直接覆盖value
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);//否则新建一个节点并且添加到链表当中
        return null;
    }

    /**
     * 新建一个节点并且添加到指定的链表的头部
     * @param hash 需要添加的key的hash值
     * @param key 需要添加的节点的key
     * @param value 需要添加的节点的value
     * @param bucketIndex 需要添加到table[]的下标
     */
    void addEntry(int hash, K key, V value, int bucketIndex) {
        //先判断是否需要进行扩容操作
        if ((size >= threshold) && (null != table[bucketIndex])) {
            //当前大小已经到了容量*扩容因子的标准
            //以当前容量为基准,直接扩大一倍的方式进行扩容
            resize(2 * table.length);
            //重新计算hash值
            hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
            //根据最新的table[]的长度计算table[]下标
            bucketIndex = indexFor(hash, table.length);
        }
        //新建节点并放在放在table[bucketIndex]链表的头部,大小+1
        createEntry(hash, key, value, bucketIndex);
    }

    /**
     * 扩容操作
     * 当HashMap添加数据的时候发现已经到了扩容标准
     * 则进行扩容
     * @param newCapacity 当前希望的新的容量
     */
    void resize(int newCapacity) {
        HashMapEntry[] oldTable = table;//记录旧的table[]
        int oldCapacity = oldTable.length;//记录旧的容量
        if (oldCapacity == MAXIMUM_CAPACITY) {//扩容前的容量已经是最大容量,无法进行扩容
            threshold = Integer.MAX_VALUE;//修改扩容基准为最大容量
            return;
        }
        //根据当前扩容后的容量新建一个table[]
        HashMapEntry[] newTable = new HashMapEntry[newCapacity];
        transfer(newTable);
        table = newTable;//转换table[]为最新的表
        //容量变化,重新计算下一次扩容的基准
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

    /**
     * 当进行扩容的时候,需要将旧的table[]数据转移到当前新的table[]上面
     * @param newTable 扩容后新建的table[]
     */
    void transfer(HashMapEntry[] newTable) {
        int newCapacity = newTable.length;//扩容后的大小
        //遍历旧的table[],将旧的节点转移到新的table[]中
        for (HashMapEntry<K,V> e : table) {
            while(null != e) {//遍历当前table[i]链表
                HashMapEntry<K,V> next = e.next;
                //通过每一个节点的hash值重新计算table[]的下标
                int i = indexFor(e.hash, newCapacity);
                //将当前节点放到新的table[]的链表的头部
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

    /**
     * 从HashMap中获得指定key对应的value
     * @param key 需要查找的key,支持null
     * @return value,没有的话为null
     */
    public V get(Object key) {
        if (key == null)//key为空,直接查找table[0]即可,相对于getEntry节省了indexFor的开销
            return getForNullKey();
        Map.Entry<K,V> entry = getEntry(key);

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

    /**
     * 当key为null的时候,获取指定的value
     * 如果有key为null的节点,必然位于table[0]的链表且唯一
     * @return value,没有的话为null
     */
    private V getForNullKey() {
        if (size == 0) {//当前HashMap还没有数据
            return null;
        }
        //遍历table[0],尝试获得key为null的节点的value
        for (HashMapEntry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }

    /**
     * 根据指定的key获得HashMap中对应的节点的value
     */
    final Map.Entry<K,V> getEntry(Object key) {
        if (size == 0) {//当前HashMap还没有数据
            return null;
        }
        //计算key的hash值
        int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
        //先计算hash值对应的table的下标
        //然后遍历对应table[]的链表
        for (HashMapEntry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            //这里可以看到如果hash值直接相等是效率最高的
            //否则走equals
            if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

    /**
     * 从HashMap中移除指定key的节点
     * @param key 键值对中的key
     * @return 被移除的key节点对应的value
     */
    public V remove(Object key) {
        Map.Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.getValue());
    }

    /**
     * 从HashMap中移除指定key的节点
     * @param key 键值对中的key
     * @return 被移除的key节点
     */
    final Map.Entry<K,V> removeEntryForKey(Object key) {
        if (size == 0) {//当前HashMap还没有数据
            return null;
        }
        //计算key的hash值
        int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
        //通过hash值计算出对应table[]下标
        int i = indexFor(hash, table.length);
        //记录当前table[]链表的头结点
        HashMapEntry<K,V> prev = table[i];
        HashMapEntry<K,V> e = prev;
        //从头开始遍历当前链表
        while (e != null) {
            HashMapEntry<K,V> next = e.next;//获得当前节点的下一个节点
            Object k;
            //这里可以看到如果hash值直接相等是效率最高的
            //否则走equals
            if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k)))) {
                //删除当前节点
                modCount++;
                size--;//HashMap大小-1
                if (prev == e)//当前需要删除的节点是链表的头结点
                    table[i] = next;//直接标记当前链表的头结点为下一个节点
                else//删除链表非头结点的节点
                    prev.next = next;//将当前节点的上一个节点直接指向当前节点的下一个节点,从而当前节点从链表中移除
                e.recordRemoval(this);
                return e;
            }
            prev = e;//记录当前节点为前置节点
            e = next;//继续遍历下一个节点
        }
        //如果遍历之后没有找到对应的key,此时链表的最后一个节点为null,这里直接返回null即可
        return e;
    }

    /**
     * 查找是否包含某一个key的节点
     */
    public boolean containsKey(Object key) {
        //实际上就是查找一边当前key的节点,如果节点不为空则包含
        return getEntry(key) != null;
    }

HashMap的核心操作就是put和get,并且支持key为null的情况。
put的时候首先尝试覆盖旧的节点,所以要先计算hash值找到对应链表,然后遍历链表寻找,如果没有,则需要新建,在新建之前要判断是否达到容量*扩容因子的基准,如果达到,首先要进行扩容操作。
扩容会将当前容量增加一倍的大小,然后还需要遍历将旧的table[]链表中的节点转移到新的table[]中,并且这个过程中还需要根据hash重新计算节点位于table[]的下标。
get操作首先计算key对应的hash值,然后获得对应的table[]的节点链表,进而遍历链表查找对应的节点并且返回value。

总结

从上面可以看到,HashMap中会有不少遍历链表的操作,这个其实就说明hash散列的重要性,如果不同的值但是最后放到了同一个table[i]的链表中,会导致某一个链表特别长,这样对于遍历效率来说不是太好。
扩容操作需要转移旧的节点等操作,所以说对于已知初始容量的HashMap来说,最好是指定容量,避免多余的扩容操作。
HashMap并不是线程安全的,如果多个线程对链表进行操作,会造成不可预期问题,此时应该尝试ConcurrentHashMap或者Collections.synchronizedMap()。

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

推荐阅读更多精彩内容

  • HashMap 是 Java 面试必考的知识点,面试官从这个小知识点就可以了解我们对 Java 基础的掌握程度。网...
    野狗子嗷嗷嗷阅读 6,666评论 9 107
  • 这是转载的笔记。文章来源:http://www.canyin88.com/baike/31287/zhongguo...
    时茶阅读 17,275评论 0 6
  • 凌晨昏昏不能眠,作詩一首。晨起甚早,食湯麵及雞子。上午處理雜務,然後坐地鐵、高鐵至珠海,又於横琴島上處理技術工作,...
    寒窗寄傲生阅读 179评论 0 0
  • 前两年的韩寒的一部电影里有句台词,当时挺流行的,「为什么我听了很多道理,却依然过不好一生。」相信很多人都同感。 我...
    wanbin阅读 745评论 1 2