💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。

【集合Map系列一】Map介绍_集合

  • 推荐:kuan 的首页,持续学习,不断总结,共同进步,活到老学到老
  • 导航
  • 檀越剑指大厂系列:全面总结 java 核心技术点,如集合,jvm,并发编程 redis,kafka,Spring,微服务,Netty 等
  • 常用开发工具系列:罗列常用的开发工具,如 IDEA,Mac,Alfred,electerm,Git,typora,apifox 等
  • 数据库系列:详细总结了常用数据库 mysql 技术点,以及工作中遇到的 mysql 问题等
  • 懒人运维系列:总结好用的命令,解放双手不香吗?能用一个命令完成绝不用两个操作
  • 数据结构与算法系列:总结数据结构和算法,不同类型针对性训练,提升编程思维,剑指大厂

非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。💝💝💝 ✨✨ 欢迎订阅本专栏 ✨✨


博客目录

  • 1.什么是 map?
  • 2.Map 类图结构
  • 3.HashMap 的数据结构?
  • 4.HashMap 中的常量?
  • 5.put 方法实现原理?
  • 6.put 方法的 hash 函数?
  • 7.hash 右移 16 位异或?
  • 8.为什么用^不用&或者|?
  • 9.如何计算数组下标?
  • 10.为什么引入红黑树?
  • 11.为什么变树阈值为 8?
  • 12.什么是泊松分布?
  • 13.转化为红黑树的条件?
  • 14.为什么不用 AVL 树?
  • 15.HashCode 作为下标?
  • 16.数组长度为 2 的幂次方


1.什么是 map?

在编程语言中,Map 集合通常是指一种实现了 Map 数据结构的容器类或数据类型。Map 集合中可以存储键值对,每个键都是唯一的,而值可以重复。Map 集合也被称为关联数组、字典或哈希表等。在 java 中称为 Map,在 python 中称为字典.

Map 集合通常提供了一些方法,例如插入元素、删除元素、查找元素等。在 Java 中,Map 集合是一个接口,它有多个实现类,例如 HashMap、TreeMap、LinkedHashMap 等。在 Python 中,Map 集合的实现包括字典(dictionary)和有序字典(ordered dictionary)等。在 C++中,Map 集合是通过 STL 库中的 map 类实现的。

Map 集合常用于存储和管理大量键值对的数据,例如在 Web 开发中,Map 集合可以用来存储 HTTP 请求的参数和对应的值;在游戏开发中,Map 集合可以用来存储游戏对象的属性和状态等。

2.Map 类图结构

在 Java 中,常见的 Map 有以下几种:

  1. HashMap:基于哈希表实现,可以支持 null 键和 null 值,不保证元素的顺序。
  2. TreeMap:基于红黑树实现,可以自动按键排序,但不能存储 null 键。
  3. LinkedHashMap:基于哈希表实现,可以按照插入顺序或访问顺序迭代元素。
  4. Hashtable:基于哈希表实现,与 HashMap 类似,但是它是线程安全的,不支持 null 键或 null 值。
  5. ConcurrentHashMap:基于分段锁实现的哈希表,可以支持高并发,不支持 null 键或 null 值。

【集合Map系列一】Map介绍_链表_02

3.HashMap 的数据结构?

HashMap 本质是一个定长的数组,数组中存放链表,HashMap 在 jdk1.8 的源码如下图

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V 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;
        }
}

当向 HashMap 中 put 值时,会首先通过 hash 函数计算出数组的位置,比如索引值为 i,将其放到 entry[i]的位置,如果当前位置有元素了,会插在这个元素的前面(jdk1.7 头插法,jdk1.8 尾插法),最先加入的元素在链表尾部。比如第一个键值对 a 通过 hash 得到数组的索引为 index=0,键值对 b 也计算 index=0,则 b.next=a,entry[0]=b;这样 index=0 的位置存放了 b,a 两个键值对,是用链表关联的。也就是说数组中存储的是最后插入的元素。

构造函数:

//构造一个具有默认初始容量 (16) 和默认负载因子 (0.75) 的空 HashMap
HashMap()

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

