HashMap在JDK1.7和1.8主要区别

HashMap在JDK1.7和1.8主要区别

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

首先从声明上来看,HashMap继承自AbstractMap 实现了Map、Cloneable、Serializable接口,点开AbstractMap 源码,发现AbstractMap 也实现了Map接口,那么HashMap为什么继承了AbstractMap 又要实现Map?完全无法解释的通,其实这就是类库设计者的写法错误。可以参考:http://stackoverflow.com/questions/2165204/why-does-linkedhashsete-extend-hashsete-and-implement-sete回答。

HashMap在不同的JDK版本中底层的数据结构也不同,1.7的是数组+链表的实现方式,而1.8变成了数组+链表+红黑树的数据结构(当链表的长度大于8,转为红黑树)。

1.JDK1.7

简单描述一下HashMap的存值过程:

首先HashMap是数组+链表的数据结构。

  1. 当向HashMap中插入键值对的时候,首先会计算出key的hash值,然后根据hash值插入到数组相应的数组下标处。
  2. 一个数组元素=一个键值对=一个链表的头节点(hash,key,value,next)next表示下一个节点对象。
  3. 当数组下标中有元素的时候,则需要将原元素移动到链表中,冲突hash值对应的键值对放入数组元素中。(这和jdk1.8不同)

在这里插入图片描述

2.JDK1.8

此版本下HashMap的数据结构改变成数组+链表+红黑树(当链表长度大于8时,链表会转换为红黑树实现)。

为什么要引入红黑树呢:当链表的长度太长时,会影响HashMap的查询效率。时间复杂度O(n)。此时利用红黑树快速增删改查的特点将时间复杂度降为O(logn)

在这里插入图片描述

2.1 存储流程

在这里插入图片描述

2.2 实际存储对象

HashMap中数组的元素以及链表节点都是Node类实现与1.7相比(Entry)只不过是换了名字

/** 
  * Node  = HashMap的内部类,实现了Map.Entry接口,本质是 = 一个映射(键值对)
  * 实现了getKey()、getValue()、equals(Object o)和hashCode()等方法
  **/  

  static class Node<K,V> implements Map.Entry<K,V> {

        final int hash; // 哈希值,HashMap根据该值确定记录的位置
        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;
        }

      /** 
        * equals()
        * 作用:判断2个Entry是否相等,必须key和value都相等,才返回true  
        */
        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;
        }
    }

2.3 相关参数

 /** 
   * 主要参数 同  JDK 1.7 
   * 即:容量、加载因子、扩容阈值(要求、范围均相同)
   */

  // 1. 容量(capacity): 必须是2的幂 & <最大容量(2的30次方)
  static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认容量 = 16 = 1<<4 = 00001中的1向左移4位 = 10000 = 十进制的2^4=16
  static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量 =  2的30次方(若传入的容量过大,将被最大值替换)

  // 2. 加载因子(Load factor):HashMap在其容量自动增加前可达到多满的一种尺度 
  final float loadFactor; // 实际加载因子
  static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认加载因子 = 0.75

  // 3. 扩容阈值(threshold):当哈希表的大小 ≥ 扩容阈值时,就会扩容哈希表(即扩充HashMap的容量) 
  // a. 扩容 = 对哈希表进行resize操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数
  // b. 扩容阈值 = 容量 x 加载因子
  int threshold;

  // 4. 其他
  transient Node<K,V>[] table;  // 存储数据的Node类型 数组,长度 = 2的幂;数组的每个元素 = 1个单链表
  transient int size;// HashMap的大小,即 HashMap中存储的键值对的数量
 

  /** 
   * 与红黑树相关的参数
   */
   // 1. 桶的树化阈值:即 链表转成红黑树的阈值,在存储数据时,当链表长度 > 该值时,则将链表转换成红黑树
   static final int TREEIFY_THRESHOLD = 8; 
   // 2. 桶的链表还原阈值:即 红黑树转为链表的阈值,当在扩容(resize())时(此时HashMap的数据存储位置会重新计算),在重新计算存储位置后,当原有的红黑树内数量 < 6时,则将 红黑树转换成链表
   static final int UNTREEIFY_THRESHOLD = 6;
   // 3. 最小树形化容量阈值:即 当哈希表中的容量 > 该值时,才允许树形化链表 (即 将链表 转换成红黑树)
   // 否则,若桶内元素太多时,则直接扩容,而不是树形化
   // 为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
   static final int MIN_TREEIFY_CAPACITY = 64;
  

