当年面试问的最多的那批 Java 集合框架现在问的还多么? 编程十年(手动狗头) , 我把这些集合框架做成盲盒, 无论你多难, 我都想去了解你, 今天来看-----pia~,HashMap ,基于哈希表实现的键值对存储容器。它允许快速的插入、查找和删除操作,通过哈希函数将键映射到唯一的索引。提供常数时间的基本操作,但不保证元素顺序。允许null键和值,但非线程安全。常用于快速数据检索、缓存实现等场景。
在了解 HashMap 之前可以先了解一下哈希表, HashMap 的底层就是基于 哈希表的。 哈希表
HashMap 详解
一. Map
1. 概念
Map 和 set 是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关。
以前常见的搜索方式有:
- 直接遍历,时间复杂度为 O(N),元素如果比较多效率会非常慢
- 二分查找,时间复杂度为 ,但搜索前必须要求序列是有序的
上述查找比较适合静态类型的查找,即一般不会对区间进行插入和删除操作了,而现实中的查找比如:
- 根据姓名查询考试成绩
- 通讯录,即根据姓名查询联系方式
- 不重复集合,即需要先搜索关键字是否已经在集合中
可能在查找时进行一些插入和删除的操作,即动态查找,那上述两种方式就不太适合了,本文介绍的 Map 是一种适合动态查找的集合容器。
2. 模型
一般把搜索的数据称为关键字(Key),和关键字对应的称为值(Value),将其称之为 Key-value 的键值对,所以模型会有两种:
- 纯 key 模型,比如:
有一个英文词典,快速查找一个单词是否在词典中快速查找某个名字在不在通讯录中 - Key-Value 模型,比如:
统计文件中每个单词出现的次数,统计结果是每个单词都有与其对应的次数 <单词,单词出现的次数>
梁山好汉的江湖绰号:每个好汉都有自己的江湖绰号
3. Map 的使用
4. 关于Map的说明
Map是一个接口类,该类没有继承自 Collection,从而想要遍历 Map 中的元素, 只能先把元素重新放到 Set 中, 然后再遍历, 该类中存储的是<K,V> 结构的键值对,并且K一定是唯一的,不能重复。
5. 关于Map.Entry<K, V>的说明
Map.Entry<K, V> 是 Map 内部实现的用来存放 <key, value> 键值对映射关系的内部接口,该接口中主要提供了 <key, value> 的获取方法,value 的设置以及 Key 的比较方式。
注意:Map.Entry<K,V> 并没有提供设置 Key 的方法
6. Map 的常用方法说明
注意:
- Map 是一个接口,不能直接实例化对象,如果要实例化对象只能实例化其实现类 TreeMap 或者 HashMap
- Map 中存放键值对的Key是唯一的,value 是可以重复的
- 在 TreeMap 中插入键值对时,key 不能为空,否则就会抛 NullPointerException 异常,value 可以为空。key 不能为空是因为 需要用 key 进行比较, 空 无法比较, 但是HashMap 的 key 和 value 都可以为空, HashMap 中的 key 不涉及比较。
- Map 中的 Key 可以全部分离出来,存储到 Set 中来进行访问(因为 Key 不能重复)。
- Map 中的 value 可以全部分离出来,存储在 Collection 的任何一个子集合中( value 可能有重复)。
- Map 中键值对的 Key 不能直接修改,value 可以修改,如果要修改 key,只能先将该 key删除掉,然后再来进行重新插入。
- TreeMap 和 HashMap的区别
二. HashMap
1. HashMap 的部分源码
属性:
构造函数:
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);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* 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; // all other fields defaulted
}
/**
* Constructs a new <tt>HashMap</tt> with the same mappings as the
* specified <tt>Map</tt>. The <tt>HashMap</tt> is created with
* default load factor (0.75) and an initial capacity sufficient to
* hold the mappings in the specified <tt>Map</tt>.
*
* @param m the map whose mappings are to be placed in this map
* @throws NullPointerException if the specified map is null
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
由构造函数可以看出, 构造时并没有开辟空间
从这段代码可以看出, 如果初始化的值不是 2 的次幂的话, 修正为 2 的次幂
得到哈希值的方法:
从这段代码可以看出, 对 key == null 进行了单独处理, 所以 HashMap 中的 key 可以为 null
put 方法:
get 方法:
2. 对于源码的一些问题讲解
下面将对一些问题进行讲解:
- 如果 new HashMap(19) 那么 开辟的空间大小是多大 ?
是32, >= 19 的最接近的 2 的 次幂 ? - 为什么容量一定要是 2 的次幂 ?
解释这个问题之前需要先解决另一个问题,
- 元素放置的位置是怎么得到的 ?
(n-1) & hash,得到位置, n 是数组的长度, hash 就是得到的 hash 值, 为什么要这么算 ?
其实 这个函数等价于 hash % n, 而 & 的效率比 % 更高, 所以 使用 (n-1) & hash
那么使用 & 了, 就可能带来一个问题, 我们都知道 & 的性质, 如果 双方出现一个 0, 的话, 该位置就是 0
那么如果 容量是 2 的次幂的话, (n-1) 一定是奇数, 这样最后一位一定是 1, 从而 (n-1) & hash 的结果才可能出现奇数或偶数, 但是如果 n 是 奇数的话, (n-1) 是偶数, 最后一位 是 0, 那么 (n-1) & hash 的结果一定是 偶数, 所以元素都放到了 偶数位置, 极大的提高了 哈希碰撞的概率, 所以, 容量一定是 2 的 次幂, 这样元素存放的位置更随机, 降低了哈希碰撞的概率。
-
得到 hash 值的方法为什么是 hash ^ (hash >>> 16)
为了混合高位和低位, 高位还是原本的高位, 低位变成了 高位与低位 异或的结果, 增加了低位的随机性同时也保持了高位的特性, 从而 使得 hash 值更加随机, 进一步降低哈希冲突的概率 -
HashMap 什么时候开辟数组的空间
第一次 put 元素的时候, 且默认大小为 16 -
HashMap 什么时候扩容 ?
超过负载因子时, 扩容是 2 倍扩容, 因为 一直要保持 是 2 的次幂 -
当两个对象 hash 值相同时会发生什么?
哈希冲突, 两个对象存放在一条链上或者一颗红黑树上 -
若两个对象的 hash 值相同, 如何获取等值对象。(equals 的对象)
遍历与 hash 值相等时相连的链表或者 红黑树, 并用 equals 进行比较, 直到 equals 结果为 true 或者 没找到 -
重新调整 HashMap 的大小时, 会发生什么 ?
两倍扩容, 然后将之前 数组里面的内容重新 哈希到 新的数组空间中 -
HashMap
最大容量: 1 << 30
默认容量: 16
默认负载因子: 0.75
树化条件: 散列表(数组) 长度 >= 64, 链表长度 >= 8
链表还原阈值: 即红黑树变为 链表的阈值, 在扩容时, 树中节点总数 <= 6 时, 红黑树转为 链表 -
由上面源码可知, 自定义的类型一定要重写 hashCode() 和 equals() 方法, 才能往 HashMap 里面存放
-
HashMap 的负载因子为 1 时, 再往里面放元素, 不一定冲突, 注意 负载因子的定义是: (已放的元素个数) / 散列表的长度。
-
由于负载因子的存在, 以及当链表的长度到达一定阈值时转为红黑树, 所以链表的长度不会很长, 所以我们认为 HashMap 的插入、删除、查找元素的时间复杂度为 O(1)
-
因为存放的位置是由 hash 函数得出来的, 所以存放的顺序是随机的, 不会按照插入时的顺序存放,下面代码中的结果也可以体现。
3. HashMap 的使用
HashMap 的常用方法就是实现从 Map 接口的几个常用方法
public static void main(String[] args) {
Map<String, Integer> hashMap = new HashMap<>();
// 放置元素
hashMap.put("hello", 3);
hashMap.put("abc", 3);
hashMap.put("the", 3);
System.out.println(hashMap);
// 获取元素
Integer val1 = hashMap.get("hello");
System.out.println("hello: " + val1);
Integer val2 = hashMap.getOrDefault("hello2", 100);
System.out.println("hello2: " + val2);
// 判断 key 值是否存在
System.out.println(hashMap.containsKey("hello"));
System.out.println(hashMap.containsKey("hello2"));
// 获取所有 key
Set<String> keySet = hashMap.keySet();
System.out.println(keySet);
// 获取 Map.Entry<>
Set<Map.Entry<String, Integer>> set = hashMap.entrySet();
for (Map.Entry<String, Integer> entry: set) {
System.out.print("key: " + entry.getKey() + " " + "val:" + entry.getValue() + " ");
}
}
三. TreeMap
TreeMap 与 HashMap 使用都差不多, 只不过 TreeMap 底层是一颗红黑树(一种特殊的二叉搜索树), 是 关于 key 有序的, 所以存放到 TreeMap 里面的 key 必须是能够比较的。所以如果 key 是自定义类型的话, 需要实现 Comparable 接口, 或者 实现一个比较器。(Comparable、比较器)
public static void main(String[] args) {
Map<String, Integer> treeMap = new TreeMap<>();
// 放置元素
treeMap.put("hello", 3);
treeMap.put("abc", 3);
treeMap.put("the", 3);
System.out.println(treeMap);
// 获取元素
Integer val1 = treeMap.get("hello");
System.out.println("hello: " + val1);
Integer val2 = treeMap.getOrDefault("hello2", 100);
System.out.println("hello2: " + val2);
// 判断 key 值是否存在
System.out.println(treeMap.containsKey("hello"));
System.out.println(treeMap.containsKey("hello2"));
// 获取所有 key
Set<String> keySet = treeMap.keySet();
System.out.println(keySet);
// 获取 Map.Entry<>
Set<Map.Entry<String, Integer>> set = treeMap.entrySet();
for (Map.Entry<String, Integer> entry: set) {
System.out.print("key: " + entry.getKey() + " " + "val:" + entry.getValue() + " ");
}
}
同样的代码, 只是一个是用 HashMap 存储, 一个用 TreeMap 存储
上图只是简单的模拟了一下怎么存储, 实际情况是很复杂的。
因为是二叉搜索树, 所以它的中序遍历就是有序的。
好啦,以上就是对 HashMap 的讲解, 希望能帮到你 !
评论区欢迎指正 !