//构造一个带指定初始容量和负载因子的空 HashMap
HashMap(int initialCapacity, float loadFactor)

Node 节点:

static class Node<K,V> implements Map.Entry<K,V> {
    // 哈希码,用来查找位置以及比对元素是否相同
    final int hash;
    // 键
    final K key;
    // 值
    V 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;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    // 重写了 hashCode, ^ 是位异或运算符
    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    // 重写 equals() 方法
    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;
    }
}

TreeNode节点:

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    // 父节点
    TreeNode<K,V> parent;
    // 左节点
    TreeNode<K,V> left;
    // 右节点
    TreeNode<K,V> right;
    TreeNode<K,V> prev;
    // 判断颜色,默认红色
    boolean red;
    TreeNode(int hash, K key, V val, Node<K,V> next) {
        super(hash, key, val, next);
    }
    // 返回根节点
    final TreeNode<K,V> root() {
        for (TreeNode<K,V> r = this, p;;) {
            if ((p = r.parent) == null)
                return r;
            r = p;
        }

4.HashMap 中的常量?

【集合Map系列一】Map介绍_集合_03

// 序列化自动生成的一个码,用来在正反序列化中验证版本一致性。
private static final long serialVersionUID = 362498820763181265L;

// 默认的初始容量 1 * 2^4 = 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

// 最大容量 1 * 2^30
static final int MAXIMUM_CAPACITY = 1 << 30;

// 默认的加载因子 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 桶的树化阈值,当桶(bucket)上的结点数大于这个值时会转成红黑树,
// 也就是上面提到的长度大于阈值(默认为8)时,将链表转化为红黑树
static final int TREEIFY_THRESHOLD = 8;

// 桶的链表还原阈值,当桶(bucket)上的结点数小于这个值时树转链表
// 一个道理
static final int UNTREEIFY_THRESHOLD = 6;

// 最小树形化容量阈值,当哈希表中的容量 > 该值时,才允许树形化链表
// 否则,若桶内元素太多时,则直接扩容,而不是树形化
// 为了避免进行扩容和树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
static final int MIN_TREEIFY_CAPACITY = 64;

// 存储元素的数组,总是2的幂次倍
transient Node<k,v>[] table;

// 存放具体元素的集
transient Set<map.entry<k,v>> entrySet;

// 存放元素的个数(不是数组的长度)
transient int size;

// 扩容和修改的计数变量
transient int modCount;

// 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
int threshold;

// 加载因子
final float loadFactor;

其中有几个需要强调的内容

threshold 临界值

  • 数组扩容的一个临界值,即当数组实际大小(容量 _ 填充因子,即:threshold = capacity _ loadFactor)超过临界值时,会进行扩容。

loadFactor 加载因子

  • 加载因子就是表示哈希表中元素填满的程度,当表中元素过多,超过加载因子的值时,哈希表会自动扩容,一般是一倍,这种行为可以称作 rehashing(再哈希)。
  • 加载因子的值设置的越大,添加的元素就会越多,确实空间利用率的到了很大的提升,但是毫无疑问,就面临着哈希冲突的可能性增大,反之,空间利用率造成了浪费,但哈希冲突也减少了,所以我们希望在空间利用率与哈希冲突之间找到一种我们所能接受的平衡,经过一些试验,定在了 0.75f。

5.put 方法实现原理?

构造方法的时候并没有初始化,而是在第一次 put 的时候初始化

jdk1.7 采用的是数组+链表的方式

jdk1.8 采用的是数组+链表/红黑树

【集合Map系列一】Map介绍_链表_04

put 方法源码分析

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
  // jdk1.8中HashMap底层数据结构使用的是Node
  Node<K,V>[] tab; Node<K,V> p; int n, i;
  // 如果table还未初始化,则初始化table,注意这里初始化使用的是resize函数[扩容函数]
  if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;
  /**
         * 这里表示如果tab[i]位置上为null,则直接插入数据
         * i=(n-1)&hash与jdk1.7中找出元素在tab上的index是一样的操作
         * 注意这里在多线程环境下会造成线程不安全问题
         */
  if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
  else {// 如果i位置上有元素,则进行链式存储
    Node<K,V> e; K k;
    // 如果tab[i]上的元素与插入元素的key完全一样,则进行覆盖操作
    if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
      e = p;
    // 判断当前元素是否是红黑树结构
    else if (p instanceof TreeNode)
      e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    else {
      for (int binCount = 0; ; ++binCount) {
        // 如果p节点的next为空,则将待插入的元素,直接添加在链表尾
        if ((e = p.next) == null) {
          // 从这里可知道jdk1.8在,如果存在链表,插入数据是直接放在链表尾的
          p.next = newNode(hash, key, value, null);
          // 当同一节点链表中元素个数>=8时,底层数据结构转向红黑树,
          if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
            treeifyBin(tab, hash);  // 将底层数据结构转向红黑树
          break;
        }
        // 判断next元素是否和插入元素相同,如果相同,则不做操作,跳出循环
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
          break;
        p = e; // 将next赋值给p,继续循环
      }
    }
    // 覆盖操作
    if (e != null) { // existing mapping for key
      V oldValue = e.value;
      // onlyIfAbsent表示是否要改变原来的值,true-不改变,false-改变
      if (!onlyIfAbsent || oldValue == null)
        e.value = value;
      afterNodeAccess(e);
      return oldValue;
    }
  }
  // 修改次数加1,fail-fast机制
  ++modCount;
  // 判断是否需要扩容
  if (++size > threshold)
    resize();
  afterNodeInsertion(evict);
  return null;
}

