目录

  • Map
  • 1.概述
  • 2.SortedMap
  • 3.实现
  • HashMap
  • 实现原理
  • Hashtable
  • ConcurrentHashMap

Map

1.概述

Map 是一个将 key 映射到 value 的对象,key 不能相同,而且最多映射一个 value。Map 取代了 Dictionary 抽象类。Map 提供了 3 个集合视图:keySet、entrySet 以及 values。Map 的顺序定义为迭代器的顺序,但有些实现类,如 TreeMap,有自己的顺序保证。

2.SortedMap

与 SortedSet 类似,相比Map接口提供了 Range View、Endpoints 和 Comparator access 的额外操作。

3.实现

Map 主要有四个常用的实现类,分别是 HashMap、HashTable、LinkedHashMap 和 TreeMap,类的继承关系如下:

java 中Dict 和 map 区别_ci

它们的特点如下:

  1. HashMap:它根据键的 hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap 最多只允许一条记录的键为 null,允许多条记录的值为 null。HashMap 非线程安全,即任一时刻如果有多个线程同时写 HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections 的 synchronizedMap 方法使 HashMap 具有线程安全的能力,或者使用 ConcurrentHashMap。
  2. Hashtable:Hashtable 是遗留类,很多 Map 的常用功能与 HashMap 类似,不同的是它继承自 Dictionary 类,并且是线程安全的,任一时间只有一个线程能写 Hashtable,并发性不如 ConcurrentHashMap,因为 ConcurrentHashMap 引入了分段锁。Hashtable 不建议在新代码中使用,不需要线程安全的场合可以用 HashMap 替换,需要线程安全的场合可以用 ConcurrentHashMap 替换。
  3. LinkedHashMap:LinkedHashMap 是 HashMap 的一个子类,保存了记录的插入顺序,在用 Iterator 遍历 LinkedHashMap 时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。
  4. TreeMap:TreeMap 实现 SortedMap 接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用 Iterator 遍历TreeMap 时,得到的记录是排过序的。如果使用排序的 Map,建议使用 TreeMap。在使用 TreeMap 时,key 必须实现 Comparable 接口或者在构造TreeMap 传入自定义的 Comparator,否则会在运行时抛出 java.lang.ClassCastException 类型的异常。
  5. 对于上述四种Map类型的类,要求 Map 中的 key 是不可变对象。不可变对象在创建后它的哈希值不会被改变。如果对象的哈希值发生变化,Map对象很可能就定位不到映射的位置了。
  6. Map 提供了三种特殊用途的Map实现:EnumMap、WeakHashMap 和 IdentityHashMap。EnumMap 与 EnumSet 类似,主要用于 enum 类型。WeakHashMap 用于存储键的 weak reference,当键值对在 WeakHashMap 之外不存在引用后,GC 可以对 WeakHashMap 中键值对进行垃圾回收

此外,有一个特殊的 HashMap 实现,IdentityHashMap。IdentityHashMap 实现了 Map 接口,在内部实现时,使用了 reference-equality 而不是 object-equality 进行相等比较,也就是说,在 IdentityHashMa p中,如果两个键 k1 和 k2 相等,那么当且仅当 k1 == k2,而在 HashMap 中相等的条件是k1 == null ? k2 == null : k1.equals(k2)。IdentityHashMap 主要于对需要保留拓扑结构的图转换中,例如序列化或者深拷贝。在 Calcite 的代码中,需要对 RelNode 的属性各种变换,但是不改变 RelNode DAG 的结构,每个节点依然复用原来的 RelNode,所以会经常使用 IdentityHashMap。

HashMap

java 中Dict 和 map 区别_位运算_02

HashMap 提供了所有 Map 的可选方法。HashMap 类似于 Hashtable,但与它不同的是,HashMap 允许 null 作为 key 和 value,并且 HashMap 并不是线程安全的。

HashMap 获取迭代器的时间与 HashMap 的实际容量加上它的大小成正比,所以,当对迭代器的要求比较高时,不要将 HashMap 的初始容量设置太大,load factor 也不要设置得太小。

hash table 中每个位置表示为一个桶 bucket,容量 capacity 表示 bucket 的数量,也就是 hash table 的长度。负载因子 load factor 表明 capacity 能使用的多少(该值可以大于 1)。HashMap 中将 key-value 对封装为一个 Node(继承于 Map.Entry)。当 hash table 中的数量超过阈值 threshold (等于 capacity * loadFactor) 时,将会进行 rehash 操作 (也就是重新构建结构),hash table 的大小会变为原来的两倍,并修改原来 Entry 存放的位置。

