HashMap在JDK1.8和JDK1.7的区别(详解)

HashMap在JDK1.8与JDK1.7的主要区别在于底层数据结构,1.7使用数组+链表,而1.8引入了红黑树,当链表长度大于8且数组长度大于等于64时转为红黑树,以提高查找效率。HashMap的put方法、扩容机制resize()以及遍历方式有所不同,初始容量通常设置为预期元素数量除以负载因子加1,并调整为2的n次幂以优化性能。
摘要由CSDN通过智能技术生成

HashMap在JDK1.8和JDK1.7的区别

结论

先说结论,HashMap在1.7和1.8中最大的区别就是底层数据结构的变化,在1.7中HashMap采用的底层数据结构是数组+链表的形式,而在1.8中HashMap采用的是数组+链表+红黑树的数据结构(当链表长度大于8且数组长度大于等于64时链表会转成红黑树,当长度低于6时红黑树又会转成链表),红黑树是一种平衡二叉搜索树,它能通过左旋、右旋、变色保持树的平衡,关于红黑树大家想了解的可以自行百度,这里不再讲述。之所以用红黑树是因为他能够大大提高查找效率,链表的时间复杂度是O(n)而红黑树的时间复杂度是O(logn),那么为啥要链表长度大于8且数组长度大于64才转成红黑树呢,简单来说就是节点太少的时候没必要转换数据结构,因为不仅转换数据结构需要浪费时间同时也要浪费空间。而为什么不直接一直用红黑树呢?这是因为树的结构太浪费空间,只有节点足够多的情况下用树才能体现出它的优势,而如果在节点数量不够多的情况下用链表的效率更高,占用的空间更小。

HashMap是如何存放元素的呢

hnYoyF.png

当我们向HashMap中存放数据时,首先会根据key的hashCode方法计算出值,然后结合数组长度和算法(如取余法、位运算等)来计算出向数组存放数据的位置索引值。如果索引位置不存在数据的话则将数据放到索引位中;如果索引位置已经存在数据,这时就发生了hash碰撞(两个不同的原始值在经过哈希运算后得到相同的结果),为了解决hash碰撞,JDK1.8前用的是数组+链表的形式,而JDK1.8后用的是数组+链表+红黑树,这里以链表为例。由于该位置的hash值和新元素的hash值相同,这时候要比较两个元素的内容如果内容相同则进行覆盖操作,如果内容不同则继续找链表中的下一个元素进行比较,以此类推如果都没重复的则在链表中新开辟一个空间存放元素。以上图为例,假设张三、王五、赵六、王五对应的数组数组下标是1,李四对应的数组下标是2。刚开始索引位置1为空,(张三,15)直接放入1位置,索引位置 2为空,(李四,22)直接放入2位置,(王五,18)发现索引位置1已经有数据,这时候调用equals方法和张三进行内容比较,发现内容不同,链表开辟新空间存放数据;(赵六,25)发现1位置已经有元素,调用equals和张三比较,内容不同,继续向下和王五比较,内容不同,开辟新空间存放数据;(王五,28)发现1位置已经有元素,调用equals和张三进行内容比较,不同,继续向下和(王五,18)比较,发现内容也相同,这时候则进行覆盖操作将原来的(王五,18)改成(王五,28)。

HashMap介绍

1.HashMap hashMap = new HashMap();

当创建一个HashMap集合对象的时候,在JDK8之前,构造方法中创建了一个长度是16的Entry[]table用来存储键值对数据。在JDK8后就不在HashMap的构造方法中创建数组了,而是在第一次调用put方法时创建数组Node[] table,用这个数组来存储键值对数据的。

初始容量大小介绍

说到数组就不得不提HashMap里面的成员变量DEFAULT_INITIAL_CAPACITY也就是容量大小,如果不指定的话默认是16,如果通过有参构造指定容器大小的话也必须是2的平方数,当然了如果传入的参数并不是2的平方数的话(最好不要这样,实在不知道写多少容量我们直接写个默认的大小16),HashMap会通过方法将它变成比这个数大且离2的平方数最近的数例如传入11他会变成16,之所以要这么做的原因是,根据上述讲解我们已经知道,当向 HashMap 中添加一个元素的时候,需要根据 key 的 hash 值,去确定其在数组中的具体位置。HashMap 为了存取高效,减少碰撞,就是要尽量把数据分配均匀,每个链表长度大致相同,这个实现的关键就在把数据存到哪个链表中的算法。这个算法实际就是取模,hash % length,计算机中直接求余效率不如位移运算。所以源码中做了优化,使用 hash & (length - 1),而实际上 hash % length 等于 hash & ( length - 1) 的前提是 length 是 2 的 n 次幂。

上面提到了如果传入的指定大小并不是2的n次幂,HashMap会将大小变成比指定大小大且是2的n次幂的数,我们看看JDK1.7和JDK1.8是如何做的。

JDK1.7

   */
    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);

        // Find a power of 2 >= initialCapacity
        int capacity = 1;
        // 从1开始不停的左移也就是变大2的n次幂,直到不比指定容量小
        // 1  2  4  8  16  32.。。。
        while (capacity < initialCapacity)
            capacity <<= 1;

        this.loadFactor = loadFactor;
        threshold = (int)(capacity * loadFactor);
        table = new Entry[capacity];
        init();
    }