6.put 方法的 hash 函数?

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • key==null
  • key 等于 null 时,值为 0,所以 HashMap 的 key 可以为 null,
  • 对比 HashTable,如果 key 为 null,会抛出异常;HashTable 的 key 不可为 null
  • key!=null
  • 首先计算 HashCode 的值,再 HashCode 的值右移 16 位再和 HashCode 的值进行异或运算
  • 此过程就是扰动函数
  • 扰动函数原理,如果是小于 32 位,则右移 16 位后高位补零,进行异或运算后高位还是 0
  • 如果是 32 位的,则右移 16 位后,高位补 0,原来的高位变成了低位,进行亦或运算,增加随机,均匀分布

7.hash 右移 16 位异或?

//jdk1.8的hash方法
static final int hash(Object key) {
  int h;
  return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//jdk1.7的hash方法
final int hash(Object k) {
  int h = hashSeed;
  // 如果使用了再次hash,并且key的类型为String,则直接使用String的hash算法返回其hash值
  if (0 != h && k instanceof String) {
    return sun.misc.Hashing.stringHash32((String) k);
  }
  // 如果走到这里h可能为0或者为1,再次异或上k的hashCode,如果h为1,表示再hash,则这里的h可能会±1,h为0的时候,h就表示k的hashCode
  h ^= k.hashCode();

  // 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).
  // 这里进行两次hash主要是为了最大可能的解决hash碰撞,防止低位不变,而高位变化时,产生hash碰撞
  h ^= (h >>> 20) ^ (h >>> 12);
  return h ^ (h >>> 7) ^ (h >>> 4);
}
1010 1010 0001 0100 1111 0101	h=11146485  大槽位
0000 0000 0000 0000 1010 1010	>>>16
1010 1010 0001 0100 0101 1111	^	//高低位数据权重保留
1111 1111 1111 1111 1111 1111	(capitity -1)=16777215
1010 1010 0001 0100 1111 0101	&结果===11146485//高低位数据的变化影响都有保留,尽可能地离散
0000 0000 0001 0101	h=21  小槽位
0000 0000 0000 0000	>>>16
0000 0000 0001 0101	^  21//不改变低位数据权重
0000 0000 0001 1111	(capitity -1)=31
0000 0000 0001 0101	&31结果===21

由于最终要和(length-1)进行与运算,数组的长度大多都是小于 2 的 16 次方的,高 16 位是用不到的。所以始终是 hashCode 的低 16 位参与运算,如何让高 16 位也参与运算呢,会让下标更加散列。右移 16 位后,高 16 位和低 16 位进行异或运算,增加随机性。

8.为什么用^不用&或者|?

增加随机性

如果用&操作符,得到 75%的 0,25%的 1

如果用|操作符,得到 75%的 1,25%的 0

如果用^操作符,得到 50%的 1,50%的 0,异或运算对均匀分布非常有用

【集合Map系列一】Map介绍_链表_05

9.如何计算数组下标?

put 方法是用如下方法计算下标的,实际计算公式是 hash&(length-1),length-1 的二进制是 0111111,使用&运算能快速定位到下标的位置

if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);

