JAVA源码阅读之HashMap

据说面试时讲自己读过源码会很不一样

个人理解散列表大概就是可以通过某些函数计算出不同key在存储的数组中对应的index,刷题的时候我们也会经常遇到需要存储26个字母的情况,于是新建数组

int[] letter = new int[26];
letter[(char)item- 'a']++;

从而实现对字母的计数。这里字母letter对应的index可以由letter-'a'计算得到,a对应0,b对应1.......也算是一个散列表吧。。后来读了书、 算法导论上叫他直接寻址表

在刚才特定场景下,我们知道这个数组只需要存储26个数字,因此能够直接新建数组,但是当全域非常大时,这样的直接寻址表明显会很浪费。于是推出主角散列表。
散列表中需要有散列函数将key值全域映射到存储散列表的数组的各个位置上。
于是首要的问题就变成了

可能不同的key值会被映射到相同的位置上

plan A 链接法:将存储数组变为List[ ]的形式,使用散列函数得到在数组中的index后,将当前key-value对插入该List中。数组中每个元素都是一个可长可短的链表。查询一个key值最多需要遍历最长的那个链表一次

plan B 开放寻址法(散列函数2.0):将散列函数的输入扩充为key值和访问次数,当数组对应的index中key值与要查找的不同时,下次会生成新的index。


基础知识就到这里吧。。散列函数怎么写。。目前我感觉是一个过于算法的问题了。。书上也讲了很多
HashMap 继承AbstractMap, 实现Map<K,V>, Cloneable, Serializable 三个接口

    /**
     * The number of key-value mappings contained in this map.
     */
    transient int size; // 所有key-value对的个数
    /**
     * The table, resized as necessary. Length MUST Always be a power of two.
     */
    transient HashMapEntry<K,V>[] table = (HashMapEntry<K,V>[]) EMPTY_TABLE;

这个table的length程序写的一定要是2的整数幂,至于为什么之后会提到。

使用HashMapEntry类型的数组

这个类型中有key、value、next、hash。这里明显用到了书上讲的第一个方法了,使用链表存储


get(Object key) 方法

对于get(Object key) 方法,会调用getForNullKey(key == null) 或getEntry(key);
在getEntry(key)中

   /**
     * Returns the entry associated with the specified key in the
     * HashMap.  Returns null if the HashMap contains no mapping
     * for the key.
     */
    final Entry<K,V> getEntry(Object key) {
        if (size == 0) { //根本没有元素存储在里面
            return null;
        }

        int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
       //一个感觉带了人名的hash函数
        for (HashMapEntry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) { //当前HashMapEntry的next
            Object k;
            if (e.hash == hash && //判断hash值
                ((k = e.key) == key || (key != null && key.equals(k))))//判断key值
                return e;
        }
        return null;
    }

如果return e说明找到了Entry<K,V>,顺利返回。

这个判断key值的环节

(k = e.key) == key 可以判断出e.key与 当前key是否== 如果两者本就是相同的引用,则会返回true。因此很明显不要更改一个引用再把它多次作为key赋值。。会出事情的,像下面这样。我们会觉得aa和bb其实不相等的。

StringBuilder aa = new StringBuilder("a");
StringBuilder bb = aa;
bb.setCharAt(0, 'b');
System.out.println(aa == bb); // true

不过真实情况下还有hash值同样作为判断条件


put(K key, V value) 方法
/**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold); // 只有一个空表
        }
        if (key == null)
            return putForNullKey(value);
        int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
        int i = indexFor(hash, table.length);
        for (HashMapEntry<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; //找到了需要更改key值对应value
            }
        }

        modCount++;
        addEntry(hash, key, value, i); // 没找到,增加这个key-val对。这里面会size++的
        return null;
    }
我们关心一下indexFor函数
    /**
     * Returns index for hash code h.
     */
    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);// length为2的整数幂
    }

在这里length为2的整数幂可以保证不论hash值具体为多少,访问数组总不会越界。(比如当length为8(1000),length-1 为(111) h & (length-1)返回h的低三位)另一个信息就是即使在同一个index下,他们的hash值不会完全相同。只是低位相同

但是这个indexFor函数还是让人挺不放心的

int i = indexFor(hash, table.length);

当前元素的index是随着hash值以及table.length变化的。。。这个问题还是挺尴尬的。。毕竟hash值我们认为只要函数不变每个key生成的hash值就一定不变。。但是table.length得要变的。。

当添加key-value对到达门限,会进行resize(2 * table.length);直接将table扩充一倍

   /**
     * Rehashes the contents of this map into a new array with a
     * larger capacity.  This method is called automatically when the
     * number of keys in this map reaches its threshold.
     *
     * If current capacity is MAXIMUM_CAPACITY, this method does not
     * resize the map, but sets threshold to Integer.MAX_VALUE.
     * This has the effect of preventing future calls.
     *
     * @param newCapacity the new capacity, MUST be a power of two;
     *        must be greater than current capacity unless current
     *        capacity is MAXIMUM_CAPACITY (in which case value
     *        is irrelevant).
     */
    void resize(int newCapacity) {
        HashMapEntry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        HashMapEntry[] newTable = new HashMapEntry[newCapacity];
        transfer(newTable);
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

其中ransfer(HashMapEntry[] newTable) 将原表内数据迁移,根据HashMapEntry中的hash重新计算在新表中的index,构造新的链表数组进行存储。


    /**
     * Transfers all entries from current table to newTable.
     */
    void transfer(HashMapEntry[] newTable) {
        int newCapacity = newTable.length;
        for (HashMapEntry<K,V> e : table) {
            while(null != e) {
                HashMapEntry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity); //存储element时必须保存hash值
                e.next = newTable[i]; //将当前元素放在该index的头部,放在尾部会增加时间复杂度
                newTable[i] = e;
                e = next;
            }
        }
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容

  • 一、基本数据类型 注释 单行注释:// 区域注释:/* */ 文档注释:/** */ 数值 对于byte类型而言...
    龙猫小爷阅读 4,243评论 0 16
  • 前言 这次我和大家一起学习HashMap,HashMap我们在工作中经常会使用,而且面试中也很频繁会问到,因为它里...
    liangzzz阅读 7,955评论 7 102
  • HashMap 是 Java 面试必考的知识点,面试官从这个小知识点就可以了解我们对 Java 基础的掌握程度。网...
    野狗子嗷嗷嗷阅读 6,636评论 9 107
  • 虽天茫茫,雨凄凄,可晴总会至 虽路漫漫,风飒飒,可春总会至 一时快意,歌舞一曲 一时失意,挥诗一首 平凡人生何牵记...
    一只特立独行的猪与阿飞阅读 183评论 0 2
  • 他问我:我是否喜欢他? 我说:犹豫说明你更爱自己。 另一个他向我说:我有点喜欢你。 我说:我更喜欢她。 她说:他会...
    重度面瘫患者阅读 215评论 0 0