• 【深入Java基础】HashMap源码分析


    HashMap源码分析

    在分析源码之前先学习一下Hash(散列表)的相关内容

    符号表是一种用于存储键值对(key-value pair)的数据结构,我们平常经常使用的数组也可以看做是一个特殊的符号表,数组中的“键”即为数组索引,值为相应的数组元素。也就是说,当符号表中所有的键都是较小的整数时,我们可以使用数组来实现符号表,将数组的索引作为键,而索引处的数组元素即为键对应的值,但是这一表示仅限于所有的键都是比较小的整数时,否则可能会使用一个非常大的数组。散列表是对以上策略的一种“升级”,但是它可以支持任意的键而并没有对它们做过多的限定。对于基于散列表实现的符号表,若我们要在其中查找一个键,需要进行以下步骤:

    • 首先我们使用散列函数将给定键转化为一个“数组的索引”,理想情况下,不同的key会被转为不同的索引,但在实际应用中我们会遇到不同的键转为相同的索引的情况,这种情况叫做碰撞。解决碰撞的方法我们后面会具体介绍。

    • 得到了索引后,我们就可以像访问数组一样,通过这个索引访问到相应的键值对。

    以上就是散列表的核心思想,散列表是时空权衡的经典例子。当我们的空间无限大时,我们可以直接使用一个很大的数组来保存键值对,并用key作为数组索引,因为空间不受限,所以我们的键的取值可以无穷大,因此查找任何键都只需进行一次普通的数组访问。反过来,若对查找操作没有任何时间限制,我们就可以直接使用链表来保存所有键值对,这样把空间的使用降到了最低,但查找时只能顺序查找。在实际的应用中,我们的时间和空间都是有限的,所以我们必须在两者之间做出权衡,散列表就在时间和空间的使用上找到了一个很好的平衡点。散列表的一个优势在于我们只需调整散列算法的相应参数而无需对其他部分的代码做任何修改就能够在时间和空间的权衡上做出策略调整。

    关于散列表的概念

    在散列表内部,我们使用桶(bucket)来保存键值对,我们前面所说的数组索引即为桶号,决定了给定的键存于散列表的哪个桶中。散列表所拥有的桶数被称为散列表的容量(capacity)

    现在假设我们的散列表中有M个桶,桶号为0到M-1。我们的散列函数的功能就是把任意给定的key转为[0, M-1]上的整数。我们对散列函数有两个基本要求:一是计算时间要短,二是尽可能把键分布在不同的桶中。对于不同类型的键,我们需要使用不同的散列函数,这样才能保证有比较好的散列效果。

    我们使用的散列函数应该尽可能满足均匀散列假设,以下对均匀散列假设的定义来自于Sedgewick的《算法》一书:

    (均匀散列假设)我们使用的散列函数能够均匀并独立地将所有的键散布于0到M – 1之间。

    以上定义中有两个关键字,第一个是均匀,意思是我们对每个键计算而得的桶号有M个“候选值”,而均匀性要求这M个值被选中的概率是均等的;第二个关键字是独立,它的意思是,每个桶号被选中与否是相互独立的,与其他桶号是否被选中无关。这样一来,满足均匀性与独立性能够保证键值对在散列表的分布尽可能的均匀,不会出现“许多键值对被散列到同一个桶,而同时许多桶为空”的情况。

    以上内容引用自https://www.cnblogs.com/absfree/p/5508570.html

    关于Java中的hashcode参见 java中的hashcode和equals

    继承的类与实现的接口

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

    继承了AbstractMap类;

    实现了Map<K,V>, Cloneable, Serializable三个接口。

    全局变量

    • 初始化容量

      初始化容量为1左移4位(即二进制00001左移4位变为10000=16)

            static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    • 最大容量

      最大容量为2^30

            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;
    • 当桶中的bin被树化时最小的hash表容量。
            static final int MIN_TREEIFY_CAPACITY = 64;

    链表Node节点的定义

    通过这个节点类的定义我们可以看到内部用到了单链表。

        static class Node<K,V> implements Map.Entry<K,V> {
            final int hash;//哈希值
            final K key;//key
            V value;//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;
            }
        }

    分析:

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

    这段代码返回Node的hashCode。调用的是Objects(注意不是Object)>下的hashCode方法:

    public static int hashCode(Object o) {
    return o != null ? o.hashCode() : 0;
    }

    Objcts下的hashCode还是调用的Object下的hashCode:

      public native int hashCode();
    

    这是一个native方法,跟不到源码。一般情况下是返回的对象的实际地址。(但也不一定)

    public final boolean equals(Object o)
        public final boolean equals(Object o) {
            if (o == this)//==为强相等(即引用的是同一个对象)
                return true;
            if (o instanceof Map.Entry) {//如果是Map.Entry的一个实例
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;//强制转换
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))//比较两个key的与两个value是否相等
                    return true;
            }
            return false;
        }
    

    获取hash值

    根据key计算hash值。如果key=null返回0,否则先计算key的hashCode=h然后在将h右移16位,最后在进行异或运算并返回。

        static final int hash(Object key) {
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }

    构造方法

    HashMap有4种构造方法。

    1.指定初始化容量和装在因子

        public HashMap(int initialCapacity, float loadFactor) {
            if (initialCapacity < 0)
                throw new IllegalArgumentException("Illegal initial capacity: " +
                                                   initialCapacity);
            if (initialCapacity > MAXIMUM_CAPACITY)
                initialCapacity = MAXIMUM_CAPACITY;
            if (loadFactor <= 0 || Float.isNaN(loadFactor))
                throw new IllegalArgumentException("Illegal load factor: " +
                                                   loadFactor);
            this.loadFactor = loadFactor;
            this.threshold = tableSizeFor(initialCapacity);
        }

    2.仅指定初始容量

        public HashMap(int initialCapacity) {
            this(initialCapacity, DEFAULT_LOAD_FACTOR);
        }

    3.使用默认值

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

    4.从其他Map实例创建HashMap对象

        public HashMap(Map<? extends K, ? extends V> m) {
            this.loadFactor = DEFAULT_LOAD_FACTOR;
            putMapEntries(m, false);//放入entry,不清除m
        }

    根据key获取value

        public V get(Object key) {
            Node<K,V> e;
            return (e = getNode(hash(key), key)) == null ? null : e.value;
        }
    
        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)//判断是否是TreeNode的一个实例,如果是则按照树的方法获取节点
                        return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                    do {//如果是链表,循环判断key的hashCode
                        if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                            return e;
                    } while ((e = e.next) != null);
                }
            }
            return null;
        }

    判断key是否存在

        public boolean containsKey(Object key) {
            return getNode(hash(key), key) != null;
        }

    put操作

        public V put(K key, V value) {
            return putVal(hash(key), key, value, false, true);
        }
    
        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)
                n = (tab = resize()).length;
            if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
            else {
                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)
                    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                else {
                    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;
                        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;
            if (++size > threshold)
                resize();
            afterNodeInsertion(evict);
            return null;
        }
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)

    这是按照链表存储时的put核心。

    参数:

    hash : key的hash值

    key : 键

    value : 值

    onlyIfAbsent : 仅当键不存在才添加

    evict : 是否回收

    分析:

    大概思路是这样的:首先判断hashtable是否为null,如果为null,则先初始化容量;然后判断头节点是否为null,如果为null则插入节点;如果头节点不为空,先判断是按照链表存储还是按照树的结构存储,如果是按照树的结构存储则就按照树的插入节点的方法插入,否则按照链表的方式插入。在按链表的方式插入时,移动“指针”(即引用),找到链尾,插入节点。在移动过程中会判断当前链表的容量与初始阈值的大小,以此来决定要不要转化为树形结构存储。当发现key以及存在时,跳出循环,然后按照onlyIfAbsent条件以及该key对应的值是否为null来决定要不要插入节点。最后,插入成功返回旧的值,插入失败返回null。

    下面是详细分析:

        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)//tab为空
                n = (tab = resize()).length;//重置大小
            if ((p = tab[i = (n - 1) & hash]) == null)//首节点为null
                tab[i] = newNode(hash, key, value, null);//插入节点
            else {//首节点不为null
                Node<K,V> e; K k;
                if (p.hash == hash &&//这里p即为首节点
                    ((k = p.key) == key || (key != null && key.equals(k))))//是首节点
                    e = p;//e指向首节点
                else if (p instanceof TreeNode)//p不是首节点,如果p是TreedNode的实例,也就是按照树结构存放的话
                    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//按照树的方式put节点
                else {//如果p不是首节点,且是按照链表存放
                    for (int binCount = 0; ; ++binCount) {//遍历链表
                        if ((e = p.next) == null) {//判断p.net是否为null,是则到了链表尾
                            p.next = newNode(hash, key, value, null);//插到尾部
                            if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //节点数量大于阈值TREEIFY_THRESHOLD
                                treeifyBin(tab, hash);//将链表存储转化为树存储
                            break;//结束循环
                        }
                        if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))//如果key存在
                            break;//结束循环
                        p = e;//上边以及赋值e=p.next,这里就相当于p=p.next,移动指针到下一个节点
                    }
                }
                if (e != null) { // existing mapping for key  //如果key存在
                    V oldValue = e.value;//得到旧value
                    if (!onlyIfAbsent || oldValue == null)//如果不是仅当键不存在才插入或者原来的值为null
                        e.value = value;//赋值value给e
                    afterNodeAccess(e);//添加之后的操作
                    return oldValue;//返回旧值
                }
            }
            ++modCount;//操作次数加1
            if (++size > threshold)//判断阈值
                resize();//更改大小
            afterNodeInsertion(evict);//某个操作
            return null;
        }

    另外还有resize()以及常用的remove()等操作,比较复杂,下次在论。

  • 相关阅读:
    jmeter随笔(11)--上传文件接口出错
    初探接口测试框架--python系列1
    jmeter随笔(10)-中文url编码问题
    jmeter随笔(9)--有两种编码风格,导致数据乱码
    jmeter随笔(8)--请求post的 数据为空
    jmeter随笔(7)--查看请求响应,为空
    jmeter随笔(5)--断言中正则表达式的特殊字符问题和中文乱码显示问号的问题
    jmeter随笔(4)--中文url编码问题
    Fiddler录制jmeter脚本,干货分享
    飞测的脚丫,往下踩
  • 原文地址:https://www.cnblogs.com/cnsec/p/13286731.html
Copyright © 2020-2023  润新知