2.4 加载因子

在这里插入图片描述

2.5 hash计算原理

​ 在将元素插入到HashMap集合中之前需要对元素进行hash计算,那么hash值的计算是如何得来的?以及为什么要使用这种方式来计算hash值?

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

插入时首先判断key是否为null,(这表示hashmap可以存储null),如果不为null,先把hashcode值赋值给hash,然后将hash无符号位右移16位,在将结果和原来的值异或运算。

//h=key.hashCode()
0110 1101 0110 1111 0110 1110 0010 1000
//无符号右移16位,其实相当于把低位16位舍去,只保留高16位
0000 0000 0000 0000 0110 1101 0110 1111
//然后高16位和原 h进行异或运算
0110 1101 0110 1111 0110 1110 0010 1000
^
0000 0000 0000 0000 0110 1101 0110 1111
=
0110 1101 0110 1111 0000 0011 0100 0111

可以看到,其实相当于,我们把高16位值和当前h的低16位进行了混合,这样可以尽量保留高16位的特征,从而降低哈希碰撞的概率。

那么为什么这样会降低哈希碰撞的概率呢?

首先将元素放入数组中是通过对数组的长度取模运算,例数组的长度是16,那么18在数组中的位置应该为18%16=2,那么hashMap中put方法如何进行这样的一个过程?

//这是 put 方法中用来根据hash()值寻找在数组中的下标的逻辑,
//n为数组长度, hash为调用 hash()方法混合处理之后的hash值。
i = (n - 1) & hash
    
    
//18的2进制为
0001 0010
//(n-1)=15
0000 1111
//与运算
0000 0010=2

同样也是2,由于n是2的n次幂,所以n-1的二进制低位都是1,那么无论什么值与它进行与 运算最大值也只可能是n-1,最小值为0。因此,这个运算就可以实现取模运算,而且位运算还有个好处,就是速度比较快。

现在解释为什么要进行高低位异或运算来获取hash值

//如果直接通过原来的h与n-1进行&运算,并且此时有另外一个数h2 它的低位和h基本相同,但是高位有很大差异
h  :0110 1101 0110 1111 0110 1110 0010 1000
h2 :0101 0110 1001 1010 0101 1101 0010 1000
n-1:0000 0000 0000 0000 0000 0000 0000 1111
//他们与n-1的&运算的值都是相同的,就是因为h和h2的高位特征完全 没有考虑进去,只有通过 (h = key.hashCode()) ^ (h >>> 16) 将高位和低位进行异或运算这样低位也保留了高位的特征,大大降低冲突的概率。

那么为什么要进行异或运算而不进行与运算、或运算?通过分析这几种运算的运算过程来解释:

与: 0 0 1 1 或: 0 0 1 1 异或: 0 0 1 1

​与: 0 1 1 0 或: 0 1 1 0 异或: 0 1 1 0

值: 0 0 1 0 值: 0 1 1 1 结果: 0 1 0 1

值的比例(0:1)分别是 3:1(与) 1:3(或) 1:1(异或)

可以看出只有进行异或运算取值的比例才是最平衡的,所以,异或运算之后,可以让结果的随机性更大,而随机性大了之后,哈希碰撞的概率当然就更小了。

3.总结

  1. 底层的数据结构不一样:1.7数据结构为数组+链表的实现方式;1.8数据结构为数组+链表+红黑树的实现方式。
  2. JDK1.8中resize()方法在表为空时,创建表;在表不为空时,扩容;而JDK1.7resize()方法只负责扩容,inflateTable()负责创建表。
  3. 1.7新增节点是采用头插法,而1.8是采用尾插法
  4. 在扩容的时候:1.7在插入数据之前扩容,而1.8插入数据成功之后扩容。

在这里插入图片描述
图片转载自:https://blog.csdn.net/carson_ho/article/details/79373134

关于HashMap的源码详细分析:https://blog.csdn.net/carson_ho/article/details/79373134

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值