jdk阅读---hashmap

常量定义

hashmap实现是存储键值对的一种数据结构。首先看它的声明:

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

可以得知HashMap是继承自抽象类AbstractMap且实现了Map接口。(这里有一个疑问,既然抽象类AbstractMap也是实现了Map,是否可以直接写成:

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

看下HashMap对应的一些常量,在此之前我们需要对hashmap的存储结构有所了解。实际是Node<K,V>[] table,数组的每个节点为一个Node链表,每个Node存储着键值对。我们将数组的每个元素称为bucket。Node的结构体如下,存储了Key的hash值,Key和Value,还有一个指向下个节点的指针。

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

HashMap内部定义的常量:

   static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 

    static final int MAXIMUM_CAPACITY = 1 << 30; //,

    static final float DEFAULT_LOAD_FACTOR = 0.75f;//
    static final int TREEIFY_THRESHOLD = 8;

    static final int UNTREEIFY_THRESHOLD = 6;

    static final int MIN_TREEIFY_CAPACITY = 64;
  • DEFAULT_INITIAL_CAPACITY:默认的buckets数目,值为16;
  • MAXIMUM_CAPACITY :最大的buckets数量,如果当前的buckets数量大于这个值,再次调用resize()将不会重新分配buckets的大小。
  • DEFAULT_LOAD_FACTOR:默认的rehash触发因子,键值对的数量大于buckets数量的75%,将发生resize()
  • TREEIFY_THRESHOLD: 大于等于这个值,该bucket下的链表将发生treeifyBin,从链表转化成红黑树。
  • UNTREEIFY_THRESHOLD: 小于等于这个值,该bucket下的节点如果是treenode,将转化成链表。
  • MIN_TREEIFY_CAPACITY:如果在treeifyBin的时候,当前的buckets数量小于这个值,将发生resize();
    实际上这种场景应该是很难触发的,在总数小于64,每次扩容两倍的情况下,就是32个buckets(16->32)根据0.75的加载因子,最大为24个键值对。即32个桶放了24个球,居然有8个球挤在一个桶里。这里的处理的确体现了作者的严谨。

常用方法

HashMap最常用的就是get,put 两个方法。下面我们拿一个简单的例子来说明HashMap的工作流程,现有如下代码:

         HashMap<String,Integer> map = new HashMap<>();

        map.put("abc",123);
        map.put("def",456);
        Integer result = map.get("abc");
        Boolean b1 = map.containsValue(123);
        Boolean b2= map.containsKey("def");
        map.forEach((key, value) -> {
            System.out.println(key+value);
        });
        map.remove("123");

首先调用默认构造函数:

    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

put方法和注释:

   final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                  boolean evict) {
       Node<K,V>[] tab; Node<K,V> p; int n, i;
       if ((tab = table) == null || (n = tab.length) == 0)
           //首次调用put将第一次resize();
           n = (tab = resize()).length;
       if ((p = tab[i = (n - 1) & hash]) == null)
           //若p为null,则当前的桶是空的,可以直接插入数据。
           tab[i] = newNode(hash, key, value, null);
       else {
           //这个hash值对应的桶之前已经有数据了。
           Node<K,V> e; K k;
           if (p.hash == hash &&
               ((k = p.key) == key || (key != null && key.equals(k))))
               e = p;
           else if (p instanceof TreeNode)
                 //这里如果node是TreeNode的实例,则这个链表就已经变成红黑树了。
               e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
           else {
               //循环查找链表的节点key是否和当前的key相等,binCount为链表长度,大于TREEIFY_THRESHOLD 触发treeifyBin
               for (int binCount = 0; ; ++binCount) {
                   if ((e = p.next) == null) {
                       //进到这里说明不相等,需要在结尾插入一个新的节点
                       p.next = newNode(hash, key, value, null);
                       if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                           treeifyBin(tab, hash);
                       break;
                   }
                   if (e.hash == hash &&
                       ((k = e.key) == key || (key != null && key.equals(k))))
                       break;
                 //已有相同的key插入,直接覆盖即可。
                   p = e;
               }
           }
           if (e != null) { // existing mapping for key
               V oldValue = e.value;
               if (!onlyIfAbsent || oldValue == null)
                   e.value = value;
               afterNodeAccess(e);
               return oldValue;
           }
       }
       ++modCount;
       //size 为当前map存储的node数量,用来判断是否需要resize
       if (++size > threshold)
           resize();
       afterNodeInsertion(evict);
       return null;
   }

