https://img2020.cnblogs.com/blog/1101843/202010/1101843-20201029092119794-1182278230.jpg

支付宝

https://img2020.cnblogs.com/blog/1101843/202010/1101843-20201029091740174-1560674849.png

微 信

HashMap

 

HashMap是Map接口的实现类,以key-value存储形式存储数据。HashMap的操作不是同步的,所以线程不安全。

 

特点:

无序性 : 存入取出元素顺序不一致

唯一性 : key唯一

可存null : 键和值都可以为null,键只能有一个为null

数据结构 : 数据结构控制的是key而非值value

 

HashMap类的继承关系

 

 

 

说明:
Cloneable 空接口,表示可以克隆。 创建并返回HashMap对象的一个副本。

Serializable 序列化接口。属于标记性接口。HashMap对象可以被序列化和反序列化。

AbstractMap 父类提供了Map实现接口。以最大限度地减少实现此接口所需的工作。

补充:HashMap已经继承了AbstractMap而 AbstractMap类实现了Map接口,为什么HashMap还要在实现Map接口呢?同样在ArrayList中 LinkedList中都是这种结构。

 

 

 据java集合框架的创始人Josh Bloch描述,这样的写法是一个失误。最开始他认为这样写在某些地方可能是有价值的。在java集合框架中,类似这样的写法很多。

JDK的维护者不认为这个小小的失误值得去修改,所以就这样存在下来了。

 

HashMap原理分析 

什么是哈希表?
哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。

它通过把关键码值映射到表中一个位置来访问记录,以加快查找速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

哈希表本质上是一个数组,这个数组中存储的是哈希函数算出的值。
目的 : 为了加快数据查找的速度。

 

 

 

 HashMap存储数据过程 

加载因子 : 默认值是0.75 ,决定了扩容的条件

// 加载因子 
final float loadFactor;

扩容的临界值 : 计算方式为(容量 乘以 加载因子) 

// 临界值 当实际大小超过临界值时,会进行扩容 
int threshold;

容量capacity : 初始化为16

扩容resize : 达到临界值就扩容。扩容后的 HashMap 容量是之前容量的两倍 。
集合元素个数size : 表示HashMap中键值对实时数量,不等于数组长度。

 

jdk8存储过程

 

 

存储过程源码

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
    //1.判断是否哈希表为空
    if ((tab = table) == null || (n = tab.length) == 0)        
         //2.如果为空初始化容量,16
        n = (tab = resize()).length;
     //3.如果不为空 , 则判断当前key的hash值对应的索引位置是否有元素。
     if ((p = tab[i = (n - 1) & hash]) == null)
        //4.如果没有,往当前索引位置放入一个新的节点
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k; 
       //5.如果有元素,判断当前索引位的节点hash值和equals与新key是否相等
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            //如果相等,则覆盖value
            e = p;
        //6.如果不相等,则判断是否是红黑树
        else if (p instanceof TreeNode)
            //如果是红黑树节点,则将元素存入红黑树节点
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            //7.如果不相等,也不是红黑树节点,则遍历所有链表节点
            for (int binCount = 0; ; ++binCount) {
                //如果到了后一个节点还没找到相等的节点
                if ((e = p.next) == null) {
                    //在尾部新增一个节点
                    p.next = newNode(hash, key, value, null);
                    //8.判断链表的长度是否大于8
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        //如果大于8直接将链表转换为红黑树
                        treeifyBin(tab, hash);
                                        break;
                }
                //如果遍历的节点的hash值和equals值与新key相同,则跳出循环
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //如果key存在,则直接覆盖value值
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    //判断HashMap中节点数是否大于临界值,如果大于则扩容,是之前的两倍
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
 }

 

 HashMap底层数据结构 

 

jdk1.8之前数据结构是:链表 + 数组

jdk1.8之后数据结构是:链表 + 数组  + 红黑树 。单链表阈值(边界值) > 8 且数组长度大于64,才将链表转换为红黑树。 目的 : 高效查询数据

扩展知识: 红黑树(Red Black Tree) 是一种自平衡二叉查找树,是在计算机科学中用到的一种数 据结构,典型的用途是实现关联数组。红黑树是在1972年由Rudolf Bayer发明的,当时被称为平 衡二叉B树(symmetric binary B-trees) 

数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。

什么是哈希冲突?两个对象调用的hashCode方法计算的哈希码值一致导致计算的数组索引值相同。
JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(或者红黑树的边界值,默认为 8)并且当前数组的长度大于64时,此时此索引位置上的所有数据改为使用红黑树存储。

JDK1.8引入红黑树大程度优化了HashMap的性能,那么对于我们来讲保证HashSet集合元素的唯一,其 实就是根据对象的hashCode和equals方法来决定的。

如果我们往集合中存放自定义的对象,那么保证 其唯一,就必须复写hashCode和equals方法建立属于当前对象的比较方式。 当位于一个链表中的元素较多,即hash值相等但是内容不相等的元素较多时,通过key值依次查找的效 率较低。

而JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度(阀值)超过 8 时且当前数组 的长度 > 64时,将链表转换为红黑树,这样大大减少了查找时间。jdk8在哈希表中引入红黑树的原因只 是为了查找效率更高。
简单的来说,哈希表是由数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的。如下图所示。

 

HashMap中哈希表的数组的大小?

创建HashMap集合对象时

JDK8前,构造方法创建一个长度是16的数组Entry[] table 来存储键值对的对象。   

JDK8后,不是在构造方法中创建对象数组,而是在第一调用put方法时创建长度是16的Node[] table数组,存储Node对象

如果节点长度即链表长度大于阈值8,并且数组长度大于64则进行将链表变为红黑树。

数据结构的源码
table用来初始化(必须是二的n次幂)(重点)

//存储元素的数组
 transient Node<K,V>[] table;

用来存缓存

//存放具体元素的集合
 transient Set<Map.Entry<K,V>> entrySet;

HashMap中存放元素的个数(重点)

//存放元素的个数,注意这个不等于数组的长度。
 transient int size;

 

HashMap源码分析

初始化容量16

 

//默认的初始容量是16 -- 1<<4相当于1*2的4次幂---1*16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

 

初始化容量必须是2的n次幂,为什么?

向HashMap中添加元素时,要根据key的hash值去确定其在数组中的具体位置。

HashMap为了存取高效,要尽量较少碰撞,就是要尽量把数据分配均匀,每个链表长度大致相同。

怎么让元素均匀分配呢?

这里用到的算法是hash&(length-1)。hash值与数组长度减一的位运算。算法本质作用是类似于取模, hash%length。

但是计算机中直接求余效率远不如位运算。 hash%length取模效果操作等于hash&(length-1)的前提是length是2的n次幂

如果不考虑效率问题,求余即可。就不需要长度必须是2的n次幂了。如果采用位运算,必须 是2的n次幂!

为什么这样能均匀分布减少碰撞呢?

2的n次幂实际就是1后面n个02的n次幂-1 实际就是n个1

举例:位运算规则说明:按&位运算(相同位的两个数字都为1,则为1;若有一个不为1,则为0)。

例如 : 数组长度8时候,均匀分布在数组中,哈希碰撞的几率比较小;
求位运算结果:
314924944 & (8-1) = 0
00010010110001010101111110010000
00000000000000000000000000000111
--------------------------------------------------
00000000000000000000000000000000 --> 结果为0
程序员计算器求解 :
314924944 & (8-1) = 0
314924945 & (8-1) = 1
314924946 & (8-1) = 2
314924947 & (8-1) = 3
314924948 & (8-1) = 4
314924949 & (8-1) = 6
314924950 & (8-1) = 7
314924951 & (8-1) = 8
314924952 & (8-1) = 0
结论是:数组索引存储的数据均匀分布了,减少哈希碰撞的几率
例如 : 数组长度10时候,没有均匀分布,碰撞几率比较大;
程序员计算器求解 :
314924944 & (10-1) = 0
314924945 & (10-1) = 1
314924946 & (10-1) = 0
314924947 & (10-1) = 1
314924948 & (10-1) = 0
314924949 & (10-1) = 1
314924950 & (10-1) = 0
314924951 & (10-1) = 1
314924952 & (10-1) = 0
结论是:数据全部分布在第一个和第二个索引位置上,大大增加了哈希碰撞的几率。效率低下

HashMap构造方法还可以手动设置初始化容量大小:

//构造一个带指定初始容量和默认加载因子 (0.75) 的空HashMap
HashMap(int initialCapacity) 

如果创建 HashMap对象时,手动设置的数组长度不是2的n次幂,HashMap通过位移运算和或运算得到 离那个数最近的数字2的幂次数。

//创建HashMap集合的对象,指定数组长度是10,不是2的幂
HashMap hashMap = new HashMap(10);
public HashMap(int initialCapacity) {//initialCapacity=10
  this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {//initialCapacity=10
  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);//initialCapacity=10
}
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {//int cap = 10
  int n = cap - 1;
  n |= n >>> 1;
  n |= n >>> 2;
  n |= n >>> 4;
  n |= n >>> 8;
  n |= n >>> 16;
  return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

 

假如初始化容量设为10,最终容量会变为最近的16!

 

 小结:

1. 根据key的hash确定存储位置时,数组长度是2的n次幂,可以保证数据的均匀插入。如果不是,会浪费数组的空间,降低集合性能!

2. 一般情况下,我们通过求余%来均匀分散数据。只不过其性能不如位运算【&】。

3. length的值为2的n次幂,hash & (length - 1) 作用完全等同于hash % length。

4. HashMap中初始化容量为2次幂原因是为了数组数据均匀分布。尽可能减少哈希冲突,提升集合性能。

5. 即便可以手动设置HashMap的初始化容量,但是最终还是会被重设为2的n次幂。

 

posted @ 2021-04-02 17:18  huangwanlin  阅读(79)  评论(0编辑  收藏  举报
Copyright 2012-2021 林云希科技有限责任公司