步骤

代码

说明

1.计算 hashCode

h=key.hashCode()

计算 key 的 hashCode 值

2.二次处理计算 hash 值

h^(h>>>16)

扰动函数

3.计算 index

hash&(length-1)

二次处理的 hash 值 & (数组长度-1)

10.为什么引入红黑树?

在 jdk1.7 以前,HashMap 用的是数组+链表,如果链表越来越长,查询的时间复杂度最坏时 O(n)

为了提高查询效率,jdk1.8 使用了红黑树,查询的平均时间复杂度为 O(logn)

在 Java 中,HashMap 是一种基于哈希表实现的 Map 集合,它可以在常数时间内执行插入、删除和查找操作,因此在大多数情况下,它是使用最广泛的 Map 实现类。然而,当哈希表中的元素数量增加时,哈希冲突的概率就会增加。若哈希冲突过于频繁,HashMap 的性能将会下降。

为了解决这个问题,Java 引入了一种名为红黑树的数据结构。当哈希表中的某个桶(bucket)中的元素数量超过一定阈值(默认为 8),桶中的元素将会被转换为一棵红黑树。这样,在进行查找、插入和删除操作时,就可以利用红黑树的特性,在 O(log n)的时间复杂度内完成操作,而不是在 O(n)的时间复杂度内进行线性查找。

使用红黑树可以提高 HashMap 在高负载情况下的性能和效率,因为红黑树的高度是 O(log n),而哈希表的平均查找时间是 O(1)。此外,红黑树还可以保证元素的有序性,这对于需要按照键排序的场景非常有用。

11.为什么变树阈值为 8?

在 jdk1.8 及以后的版本,HashMap采用的数据结构是,数组+链表/红黑树,在链表长度为 8 时,并且数组长度大于等于 64 时,开始由链表转化为红黑树。

红黑树节点的大小大概是普通节点大小的两倍,转为红黑树,牺牲了空间换时间,更多的是一种兜底的策略,保证极端情况下的查找效率。

阈值为什么要选 8 呢?和统计学有关。理想情况下,使用随机哈希码,链表里的节点符合泊松分布,出现节点个数的概率是递减的,节点个数为 8 的情况,发生概率仅为 0.00000006。

//链表个数的概率
* 0:    0.60653066
* 1:    0.30326533
* 2:    0.07581633
* 3:    0.01263606
* 4:    0.00157952
* 5:    0.00015795
* 6:    0.00001316
* 7:    0.00000094
* 8:    0.00000006
* more: less than 1 in ten million

至于红黑树转回链表的阈值为什么是 6,而不是 8?是因为如果这个阈值也设置成 8,假如发生碰撞,节点增减刚好在 8 附近,会发生链表和红黑树的不断转换,导致资源浪费。

链表的时间复杂度是 O(n),红黑树的时间复杂度是 O(logn),红黑树的时间复杂度是优于链表的。因为树节点所占空间是普通节点的 2 倍。所以当节点足够多时选择使用红黑树。也就是说,当节点比较少的时候,尽管红黑树的时间复杂度表现比链表好一些,但红黑树所占空间比链表大,综合考虑,在节点较多时,红黑树所占空间劣势相比查询性能的提升不那么明显时,转化为红黑时。

当 k=9 时,也就是发生的碰撞次数为 9 次时,概率为亿分之三,碰撞的概率已经无限接近为 0。

如果设置为 9,意味着,几乎永远都不会再次发生碰撞,换句话说,链表的长度此时为 8,要发生碰撞才会从链表变树。但永远都不会变树,因为概率太小了。因此设置为 9,实在没必要。

