栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

【HashMap源码】jdk1.7/jdk1.8 HashMap源码解读

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

【HashMap源码】jdk1.7/jdk1.8 HashMap源码解读

jdk1.7/jdk1.8 HashMap源码解读 HashMap实现原理

基于hashing原理,通过put(key,value)和get(key)方法存储和获取对象

当我们存储对象的时候,把键值对传给put(key,value)方法时,它调用键对象key的hashCode()方法来计算hashCode,然后找到bucket位置,来存储值对象value。

当获取对象时,也是先计算key的hashCode,找到数组中对应位置的bucket位置,然后通过key的equals()方法找到正确的键值对key-value,然后返回值对象value。

HashMap里有hash碰撞(冲突),HashMap使用链表来解决碰撞问题,当发生了碰撞,对象将会存储在链表的下一个节点中。

HashMap的每个链表中存储的是key-value对象,当两个不同的键对象key的hashCode相同时,那就发生了碰撞(冲突),那它会存储在同一个bucket的位置,bucket的位置存不下这两个点,那么就通过链表来存储。(在jdk8里,链表长度大于8的话就会变成红黑树)

取数据可通过键对象key的equals()方法用来找到正确的键值对key-value。

HashMap底层数据结构

JDK8以前的HashMap的底层数据结构是: 数组+链表,它之所以有相当快的查询速度,主要是因为它是通过Key计算hashCode来决定一维数组中存储的位置,而增删速度靠的是链表保证。

简单来说,HashMap由数组+链表组成的,Entry数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

JDK8中HashMap的底层数据结构:用**数组+链表+红黑树的结构来优化,链表长度大于8同时满足HashMap中元素个数大于64则 变红黑树**,长度小于6变回链表

什么是Hash表?

