java中常见的Map

本文深入探讨了Java中的HashMap与HashTable在性能优化、多线程环境下的区别,包括扩容机制、链表插入方式、线程安全特性及两者在不同场景下的应用。着重分析了HashMap在JDK8的优化以及与HashTable在多线程环境下的表现差异,揭示了它们在实际应用中的选择依据。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、线程不安全的Map

HashMap

获取entry数组下标的方式:按位与

  • 根据key获得一个hashValue[注:hashValue=hash(key)],然后用hashValue对length-1进行按位与运算得到数组的下标,即:hashValue&(length-1)
  • 数组的length必须是2的整数次幂,原因如下:
    • 第一:若length是2的整数次幂,则hashValue&(length-1)等价于hashValue%length,那么hashValue&(length-1)同样也实现了均匀散列,但是(位运算)效率会更高。
      1>归纳:
          2^1 -1 = 0000 0001 
          2^2 -1 = 0000 0011 
          2^3 -1 = 0000 0111 
          2^n -1 = 0000 (n个1) 
      2>举例:
          若:hashValue=29,length=16
          则:hashValue & (length -1)  ==>  29 & (2^4-1)  ==>  00011101 & 00001111 = 00001101 ==>  13  ==>  0<= hashValue & (length -1) <=length -1
              hashValue % length         ==>  29 % 16        ==>     13                 ==>  0<= hashValue % length <=length -1
          故:hashValue & (length -1) == hashValue % length
      3>结论:当length=2^n时,hashValue & (length -1) == hashValue % length,且二者的结果范围都是:0到length-1之间的整数。
    • 第二:若length为奇数,则length-1为偶数,偶数(二进制)的最后一位是0,从而导致hashValue&(length-1)的最后一位永远为0,即:hashValue&(length-1)的结果永远为偶数,最终导致数组中下标为奇数的空间全部被浪费掉。

 代码:

int hash = hash(key);
int i = indexFor(hash, table.length);
static int indexFor(int h, int length) {
     return h & (length-1);
}


HashMap在jdk8中的优化:

  • 数据结构:
    •  jdk7:entry数组,entry是一个单向链表。
    • jdk8:entry数组,entry是一个 单向链表 或 红黑树。
  • 扩容:
    • jdk7:初始容量16,容量到达阈值后进行扩容,每次扩容后,容量变为之前的2倍。
    • jdk8:
      •  初始容量16,容量到达阈值后进行扩容,每次扩容后,容量变为之前的2倍。
      • 当链表长度大于等于8且当前容量还没达到最小树化容量(64)时,会进行扩容以减少冲突。这个逻辑在树化方法中:java.util.HashMap#treeifyBin
      • 说明:
        • 刚开始时HashMap的容量较小,故哈希碰撞的几率会比较大一些,即出现长链表的可能性会稍微大一些。因为容量较小而产生的长链表,我们应该优先选择扩容来降低冲突,而不是树化。
        •  eg:可能存在多个长链表,一次扩容可以同时降低这些链表的长度,若选择树化来降低冲突,则需要操作n次。
  • jdk8的树化(将链表转化为红黑树)和去树化:
    • 树化:当容量超过最小树化容量(64)时,如果存在链表长度大于等于树化阈值(8)时就会树化,故最坏的情况下的查找时间复杂度为O(logN)。jdk7最坏的情况下会遍历整个链表,时间复杂度为O(N)。
    • 去树化:扩容的时候,若发现红黑树中节点的数量小于等于去树化阈值(6)时,会将红黑树转换为链表。
    • 说明:
      • 若将去树化的阈值也设计为7,则当一个HashMap不停的 插入元素,再删除元素 时就会导致不断地进行树化和去树化的操作,导致效率降低。
      • TreeNodes占用的空间是普通Node的两倍,这样设计是为了追求时间和空间的平衡。
  • 线程安全:
    • jdk7:线程不安全,多个线程同时扩容时,可能会生成循环链表,导致cpu飙高。
    • jdk8:线程不安全,不会生成循环链表,但是put操作时存在丢失数据的情况。
  • 链表插入新节点的方式:
    • jdk7:新节点添加到头部
    • jdk8:新节点添加到尾部

TreeMap

  • 数据结构:基于红黑树实现。
  • 特点:键值对默认根据key升序排序,也可以自己定义比较器。

二、线程安全的Map


ConcurrentHashMap

HashTable

获取数组下标的方式:取模法

  • 根据key获得一个hashValue[注:hashValue=hash(key)&0x7FFFFFFF],然后用hashValue对数组的长度取模得到数组的下标,即:hashValue%length
  • 取模法基本能保证元素在哈希表中散列的比较均匀,但是取模会用到除法运算,效率很低。

代码:
        int hash = hash(key);
        int index = (hash & 0x7FFFFFFF) % tab.length;
 

HashMap VS HashTable

不同点:

  • HashMap多线程下是不安全的,HashTable是线程安全的。
  • HashMap的key和value都允许为null,HashTable的key和value都不允许为null(key或value为null时会抛出空指针异常)。
  • HashMap的默认容量是16,扩容后的容量是之前的2倍;HashTable的默认容量是11,扩容后的容量是之前的2倍+1。
  • 获取bucket的方式不同:HashTable通过取模法来获取bucket的下标,  HashMap通过 按位与 来获取bucket的下标。

相同点:

  • 都是Map的子类。
  • 都是基于Entry数组实现的。

### Java 高级 Map 的用法和特性 #### 支持 Lambda 表达式的默认方法 自 Java 8 起,`Map` 接口引入了一些新的默认方法来简化常见的操作。这些新功能允许更简洁地处理键值对数据结构。 对于不存在的键提供默认映射函数是一个典型例子: ```java map.computeIfAbsent(key, k -> new ArrayList<>()).add(value); ``` 此代码片段展示了如何优雅地初始化列表类型的值而无需显式检查是否存在给定键[^1]。 #### 原子更新现有条目 当需要基于当前值修改已有条目的时候可以使用 `computeIfPresent()` 方法: ```java map.computeIfPresent("key", (k, v) -> v * multiplier); ``` 这段代码会找到指定键对应的整数值并将其乘以某个因子;如果该键不在映射中则什么也不做. #### 合并两个映射对象 另一个有用的方法是 `merge()`, 它能够方便地实现将一个键关联的新值与已存在的旧值相结合的功能. ```java // 如果 key 存在于 map 中,则执行 mergeFunction 来决定最终存储的结果. map.merge(key, newValue, (v1,v2)->v1+v2); ``` 上述示例说明了怎样安全有效地把来自不同源的数据汇总到一起而不必担心重复写入的问题. #### 不可变集合的支持 为了提高程序的安全性和性能,在某些情况下创建不可改变版本的地图可能是有益处的。通过静态工厂方法如 `Collections.unmodifiableMap()` 或者直接利用 `Map.of()` 和 `Map.copyOf()` 可快速获得只读视图实例: ```java var immutableMap = Map.of("one", 1L,"two",2L); System.out.println(immutableMap.get("one")); // 输出: 1 ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值