JDK1.8

 // 假设cap为13
    static final int tableSizeFor(int cap) {
   
        // n=12 int类型在内存中占4个字节,,一个字节占8位
        int n = cap - 1;
        /*
         * 00000000 00000000 00000000 00001100   12
         * 00000000 00000000 00000000 00000110   右移1位
         * ---------------------------------------------- 或运算
         * 00000000 00000000 00000000 00001110    14
         */
        n |= n >>> 1; // n=14
        /*
         * 00000000 00000000 00000000 00001110   12
         * 00000000 00000000 00000000 00000111   右移2位
         * ---------------------------------------------- 或运算
         * 00000000 00000000 00000000 00001111    15
         */
        n |= n >>> 2;//计算完n=15
        /*
         * 00000000 00000000 00000000 00001111   15
         * 00000000 00000000 00000000 00000000   右移4位
         * ---------------------------------------------- 或运算
         * 00000000 00000000 00000000 00001111    15
         */
        n |= n >>> 4;// 计算完n=15
        /*
         * 00000000 00000000 00000000 00001111   15
         * 00000000 00000000 00000000 00000000   右移8位
         * ---------------------------------------------- 或运算
         * 00000000 00000000 00000000 00001111    15
         */
        n |= n >>> 8;// 计算完n=15
        /*
         * 00000000 00000000 00000000 00001111   15
         * 00000000 00000000 00000000 00000000   右移16位
         * ---------------------------------------------- 或运算
         * 00000000 00000000 00000000 00001111    15
         */
        n |= n >>> 16;// 计算完n=15
        
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

JDK1.8中的做法简单来说就是通过位运算将最高非0位后面全置为1,由上面可以看出通过这五次的计算,最后的结果刚好可以填满32位的空间,也就是一个int类型的空间,这就是为什么必须是int类型,且最多只无符号右移16位!那么为啥传进来的时候进行减1操作呢?因为当传入的参数刚好是2的n次幂时,算出来的结果是比传进来的值大的最小n次幂,比如恰好传进来16如果不减一的话最后计算出来的结果是64,和我们想要的结果不符合,因此在传进来的时候减一能够避免出现恰好是n次幂的情况导致结果不对。大家也可以手动算一算,这样就能明白了。

负载因子和阈值

负载因子static final float DEFAULT_LOAD_FACTOR = 0.75f和阈值int threshold的话是用来给map扩容判断时用的,负载因子也能够在构造函数中进行指定(不推荐),每次进行put的时候都会进行判断是否需要扩容,当size超过了阈值=总容量*负载因子,则会扩容,默认情况下总容量是16,负载因子是0.75,至于为啥是0.75这是由于经过大量的实验证明该系数是最合适的,如果设置过小,HashMap每put少量的数据,都要进行一次扩容,而扩容操作会消耗大量的性能。如果设置过大的话,如果设成1,容量还是16,假设现在数组上已经占用的15个,再要put数据进来,计算数组index时,发生hash碰撞的概率将达到15/16,这违背的HashMap减少hash碰撞的原则。

HashMap构造方法

JDK1.7中的无参构造方法:

 /**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
   
        // 负载因子
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        // 阈值:总容量*负载因子
        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
        // 存放键值对的entry
        table = new Entry[DEFAULT_INITIAL_CAPACITY];
        init();
    }

JDK1.8中的构造方法:

public HashMap() {
   
        // 将负载因子设置为默认的0.75f,并没有在构造方法中创建Entry的数组,而是放到put方法中了
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
public HashMap(int initialCapacity, float loadFactor) {
   
        // 给的初始大小是不是小于0
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        // 是否超过最大容量,是的话则直接赋值最大容量
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        // 给的加载因子是不是个大于0的数字
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        // 设置加载因子的大小
        this.loadFactor = loadFactor;
        // 调整给定的数组大小
        this.threshold = tableSizeFor(initialCapacity);
    }

this.threshold = tableSizeFor(initialCapacity);判断指定的初始化容量是否是2的n次幂,如果不是那么会变为比指定初始化容量大的最小的2的n次幂。但是注意,在tableSizeFor方法体内部将计算后的数据返回给调用这里了,并且直接赋值给threshold边界值了。有些人会觉得这里是一个bug,应该这样书写:this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;这样才符合threshold的意思(当HashMap的size到达threshold这个阈值时会扩容)。但是请注意,在jdk8以后的构造方法中,并没有对table这个成员变量进行初始化,table的初始化被推迟到了put方法中,在put方法中会对threshold重新计算。

JDK1.8中的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
        Node<K,V>[] tab;
    	.......
    }

在put方法中又调用了putVal(hash(key), key, value, false, true);以及hash(key)方法,首先我们先来看下hash(key)方法

static final int hash(Object key) {
   
        int h;
        // 如果key是null则hash值为0
        //(h = key.hashCode()) ^ (h >>> 16)
        // 先获取key的hashCode并赋值给h, 然后和h和h右移16位后做异或运算得到hash值
        return (key == null) ? 0 : (h = key
  • 1
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值