为了账号安全,请及时绑定邮箱和手机立即绑定

HashMap原理解析

标签:
Java 算法

Map在我们工作中是相当常用的一个数据结构,它的实现类HashMap被我们广泛应用,下面我们一起来分析以下它是如何工作的吧!

1.HashMap特点

  • 允许null值,非线程安全
  • 存储采用键值对方式,一键对应一值,key值不可重复。
  • 初始长度为16,默认加载因子为0.75,map长度超过160.75的长度后自动扩容为原有的1.5倍,如再超过当前map长度加载因子的长度时继续扩容
    结构上采用数组加链表的方式,结构如图
    图片描述
    我们再使用HashMap时一般使用的是put(),get(),remove()方法,以及内部扩容机制,下面我们来看一下
    说到HashMap的源码,我们就不得不看一下源码中的全局变量
 /**
     * HashMap的默认初始化大小
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * HashMap长度的最大值
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * HashMap的默认加载因子
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     *链表被转换成红黑树的阈值,意义为当链表的长度大于8时将链表转为红黑树,
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 当红黑树的长度小于等于6时,将红黑树转话为链表
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * 这个值为如果一个元素中挂载的链表太长,便会对整个HashMap中的元素重新计算Hash值,并且对HashMap扩容,但只能是对于当前Map长度小于64时才会生效
     */
    static final int MIN_TREEIFY_CAPACITY = 64;
//当前代码为HashMap中存取元素的对象 next指向的是下一个Node(节点对象)
 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 K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

1.put()方法主要逻辑如下

  • 获取key的Hash值
  • 根据key的Hash值来进行运算获取数组下标,如此时根据数组下标获取无数据,则将数据放入此下标,如根据下标获取出来的有值,则将数据挂载到值的next中。
  • 如数组值的链表中数据超过8个,结构由链表转换为红黑树,如数组值少于8个,则将红黑树转换为链表
  • 当数组中的元素第一次拥有next值时,map进行扩充,再拥有时还扩充,但当map的长度大于或等于64时,数据再挂载到next值时,变不会再进行扩充,只有当数组的长度大于长度*加载因子的长度时才会扩充
    以上是put()方法的主要逻辑,为了方便大家理解,我把源码贴出来,每步标上注释
//当前代码为获取key的hash值,并对hash值进行扰动,扰动的意思为减少hash碰撞
 static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
//当前代码为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;
        //table是HashMap全局变量,table中存储了Node数组,判断table是否为空,如果为空调用resize()方法
        if ((tab = table) == null || (n = tab.length) == 0)
        //resize()方法为对数组的大小进行调整以及链表红黑树转换,并对新的Node数组进行重新划分位置
            n = (tab = resize()).length;
            //如果table数组中不存在这个元素,则直接进行添加
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //如果table数组中存在此数据,则判段他们Key的Hash与equals是否同时相等
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
                //如果从table中获取的数据是TreeNode类型,则调用红黑树put方法,将此数据put到红黑树中
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            //如果从table中获取的数据链中最后一个元素next位置为null,则挂载到最后一个数据上
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //如果从table中获取的数据链的长度大于等于7,则将链表转换称红黑树(或对map进行扩容)
                        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;
                    p = e;
                }
            }
            //如果传入数据中的value,替换掉了原有数据中的value,则将原有value返回
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //如果当前map的实际长度大于长度*加载因子的长度时,则进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

2.get()方法核心逻辑如下

  • 获取传入key的Hash值
  • 根据当前key的Hash值与key本身去匹配,如两者都匹配则返回,如无则返回null
//获取key的hash值
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
//get核心方法
 final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //获取当前table数组,不为空则继续
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //如果当前table不为空,则根据标获取数据,如获取到数据则与传入信息进行比对,如成功则立即结束,减少对数据中next的便利,减少消耗
            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);
                //如果是链表则循环判断next中的数据
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

3.remove()方法核心逻辑如下

  • 获取当前需要删除key值的Hash
  • 根据hash与需要删除key进行比对,同时满足则删除,删除之前需要判断删除元素的next值是否为null,如为空直接将数据置为null,如不为空则将next中的值放在当前位置
//获取当前key的Hash值
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
//删除核心方法
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;
        //判断table是否为空长度是否为大于0,根据下标获取值是否可以获取到,获取不到直接返回null
        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) {
                //如p是红黑树类型,则调用红黑树获取节点方法
                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);
                }
            }
            //如果node不为null,这个if一定会进,用于判断如果node节点中next中有数据,则用next直接覆盖node原有位置
            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);
                //node中的next直接覆盖元素
                else if (node == p)
                    tab[index] = node.next;
                //此时node的值为p.next,故如删除node,则将node.next赋值给p.next就可以
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

至此HashMap的主要使用方法分析完毕,如有错误欢迎指正,共同学习

大家可以想一个问题,为什么当链表的长度大于8时要转换成红黑树,而不是转换成二叉树呢,欢迎留言

码字不易,觉得不错点赞哦!

点击查看更多内容
1人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消