HashMap关于面试的理解性的解读

HashMap源码解读

今天面蘑菇街的时候遇到了面试官问我put过程和链表退化的问题,发现自己对HashMap还是不够了解。故作此篇源码分析,旨在加强自身理解。

put

map,是一个k-v存储结构,put也仅仅存入key 和 v即可。

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

然后调用了putval这个函数,结合源码注释进行解读。

    /**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     *				key的哈希值
     * @param key the key
     *				key的值
     * @param value the value to put
     *				v的值
     * @param onlyIfAbsent if true, don't change existing value
     *				如果onlyIfAbsent==true,不覆盖
     * @param evict if false, the table is in creation mode.
     *				exict==false,
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 如果列表为空,或者链表容量为0,则扩容
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
        		//由key计算出的hash值,再计算出hash值在hash表中的位置,如果位置为空,则创建新的存储节点插入。
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            // 如果想要插入散列表的位置上一级有了元素,看看他们的key值是否相同
            // 待插入节点的key值等于原有节点的key值,则覆盖更新。
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
            		//如果挂在散列表的结点已经变成红黑树(节点数量>7),则以红黑树的方式继续插入
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            		//未变成红黑树
                for (int binCount = 0; ; ++binCount) {
                		//在链表里,他们拥有相同的hash值了,所以在链表里试图插入
                		//如果成功遍历到链表尾部,就创建新节点进行插入,插入后检查是否满足树化阈值
                		//如果未能成功遍历到链表尾部,且遇到相同的key,打断查找。e.key=node.key
                    if ((e = p.next) == null) {
                    		//找到合适的链表位置插入。
                        p.next = newNode(hash, key, value, null);
                        // 如果达到了树化的阈值,进行树化操作
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果在链表里找到了相等key值的结点,就进行覆盖
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // onlyIfAbsent 默认为true,覆盖value值。
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value; 
                //在hash表中,这个函数的内容是空的
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //统计节点数量
        ++modCount;
        //看散列表是否需要扩容,
        if (++size > threshold)
            resize();
        //在hash表中,这个函数的内容是空的
        afterNodeInsertion(evict);
        return null;
    }

get

HashMap是一个典型的存储复杂,查找快的存储结构,故get值也是值得一看

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

通过只算好key的hash值,在getNode中进行查找

    /**
     * Implements Map.get and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //散列表空表,散列表的中hash值计算出的位置为null,都为查找不到
        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 {
                		//非树化结构的话,链表进行遍历,找到key值相同的结点。
                		//如果这都没找到,直接返回null了
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

remove

因为被面试官问到了退化问题,这个remove我不想放过

支持两种,一种是按key,一种是按key&&value

    public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
    
    public boolean remove(Object key, Object value) {
        return removeNode(hash(key), key, value, true, true) != null;
    }

看来删除节点的核心为removeNode方法

    /**
     * Implements Map.remove and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to match if matchValue, else ignored
     * @param matchValue if true only remove if value is equal
     *				就是是否拿值也来当做匹配参数
     * @param movable if false do not move other nodes while removing
     * @return the node, or null if none
     */
    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;
        //对空表,散列表的中hash值计算出的位置为null,都视为查找不到,与get中相同。
        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;
            //判断头结点是否为目标,如果是则找到目标删除节点,赋值给node
            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);
                }
            }
            //开始删除了!如果matchValue==true,则需要验证key相同的前提下,value是否相同,如果不相同,则没找到删除节点。
            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;
                //hashmap中此函数为空
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

注意,我在面试的时候被问及退化的问题,我在面试中回答的是,应该会退化成链表,因为退化成链表的话,查询的性能更优,但是我的思考方式是存在有问题的,因为看到了之前一篇博客里的想法:出于平均查找长度考量,在链表值到8的时候,进行树化。

树退化成链表

这个步骤是存在的,就在上面代码的removeTreeNode的方法中

在这里插入图片描述

至于真正的退化的代码,则如下,把树节点装配成链表。

        /**
         * Returns a list of non-TreeNodes replacing those linked from
         * this node.
         */
        final Node<K,V> untreeify(HashMap<K,V> map) {
            Node<K,V> hd = null, tl = null;
            for (Node<K,V> q = this; q != null; q = q.next) {
                Node<K,V> p = map.replacementNode(q, null);
                if (tl == null)
                    hd = p;
                else
                    tl.next = p;
                tl = p;
            }
            return hd;
        }

真正的原因

挂在散列表上的链表长度,小于8的话,平均查找长度基本在O(3)水平,如果超7,平均查找长度就大于O(4)了。故进行树化更能发挥HashMap快速查询的优势。但并不是说短链表的查找性能比红黑树好了,一开始不直接用红黑树是因为转化等消耗不值得。如果仅仅保存少量数据就存红黑树里,会造成空间的浪费。

顺便了解一下存储的结点

链表的结点

    /**
     * Basic hash bin node, used for most entries.  (See below for
     * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
     */
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {}
        public final boolean equals(Object o) {  }
    }

红黑树的结点

    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
        //...红黑树操作省略
    }

TreeNode<K,V> 继承与 LinkedHashMap.Entry<K,V>继承于 HashMap.Node<K,V>

所以占用空间的角度看,红黑树的存储结构比HashMap.Node多不少。因此每月必要一开始就使用红黑树来存储数据。

如有纰漏,感谢指出!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值