默认的 load factor 为 0.75 ,对时间和空间的花费会达到一个较好的情况。高 load factor 虽然会减少空间开销,但会增加查找成本。如果初始化的 capacity 大于 entry 的最大数量除以 load factor,则不会 rehash。

java 中Dict 和 map 区别_线程安全_03

HashMap 并不是线程安全的,它的迭代器也具有快速失效的特点。

HashMap 的最大 bucket 数量为 1 << 30。

java 中Dict 和 map 区别_位运算_04

实现原理

HashMap 的实现基于 hash table,本质上是一个数组。数组上每个位置被称为一个桶。

java 中Dict 和 map 区别_线程安全_05

java 中Dict 和 map 区别_线程安全_06

注意:table 的大小为 2 的幂,默认大小为 16。而 hash table 的大小最好为素数,这是因为合数大小的 hash table,如果采用求余数的方法来算哈希值(这里假设根据数 k 来算哈希值),那么对于出现哈希冲突的情况下, k 的二进制的某些位不一致影响并不大。举个例子来说,对 20、28 求 8 的余数,均为 4,出现哈希冲突,而 20、28 的二进制 10100、11100 的大于第 3 位的数不一致,对哈希值的运算也没有影响,但如果把 8 改为 11,那么有影响的位就多了。HashTable 的默认大小为 11 就是一个例子。至于为什么 HashMap 并没遵守这个原则,在于 HashMap 算桶的位置方法除了取模,也有高位运算的方法。

java 中Dict 和 map 区别_位运算_07

HashMap 解决哈希冲突的方式链地址方法。桶中存储的可能为一个 Entry,但当出现哈希冲突时,桶中存储的是一个链表结构,但当该链表过长时(默认为 8),就会转为红黑树,提高 HashMap 的性能。

java 中Dict 和 map 区别_位运算_08

相反的,红黑数过短(默认为 6),会转换为链表。

java 中Dict 和 map 区别_java 中Dict 和 map 区别_09

但需要注意的是,需要满足 bucket 数量大于 MIN_TREEIFY_CAPACITY (默认为 64)才会进行数化,否者采用 resize。

java 中Dict 和 map 区别_ci_10

接下来我们查看 put 方法是如何实现的。

put 方法会调用 putVal 方法。

java 中Dict 和 map 区别_线程安全_11

其中传参时,第一参数是调用了 hash 方法。key 为 null 时返回 0,否则,hash 方法会将 key 的 hashCode 与它的高 16 位进行异或操作,返回得到的结果,这样减少 hash table 长度为 2 的幂导致的高位无影响。

java 中Dict 和 map 区别_线程安全_12

在 putVal 方法中,tab 为底层的 hash table,n 为 tab 长度,i 为应该应该加入的 bucket 位置(取模操作),p 用于查找新 Node 应该放置的位置(并不是应该放置的位置)。

java 中Dict 和 map 区别_java 中Dict 和 map 区别_13

java 中Dict 和 map 区别_java 中Dict 和 map 区别_14

通过 put 例子,我们可以看出,计算 bucket 位置的流程,示例如下:

java 中Dict 和 map 区别_线程安全_15

而对于 put 方法的实现流程,可用下图概括:

java 中Dict 和 map 区别_线程安全_16

接下来我们看看 rehash 是如何实现的。

rehash 中主要使用到了 resize 方法。resize 方法会开辟一个两倍原始大小的 table 并把原 table 的元素迁移过去,而这个方法中,最重要过程是迁移。由于确定 Node 在那个 bucket 的位置只是取模决定的(取模用到的值是高位运算后的值,而高位运算用到的 hash 函数并没有牵涉到 table 长度),table 变为两倍,那么 Node 在新 bucket 的位置要么在原索引位置 k,要么在 k + oldCap 位置,这取决于:新的 capacity - 1上最高的 1 的位置 p,hash 函数返回值的 p 位置为 0 还是 1。为 0 则在原位置,否则在新位置。基于此,我们只需要查看那一位的情况即可确认位置,而不用全部通过高位运算以及取模运算来计算新位置。

这是对于链表:

java 中Dict 和 map 区别_线程安全_17

这是对于红黑树:

java 中Dict 和 map 区别_java 中Dict 和 map 区别_18

Hashtable

java 中Dict 和 map 区别_位运算_19

Hashtable 是线程安全的,但是其实现并发安全的手段比较粗暴,只是简单的以自身作为对象锁,将相关方法都声明为synchronized,故每次只有一个线程能调用这些同步方法。

Hashtable 不允许 null key 或 null value。

ConcurrentHashMap

// TODO 02/12/2022 meyok