散列表(Hash Table,也叫哈希表)通过**关键码值(Key Value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找速度,这个映射函数叫做散列函数**,存放记录的数组叫做散列表。hash表里可以存储元素的位置叫做bucket(桶)。 什么是哈希冲突,如何解决?

不同的key值产生了相同的Hash地址,H(key1) == H(key2)解决方案:

开放地址法:不断地探测序列,查找一个空的单元进行插入。有线性探测、再平方、伪随机链地址法:对于相同的hashCode值,使用链表进行连接。使用数组来存储每一个链表。HashMap中使用的方案。公共溢出区法:建立一个特殊的存储空间,专门存储这些冲突的数据,适用数据冲突比较少的情况。再散列法:准备若干个hash函数,如果使用第一个hash函数发生了冲突,就使用第二个hash函数 以此类推。 HashMap源码读过吗? 核心成员变量有哪些知道吗? JDK7中HashMap源码之核心变量

    Entry[] table 这个Entry类型的数组存储了HashMap的真正数据。size大小 代表HashMap内存储了多少个键值对。capacity容量 实际上HashMap没有一个成员叫capacity,它是作为table这个数组的大小而隐式存在的。threshold阈值和loadFactor装载因子 threshold是通过capacity*loadFactor得到的(threshold = capacity * loadFactor)。当size超过threshold时(刚好相等时不会扩容),HashMap扩容会再次计算每个元素的哈希位置。entrySet、KeySet和values这三个都是一种视图,真正的数据来自table
JDK8中HashMap源码之核心变量
    Node[] table 这个Node类型的数组存储了HashMap的真正数据 static class Node implements Map.Entry。size大小 代表HashMap内存储了多少个键值对。capacity容量 实际上HashMap没有一个成员叫capacity,它是作为table这个数组的大小而隐式存在的。threshold阈值和loadFactor装载因子 threshold是通过capacity*loadFactor得到的(threshold = capacity * loadFactor)。当size超过threshold时(刚好相等时不会扩容),HashMap会扩容,哈希计算比之前有改进。entrySet、KeySet和values这三个都是一种视图,真正的数据来自tableTREEIFY_THRESHOLD 树化阈值 转换成红黑树的临界值,默认8UNTREEIFY_THRESHOLD 反树化阈值 红黑树转换成链表的临界值,默认6MIN_TREEIF_CAPACITY 链表被树化的最小数组容量 最小树形化阈值 默认64
为什么到8转为红黑树 到6转为链表?

为什么链表长度为8转为红黑树?

TreeNode(红黑树中)占用空间是普通Node(链表中)的两倍,为了时间和空间的权衡。节点的分布频率会遵循泊松分布,链表长度达到8个元素的概率为0.00000006

为什么链表长度为6,红黑树就转为链表呢?

若是7,则当极端情况下(频繁插入和删除的都是同一个哈希桶)对一个链表长度为8的哈希桶进行频繁的删除和插入,同样也会导致频繁的树化<=>非树化 插入和获取数据的过程清楚吗?

JDK1.7中HashMap插入的过程:— put操作

    public V put(K key, V value) {
	    // 步骤1
        if (table == EMPTY_TABLE) {	//判断如果表空进行初始化
            inflateTable(threshold);
        }
        // 步骤2
        if (key == null)		//判断是否键为空
            return putForNullKey(value);	//遍历table[0]是否存在 e.key == null
        // 步骤3
        int hash = hash(key);	//计算hash值
        int i = indexFor(hash, table.length);	//计算bucket的位置
        for (Entry e = table[i]; e != null; e = e.next) {	//遍历链表
            Object k;
            // hash相同 equals比较是真 则替换 返回旧值
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(hash, key, value, i);	//添加数据 这里包含hash冲突的处理。
        return null;
    }

jdk7 中的hash函数处理

final int hash(Object k) {
 
    int h = hashSeed;
//如果为干扰因子不为0,且传入的key类型为String,则使用特定的算法(sun.misc.Hashing.stringHash32((String) k))对该key进行hash计算。并返回
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }
//如果哈希干扰因子为0 或者 k的类型不为String则使用异或操作变更key的hashcode
    h ^= k.hashCode();

//为了减少Hash冲突出现次数进行必要的位干扰,默认负载因子是8.
    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

扰动实现复杂,一共九次扰动: 5次异或 加上4次位运算

void addEntry(int hash, K key, V value, int bucketIndex) {
      if ((size >= threshold) && (null != table[bucketIndex])) {
          //当size大小大于或等于阈值的时候,将会进行扩容操作,将数据元素重新计算位置后放入newTable中,Hash位置会重新计算
          resize(2 * table.length);	//2倍扩容
          hash = (null != key) ? hash(key) : 0;
          bucketIndex = indexFor(hash, table.length);
      }
    
      createEntry(hash, key, value, bucketIndex);	//创建Entry的时候会有hash冲突的代码
}

不小于threshold阈值直接2倍扩容,Hash位置会重新计算。

void createEntry(int hash, K key, V value, int bucketIndex) {
      Entry e = table[bucketIndex];
    //根据头节点构建----头插法的实现
      table[bucketIndex] = new Entry<>(hash, key, value, e);
      size++;
}
Entry(int h, K k, V v, Entry n) {
    value = v;
    next = n;
    key = k;
    hash = h;
}

哈希冲突时链表插入数据使用头插(JDK1.7)

JDK7中获取数据的过程—get操作

 //获取key值为key的元素值  
    public V get(Object key) {  
        if (key == null)//如果Key值为空,则获取对应的值,这里也可以看到,HashMap允许null的key,其内部针对null的key有特殊的逻辑  
            return getForNullKey();  
        Entry entry = getEntry(key);//获取实体  

        return null == entry ? null : entry.getValue();//判断是否为空,不为空,则获取对应的值  
    }  

    //获取key为null的实体  
    private V getForNullKey() {  
        if (size == 0) {//如果元素个数为0,则直接返回null  
            return null;  
        }  
        //key为null的元素存储在table的第0个位置  
        for (Entry e = table[0]; e != null; e = e.next) {  
            if (e.key == null)//判断是否为null  
                return e.value;//返回其值  
        }  
        return null;  
    }  

get方法通过key值返回对应value,如果key为null,直接去table[0]处检索。

//获取键值为key的元素  
    final Entry getEntry(Object key) {  
        if (size == 0) {//元素个数为0  
            return null;//直接返回null  
        }  

        int hash = (key == null) ? 0 : hash(key);//获取key的Hash值  
        for (Entry e = table[indexFor(hash, table.length)];//根据key和表的长度,定位到Hash桶  
             e != null;  
             e = e.next) {//进行遍历  
            Object k;  
            if (e.hash == hash &&  
                ((k = e.key) == key || (key != null && key.equals(k))))//判断Hash值和对应的key,合适则返回值  
                return e;  
        }  
        return null;  
    }  

JDK8中HashMap源码插入的过程:

    首先判断table成员是否初始化,如果没有,则调用resize扩容通过传入键值对的key的hashCode和容量,马上得到了该映射所在的table数组下标。并通过数组的取下标操作,得到该哈希桶的头节点。如果没有发生哈希碰撞(头节点为null),那么直接执行新增操作。如果发生了哈希碰撞(头节点不为null),那么分为两种情况:
      如果与桶内某个元素==返回True,或者equals判断相同,执行替换操作(图中的直接覆盖value)如果与桶内所有元素判断都不相等,执行新增操作,可能是链表也可能是红黑树的插入。
    链表新增操作后,会有两个判断:
      如果哈希桶是单链表结构,且桶内节点数量超过了TREEIFY_THRESHOLD(8),且size大于等于了MIN_TREEIFY_CAPACITY(64),那么将该哈希桶转换成红黑树结构。(图中链表长度是否大于8)链表长度不大于8时,如果新增后size大于了threshold,那么调用resize扩容。
static final int hash(Object key) { // 计算key的hash值
    int h;
    // 1.先拿到key的hashCode值; 2.将hashCode的高16位参与运算
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//进行异或
}

将key的hashCode与该hashCode的无符号右移16位,异或起来得到的。

扰动实现简单 一共2次扰动 1次是异或 加上1次位运算

jdk8中hashmap 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[] tab; Node p; int n, i;
    // 1.校验table是否为空或者length等于0,如果是则调用resize方法进行初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 2.通过hash值计算索引位置,将该索引位置的头节点赋值给p,如果p为空则直接在该索引位置新增一个节点即可
    if ((p = tab[i = (n - 1) & hash]) == null)	//头节点是否为空
        tab[i] = newNode(hash, key, value, null);
    else {
        // table表该索引位置不为空,则进行查找
        Node e; K k;
        // 3.判断p节点的key和hash值是否跟传入的相等,如果相等, 则p节点即为要查找的目标节点,将p节点赋值给e节点
        if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 4.判断p节点是否为TreeNode, 如果是则调用红黑树的putTreeval方法查找目标节点
        else if (p instanceof TreeNode)
            e = ((TreeNode)p).putTreeval(this, tab, hash, key, value);
        else {
            // 5.走到这代表p节点为普通链表节点,则调用普通的链表方法进行查找,使用binCount统计链表的节点数
            for (int binCount = 0; ; ++binCount) {
                // 6.如果p的next节点为空时,则代表找不到目标节点,则新增一个节点并插入链表尾部
                if ((e = p.next) == null) {		//一直遍历到链表的尾部,因为链表的尾部是null
                    p.next = newNode(hash, key, value, null);
                    // 7.校验节点数是否超过8个,如果超过则调用treeifyBin方法将链表节点转为红黑树节点,
                    // 减一是因为循环是从p节点的下一个节点开始的
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        treeifyBin(tab, hash);
                    break;
                }
                // 8.如果e节点存在hash值和key值都与传入的相同,则e节点即为目标节点,跳出循环
                if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;  // 将p指向下一个节点
            }
        }
        // 9.如果e节点不为空,则代表目标节点存在,使用传入的value覆盖该节点的value,并返回oldValue
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e); // 用于linkedHashMap
            return oldValue;
        }
    }
    ++modCount;
    // 10.如果插入节点后节点数超过阈值,则调用resize方法进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);  // 用于linkedHashMap
    return null;
}

