详解Java集合中

Collection集合接口详解
在这里插入图片描述
Map接口详解
在这里插入图片描述
HashMap

红黑树简介
红黑树是一种特殊的平衡二叉树(左子树与右子树的高度差不超过1),它有如下的特征:

  • 节点是红色或黑色 根节点是黑色的 所有叶子都是黑色。(叶子是NULL节点)
  • 每个红色节点的两个子节点都是黑色的(从每个叶子到根的所有路径上不能有两个连续的红色节点)
  • 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。 所以红黑树的时间复杂度为: O(lgn)。
    在这里插入图片描述

jdk1.8:数组+链表+红黑树
HashMap的底层首先是一个数组,元素存放的数组索引值就是由该元素的哈希值(key-value中key的哈希值)确定的,这就可能产生一种特殊情况——不同的key哈希值相同。

在这样的情况下,于是引入链表,如果key的哈希值相同,在数组的该索引中存放一个链表,这个链表就包含了所有key的哈希值相同的value值,这就解决了哈希冲突的问题。

但是如果发生大量哈希值相同的特殊情况,导致链表很长,就会严重影响HashMap的性能,因为链表的查询效率需要遍历所有节点。于是在jdk1.8引入了红黑树,当链表的长度大于8,且HashMap的容量大于64的时候,就会将链表转化为红黑树。

// jdk1.8
// HashMap#putVal

// binCount 是该链表的长度计数器,当链表长度大于等于8时,执行树化方法
// TREEIFY_THRESHOLD = 8
if (binCount >= TREEIFY_THRESHOLD - 1)
    treeifyBin(tab, hash);

// HashMap#treeifyBin    
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // MIN_TREEIFY_CAPACITY=64
    // 若 HashMap 的大小小于64,仅扩容,不树化
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

加载因子为什么是0.75

所谓的加载因子,也叫扩容因子或者负载因子,它是用来进行扩容判断的。

假设加载因子是0.5,HashMap初始化容量是16,当HashMap中有16 * 0.5=8个元素时,HashMap就会进行扩容操作。

而HashMap中加载因子为0.75,是考虑到了性能和容量的平衡。

由加载因子的定义,可以知道它的取值范围是(0, 1]。

  • 如果加载因子过小,那么扩容门槛低,扩容频繁,这虽然能使元素存储得更稀疏,有效避免了哈希冲突发生,同时操作性能较高,但是会占用更多的空间。
  • 如果加载因子过大,那么扩容门槛高,扩容不频繁,虽然占用的空间降低了,但是这会导致元素存储密集,发生哈希冲突的概率大大提高,从而导致存储元素的数据结构更加复杂(用于解决哈希冲突),最终导致操作性能降低。
  • 还有一个因素是为了提升扩容效率。因为HashMap的容量(size属性,构造函数中的initialCapacity变量)有一个要求:它一定是2的幂。所以加载因子选择了0.75就可以保证它与容量的乘积为整数。

为什么说是说容量必须是2的幂次倍?

我们先来看源码中HashMap的构造函数有哪些:

在这里插入图片描述
从源码中可以看出:HashMap提供四种构造方法:一是给定初始容量和加载因子的构造方法,二是给定初始容量,使用默认的加载因子,三是什么参数都不给,使用默认的初始容量和默认的加载因子,四是传进一个Map,使用默认的加载因子。从上面的构造方法可以看出,无论是使用默认的初始容量,还是使用默认的初始容量,当你调用HashMap的构造方法时,HashMap是没有进行初始化容量,也就是现在是一个空的HashMap(容量为0),这是因为HashMap使用的懒加载机制,只有你第一次向HashMap中添加元素时,才进行第一次的容量设置,查看put(K,V)的源码:
在这里插入图片描述
从上图中可以看出,put(K key, V value)调用了putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)的方法,在putVal的方法中,当第一次向HashMap中添加对象时,会进行一个判空的处理,这时就调用resize()方法对HashMap进行容量设置,此时会有两种情况的根据容量初始化。

第一种情况:当我们没有设置初始化容量时,HashMap就使用默认的初始化容量,也就是16.
在这里插入图片描述
第二种情况:当我们设置了初始化容量,HashMap就会按照我们设置的容量进行设置吗?答案是不一定。当你设置的初始化容量是2的n次方时,就会按照你设置的容量设置;当你设置的初始化容量不是2的n次方时,就会按照大于你设置的那个值但是最接近你设置的那个值的2的n次方进行设置。听起来比较拗口,下面从源码和实例进行说明。

从HashMap的第一个构造方法我们可以看出,当我们给定初始容量时,会调用tableSizeFor(initialCapacity)方法进行容量设置,

// 构造函数
public HashMap(int initialCapacity, float loadFactor) {
  // ……
  this.loadFactor = loadFactor;// 加载因子
  this.threshold = tableSizeFor(initialCapacity);
}

/**
* Returns a power of two size for the given target capacity.返回2的幂
* MAXIMUM_CAPACITY = 1 << 30
*/
static final int tableSizeFor(int cap) {
  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;
}

在这里插入图片描述
这一系列的无符号右移操作和按位或运算返回的什么结果,我们可以通过一个测试来说明。测试类如下:
在这里插入图片描述
结合源码和实例,就可以体会到HashMap给定了初始容量时,它本身处理这个值得机制了。

还有就是HashMap什么时候进行扩容呢?我们回到put()方法的源码中,发现有下面一段代码:
在这里插入图片描述
其中size是HashMap中添加键值对的数量,而threshold是容量加载因子(capacity * load factor)得出的值,也是就当我们的容量是16,加载因子是0.75时,当键值对的数量大于160.75=12时,HashMap会进行扩容操作,回到resize()方法中,看它是怎么扩容的?

在这里插入图片描述
可以看出,在容量不大于最大值的情况下,HashMap是以2倍的容量进行扩容的。
总结:
HashMap计算添加元素的位置时,使用的位运算,这是特别高效的运算;另外,HashMap的初始容量是2的n次幂,扩容也是2倍的形式进行扩容,是因为容量是2的n次幂,可以使得添加的元素均匀分布在HashMap中的数组上,减少hash碰撞,避免形成链表的结构,使得查询效率降低!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值