P(N(1)=k)=0.6065∗(0.5k)/k!
当k=0时,P= 0.6065
当k=1时,p= 0.3032
当k=2时,p= 0.0758
当k=3时,p= 0.0126
当k=4时,p= 0.0015
当k=5时,p= 0.0001
当k=6时,p= 0.000013
当k=7时,p= 0.0000009
当k=8时,p= 0.00000006
当k=9时,p= 0.000000003

12.什么是泊松分布?

泊松分布是一个离散概率分布,表示在一定时间或空间范围内,事件发生的次数符合某个平均值的概率分布。泊松分布的概率质量函数为:

P(X=k) = (λ^k * e^(-λ)) / k!

其中,X 表示事件发生的次数,k 为非负整数,λ 为事件发生的平均次数。e 为欧拉数,约等于 2.71828。

泊松分布的特点是,当事件发生的概率很小,但发生次数很多时,可以用泊松分布来描述。例如,在一定时间内,电话呼叫中心接到的电话数量、网站访问量、交通事故数量等,都可以用泊松分布来描述。

泊松分布可以用于很多领域,例如工业生产、交通管理、金融分析等。在计算机科学中,泊松分布在网络流量和负载均衡等领域也有广泛的应用。

13.转化为红黑树的条件?

如果链表的长度大于 8,一定会转化为红黑树吗?答案是不一定的

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            //树化>=8-1=7,binCount从0开始的,所以实际判断链表个数>=8
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
}
final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
  			//第二个条件是判断数组长度是不是大于等于64,小于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);
        }
    }

转化为红黑树的条件,先判断链表的长度是不是大于等于 8,再判断 table 数组的长度是否大于等于 64,小于 64 则扩容,大于等于 64,才转化为红黑树

14.为什么不用 AVL 树?

HashMap 查找时间复杂度:

  • 在没有地址冲突时,效率 O(1)
  • 有少量地址冲突,在冲突的地址拉链(建链表),效率在 O(1) ~ O(logn) 之间
  • 有大量地址冲突,在冲突的地址建红黑树,效率 O(logn)

AVL 树和红黑树有以下区别:

  • AVL 树更加平衡,提供更快的查询速度,一般读取密集型任务,用 AVL 树。
  • 红黑树更适合插入和修改密集型任务。
  • 通常,AVL 树的旋转比红黑树更加复杂。
  • AVL 以及红黑树是高度平衡的树数据结构。它们非常相似,真正的区别在于 在任何添加/删除操作时完成的旋转操作次数。
  • 两种实现都缩放为 O(logN),其中 N 是叶子的数量,但实际上 AVL 树在查找 密集型任务上更快:利用更好的平衡,树遍历平均更短。另一方面,插入和删除方 面,AVL 树速度较慢:需要更高的旋转次数才能在修改时正确地重新平衡数据结构。
  • 在 AVL 树中,从根到任何叶子的最短路径和最长路径之间的差异最多为 1。在 红黑树中,差异可以是 2 倍。
  • 两个都是 O(logn)查找,但平衡 AVL 树可能需要 O(logn)旋转,而红黑树将需要最多两次旋转使其达到平衡(尽管可能需要检查 O(logn)节点以确定旋转的位置)。旋转本身是 O(1)操作,因为你只是移动指针。

红黑树:适用于大量插入和删除,因为它是非严格的平衡树;只要从根节点到叶子节点的最长路径不超过最短路径的 2 倍,就不用进行平衡调节.查找效率是 O(logn),红黑树舍去了严格的平衡,使其插入,删除,查找的效率稳定在 O(logn)

AVL 树:是一种自平衡二叉搜索树。在 AVL 树中,任何节点的两个子树的高度最大差别为 1,也就是说,AVL 树中所有节点的左子树与右子树高度差的绝对值都不超过 1。AVL 允许的差值小;在进行大量插入和删除操作时,会频繁地进行平衡调整,严重降低效率;AVL 也是 O(logn);查找没问题 O(logn),但是为了保证高度平衡,动态插入和删除的代价也随之增加,综合效率肯定达不到 O(logn)

15.HashCode 作为下标?

为什么不能用HashCode直接作为下标,原因如下:

HashCode 的值范围在-(231)到 231-1,HashMap 的容量范围是 16 ~ 230,HashMap 通常取不到最大值,且机器设备也无法提供这么大的数组空间,hashCode 的值可能不在 HashMap 的 index 范围内,导致无法匹配。解决方法是右移 16 位进行异或运算。

在 Java 中,HashCode 是一个用于散列算法的整数值,可以用于快速查找和比较对象。在 Map、Set 等集合类中,HashCode 被用于确定对象在集合中的位置。

然而,将 HashCode 直接作为下标存储对象可能会存在冲突的情况。因为 HashCode 是一个有限的整数值,而对象的数量是无限的,所以不同的对象可能会有相同的 HashCode 值。如果直接使用 HashCode 作为下标存储对象,就可能会导致对象被覆盖或丢失。

为了解决这个问题,Java 中的 HashMap 等集合类采用了一种称为“拉链法”的解决方案。具体来说,HashMap 内部维护了一个桶(bucket)数组,每个桶对应一个链表,相同 HashCode 的对象会被存储在同一个桶对应的链表中。当需要查找、插入或删除对象时,HashMap 会先计算对象的 HashCode 值,然后根据 HashCode 值找到对应的桶和链表,再在链表中进行操作。

通过采用拉链法,HashMap 等集合类可以有效地解决 HashCode 冲突的问题,保证对象的存储和访问的正确性。因此,不能直接使用 HashCode 作为下标存储对象,而应该使用 HashMap 等集合类提供的接口方法来进行对象的存储和访问。

16.数组长度为 2 的幂次方

为什么 HashMap 的数组长度要保持为 2 的幂次方呢?

  • 只有是 2 的幂次方,hash&(length-1)才等价于 hash%length,实现 key 的快速定位;
  • 2 的幂次方可以减少 hash 冲突,这样可以保证对数组长度取模后得到的结果是均匀分布的。提高查询效率;
  • 扩容时直接数组长度翻倍,提升了扩容的效率;

在哈希表(Hashmap)中,通过哈希函数将键(key)映射到对应的存储桶(bucket)中。为了确定键在哈希表中的存储位置,常常使用与运算符来实现。

当哈希表的存储桶数量为 2 的幂次方时,使用与运算符可以有效地计算键的下标。这是因为 2 的幂次方的二进制表示中,只有最高位为 1,其他位都为 0。假设哈希表的存储桶数量为n,其二进制表示为1000...00(共k位,最高位为 1,其余位为 0),那么使用与运算符&进行操作时,结果如下:

  • hash & (n - 1)

由于n - 1的二进制表示为0111...11,其共有k位,最高位为 0,其余位为 1。这样,与运算符会将hash的二进制表示的低k位保留下来,而高位则会被置为 0。通过这种方式,可以将一个大范围的哈希值映射到0n-1的范围内。

例如,假设哈希表的存储桶数量为 16(即n = 16),其二进制表示为10000,而n - 1的二进制表示为01111。对于一个哈希值hash,通过hash & (n - 1)的操作,可以将其下标限定在015的范围内。

使用与运算符的好处是计算速度快,特别是在大多数哈希表实现中,执行位运算比执行除法运算更高效。因此,与运算符通常用于哈希表中计算键的下标,以提高性能和效率。

需要注意的是,哈希表的性能和均匀性也与哈希函数的选择和实现方式有关。一个好的哈希函数能够尽量避免碰撞(collision)并分布键的映射均匀。

通过这种方式,使用与运算符可以将不同的字符串键映射到不同的存储桶中。这样,我们可以有效地在哈希表中存储和检索数据,提高了访问效率。

//putVal方法中
if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

觉得有用的话点个赞 👍🏻 呗。
❤️❤️❤️本人水平有限,如有纰漏,欢迎各位大佬评论批评指正!😄😄😄

💘💘💘如果觉得这篇文对你有帮助的话,也请给个点赞、收藏下吧,非常感谢!👍 👍 👍

🔥🔥🔥Stay Hungry Stay Foolish 道阻且长,行则将至,让我们一起加油吧!🌙🌙🌙

【集合Map系列一】Map介绍_红黑树_06