JDK8 hashmap中的get方法

    调用key的hashCode方法,根据返回值定位到map里数组对应的下标判断这个数组下标对应的头节点是不是为null,如果是,返回null如果头节点不是null,判断这个引用对应对象的key值的equals方法,跟查询的key值进行对比,判断是否为true,如果为true则返回这个对象的value值,否则继续遍历下一个节点。如果遍历完map中所有的节点都无法满足上面的判断 则返回null
public V get(Object key) {
    Node e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;	//计算hash值
}
 
final Node getNode(int hash, Object key) {
    Node[] tab; Node first, e; int n; K k;
    // 1.对table进行校验:table不为空 && table长度大于0 && 
    // table索引位置(使用table.length - 1和hash值进行位与运算)的节点不为空
    if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {//判断头节点是否为null
        // 2.检查first节点的hash值和key是否和入参的一样,如果一样则first即为目标节点,直接返回first节点
        if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))// always check first node
            return first;
        // 3.如果first不是目标节点,并且first的next节点不为空则继续遍历
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                // 4.如果是红黑树节点,则调用红黑树的查找目标节点方法getTreeNode
                return ((TreeNode)first).getTreeNode(hash, key);
            do {
                // 5.执行链表节点的查找,向下遍历链表, 直至找到节点的key和入参的key相等时,返回该节点
                if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    // 6.找不到符合的返回空
    return null;
}
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/777292.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号