说下几个自我感觉的重点:

  • hash运算时h >>> 16 的作用:
    hash方法取到的值还不是桶的数量,实际是取了hash值的低位(如果是16个buckets,那就是4bit , 32则是5bit ...),h >>> 16 让hash值的高16位和低16位异或运算,可以让object的hash值的各位最大程度上参与运算。
  • 为什么HashMap可以存储null值?
    1.null的hash函数做了特异处理,为0,即放到第一个bucket中。
    2.对null值做了判断 用p.hash == hash && ((k = p.key) == key的方式让null值可以被更新。
  • 原本在不同buckets中的node,通过resize()会放到同一个bucket中吗?
    不会,resize()每次增大一倍,每个节点放置的bucket位置重新计算,如果旧table该节点在位置table[i],则新位置可能在table[i]或者table[i+oldCap]; 举个例子:buckets数量由16扩展至32,原node A在位置table[2],A的hash值是10010,则分配到新的table[2+16]。如果是01011,则依然停留在table[2]。而且不改变原node链表的相对位置关系。
  • resize()很耗时,因此在初次分配的时候指定好大概大小?
    resize()操作实际发生次数是比较少的,单次resize()是比较占用cpu,但只是一瞬间,resize()的时间复杂度是o(n)*平均链表长度 = o(n)。实际业务中,HashMap存储的数据量一般不会太大,大的数据量操作一般是借助存储服务来完成,所以实际上对性能是不会产生太大影响的,当然初次分配了一个合理的预估值,会减少一定的cpu时间。
  • 接下里的get 和remove也很简单了,分为以下步骤:
    (1) hash要查找/删除的key,找到对应的bucket
    (2) 循环链表找到要查找/删除的节点并返回/删除,其中删除的实现:pre.next = node.next;

注意下remove中几个参数的意思:node:要查找的目标节点;p:,如果node是bucket中的第一个节点,pre 即是node,否则是node的pre节点;matchValue:是否需要key,value都匹配才删除。
get的代码:

    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

remove的代码:

    final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)
                    tab[index] = node.next;
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

containsValue和containsKey方法

因为HashMap是按照Key来排序的,调用containsValue将循环遍历所有node来查找,效率低下,不建议过多使用containsValue函数。

forEach方法

因为HashMap不是线程安全的,如果在forEach执行过程中table中的内容发生了变化。modCount会发生变化,此时抛出一个ConcurrentModificationException,实现思想和乐观锁一致。

HashMap的迭代器。

附示例代码

        HashMap<String,String> ss = new HashMap<>();
        ss.put("1","a");
        ss.put("2","b");
        Iterator<Map.Entry<String ,String>> iterator = ss.entrySet().iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
        }

我们可以通过HashMap.entrySet()来获取一个该对象的Entry集合entrySet变量,开始我很疑惑,HashMap中并没有对这个变量做增加元素等操作。那怎么能平白产生数据呢?后来看到了HashMap的内部类复写了EntrySet的iterator方法。返回的是这个类对象。

    final class EntryIterator extends HashIterator
        implements Iterator<Map.Entry<K,V>> {
        public final Map.Entry<K,V> next() { return nextNode(); }
    }

其中nextNode实现

        final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            if ((next = (current = e).next) == null && (t = table) != null) {
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }

这下明白了,变量entrySet并没有存储实际数据,只是通过iterator方法返回了Hashmap中的table存储的Node。需要注意的是,iterator对象中存储了table的位置,和当前节点的引用。这样在调用next的时候才可以返回下一个值。

LinkedHashMap

LinkedHashMap继承自HashMap,比其多了一个功能,就是可以按照插入顺序遍历出元素。原理: LinkedHashMap内部维护了一个链表,且重写了newNode方法,在新建节点的同时,就已经完成了链表的维护.LinkedHashMap#Entry<K,V>#newNode代码如下:

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

推荐阅读更多精彩内容

  • HashMap 是 Java 面试必考的知识点,面试官从这个小知识点就可以了解我们对 Java 基础的掌握程度。网...
    野狗子嗷嗷嗷阅读 6,649评论 9 107
  • 一、HashMap概述 HashMap基于哈希表的Map接口的实现。此实现提供所有可选的映射操作,并允许使用nul...
    小陈阿飞阅读 632评论 0 2
  • HashMap HashMap概述 HashMap基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,...
    史路比阅读 290评论 0 6
  • HashMap源码分析 HashMap是对Map接口的一种实现,底层数据结构使用了散列表(Hash table)。...
    Leocat阅读 419评论 0 0
  • 最近一直特别忙,好不容易闲下来了。准备把HashMap的知识总结一下,很久以前看过HashMap源码。一直想把集...
    鵬_鵬阅读 473评论 0 3