目录
2.3. 说一下 WeakHashMap 与 HashMap 和 LinkedHashMap 的区别?
可以学到什么?
哈希表(数据结构 + 算法),Java基础知识(集合容器),Java并发编程(ConcurrentHashMap)等
一、哈希表(原称:散列表)
哈希表又称散列表
1. 出现的背景
由于顺序存储的结构类型需要一个一个地按顺序访问元素,当这个总量很大且我们所要访问的元素比较靠后时,性能就会很低。于是,后面需要出现一种新的数据结构进行一个替代,哈希表"诞生"。
2. 什么是哈希表
哈希表(Hash Table),又叫散列表,是能够通过给定的关键字的值直接访问到具体对应的值的一个数据结构。
也就是说,把关键字映射到一个表中的位置来直接访问记录,以加快访问速度。
通常,我们把这个关键字称为 Key,把对应的记录称为 Value,所以也可以说是通过 Key 访问一个映射表来得到 Value 的地址。
而这个映射表,也叫作散列函数或者哈希函数,存放记录的数组叫作散列表。
其中有个特殊情况,就是通过不同的 Key,可能访问到同一个地址,这种现象叫作碰撞(Collision)。
而通过某个 Key 一定会得到唯一的 Value 地址。
打个比方:
让我们想一下,若在手机通信录中查找一个人,那我们应该不会从第 1 个人一直找下去,因为这样实在是太慢了。
我们其实是这样做的:
- 首先看这个人的名字的首字母是什么,比如姓张,那么我们一定会滑到最后,因为“Z”姓的名字都在最后。
- 还有在查字典时,要查找一个单词,肯定不会从头翻到尾,而是首先通过这个单词的首字母,找到对应的那一页;
再找第 2 个字母、第 3 个字母……这样可以快速跳到那个单词所在的页。 - 其实这里就用到了散列表的思想。
复杂度分析:
哈希表(Hash Table)是一种空间换时间的存储结构,是在算法中提升效率的一种比较常用的方式,但是所需空间太大也会让人头疼,所以通常需要在二者之间权衡。
3. 哈希函数实现
目前,这个哈希函数比较常用的实现方法比较多,通常需要考虑几个因素:关键字的长度、哈希表的大小、关键字的分布情况、记录的查找频率,等等。
下面简单介绍几种哈希函数:
- 直接寻址法:取关键字或关键字的某个线性函数值为散列地址。
- 数字分析法:通过对数据的分析,发现数据中冲突较少的部分,并构造散列地址。
例如同学们的学号,通常同一届学生的学号,其中前面的部分差别不太大,所以用后面的部分来构造散列地址。 - 平方取中法:当无法确定关键字里哪几位的分布相对比较均匀时,可以先求出关键字的平方值,然后按需要取平方值的中间几位作为
散列地址。这是因为:计算平方之后的中间几位和关键字中的每一位都相关,所以不同的关键字会以较高的概率产生不同的散列地
址。 - 取随机数法:使用一个随机函数,取关键字的随机值作为散列地址,这种方式通常用于关键字长度不同的场合。
- 除留取余法:取关键字被某个不大于散列表的表长 n 的数 m 除后所得的余数 p 为散列地址。
这种方式也可以在用过其他方法后再使用。该函数对 m 的选择很重要,一般取素数或者直接用 n。
4. 对哈希函数产生冲突的解决办法
哈希表为什么会产生冲突呢?
前面提到过,有时不同的 Key 通过哈希函数可能会得到相同的地址,这在我们操作时可能会对数据造成覆盖、丢失。
之所以产生冲突是由于哈希函数有时对不同的 Key 计算之后获得了相同的地址。
冲突的处理方式也有很多,下面介绍几种。
- 开放地址法(也叫开放寻址法):实际上就是当需要存储值时,对Key哈希之后,发现这个地址已经有值了,这时该怎么办?
不能放在这个地址,不然之前的映射会被覆盖。这时对计算出来的地址进行一个探测再哈希,比如往后移动一个地址,如果没人占用,就用这个地址。如果超过最大长度,则可以对总长度取余。这里移动的地址是产生冲突时的增列序量。 - 再哈希法:在产生冲突之后,使用关键字的其他部分继续计算地址,如果还是有冲突,则继续使用其他部分再计算地址。
这种方式的缺点是时间增加了。 - 链地址法:链地址法其实就是对Key通过哈希之后落在同一个地址上的值,做一个链表。
其实在很多高级语言的实现当中,也是使用这种方式处理冲突的,我们会在后面着重学习这种方式。 - 建立一个公共溢出区:这种方式是建立一个公共溢出区,当地址存在冲突时,把新的地址放在公共溢出区里。
5. 哈希表的存储结构
一个好的哈希表设计,除了需要选择一个性能较好的哈希函数,因为冲突是无法避免的,所以通常还需要有一个好的冲突处理方式。
这里我们选择除留取余法作为哈希函数,选择链地址法作为冲突处理方式。
具体存储结构如下图所示。
6. 哈希表的用法
哈希表有两种用法:
- 一种是 Key 的值与 Value 的值一样,一般我们称这种情况的结构为 Set(集合);
- 另一种就是 Key 和 Value 所对应的内容不一样时,那么我们称这种情况为 Map,也就是人们俗称的键值对集合。
根据哈希表的存储结构,我们可以得出哈希表的以下特点。
7. 哈希表的特点
特点一:访问速度很快
由于散列表有散列函数,可以将指定的 Key 都映射到一个地址上,所以在访问一个 Key(键)对应的 Value(值)时,根本不需要一个一个地进行查找,可以直接跳到那个地址。
所以我们在对散列表进行添加、删除、修改、查找等任何操作时,速度都很快。
特点二:需要额外的空间
首先,散列表实际上是存不满的,如果一个散列表刚好能够存满,那么肯定是个巧合。而且当散列表中元素的使用率越来越高时,性能会下降,所以一般会选择扩容来解决这个问题。另外,如果有冲突的话,则也是需要额外的空间去存储的,比如链地址法,不但需要额外的空间,甚至需要使用其他数据结构。
这个特点有个很常用的词可以表达,叫作“空间换时间”,在大多数时候,对于算法的实现,为了能够有更好的性能,往往会考虑牺牲些空间,让算法能够更快些。
特点三:无序
散列表还有一个非常明显的特点,那就是无序。
为了能够更快地访问元素,散列表是根据散列函数直接找到存储地址的,这样我们的访问速度就能够更快,但是对于有序访问却没有办法应对。
特点四:可能会产生碰撞
没有完美的散列函数,无论如何总会产生冲突,这时就需要采用冲突解决方案,这也使散列表更加复杂。
通常在不同的高级语言的实现中,对于冲突的解决方案不一定一样。
8. 哈希表的适用场景
根据散列表的特点可以想到,散列表比较适合无序、需要快速访问的情况。
场景一:缓存
通常我们开发程序时,对一些常用的信息会做缓存,用的就是散列表,比如我们要缓存用户的信息,一般用户的信息都会有唯一标识的字段,比如 ID。这时做缓存,可以把 ID 作为 Key,而 Value 用来存储用户的详细信息,这里的 Value 通常是一个对象(高级语言中的术语,前面提到过),包含用户的一些关键字段,比如名字、年龄等。
在我们每次需要获取一个用户的信息时,就不用与数据库这类的本地磁盘存储交互了(其实在大多数时候,数据库可能与我们的服务在一台机器上,还会有相应的网络性能损耗),可以直接从内存中得到结果。这样不仅能够快速获取数据,也能够减轻数据库的压力。
有时我们要查询一些数据,这些数据与其他数据是有关联的,如果我们进行数据库的关联查询,那么效率会非常低,这时可以分为两部分进行查询:将被关联的部分放入散列表中,只需要遍历一遍;对于另一部分数据,则通过程序手动关联,速度会很快,并且由于我们是通过散列表的 Key、Value 的对应关系对应数据的,所以性能也会比较好。
我之前所在的一家公司曾要做一个大查询,查询和数据组装的时间达到了 40 秒,当然,数据量本身也比较大。但是,40 秒实在让人无法忍受,于是我优化了这段代码,发现可以通过散列表处理来减少很多重复的查询,最终做到了4秒左右的查询耗时,速度快了很多。
场景二:快速查找
这里说的查找,不是排序,而是在集合中找出是否存在指定的元素。
这样的场景很多,比如我们要在指定的用户列表中查找是否存在指定的用户,这时就可以使用散列表了。在这个场景下使用的散列表其实是在上面提到的 Set 类型,实际上不需要 Value 这个值。
还有一个场景,我们一般对网站的操作会有个IP地址黑名单,我们认为某些 IP 有大量的非法操作,于是封锁了这些 IP 对我们网站的访问。这个 IP 是如何存储的呢?就是用的散列表。当一个访问行为发送过来时,我们会获取其 IP,判断其是否存在于黑名单中,如果存在,则禁止其访问。这种情况也是使用的 Set。
当然,对于上面说的两个例子,用列表也是可以实现的,但是访问速度会受到很大的影响,尤其是列表越来越长时,查找速度会很慢。
散列表则不会。
二、Map 接口
1. 简介
Java提供了专门的集合类用来存放键值对关系的对象,即java.util.Map接口。
我们通过查看Map接口描述,发现Map接口下的集合与Collection接口下的集合,它们存储数据的形式不同,如下图。
一:我们知道Collection中的集合,元素是孤立存在的(理解为单身),向集合中存储元素采用一个个元素的方式存储。
二:Map中的集合,元素是成对存在的(理解为夫妻)。每个元素由键与值两部分组成,通过键可以找对所对应的值。
三:Collection中的集合称为单列集合,Map中的集合称为双列集合。
四:需要注意的是,Map中的集合不能包含重复的键,值可以重复;每个键只能对应一个值。
2. 常用子类
通过查看Map接口描述,看到Map有多个子类,这里我们主要讲解常用的HashMap集合、LinkedHashMap集合。
- HashMap<K,V>:存储数据采用的哈希表结构,元素的存取顺序不能保证一致。
由于要保证键的唯一、不重复,需要重写键的 hashCode() 方法、equals() 方法。 - LinkedHashMap<K,V>:HashMap下有个子类LinkedHashMap,存储数据采用的哈希表结构+链表结构。
通过链表结构可以保证元素的存取顺序一致;
通过哈希表结构可以保证的键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。 - TreeMap<K,V>:TreeMap 集合和 Map 相比没有特有的功能,底层的数据结构是红黑树;
可以对元素的键进行排序,排序方式有两种:自然排序和比较器排序
tips:Map接口中的集合都有两个泛型变量<K,V>,在使用时,要为两个泛型变量赋予数据类型。
两个泛型变量<K,V>的数据类型可以相同,也可以不同。
3. 常用方法
Map接口中定义了很多方法,常用的如下:
- public V put(K key, V value) - 把指定的键与指定的值添加到Map集合中。
- public V remove(Object key) - 把指定的键 所对应的键值对元素 在Map集合中删除,返回被删除元素的值。
- public V get(Object key) - 根据指定的键,在Map集合中获取对应的值。
- public Set keySet() - 获取Map集合中所有的键,存储到Set集合中。
- public Set<Map.Entry<K,V>> entrySet() - 获取到Map集合中所有的键值对对象的集合(Set集合)。
- public boolean containKey(Object key) - 判断该集合中是否有此键。
Map接口的方法演示
public class MapDemo {
public static void main(String[] args) {
//创建 map对象
HashMap<String, String> map = new HashMap<String, String>();
//添加元素到集合
map.put("黄晓明", "杨颖");
map.put("文章", "马伊琍");
map.put("邓超", "孙俪");
System.out.println(map);
//String remove(String key)
System.out.println(map.remove("邓超"));
System.out.println(map);
// 想要查看 黄晓明的媳妇 是谁
System.out.println(map.get("黄晓明"));
System.out.println(map.get("邓超"));
}
}
tips:使用put方法时,若指定的键(key)在集合中没有,则没有这个键对应的值,返回null,并
把指定的键值添加到集合中;
若指定的键(key)在集合中存在,则返回值为集合中键对应的值(该值为替换前的值),并把指定键所对应的值,替换成指定的新
值。
4. 遍历方式
方式一:键找值方式
通过元素中的键,获取键所对应的值
分析步骤:
- 获取Map中所有的键,由于键是唯一的,所以返回一个Set集合存储所有的键。方法提示:keyset()
- 遍历键的Set集合,得到每一个键。
- 根据键,获取键所对应的值。方法提示:get(K key)
遍历图解:
方式二:键值对方式
即通过集合中每个键值对(Entry)对象,获取键值对(Entry)对象中的键与值。
Entry键值对对象:
我们已经知道,Map中存放的是两种对象,一种称为key(键),一种称为value(值),它们在在Map中是一一对应关系,这一对对象又称做Map中的一个Entry(项)。
Entry将键值对的对应关系封装成了对象。即键值对对象,这样我们在遍历Map集合时,就可以从每一个键值对(Entry)对象中获取对应的键与对应的值。
在Map集合中也提供了获取所有Entry对象的方法:
- public Set<Map.Entry<K,V>> entrySet() - 获取到Map集合中所有的键值对对象的集合(Set集合)。
获取了Entry对象 , 表示获取了一对键和值,那么同样Entry中 , 分别提供了获取键和获取值的方法:
- public K getKey() - 获取Entry对象中的键。
- public V getValue() - 获取Entry对象中的值。
操作步骤与图解:
- 获取Map集合中,所有的键值对(Entry)对象,以Set集合形式返回。
方法提示:entrySet() - 遍历包含键值对(Entry)对象的Set集合,得到每一个键值对(Entry)对象
- 通过键值对(Entry)对象,获取Entry对象中的键与值。 方法提示:getkey() getValue()
遍历图解:
tips:Map集合不能直接使用迭代器或者foreach进行遍历。但是转成Set之后就可以使用了。
三、SortedMap 接口
1. 简介
SortedMap接口是Java中提供的一种特殊的Map类型,它继承自Map接口,并添加了新的操作方法,可以根据键值按自然顺序或自定义排序方式对Map进行操作,保证Map中所有键值对的有序性。
SortedMap接口具有以下特点:
- 键值对按特定序列排列,排序时会根据键的自然次序(例如:String类型的键会按字母顺序排序)或自定义比较器所定义的顺序,而不是插入的顺序。
- 提供了访问第一个和最后一个键值对的方法。
- 提供了子Map方法,用于截取某个范围内的键值对。
- 所有实现SortedMap的类都必须实现Comparable接口或提供Comparator子类来定义键的排序方式。
2. 排序方式
SortedMap接口的键值对的排序方式分为两种:
- 自然排序 - 如果键实现了Comparable接口,则按照键的自然排序方式进行排序。
- 定制排序 如果键没有实现Comparable接口,或者需要使用一种非自然排序方式,则需要提供一个Comparator子类定义排序方式。
import java.util.SortedMap;
import java.util.TreeMap;
class SortedMapDemo {
public static void main(String[] args) {
SortedMap<String, Integer> map = new TreeMap<>();
map.put("apple", 10);
map.put("banana", 20);
map.put("peach", 30);
map.put("orange", 40);
map.put("kiwi", 50);
System.out.println(map);
}
}
{apple=10, banana=20, kiwi=50, orange=40, peach=30}
import java.util.Comparator;
import java.util.SortedMap;
import java.util.TreeMap;
class SortedMapDemo {
public static void main(String[] args) {
Comparator<String> cmp = (a, b) -> b.compareTo(a);
SortedMap<String, Integer> map = new TreeMap<>(cmp);
map.put("apple", 10);
map.put("banana", 20);
map.put("peach", 30);
map.put("orange", 40);
map.put("kiwi", 50);
System.out.println(map);
}
}
{peach=30, orange=40, kiwi=50, banana=20, apple=10}
3. 常用方法
SortedMap接口提供了一些特殊的操作方法,以下是常用的方法:
firstKey & lastKey
firstKey方法用于返回有序Map中第一个(最小)键对应的值,lastKey方法用于返回有序Map中最后一个(最大)键对应的值。
import java.util.SortedMap;
import java.util.TreeMap;
class SortedMapDemo {
public static void main(String[] args) {
SortedMap<String, Integer> map = new TreeMap<>();
map.put("apple", 10);
map.put("banana", 20);
map.put("peach", 30);
map.put("orange", 40);
map.put("kiwi", 50);
System.out.println(map.firstKey()); // 输出 apple
System.out.println(map.lastKey()); // 输出 kiwi
}
}
subMap
subMap方法用于从有序Map中截取出一个子Map,包含指定范围内的键值对。
方法有两个参数,分别是起始键和截止键,包含起始键对应的值但不包含截止键对应的值。
如果不指定截止键,则截取从起始键(包含)到有序Map中最后一个键对应的值。
import java.util.SortedMap;
import java.util.TreeMap;
class SortedMapDemo {
public static void main(String[] args) {
SortedMap<String, Integer> map = newTreeMap<>();
map.put("apple", 10);
map.put("banana", 20);
map.put("peach", 30);
map.put("orange", 40);
map.put("kiwi", 50);
SortedMap<String, Integer> subMap = map.subMap("banana", "orange");
System.out.println(subMap); // 输出 {banana=20, kiwi=50, orange=40}
}
}
注意,subMap方法返回的子Map是原始Map的视图,对子Map的修改会影响原始Map。
4. 实现类
Java中提供了两个实现了SortedMap接口的常用类:
- TreeMap:基于红黑树实现,具有O(log n)的时间复杂度。
- ConcurrentSkipListMap:基于跳表实现,具有同步和并发安全性。
import java.util.SortedMap;
import java.util.TreeMap;
class SortedMapDemo {
public static void main(String[] args) {
SortedMap<String, Integer> map = new TreeMap<>();
map.put("apple", 10);
map.put("banana", 20);
map.put("peach", 30);
map.put("orange", 40);
map.put("kiwi", 50);
System.out.println(map);
}
}
{apple=10, banana=20, kiwi=50, orange=40, peach=30}
5. 总结
当需要对Map中的键值对进行排序时,可以使用SortedMap接口和它的实现类TreeMap和ConcurrentSkipListMap。
其中,TreeMap基于红黑树实现,可以在O(log n)的时间内对键值对进行增删改查和排序操作。
SortedMap接口提供了访问第一个和最后一个键值对,以及截取子Map的操作方法。
通过Comparator子类可以实现定制排序。
四、AbstractMap 抽象类
1. 简介
他是Java中的一个抽象类,包含了Map接口的大部分方法,提供了一些通用的功能,可以作为其他具体Map实现的基类。该类提供Map接口的框架实现,以最大限度地减少实现该接口所需的工作量。要实现不可修改的映射,程序员只需要扩展该类并提供entrySet方法的实现,该方法返回映射映射的集合视图。
通常,返回的集合将反过来在AbstractSet之上实现。此集合不应支持add()或remove()方法,其迭代器也不应支持remove方法。要实现可修改的映射,程序员必须额外覆盖该类的put方法(否则会抛出
UnsupportedOperationException),并且由entrySet().iterator()返回的迭代器必须额外实现其remove()方法。
程序员通常应该提供一个void(无参数)和map构造函数,按照map接口规范中的建议。该类中每个非抽象方法的文档详细描述了其实现。
如果正在实现的映射允许更有效的实现,则可以重写这些方法中的每个方法。该类是Java集合框架的成员;
2. 特点
- 他是一个抽象类,既然是抽象类,那么就不能被实例化,需要通过继承它创建具体的Map实现。
- 实现了Map接口的大部分方法,包括put(),get() ,remove()等方法。
- 提供了一些通用的方法,例如:entrySet()、keySet()、values()等方法,通常用于获取Map中的键值对,键集合和值集合。
- 提供默认的实现,可以具体简化Map实现的开发过程。
3. 优缺点
优点:
- 提供了 Map 接口的通用实现,减少了子类实现所有方法的复杂度。
- 对于 Map 接口中定义的方法,AbstractMap 提供了默认的实现,这些方法通常会抛出 UnsupportedOperationException 异常或
返回默认值,这适用于不支持修改操作的 Map。 - 对于需要修改 Map 的操作(如 put、remove 等),AbstractMap 提供了具体的实现,这样子类只需要重写它们即可。
- AbstractMap 可以作为定制 Map 的基类,用于创建具有特定行为的 Map 实现。
缺点:
- 由于 AbstractMap 是一个抽象类,它不能直接实例化,只能作为其他具体 Map 实现的基类。这可能会限制一些灵活性和自定义性。
- AbstractMap 并没有提供对并发访问的支持,因此在多线程环境下使用时需要额外的同步措施。
4. 使用场景
- AbstractMap 可以作为其他具体 Map 实现的基类,用于实现自定义的 Map 类。
- 它可以用于简化 Map 的实现,提供一些通用的方法和默认实现。
5. 案例演示
使用 AbstractMap 实现的简单案例,展示了如何创建一个自定义的 Map 类并实现其中的方法:
import java.util.AbstractMap;
import java.util.Set;
public class CustomMap<K, V> extends AbstractMap<K, V> {
private Entry<K, V> entry;
public CustomMap(K key, V value) {
entry = new SimpleEntry<>(key, value);
}
@Override
public Set<Entry<K, V>> entrySet() {
return Set.of(entry);
}
@Override
public V put(K key, V value) {
V oldValue = entry.getValue();
entry = new SimpleEntry<>(key, value);
return oldValue;
}
@Override
public V get(Object key) {
if (entry.getKey().equals(key)) {
return entry.getValue();
}
return null;
}
@Override
public V remove(Object key) {
if (entry.getKey().equals(key)) {
V oldValue = entry.getValue();
entry = null;
return oldValue;
}
return null;
}
}
在这个案例中,我们创建了一个 CustomMap 类,它继承了 AbstractMap 并实现了其中的方法。在构造函数中,我们创建了一个SimpleEntry 对象,用于存储键值对。然后,我们重写了 entrySet() 方法,
返回一个包含这个 SimpleEntry 的 Set。
我们还重写了 put()、get() 和 remove() 方法,以实现对键值对的操作。
测试:
public class Main {
public static void main(String[] args) {
CustomMap<String, Integer> map = new CustomMap<>("key", 10);
System.out.println(map.get("key")); // 输出: 10
map.put("key", 20);
System.out.println(map.get("key")); // 输出: 20
map.remove("key");
System.out.println(map.get("key")); // 输出: null
}
}
五、HashMap 实现类
1. 简介
Hash
又叫哈希、散列,它的作用是使用有限的特征去映射无限的信息,这个过程需要借助算法来实现,这个算法就叫做哈希算法。
hash算法没有固定公式,可以理解为一种思想。
hash碰撞
既然输入是无限的,而我们使用的特征空间是有限的,那么不管使用的hash算法多么复杂和精妙,都有可能出现不同的输入信息计算出
的特征是相同的,这就是hash碰撞,也叫hash冲突。比如不同的java对象,调用Object类的hashCode()方法,就有可能计算出相同的
hash值。
Map
一个java接口,用来保存映射关系,这里说的映射关系,指的是 <key, value>形式键值对,具体怎么保存,需要由实现类来实现。
HashMap
Map接口的实现类,它内部会初始化一个Node结点类型的数组,k-v键值对就保存在Node中。
这些Node结点在数组中的位置,是通过对k-v中的key进行hash运算,根据计算出的值来确定的,所以叫做HashMap。
2. 结构
以JDK 1.8为例,HashMap内部定义了一个 Node<K,V>[] 类型的成员变量,这是一个结点数组。而定义Node<K,V>的部分源码如下:
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; }
可以看到Node<K,V>对象中保存了我们要放入的 key,value,以及一个hash值,一个指向下一个结点的指针;
而这些属性都是通过构造函数传入的。
除了数组以外,遇到hash冲突时,HashMap使用了链表的方式存储hash冲突的元素,
当链表数量超过阈值时,还可能会对链表进行树化,转换成红黑树结构来存储。
整体结构图如下(实际红黑树叶子结点必须为黑色,此处仅为示意):
3. 构造方法
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);
}
构造方法中,并没有对存放数据的Node数组进行初始化,而仅仅是确定了两个属性 loadFactor 和 threshold。
当我们执行 put 方法放入元素时,检测到Node数组为null,才会调用 resize方法进行扩容,完成数组创建。
loadFactor:加载因子,默认值为0.75;
threshold:扩容阈值,计算方式为 容量 * 加载因子。容量默认为16,也就是说扩容阈值默认为12。
当我们在HashMap的构造函数中传入了初始容量 initialCapacity时,threshold会被计算出来,比如传入20,计算出的初始 threshold就是32;传入32,则算出来也是32;也就是说,tableSizeFor方法计算结果总是 >= 初始容量的最近的2的幂。这时的threshold仅仅起一个计数的所用,用来代表应该分配的初始容量,正确的 threshold (扩容阈值)会在首次扩容的时候确定来(容量 * 加载因子)。
4. 源码
put
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//数组为空或长度为0,先扩容。
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;
//槽中已有元素,判断是否为相同的key,是则直接覆盖。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果不是相同key,但是已有元素为树结点,则进行红黑树结点的覆盖/插入。
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);
//TREEIFY_THRESHOLD默认为8,binCount等于7时,此时链表上已经有9个结点,需要进行树化。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
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;
}
put方法中确定下标使用的运算是:(n - 1) & hash,相当于是拿hash值对数组长度进行%取模运算,只是直接使用位运算更加高效。
这里的hash则是通过调用key的hashCode方法,然后位运算得到:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
用流程图的方式描述 put方法的流程如下:
resize
resize(扩容)
由put方法的流程可知,调用resize方法扩容出现在首次put,以及put后元素个数大于扩容阈值时,扩
容的核心操作是创建一个更大的数组(HashMap采用2倍扩容),然后把原来的元素放到新数组中正确的位置上。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//如果Node数组已存在,2倍扩容得到新数组长度。
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//初始化,构造函数中指定初始容量走此逻辑
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//初始化,构造函数中未指定初始容量走此逻辑
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//确保扩容阈值被计算
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//如果原来的结点为单结点,直接重新计算hash值,放到新数组中。
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
/*
如果原来的结点为树形结点,进入树的迁移逻辑,如果迁移后发现节点数<=解除树化阈值
(UNTREEIFY_THRESHOLD,默认为6),则将红黑树拆成链表结构。
*/
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//链表走此逻辑。
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
/*
原链表中结点的hash值重新加算后,可能会拆解为两个链表,一个头结点还在原下标处(低处),而另一个头结点的下
标为原下标+原数组长度(高处),这里(e.hash & oldCap) == 0成立,则表示这个结点应该继续待在低处,反之在高处。
*/
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
关于(e.hash & oldCap) == 0的理解:
get
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//如果对应下标的结点就是,则直接返回。
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//如果对应下标的结点不是,但它后面还有结点。
if ((e = first.next) != null) {
//如果是树结构,在树中查找。
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//如果是链表结构,则遍历查找。
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
5.常用API
六、HashTable(遗弃类)
1. 简介
Hashtable和HashMap类似,同样是基于哈希表实现的,同样每个元素是一个key-value对,但其内部
只是通过单链表解决哈希冲突问题,而没有红黑树结构,当HashTable容量不足(超过了阀值)时,同样
会进行扩容操作。
Hashtable类声明如下:
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable
- 它继承于Dictionary,实现了Map、Cloneable、 Serializable等接口。
- Hashtable实现了Map接口,可以对它进行哈希表操作;
- 实现了Cloneable接口,能被克隆;
- 实现了Serializable接口,因此它支持序列化,能够通过序列化传输。
Hashtable是JDK1.0引入的类,Hashtable的很多方法都用synchronized修饰,是线程安全的,可以
用于多线程环境中。
2. 源码
HashTable有如下几个成员变量:
// 存储链表的数组
private transient Entry<?,?>[] table;
// 键值对的个数
private transient int count;
// 下一次resize的阈值大小 = HashTable容量 * 负载因子
private int threshold;
// 负载因子
private float loadFactor;
// HashTable结构修改次数,用于fail-fast机制
private transient int modCount = 0;
HashTable中的节点都被封装成为了Entry类型数据:
private static class Entry<K,V> implements Map.Entry<K,V> {
// 哈希值
final int hash;
final K key;
V value;
// 指向链表中的下一节点
Entry<K,V> next;
// 构造方法
protected Entry(int hash, K key, V value, Entry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
@SuppressWarnings("unchecked")
protected Object clone() {
return new Entry<>(hash, key, value,
(next==null ? null : (Entry<K,V>) next.clone()));
}
// Map.Entry Ops
public K getKey() {
return key;
}
public V getValue() {
return value;
}
// 设置value,若value是null,则抛出NullPointerException异常
public V setValue(V value) {
if (value == null)
throw new NullPointerException();
V oldValue = this.value;
this.value = value;
return oldValue;
}
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
return (key==null ? e.getKey()==null : key.equals(e.getKey())) &&
(value==null ? e.getValue()==null : value.equals(e.getValue()));
}
public int hashCode() {
return hash ^ Objects.hashCode(value);
}
public String toString() {
return key.toString()+"="+value.toString();
}
}
HashTable有如下四个构造方法:
// 参数指定了HashTable初始化时的容量以及负载因子
public Hashtable(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
table = new Entry<?,?>[initialCapacity];
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
// 参数指定了HashMap初始化时的容量,负载因子默认为0.75
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
// 无参构造方法,默认的初始化容量为11,负载因子默认为的0.75
public Hashtable() {
this(11, 0.75f);
}
// 根据其他Map来创建HashTable,负载因子为0.75
public Hashtable(Map<? extends K, ? extends V> t) {
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}
我们下面来看HashTable的几个关键方法:put方法、get方法和remove方法。
put(K key, V value)
public synchronized V put(K key, V value) {
// 若插入元素的value为null则抛出NullPointerException异常
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
// 计算key的hashcode
int hash = key.hashCode();
// 计算key在table数组中的下标
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
// 若数组对应下标不为null,则表明发生了哈希冲突
for(; entry != null ; entry = entry.next) {
// 若链表中已经存在键值为key的节点,则将key对应的value替换
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
// 返回旧的value
return old;
}
}
// 将元素添加到对应下标的链表中
addEntry(hash, key, value, index);
return null;
}
从上面的源码中我们可以看出,HashTable的key和value都不可以为null,若value为null,则程序会
直接抛出NullPointerException异常,若key为null,则在计算key的hashcode时,也会抛出
NullPointerException异常。
若链表中没有找到键值为key的节点,则通过addEntry方法将键值对添加到HashTable中:
private void addEntry(int hash, K key, V value, int index) {
// HashTable的结构修改次数加1
modCount++;
Entry<?,?> tab[] = table;
// 若节点个数 >= 阈值,则通过rehash()方法进行扩容操作
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
rehash();
tab = table;
hash = key.hashCode();
// 扩容之后,计算节点对应的新下标
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
// 将节点插入到链表的表头
tab[index] = new Entry<>(hash, key, value, e);
// 节点数目加1
count++;
}
若节点个数 >= 阈值,则通过rehash()方法进行扩容操作,我们来看一看rehash()方法:
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// 计算新的容量newCapacity = (oldCapacity << 1) + 1,即新容量 = 旧容量 * 2 + 1
int newCapacity = (oldCapacity << 1) + 1;
// 若新容量大于MAX_ARRAY_SIZE,MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
if (newCapacity - MAX_ARRAY_SIZE > 0) {
// 若旧容量等于MAX_ARRAY_SIZE,则直接返回
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
// 将新容量设置为MAX_ARRAY_SIZE
newCapacity = MAX_ARRAY_SIZE;
}
// 创建新的Entry数组
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
// 计算threshold
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
// 将旧数组的节点复制到新Entry数组中,i为数组下标
for (int i = oldCapacity ; i-- > 0 ;) {
// old为数组下标对应的链表节点
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
// 重新计算节点的下标
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
// 将节点插入到新数组中
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
get(Object key)
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
// 计算key的hashcode
int hash = key.hashCode();
// 计算key对应的数组下标
int index = (hash & 0x7FFFFFFF) % tab.length;
// 遍历数组下表对应的链表
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
// 找到匹配的节点,直接返回
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
// 返回null
return null;
}
remove(Object key)
public synchronized V remove(Object key) {
Entry<?,?> tab[] = table;
// 计算key的hashcode
int hash = key.hashCode();
// 计算key对应的数组下标
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
// 遍历数组下表对应的链表,找到对应节点并删除
// 因为是单链表,因此要保留待删节点的前一个节点,才能有效地删除节点
for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
modCount++;
if (prev != null) {
prev.next = e.next;
} else {
tab[index] = e.next;
}
count--;
V oldValue = e.value;
e.value = null;
return oldValue;
}
}
return null;
}
containsKey
因为HashTable的key和value都不可以为null,所以,判断一个key在HashTable中是否存在,可以用
get(Object)方法的返回值是否为null来判断,同时,也可以用containsKey(Object)方法来判断,该方法与
get(Object)方法很相似:
public synchronized boolean containsKey(Object key) {
Entry<?,?> tab[] = table;
// 计算key的hashcode
int hash = key.hashCode();
// 计算key对应的数组下标
int index = (hash & 0x7FFFFFFF) % tab.length;
// 遍历数组下表对应的链表
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
// 找到匹配的节点,返回true
if ((e.hash == hash) && e.key.equals(key)) {
return true;
}
}
// 返回false
return false;
}
clear()
// 删除HashTable中所有的键值对
public synchronized void clear() {
Entry<?,?> tab[] = table;
modCount++;
for (int index = tab.length; --index >= 0; )
// tab[index] = null,表明JVM可以对节点的内存进行回收,同时tab也不再拥有其内存空间
tab[index] = null;
count = 0;
}
HashTable中的modCount的作用这里不再解释,可以参考这篇博客。
3. HashTable与HashMap的主要异同点
- 它们都是通过哈希表来实现的,而且都是通过链表来解决哈希冲突的,但是HashMap在链表达到一定长度之后,会将其转化为红黑树。
- 它们计算节点哈希值的方式不同,若key的hashcode为h,则HashMap通过h ^ (h >>> 16)来计算节点的哈希值,而HashTable则将h作为节点的哈希值。
- 它们计算节点对应数组索引下标的方式也不同,HashMap通过haseCode & (capacity - 1)是用来计算节点对应的数组下标,HashTable通过(hashCode & 0x7FFFFFFF) % capacity来计算节点对应的数组下标。hashCode & 0x7FFFFFFF的目的是为了将负的hash值转化为正值。
- HashTable的默认容量为11,而HashMap默认容量为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。但是,它们的默认负载因子都是0.75。
- Hashtable扩容时,会将容量变为原来的2倍加1,而HashMap扩容时,会将容量变为原来的2倍。
- Hashtable中key和value都不允许为null,而HashMap中key和value都允许为null(key只能有一个为null,而value则可以有多个为null)若Hashtable中的key或者value为null,则程序运行时会抛出NullPointerException异常。
- HashTable中的大部分的方法都被synchronized修饰,所以HashTable是线程安全的,可以用于多线程环境中,而HashMap则不行
七、LinkedHashMap 实现类
1. 简介
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
LinkedHashMap继承自HashMap,它的多种操作都是建立在HashMap操作的基础上的。
同HashMap不同的是,LinkedHashMap维护了一个Entry的双向链表,保证了插入的Entry中的顺序。这也是Linked的含义。
结构图如下:
加入插入顺序为key1,key2,key3,key4,那么就会维护一个红线所示的双向链表。
为了实现双向链表,LinkedHashMap中提供了如下的Entry:
/**
* LinkedHashMap中的node直接继承自HashMap中的Node。并且增加了双向的指针
*/
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
可以说,LinkedHashMap=HashMap+双向链表
2. 原理
2.1. 主要元素
/**
* 头指针,指向第一个node
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* 尾指针,指向最后一个node
*/
transient LinkedHashMap.Entry<K,V> tail;
/**
* 一个条件变量,它控制了是否在get操作后需要将新的get的节点重新放到链表的尾部
* LinkedHashMap可以维持了插入的顺序,但是这个顺序不是不变的,可以被get操作打乱。
*
* @serial
*/
final boolean accessOrder;
其他的元素就是直接继承HashMap中的。
2.2. 构造函数
/**
* Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
* with the specified initial capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
/**
* 构造一个空的,按插入序(accessOrder = false)的LinkedHashMap,使用默认初始大小和负载因子0.75
*
* @param initialCapacity the initial capacity
* @throws IllegalArgumentException if the initial capacity is negative
*/
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
/**
* 默认构造函数也是关闭了get改变顺序,使用插入序。
*/
public LinkedHashMap() {
super();
accessOrder = false;
}
/**
* Constructs an insertion-ordered <tt>LinkedHashMap</tt> instance with
* the same mappings as the specified map. The <tt>LinkedHashMap</tt>
* instance is created with a default load factor (0.75) and an initial
* capacity sufficient to hold the mappings in the specified map.
*
* @param m the map whose mappings are to be placed in this map
* @throws NullPointerException if the specified map is null
*/
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super();
accessOrder = false;
putMapEntries(m, false);
}
/**
* 这个构造方法指定了accessOrder
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @param accessOrder the ordering mode - <tt>true</tt> for
* access-order, <tt>false</tt> for insertion-order
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
注意:构造函数如果不明确传入accessOrder的话,默认都是按插入序的。
2.3. 维护链表的操作
维护链表主要使用三个方法:
afterNodeRemoval,afterNodeInsertion,afterNodeAccess
这三个方法的主要作用是,在删除,插入,获取节点之后,对链表进行维护。
简单来说,这三个方法中执行双向链表的操作:
afterNodeRemoval
顾名思义,在节点remove操作后进行调用:
//在节点删除后,维护链表,传入删除的节点
void afterNodeRemoval(Node<K,V> e) { // unlink
//p指向待删除元素,b执行前驱,a执行后驱
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
//这里执行双向链表删除p节点操作,很简单。
p.before = p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a == null)
tail = b;
else
a.before = b;
}
afterNodeAccess
//在节点被访问后根据accessOrder判断是否需要调整链表顺序
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
//如果accessOrder为false,什么都不做
if (accessOrder && (last = tail) != e) {
//p指向待删除元素,b执行前驱,a执行后驱
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
//这里执行双向链表删除操作
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
//这里执行将p放到尾部
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
//保证并发读安全。
++modCount;
}
}
afterNodeInsertion
这是一个很奇葩的方法,虽然名字是在插入之后进行的维护链表的操作,但是默认实际上这个什么都没做,看代码:
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
//removeEldestEntry(first)默认返回false,所以afterNodeInsertion这个方法其实并不会执行
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
为什么不执行也可以呢,这个要到put操作的时候就能看出来了?
那么removeEldestEntry这个方法有什么用呢,看名字可以知道是删除最久远的节点,也就是
head节点,这个方法实际是给我们自己扩展的。
默认是没有用的,接下来实现LRU的代码中将可以看到它的作用。
2.4. 重要方法
get
LinkedHashMap重写了HashMap的get方法,如下:
/**
* 调用hashmap的getNode方法获取到值之后,维护链表
* @param key
* @return
*/
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
这个方法的实现简单易懂。
put
HashMap#putVal(…)
LinkedHashMap没有重写HashMap的put方法,所以执行put操作的时候,还是使用的是HashMap的put方法。
那么这样如何保证链表的逻辑呢?原因就是HashMap的putVal方法中实际调用了维护链表的方法。
下面是关键代码:HashMap的putVal()方法
//默认的传入的evict是true
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
................
................
................
................
if (e != null) { // existing mapping for key
//如果e不为null,此时的e指向的就是在map中的那个插入点,所以这个时候来赋值。
V oldValue = e.value;
// onlyIfAbsent入口参数,为true,则不更新value(前面已说明)。
//这个地方的主要作用主要控制如果map中已经有那个key了,是否需要需要更新值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//这里其实是插入成功后执行的,获得的效果就是将e放到了链表结尾。
//所以afterNodeInsertion方法就算什么都不做也可以。
//但是如果accessOrder为false,那么我们新插入的节点,都不会进入链表了
afterNodeAccess(e);
return oldValue;
}
}
//fast-fail机制的实现,为了保证并发读安全。
++modCount;
//容器中的键值对数自增,如果大于了阈值,开始扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
在put方法中,HashMap会在合适的位置使用 afterNodeAccess(e),和
afterNodeInsertion(evict);方法。
因为在HashMap中也定义了这三个函数,但是都是为空函数,在LInkedHashMap中只是重
写了这3个方法。
我们在使用map.put(key,value)的时候,实际调用HashMap#putVal(key,value)方法,然后
再调用afterNodeAccess方法,那么这个时候调用的会是子类的afterNodeAccess方法吗?
这个就要涉及到多态的知识了,可以从虚拟机方面去解释:在虚拟机加载类的解析过程中,对
方法调用有两种分派方式,静态分派对应重载,动态分派对应重写。
这里对应的就是动态分派。
动态分配是在运行时发生的,它对于方法是按照实际类型来首先寻找的。
找不到再往父类走。这里的实际类型其实值new 后面跟着的类。
所以这里不用担心会调用到父类的方法。
afterNodeInsertion方法不是没有用,而是留给扩展用的,下面会展示。
还有一点,put操作中使用afterNodeAccess来将新插入的节点放到尾部。但是这个方法要受
到accessOrder的控制,如果accessOrder为false(默认就为false)那么新插入的节点应该就不能
插入到链表中了。这样设计有什么特殊的意义吗?
Remove
HashMap#removeNode(…)
和put操作一样,也是直接使用HashMap的方法来完成的:
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
//判断table是否为空,该key对应的桶是否为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//到这里了其实node就已经指向了要删除的节点了
//matchValue的作用是指现在是否需要值匹配。因为可能没有传入value,所以判断一下
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
//调用维护链表的操作
afterNodeRemoval(node);
return node;
}
}
return null;
}
3. LinkedHashMap用作实现LRU
LRU,即最近最少使用。LRU中保存的数据如果满了,那么就会将最近最少使用的数据删除。
LinkedHashMap通过使accessOrder为true,可以满足这种特性。代码如下:
leetcode 146. LRU缓存机制
class LRUCache extends LinkedHashMap {
private int capacity;
public LRUCache(int capacity) {
//accessOrder为true
super(capacity, 0.75F, true);
this.capacity = capacity;
}
public int get(int key) {
return (int)super.getOrDefault(key, -1);
}
public void put(int key, int value) {
super.put(key, value);
}
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > capacity;
}
}
这里重写了removeEldestEntry方法,然后removeEldestEntry方法在afterNodeInsertion中被调用,如果这个方法返回真,那么就会删除head指向的节点。根据每次get的节点都会放到尾部
的特性,所以head指向的节点就是最久没有使用到的节点,所以可以删除。
由于我们每次put完(HashMap#putVal())都会调用这个afterNodeInsertion方法,所以可
以上面的设计可以使put过后如果size超了,将删除最久没有使用的一个节点,从而腾出空间给新
的节点。
4. 总结
一句话总结,LinkedHashMap就是HashMap中将其 node 维护成了一个双向链表。
只要搞懂了HashMap就可以很容易搞懂LinkedHashMap。
八、TreeMap 实现类
1. 简介
前面知识梳理:
其实 Map 在 Java 里面主要分为两种:
HashMap 和 TreeMap,区别就是 TreeMap 有序,HashMap 无序。
如果只需要存映射,那么 HashMap 就够了,但是如果需要存有顺序的 key 那么就用 TreeMap。
写程序需要知道怎么构建 comparator 去自定义排序,还要知道 floorKey 和 floorEntry。
TreeMap 存储 K-V 键值对,通过红黑树(R-B tree)实现。
TreeMap 继承了 NavigableMap 接口,NavigableMap 接口继承了
SortedMap 接口,可支持一系列的导航定位以及导航操作的方法,当然只是提供了接口,需要
TreeMap 自己去实现;
TreeMap 实现了 Cloneable 接口,可被克隆,实现了 Serializable 接口,可序列化;
TreeMap 因为是通过红黑树实现,红黑树结构天然支持排序,默认情况下通过 Key 值的自然顺序
进行排序。
TreeMap 是一个能比较元素大小的 Map 集合,会对传入的 key 进行了大小排序。可以使用元素
的自然顺序,也可以使用集合中自定义的
比较器来进行排序。
2. 特点
代码方面理解:
- TreeMap 是有序的 key-value 集合,通过红黑树实现。根据键的自然顺序进行排序或根据提供的 Comparator 进行排序。
- TreeMap 继承了 AbstractMap,实现了 NavigableMap 接口,支持一系列的导航方法,给定具体搜索目标,可以返回最接近的匹配项。如 floorEntry()、ceilingEntry() 分别返回小于等于、大于等于给定键关联的 Map.Entry() 对象,不存在则返回 null。lowerKey()、floorKey、ceilingKey、higherKey()只返回关联的key。
使用方面理解:
- 元素中键不能重复
- 元素会按照大小顺序排序
例证如下:
元素不能重复,重复会覆盖
public class Demo {
@Test
public void test() {
// 创建对象
TreeMap<Integer, String> map = new TreeMap<>();
// 添加元素
map.put(1, "abc");
map.put(1, "def");
map.put(1, "ghi");
System.out.println(map);
}
}
输出结果:
{1=ghi}
key的取出顺序和放入顺序无关,会按照key从小到大默认排序
@Test
public void test() {
// 创建对象
TreeMap<Integer, String> map = new TreeMap<>();
// 添加元素
map.put(9, "abc");
map.put(2, "def");
map.put(1, "ghi");
System.out.println(map);
}
输出结果:
{1=ghi, 2=def, 9=abc}
3. 数据结构
TreeMap底层由红黑树构成,而红黑树是一种特殊的二叉查找树。
常见的树型结构:
3.1 二叉查找树
符合以下特点的树,称为二叉查找树
定位
- 若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 左、右子树也分别为二叉排序树;
- 没有相等的结点;
结论:
二叉查找树就是每个结点的值按照大小排列的二叉树,二叉查找树方便对结点的值进行查找。
图:
查找
查找方式:
从根结点开始,如果要查找的数据等于结点的值, 那就返回。
如果要查找的数据小于结点的值,那就在左子树中递归查找;
如果要查找的数据大于结点的值,那就在右子树中递归查找。
图:
3.2 平衡二叉树
定义
为了避免出现"瘸子"的现象,减少树的高度,提高我们的搜素效率,又存在一种树的结构:“平衡二叉树”
它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
旋转
前言
在构建一棵平衡二叉树的过程中,当有新的结点要插入时,检查是否因插入后而破坏了树的平衡,
如果是,则需要做旋转去改变树的结构。
两种旋转方式
左旋:
左旋就是将结点的右支往左拉,右子结点变成父结点,并把晋升之后多余的左子结点出让给降级结点的右子结
点;
右旋:
将结点的左支往右拉,左子结点变成了父结点,并把晋升之后多余的右子结点出让给降级结点的 左子结点
四种失衡情况
左左情况,需要以10为基准结点进行右旋
左右情况,先以7为基准结点进行左旋,再以11为基准结点进行右旋
右左情况,先以15为基准结点进行右旋,再以11为基准结点进行左旋
右右情况,以11未基准结点进行左旋
3.3 红黑树
定义
红黑树是一种自平衡的二叉查找树。
红黑树的每一个结点上都有存储位表示结点的颜色,可以是红或者黑。
红黑树不是高度平衡的,它的平衡是通过"红黑树的特性"进行实现的。
特性
- 每一个结点或是红色的,或者是黑色的;
- 根结点必须是黑色;
- 每个叶结点是黑色的(叶结点是Nil)
- 如果某一个结点是红色,那么它的子结点必须是黑色(不能出现两个红色结点相连的情况)
- 对每一个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点;
- 红黑树中左右子树的深度差不能超过短的二倍(例如左子树深度2,右子树深度4,这种就是不符合的)
图:
3.4 源码
get()
@Test
public void test() {
// 创建对象
TreeMap<Integer, String> map = new TreeMap<>();
// 添加元素
map.put(9, "abc");
map.put(2, "def");
map.put(1, "ghi");
String s = map.get(2);
System.out.println(s);
}
输出结果:
def
我们看一下get()方法的源码:
public V get(Object key) {
//调用方法根据键获取Entry对象
Entry<K,V> p = getEntry(key);
//判断对象如果是null返回null,如果不是null返回对象中的值
return (p==null ? null : p.value);
}
其中Entry<K,V>是什么呢,不妨继续看一下源码
//Entry类型表示结点
static final class Entry<K,V> implements Map.Entry<K,V> {
K key; //key表示键
V value; //value表示值
Entry<K,V> left; //left表示左子结点的地址
Entry<K,V> right; //rigth表示右子结点的地址
Entry<K,V> parent; //parent表示父结点的地址
boolean color = BLACK; //color表示结点的颜色
//下面方法省略…………
}
具体getEntry()方法如下:可以看出来
TreeMap中key不允许为null
final Entry<K,V> getEntry(Object key) {
//判断有没有传入comparator
if (comparator != null)
//调用方法,使用比较器做查询
return getEntryUsingComparator(key);
//判断传入的键是否为null
if (key == null)
//如果要查询的键是null则抛出空指针异常
throw new NullPointerException();
@SuppressWarnings("unchecked")
//把Object类型的键向下转型为Comparable
Comparable<? super K> k = (Comparable<? super K>) key;
//先把二叉树的根结点赋值给p
Entry<K,V> p = root;
//如果p不为null,一直循环比较
while (p != null) {
//调用Comparable的compareTo()方法进行比较
int cmp = k.compareTo(p.key);
//如果cmp小于0,表示要查找的键小于结点的数字
if (cmp < 0)
//把p左子结点赋值给p对象
p = p.left;
//如果cmp大于0,表示要查找的键大于结点的数字
else if (cmp > 0)
//把P右子结点赋值给p对象
p = p.right;
else
//要查找的键等于结点的值,就把当前Entry对象直接返回
return p;
}
//已经找到叶子结点,没有找到要查找的数字返回null
return null;
}
传入比较器之后,通过比较器查询
//传入比较器的情况下
final Entry<K,V> getEntryUsingComparator(Object key) {
@SuppressWarnings("unchecked")
//把Object类型的Key向下转型为对应的键的类型
K k = (K) key;
//给比较器对象起名字cpr
Comparator<? super K> cpr = comparator;
if (cpr != null) {
//把二叉树的根结点赋值给P对象
Entry<K,V> p = root;
//循环用要查找的数字和结点中的数字进行比较
while (p != null) {
//调用比较器的compare()
int cmp = cpr.compare(k, p.key);
if (cmp < 0)
p = p.left;
else if (cmp > 0)
p = p.right;
else
return p;
}
}
return null;
}
put()
public V put(K key, V value) {
//获取根结点赋值给变量t
Entry<K,V> t = root;
//判断根结点是否为null
if (t == null) {
//对key进行非空和类型校验
compare(key, key);
//新建一个结点
root = new Entry<>(key, value, null);
//设置集合长度为1
size = 1;
//记录集合被修改的次数
modCount++;
//添加成功返回null
return null;
}
这里看一下compare方法具体是啥
如果put的key是null的情况下会抛出空指针异常,或者说key即没有实现comparator比较器,
也没有实现Comparable接口的话,也会抛出空指针异常。
// 非空和类型校验
final int compare(Object k1, Object k2) {
return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
: comparator.compare((K)k1, (K)k2);
}
接着看put()方法的下半部分
//如果根结点不是null则执行下面代码
int cmp;
Entry<K,V> parent;
//把比较器对象赋值给变量cpr
Comparator<? super K> cpr = comparator;
//判断比较器对象如果不是空则执行下面代码
if (cpr != null) {
do {
//把当前结点赋值给变量parent
parent = t;
//比较当前结点的键和要存储的键的大小
cmp = cpr.compare(key, t.key);
//如果要存储的键小于当前结点,则继续和左边的结点进行比较
if (cmp < 0)
t = t.left;
//如果要存储的键大于当前结点,则继续和右边的结点进行比较
else if (cmp > 0)
t = t.right;
else
//如果要存储的键等于当前结点的键,则调用setValue()方法设置新的值
//并结束循环
return t.setValue(value);
//循环直到遍历到叶子结点结束为止
} while (t != null);
}
//如果比较器对象是空则执行下面代码
else {
//如果要保存的键为空,抛出空指针异常
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
//把键转型为Comparable类型
Comparable<? super K> k = (Comparable<? super K>) key;
do {
//把当前结点赋值给变量parent
parent = t;
//比较要存储的键和当前结点的键
cmp = k.compareTo(t.key);
//如果要存储的键小于当前结点,则继续和左边的结点比较
if (cmp < 0)
t = t.left;
//如果要存储的键大于当前结点,则继续和右边的结点比较
else if (cmp > 0)
t = t.right;
else
//如果要存储的键等于当前结点的键,则调用setValue()方法设置新的值
//并结束循环
return t.setValue(value);
//循环直到遍历到叶子结点结束为止
} while (t != null);
}
//遍历结束如果没有找到相同的键,则执行下面代码
//创建新的结点对象,保存键值对,设置父结点
Entry<K,V> e = new Entry<>(key, value, parent);
//如果新的键小于父结点的键,则保存在左边
if (cmp < 0)
parent.left = e;
else
//如果新的键大于父结点的键,则保存在右边
parent.right = e;
//维持红黑树的平衡
fixAfterInsertion(e);
//集合长度加一
size++;
//集合修改次数加一
modCount++;
//返回被覆盖的值是null
return null;
}
3.5 自定义
代码层
使用二叉树实现TreeMap集合,编写put(),get(),remove()等关键方法
package com.exercise;
import java.util.Comparator;
/**
* 自定义一个TreeMap
*
* @author zhengge
* @date 2024/5/12 9:21
*/
public class MyTreeMap<K, V> {
// 自定义一个内部类
private class Entry<K, V> {
// 键
K key;
// 值
V value;
// 左子结点
Entry<K, V> left;
// 右子结点
Entry<K, V> right;
// 父结点
Entry<K, V> parent;
//有参构造器
public Entry(K key, V value, Entry<K, V> left, Entry<K, V> right, Entry<K, V> parent) {
this.key = key;
this.value = value;
this.left = left;
this.right = right;
this.parent = parent;
}
}
// 定义一个比较器
private final Comparator<? super K> comparator;
// 无参构造给comparator赋值
public MyTreeMap() {
comparator = null;
}
// 有参构造给comparator赋值
public MyTreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
// 根结点
private Entry<K, V> root;
// 定义集合的长度
private int size;
/**
* @param
* @return int
* @description //获取长度
* @date 2024/5/12 9:09
* @author wty
**/
public int size() {
return size;
}
/**
* @param
* @return V
* @description //根据键获取值
* @param: k
* @date 2024/5/12 9:10
* @author wty
**/
public V get(K key) {
Entry<K, V> entry = getEntry(key);
return null == entry ? null : entry.value;
}
/**
* @param
* @return com.exercise.MyTreeMap<K, V>.Entry<K,V>
* @description //根据键获取值(通用方法)
* @param: key
* @date 2024/5/12 9:11
* @author wty
**/
private Entry<K, V> getEntry(Object key) {
// 非空校验
if (null == key) {
throw new NullPointerException();
}
// 给跟结点起一个名字
Entry<K, V> t = root;
// 判断有没有传入比较器
// 传入了比较器
if (null != comparator) {
// 向下转型
K k = (K) key;
// 循环
while (null != t) {
int cmp = comparator.compare(k, t.key);
if (cmp < 0) {
t = t.left;
} else if (cmp > 0) {
t = t.right;
} else {
return t;
}
}
} else {
// 没有传入比较器
Comparable<K> k = (Comparable<K>) key;
while (null != t) {
int cmp = k.compareTo(t.key);
if (cmp > 0) {
t = t.right;
} else if (cmp < 0) {
t = t.left;
} else {
return t;
}
}
}
// 如果找不到,就返回null
return null;
}
/**
* @param
* @return java.lang.String
* @description //添加元素
* @param: key
* @param: value
* @date 2024/5/12 9:19
* @author wty
**/
public V put(K key, V value) {
//给根结点赋值
Entry<K, V> t = root;
// 非空校验
if (null == key) {
throw new NullPointerException();
}
// 判断集合是否为空
if (null == t) {
// 创建一个新的结点
Entry<K, V> entry = new Entry<>(key, value, null, null, null);
// 给根结点赋值
root = entry;
// 集合长度+1
size++;
return null;
}
// 创建键值对,表示新增结点的父结点
Entry<K, V> parent = t;
int cmp = 0;
// 判断是否有比较器
// 有比较器
if (null != comparator) {
while (null != t) {
parent = t;
//判断键
cmp = comparator.compare(key, t.key);
if (cmp > 0) {
t = t.right;
} else if (cmp < 0) {
t = t.left;
} else {
// 用新的值替换旧的值,把旧的值替换掉
V v = t.value;
t.value = value;
return v;
}
}
} else {
// 没有比较器
Comparable<? super K> k = (Comparable<? super K>) key;
while (null != t) {
parent = t;
cmp = k.compareTo(t.key);
if (cmp > 0) {
t = t.right;
} else if (cmp < 0) {
t = t.left;
} else {
// 用新的值替换旧的值,把旧的值替换掉
V v = t.value;
t.value = value;
return v;
}
}
}
// 要添加的键值对 键不重复
Entry<K, V> entry = new Entry<>(key, value, null, null, parent);
if (cmp > 0) {
parent.right = entry;
} else {
parent.left = entry;
}
// 集合长度增加
size++;
return null;
}
/**
* @param
* @return V
* @description //移除元素
* @param: key
* @date 2024/5/12 9:30
* @author wty
**/
public V remove(K key) {
Entry<K, V> entry = getEntry(key);
if (null == entry) {
return null;
}
// 删除操作
// 1.删除中间结点
// 1.1没有左子树,只有右子树
if (entry.left == null && entry.right != null) {
// 判断要删除的结点是父结点的右子结点
if (entry.parent.right == entry) {
entry.parent.right = entry.right;
} else if (entry.parent.left == entry) {
entry.parent.left = entry.right;
} else {
root = entry.right;
}
// 让被删除结点的子结点,指向父结点
entry.right.parent = entry.parent;
}
// 1.2没有右子树,只有左子树
else if (entry.right == null && entry.left != null) {
// 判断要删除的结点是父结点的右子结点
if (entry.parent.right == entry) {
entry.parent.right = entry.left;
} else if (entry.parent.left == entry) {
entry.parent.left = entry.left;
} else {
root = entry.left;
}
// 让被删除结点的子结点,指向父结点
entry.left.parent = entry.parent;
}
// 2.删除根结点 既有右子树,又有左子树
else if (entry.right != null && entry.left != null) {
//找到后继结点
Entry<K, V> target = entry.right;
// 用右子树的最左子结点去替换
while (target.left != null) {
target = target.left;
}
// 右子结点作为后继结点
if (entry.right == target) {
target.parent = entry.parent;
if (entry == root) {
root = target;
} else if (entry.parent.right == entry) {
entry.parent.right = target;
} else if (entry.parent.left == entry) {
entry.parent.left = target;
}
// 被删除结点左子结点重新指向新的父结点
entry.left.parent = target;
target.left = entry.left;
} else {
// 右子树的最左子结点作为后继结点
if (target.right == null) {
// 后继结点没有子结点
target.parent.left = null;
} else {
// 后继结点有子结点
target.parent.left = target.right;
target.right = target.parent;
}
// 让后继结点替换掉被删除结点
if (entry == root) {
root = target;
} else if (entry.parent.right == entry) {
entry.parent.right = target;
} else if (entry.parent.left == entry) {
entry.parent.left = target;
}
// 被删除结点左右子树需要指向后继结点
entry.left.parent = target;
entry.right.parent = target;
target.left = entry.left;
target.right = entry.right;
}
} else {
// 3.删除叶子结点
if (entry.parent.right == entry) {
entry.parent.right = null;
} else if (entry.parent.left == entry) {
entry.parent.left = null;
} else {
root = null;
}
}
// 给集合长度减少1
size--;
return entry.value;
}
/**
* @param
* @return java.lang.String
* @description //打印树的结构
* @date 2024/5/12 9:55
* @author wty
**/
@Override
public String toString() {
// 非空检验
if (null == root) {
return "{}";
}
String s = "{";
String s1 = methodToString(root);
s = s + s1.substring(0, s1.length() - 2) + "}";
return s;
}
/**
* @param
* @return java.lang.String
* @description //打印树的结构(递归调用)
* @param: entry
* @date 2024/5/12 9:55
* @author wty
**/
private String methodToString(Entry<K, V> entry) {
String s = "";
// 拼接左子树
if (entry.left != null) {
s += methodToString(entry.left);
}
// 拼接中间结点
s += entry.key + "=" + entry.value + ", ";
// 拼接右子树
if (entry.right != null) {
s += methodToString(entry.right);
}
return s;
}
}
测试层
测试代码
@Test
public void test() {
MyTreeMap<Integer, String> treeMap = new MyTreeMap<>();
treeMap.put(5, "abc");
treeMap.put(3, "def");
treeMap.put(6, "ghi");
treeMap.put(1, "jkl");
treeMap.put(4, "mno");
System.out.println(treeMap);
}
运行结果:
{1=jkl, 3=def, 4=mno, 5=abc, 6=ghi}
继续测试get
@Test
public void test() {
MyTreeMap<Integer, String> treeMap = new MyTreeMap<>();
treeMap.put(5, "abc");
treeMap.put(3, "def");
treeMap.put(6, "ghi");
treeMap.put(1, "jkl");
treeMap.put(4, "mno");
System.out.println(treeMap);
System.out.println(treeMap.get(3));
}
运行结果:
{1=jkl, 3=def, 4=mno, 5=abc, 6=ghi}
def
最后测试一下删除remove方法
@Test
public void test() {
MyTreeMap<Integer, String> treeMap = new MyTreeMap<>();
treeMap.put(5, "abc");
treeMap.put(3, "def");
treeMap.put(6, "ghi");
treeMap.put(1, "jkl");
treeMap.put(4, "mno");
System.out.println(treeMap);
System.out.println(treeMap.get(3));
treeMap.remove(4);
System.out.println(treeMap);
}
运行结果:
{1=jkl, 3=def, 4=mno, 5=abc, 6=ghi}
def
{1=jkl, 3=def, 5=abc, 6=ghi}
3.6 API
1. 构造方法
方法名 | 方法说明 | 方法名 | 方法说明 |
public TreeMap() | 创建一个空TreeMap,keys按照自然排序 | public TreeMap(Comparator comparator) | 创建一个空TreeMap,按照指定的comparator排序 |
public TreeMap(Map m) | 由给定的map创建一个TreeMap,keys按照自然排序 | public TreeMap(SortedMap m) | 由给定的有序map创建TreeMap,keys按照原顺序排序 |
2. 常用方法
增添元素
- V put(K key, V value):将指定映射放入该TreeMap中
- V putAll(Map map):将指定map放入该TreeMap中
删除元素
- void clear():清空TreeMap中的所有元素
- V remove(Object key):从TreeMap中移除指定key对应的映射
修改元素
- V replace(K key, V value):替换指定key对应的value值
- boolean replace(K key, V oldValue, V newValue):当指定key的对应的value为指定值时,替换该值为新值
查找元素
- boolean containsKey(Object key):判断该TreeMap中是否包含指定key的映射
- boolean containsValue(Object value):判断该TreeMap中是否包含有关指定value的映射
- Map.Entry<K, V> firstEntry():返回该TreeMap的第一个(最小的)映射
- K firstKey():返回该TreeMap的第一个(最小的)映射的key
- Map.Entry<K, V> lastEntry():返回该TreeMap的最后一个(最大的)映射
- K lastKey():返回该TreeMap的最后一个(最大的)映射的key
- v get(K key):返回指定key对应的value
- SortedMap<K, V> headMap(K toKey):返回该TreeMap中严格小于指定key的映射集合
- SortedMap<K, V> subMap(K fromKey, K toKey):返回该TreeMap中指定范围的映射集合(大于等于fromKey,小于toKey)
遍历接口
- Set<Map<K, V>> entrySet():返回由该TreeMap中的所有映射组成的Set对象
- void forEach(BiConsumer<? super K,? super V> action):对该TreeMap中的每一个映射执行指定操作
- Collection<V> values():返回由该TreeMap中所有的values构成的集合
其他方法
- Object clone():返回TreeMap实例的浅拷贝
- Comparator<? super K> comparator():返回给该TreeMap的keys排序的comparator,若为自然排序则返回null
- int size():返回该TreepMap中包含的映射的数量
九、ConcurrentHashMap 实现类
1. 简介
ConcurrentHashMap 是线程安全并且高效的 HashMap。
本节让我们一起研究下该容器是如何在 保证线程安全的同时又能保证高效的操作。
2. 为什么要使用 ConcurrentHashMap
在并发编程中使用HashMap可能导致程序死循环。
而使用线程安全的HashTable效率又非常低下,基于以上两个原因,便有了ConcurrentHashMap的登
场机会。
2.1 线程不安全的HashMap。
在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,
所以在并发情况下不能使用HashMap。
例如,执行以下代码会引起死循环:
HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表
形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获取
Entry。
2.2 效率低下的HashTable
HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的
效率非常低下。
因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进
入阻塞或轮询状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不
能使用get方法来获取元素,所以竞争越激烈效率越低。
2.3 为什么不使用用HashTable?
实际问题:ConcurrentHashMap的锁分段技术可有效提升并发访问率?
HashTable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问HashTable的线程
都必须竞争同一把锁,假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么 当多线
程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并发访问效率,
这就是ConcurrentHashMap所使用的锁分段技术。首先将数据分成一段一段地存储,然后给每一
段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数 据也能被其他线程
访问。
3. 结构
通过ConcurrentHashMap的类图来分析ConcurrentHashMap的结构,如图6-1所示。
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁(ReentrantLock),在
ConcurrentHashMap里扮演锁的角色;HashEntry则用于存储键值对数 据。一个ConcurrentHashMap里包含一个Segment数组。
Segment的结构和HashMap类似,是一种 数组和链表结构。一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构
的元 素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时, 必须首先获得与它对应的
Segment锁,如图6-2所示。
4.重要方法
initial
ConcurrentHashMap初始化方法是通过initialCapacity、loadFactor和concurrencyLevel等几个参数来初始化segment数组、段偏移
量segmentShift、段掩码segmentMask和每个segment里的HashEntry数组来实现的。
初始化segments数组
让我们来看一下初始化segments数组的源代码。
由上面的代码可知,segments数组的长度ssize是通过concurrencyLevel计算得出的。为了
能 通过按位与的散列算法来定位segments数组的索引,必须保证segments数组的长度是2的N次
方 (power-of-two size),所以必须计算出一个大于或等于concurrencyLevel的最小的2的N次
方值来作为segments数组的长度。假如concurrencyLevel等于14、15或16,ssize都会等于16,
即容器里锁的个数也是16。
注意:concurrencyLevel的最大值是65535,这意味着segments数组的长度最大为65536,
对应的二进制是16位。
初始化segmentShift和segmentMask
这两个全局变量需要在定位segment时的散列算法里使用,sshift等于ssize从1向左移位的
次数,在默认情况下concurrencyLevel等于16,1需要向左移位移动4次,所以sshift等于4。
segmentShift用于定位参与散列运算的位数,segmentShift等于32减sshift,所以等于28,这里
之所 以用32是因为ConcurrentHashMap里的hash()方法输出的最大数是32位的,后面的测试中
我们可以看到这点。
segmentMask是散列运算的掩码,等于ssize减1,即15,掩码的二进制各个位的值都是1。
因为ssize的最大长度是65536,所以segmentShift最大值是16,segmentMask最大值是
65535,对应的二进制是16位,每个位都是1。
初始化每个segment
输入参数initialCapacity是ConcurrentHashMap的初始化容量,loadfactor是每个segment
的负 载因子,在构造方法里需要通过这两个参数来初始化数组中的每个segment。
上面代码中的变量cap就是segment里HashEntry数组的长度,它等于initialCapacity除以
size的倍数c,如果c大于1,就会取大于等于c的2的N次方值,所以cap不是1,就是2的N次方。
segment的容量threshold=(int)cap*loadFactor,默认情况下initialCapacity等于16,
loadfactor等于0.75,通过运算cap等于1,threshold等于零。
定位Segment
既然ConcurrentHashMap使用分段锁Segment来保护不同段的数据,那么在插入和获取元
素 的时候,必须先通过散列算法定位到Segment。
可以看到ConcurrentHashMap会首先使用Wang/Jenkins hash的变种算法对元素的
hashCode进行一次再散列。
之所以进行再散列,目的是减少散列冲突,使元素能够均匀地分布在不同的Segment上, 从
而提高容器的存取效率。假如散列的质量差到极点,那么所有的元素都在一个Segment中, 不仅
存取元素缓慢,分段锁也会失去意义。笔者做了一个测试,不通过再散列而直接执行散列计算。
计算后输出的散列值全是15,通过这个例子可以发现,如果不进行再散列,散列冲突会非 常
严重,因为只要低位一样,无论高位是什么数,其散列值总是一样。我们再把上面的二进制 数据
进行再散列后结果如下(为了方便阅读,不足32位的高位补了0,每隔4位用竖线分割下)。
可以发现,每一位的数据都散列开了,通过这种再散列能让数字的每一位都参加到散列 运算
当中,从而减少散列冲突。
ConcurrentHashMap通过以下散列算法定位segment。
默认情况下segmentShift为28,segmentMask为15,再散列后的数最大是32位二进制数
据, 向右无符号移动28位,意思是让高4位参与到散列运算中,(hash>>>segmentShift)&
segmentMask的运算结果分别是4、15、7和8,可以看到散列值没有发生冲突。
get
Segment的get操作实现非常简单和高效。
先经过一次再散列,然后使用这个散列值通过散 列运算定位到Segment,再通过散列算法定
位到元素,代码如下。
get操作的高效之处在于整个get过程不需要加锁,除非读到的值是空才会加锁重读。我们知
道HashTable容器的get方法是需要加锁的,那么ConcurrentHashMap的get操作是如何做到不加
锁的呢?
原因是它的get方法里将要使用的共享变量都定义成volatile类型,如用于统计当前Segement
大小的count字段和用于存储值的HashEntry的value。定义成volatile的变量,能够在线程之间保
持可见性,能够被多线程同时读,并且保证不会读到过期的值,但是只能被单线程写 (有一种情
况可以被多线程写,就是写入的值不依赖于原值),在get操作里只需要读不需要写 共享变量
count和value,所以可以不用加锁。之所以不会读到过期的值,是因为根据Java内存模 型的
happen before原则,对volatile字段的写入操作先于读操作,即使两个线程同时修改和获取
volatile变量,get操作也能拿到最新的值,这是用volatile替换锁的经典应用场景。
在定位元素的代码里我们可以发现,定位HashEntry和定位Segment的散列算法虽然一样,
都与数组的长度减去1再相“与”,但是相“与”的值不一样,定位Segment使用的是元素的
hashcode通过再散列后得到的值的高位,而定位HashEntry直接使用的是再散列后的值。
其目的是避免两次散列后的值一样,虽然元素在Segment里散列开了,但是却没有在
HashEntry里散列开。
put
由于put方法里需要对共享变量进行写入操作,所以为了线程安全,在操作共享变量时必须加
锁。
put方法首先定位到Segment,然后在Segment里进行插入操作。
插入操作需要经历两个步骤,第一步判断是否需要对Segment里的HashEntry数组进行扩
容,第二步定位添加元素的位 置,然后将其放在HashEntry数组里。
是否需要扩容
在插入元素前会先判断Segment里的HashEntry数组是否超过容量(threshold),如果超过
阈 值,则对数组进行扩容。
值得一提的是,Segment的扩容判断比HashMap更恰当,因为HashMap是在插入元素后判
断元素是否已经到达容量的,如果到达了就进行扩容,但是很有可能扩容 之后没有新元素插入,
这时HashMap就进行了一次无效的扩容。
如何扩容
在扩容的时候,首先会创建一个容量是原来容量两倍的数组,然后将原数组里的元素进行再
散列后插入到新的数组里。
为了高效,ConcurrentHashMap不会对整个容器进行扩容,而只对某个segment进行扩容。
size操作
如果要统计整个ConcurrentHashMap里元素的大小,就必须统计所有Segment里元素的大
小后求和。
Segment里的全局变量count是一个volatile变量,那么在多线程场景下,是不是直接把所有
Segment的count相加就可以得到整个ConcurrentHashMap大小了呢?
不是的,虽然相加时可以获取每个Segment的count的最新值,但是可能累加前使用的count
发生了变化,那么统计结果就不准了。
所以,最安全的做法是在统计size的时候把所有Segment的put、remove和clean方法 全部锁
住,但是这种做法显然非常低效。
因为在累加count操作过程中,之前累加过的count发生变化的几率非常小,所以
ConcurrentHashMap的做法是先尝试2次通过不锁住Segment的方式来统计各个Segment大
小,如 果统计的过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment的
大小。
那么ConcurrentHashMap是如何判断在统计的时候容器是否发生了变化呢?
使用modCount变量,在put、remove和clean方法里操作元素前都会将变量modCount进行
加1,那么在统计size前后比较modCount是否发生变化,从而得知容器的大小是否发生变化。
十、WeakHashMap 实现类
1. 回顾 HashMap 和 LinkedHashMap
其实,WeakHashMap 与 HashMap 和 LinkedHashMap 的数据结构大同小异,所以我们先
回顾后者的实现原理。
1.1. 说一下 HashMap 的实现结构
HashMap 是基于分离链表法解决散列冲突的动态散列表。
- 1、HashMap 在 Java 7 中使用的是 “数组 + 链表”,发生散列冲突的键值对会用头插法添
加到单链表中;
- 2、HashMap 在 Java 8 中使用的是 “数组 + 链表 + 红黑树”,发生散列冲突的键值对会用
尾插法添加到单链表中。如果链表的长度大于 8 时且散列表容量大于 64,会将链表树化为红
黑树。
HashMap 实现示意图
1.2. 说一下 LinkedHashMap 的实现结构
LinkedHashMap 是继承于 HashMap 实现的哈希链表。
- 1、LinkedHashMap 同时具备双向链表和散列表的特点。当 LinkedHashMap 作为散列表时,主要体现出 O(1) 时间复杂度的查询效率。当 LinkedHashMap 作为双向链表时,主要体现出有序的特性;
- 2、LinkedHashMap 支持 FIFO 和 LRU 两种排序模式,默认是 FIFO 排序模式,即按照插入顺序排序。Android 中的 LruCache 内存缓存和 DiskLruCache 磁盘缓存也是直接复用 LinkedHashMap 提供的缓存管理能力。
LinkedHashMap 示意图:
2. 认识 WeakHashMap
2.1. WeakReference 弱引用的特点
WeakHashMap 中的 “Weak” 指键 Key 是弱引用,也叫弱键。
弱引用是 Java 四大引用类型之一,一共有四种引用类型,分别是强引用、软引用、弱引用和虚引用。
我将它们的区别概括为 3 个维度:
- 维度 1 - 对象可达性状态的区别: 强引用指向的对象是强可达的,只有强可达的对象才会认
为是存活的对象,才能保证在垃圾收集的过程中不会被回收;
- 维度 2 - 垃圾回收策略的区别: 不同的引用类型的回收激进程度不同,
-
- 强引用指向的对象不会被回收;
- 软引用指向的对象在内存充足时不会被回收,在内存不足时会被回收;
- 弱引用和虚引用指向的对象无论在内存是否充足的时候都会被回收;
- 维度 3 - 感知垃圾回收时机: 当引用对象关联的实际对象被垃圾回收时,引用对象会进入关
联的引用队列,程序可以通过观察引用队列的方式,感知对象被垃圾回收的时机。
感知垃圾回收示意图
提示: 关于 “Java 四种引用类型” 的区别,在小彭的 Java 专栏中深入讨论过
《说一下 Java 的四种引用类型》,去看看。
2.2. WeakHashMap 的特点
WeakHashMap 是使用弱键的动态散列表,用于实现 “自动清理” 的内存缓存。
1、WeakHashMap 使用与 Java 7 HashMap 相同的 “数组 + 链表” 解决散列冲突,发生散
列冲突的键值对会用头插法添加到单链表中;
2、WeakHashMap 依赖于 Java 垃圾收集器自动清理不可达对象的特性。当 Key 对象不再被持
有强引用时,垃圾收集器会按照弱引用策略自动回收 Key 对象,并在下次访问
WeakHashMap 时清理全部无效的键值对。因此,WeakHashMap 特别适合实现 “自动清
理” 的内存活动缓存,当键值对有效时保留,在键值对无效时自动被垃圾收集器清理;
3、需要注意,因为 WeakHashMap 会持有 Value 对象的强引用,所以在 Value 对象中一定不
能持有 key 的强引用。否则,会阻止垃圾收集器回收 “本该不可达” 的 Key 对象,使得
WeakHashMap 失去作用。
4、与 HashMap 相同,LinkedHashMap 也不考虑线程同步,也会存在线程安全问题。
可以使用 Collections.synchronizedMap 包装类,其原理也是在所有方法上增加
synchronized 关键字。
WeakHashMap 示意图
自动清理数据
2.3. 说一下 WeakHashMap 与 HashMap 和 LinkedHashMap 的区别?
WeakHashMap 与 HashMap 都是基于分离链表法解决散列冲突的动态散列表,
两者的主要区别在键 Key 的引用类型上:
- HashMap 会持有键 Key 的强引用,除非手动移除,否则键值对会长期存在于散列表中;
- WeakHashMap 只持有键 Key 的弱引用,当 Key 对象不再被外部持有强引用时,键值对会
被自动被清理。
WeakHashMap 与 LinkedHashMap 都有自动清理的能力,
两者的主要区别在于 淘汰数据的策略上:
- LinkedHashMap 会按照 FIFO 或 LRU 的策略 “尝试” 淘汰数据,需要开发者重写
removeEldestEntry() 方法实现是否删除最早节点的判断逻辑;
- WeakHashMap 会按照 Key 对象的可达性淘汰数据,当 Key 对象不再被持有强引用时,会
自动清理无效数据
2.4. 重建 Key 对象不等价的问题
WeakHashMap 的 Key 使用弱引用,也就是以 Key 作为清理数据的判断锚点,当 Key 变
得不可达时会自动清理数据。
此时,如果使用多个 equals 相等的 Key 对象访问键值对,就会出现第 1 个 Key 对象不可
达导致键值对被回收,而第 2 个 Key 查询键值对为 null 的问题。
这说明 equals 相等的 Key 对象在 HashMap 等散列表中是等价的,但是在
WeakHashMap 散列表中是不等价的。
因此,如果 Key 类型没有重写 equals 方法,那么 WeakHashMap 就表现良好,否则会存
在歧义。
例如下面这个 Demo 中,首先创建了指向 image_url1 的图片 Key1,再重建了同样指向
image_url1 的图片 Key2。
在 HashMap 中,Key1 和 Key2 等价,但在 WeakHashMap 中,Key1 和 Key2 不等价。
class ImageKey {
private String url;
ImageKey(String url) {
this.url = url;
}
public boolean equals(Object obj) {
return (obj instanceOf ImageKey) && Objects.equals(((ImageKey)obj).url, this.url);
}
}
WeakHashMap<ImageKey, Bitmap> map = new WeakHashMap<>();
ImageKey key1 = new ImageKey("image_url1");
ImageKey key2 = new ImageKey("image_url2");
// key1 equalsTo key3
ImageKey key3 = new ImageKey("image_url1");
map.put(key1, bitmap1);
map.put(key2, bitmap2);
System.out.println(map.get(key1)); // 输出 bitmap1
System.out.println(map.get(key2)); // 输出 bitmap2
System.out.println(map.get(key3)); // 输出 bitmap1
// 使 key1 不可达,key3 保持
key1 = null;
// 说明重建 Key 与原始 Key 不等价
System.out.println(map.get(key1)); // 输出 null
System.out.println(map.get(key2)); // 输出 bitmap2
System.out.println(map.get(key3)); // 输出 null
默认的 Object#equals 是判断两个变量是否指向同一个对象:
Object.java
public boolean equals(Object obj) {
return (this == obj);
}
2.5. Key 弱引用和 Value 弱引用的区别
不管是 Key 还是 Value 使用弱引用都可以实现自动清理,至于使用哪一种方法各有优缺
点,适用场景也不同。
- Key 弱引用: 以 Key 作为清理数据的判断锚点,当 Key 不可达时清理数据。优点是容器外
不需要持有 Value 的强引用,缺点是重建的 Key 与原始 Key 不等价,重建 Key 无法阻止数
据被清理;
- Value 弱引用: 以 Value 作为清理数据的判断锚点,当 Value 不可达时清理数据。
优点是重建 Key 与与原始 Key 等价,缺点是容器外需要持有 Value 的强引用。
类型 | 优点 | 缺点 | 场景 |
Key 弱引用 | 外部不需要持有 Value 的强引用,使用更简单 | 重建 Key 不等价 | 未重写 equals |
Value 弱引用 | 重建 Key 等价 | 外部需要持有 Value 的强引用 | 重写 equals |
举例 1: 在 Android Glide 图片框架的多级缓存中,因为图片的 EngineKey 是可重建的,存在
多个 EngineKey 对象指向同一个图片Bitmap,所以 Glide 最顶层的活动缓存采用的是 Value
弱引用。
EngineKey.java
class EngineKey implements Key {
// 重写 equals
@Override
public boolean equals(Object o) {
if (o instanceof EngineKey) {
EngineKey other = (EngineKey) o;
return model.equals(other.model)
&& signature.equals(other.signature)
&& height == other.height
&& width == other.width
&& transformations.equals(other.transformations)
&& resourceClass.equals(other.resourceClass)
&& transcodeClass.equals(other.transcodeClass)
&& options.equals(other.options);
}
return false;
}
}
举例 2: 在 ThreadLocal 的 ThreadLocalMap 线程本地存储中,因为 ThreadLocal 没有重写
equals,不存在多个 ThreadLocal 对象指向同一个键值对的情况,所以 ThreadLocal 采用的
是 Key 弱引用。
ThreadLocal.java
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
// 未重写 equals
}
3. WeakHashMap 源码分析
现在,我们来分析 WeakHashMap 中主要流程的源码。
事实上,WeakHashMap 就是照着 Java 7 版本的 HashMap 依葫芦画瓢的,没有树化的逻
辑。
考虑到我们已经对 HashMap 做过详细分析,所以我们没有必要重复分析 WeakHashMap
的每个细节,而是把重心放在 WeakHashMap 与 HashMap 不同的地方。
3.1. 属性
先用一个表格整理 WeakHashMap 的属性:
版本 | 数据结构 | 节点实现类 | 属性 |
Java 7 HashMap | 数组 + 链表 | Entry(单链表) | 1、table(数组) 2、size(尺寸) 3、threshold (扩容阈值) 4、loadFactor (装载因子上限) 5、modCount (修改计数) 6、默认数组容量 167、最大数组容量 2^308、默认负载因子 0.75 |
WeakHashMap | 数组 + 链表 | Entry(单链表,弱引用的子类型) | 9、queue(引用队列) |
3.2. WeakHashMap.java
public class WeakHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V> {
// 默认数组容量
private static final int DEFAULT_INITIAL_CAPACITY = 16;
// 数组最大容量:2^30(高位 0100,低位都是 0)
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认装载因子上限:0.75
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 底层数组
Entry<K,V>[] table;
// 键值对数量
private int size;
// 扩容阈值(容量 * 装载因子)
private int threshold;
// 装载因子上限
private final float loadFactor;
// 引用队列
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
// 修改计数
int modCount;
// 链表节点(一个 Entry 等于一个键值对)
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
// Key:与 HashMap 和 LinkedHashMap 相比,少了 key 的强引用
// final K key;
// Value(强引用)
V value;
// 哈希值
final int hash;
Entry<K,V> next;
Entry(Object key, V value, ReferenceQueue<Object> queue, int hash, Entry<K,V> next) {
super(key /*注意:只有 Key 是弱引用*/, queue);
this.value = value;
this.hash = hash;
this.next = next;
}
}
}
WeakHashMap 与 HashMap 的属性几乎相同,主要区别有 2 个:
1、ReferenceQueue: WeakHashMap 的属性里多了一个 queue 引用队列;
2、Entry: WeakHashMap#Entry 节点继承于 WeakReference,表面看是 WeakHashMap
持有了 Entry 的强引用,其实不是。
注意看 Entry 的构造方法,WeakReference 关联的实际对象是 Key。
所以,WeakHashMap 依然持有 Entry 和 Value 的强引用,仅持有 Key 的弱引用。
3.3. 引用关系示意图
不出意外的话又有小朋友出来举手提问了
疑问 1:说一下 ReferenceQueue queue 的作用?
ReferenceQueue 与 Reference 配合能够实现感知对象被垃圾回收的能力。
在创建引用对象时可以关联一个实际对象和一个引用队列,当实现对象被垃圾回收后,引用对象
会被添加到这个引用队列中。
在 WeakHashMap 中,就是根据这个引用队列来自动清理无效键值对。
疑问 2:为什么 Key 是弱引用,而不是 Entry 或 Value 是弱引用?
首先,Entry 一定要持有强引用,而不能持有弱引用。
这是因为 Entry 是 WeakHashMap 内部维护数据结构的实现细节,并不会暴露到
WeakHashMap 外部,即除了 WeakHashMap 本身之外没有其它地方持有 Entry 的强引用。
所以,如果持有 Entry 的弱引用,即使 WeakHashMap 外部依然在使用 Key 对象,
WeakHashMap 内部依然会回收键值对,这与预期不符。
其次,不管是 Key 还是 Value 使用弱引用都可以实现自动清理。
至于使用哪一种方法各有优缺点,适用场景也不同,这个在前文分析过了。
3.4. WeakHashMap 如何清理无效数据?
在通过 put / get /size 等方法访问 WeakHashMap 时,其内部会调用
expungeStaleEntries() 方法清理 Key 对象已经被回收的无效键值对。
其中会遍历 ReferenceQueue 中持有的弱引用对象(即 Entry 节点),并将该结点从散列表中
移除。
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
// 添加键值对
public V put(K key, V value) {
...
// 间接 expungeStaleEntries()
Entry<K,V>[] tab = getTable();
...
}
// 扩容
void resize(int newCapacity) {
// 间接 expungeStaleEntries()
Entry<K,V>[] oldTable = getTable();
...
}
// 获取键值对
public V get(Object key) {
...
// 间接 expungeStaleEntries()
Entry<K,V>[] tab = getTable();
...
}
private Entry<K,V>[] getTable() {
// 清理无效键值对
expungeStaleEntries();
return table;
}
// ->清理无效键值对
private void expungeStaleEntries() {
// 遍历引用队列
for (Object x; (x = queue.poll()) != null; ) {
// 疑问 3:既然 WeakHashMap 不考虑线程同步,为什么这里要做加锁,岂不是突兀?
synchronized (queue) {
Entry<K,V> e = (Entry<K,V>) x;
// 根据散列值定位数组下标
int i = indexFor(e.hash /*散列值*/, table.length);
// 遍历桶寻找节点 e 的前驱结点
Entry<K,V> prev = table[i];
Entry<K,V> p = prev;
while (p != null) {
Entry<K,V> next = p.next;
if (p == e) {
// 删除节点 e
if (prev == e)
// 节点 e 是根节点
table[i] = next;
else
// 节点 e 是中间节点
prev.next = next;
// Must not null out e.next;
// stale entries may be in use by a HashIterator
e.value = null; // Help GC
size--;
break;
}
prev = p;
p = next;
}
}
}
}
4. 知识总结
1、WeakHashMap 使用与 Java 7 HashMap 相同的 “数组 + 链表” 解决散列冲突,发生散
列冲突的键值对会用头插法添加到单链表中;
2、WeakHashMap 能够实现 “自动清理” 的内存缓存,其中的 “Weak” 指键 Key 是弱引
用。
当 Key 对象不再被持有强引用时,垃圾收集器会按照弱引用策略自动回收 Key 对象,并在
下次访问 WeakHashMap 时清理全部无效的键值对;
3、WeakHashMap 和 LinkedHashMap 都具备 “自动清理” 的 能力,WeakHashMap 根
据 Key 对象的可达性淘汰数据,而 LinkedHashMap 根据 FIFO 或 LRU 策略尝试淘汰数据;
4、WeakHashMap 使用 Key 弱引用,会存在重建 Key 对象不等价问题。
十一、Properties(遗弃类)
1. 简介
Properties 继承于 Hashtable。
表示一个持久的属性集,属性列表以key-value的形式存在,key和value都是字符串。
Properties 类被许多Java类使用。
例如,在获取环境变量时它就作为System.getProperties()方法的返回值。
我们在很多需要避免硬编码的应用场景下需要使用properties文件来加载程序需要的配置信息,
比如JDBC、MyBatis框架等。
Properties类则是properties文件和程序的中间桥梁,不论是从properties文件读取信息还是写入信息到
properties文件都要经由Properties类。
2. 常用API
除了从Hashtable中所定义的方法,Properties定义了以下方法:
3. 演示案例
3.1. 使用properties配置文件
开发中获得连接的4个参数(驱动、url、用户名、密码)通常都存放在配置文件中,方便后期维护。
程序如果更换数据库,只需修改配置文件即可。
1、properties文件的要求:
- 文件位置:建议放在src下
- 文件名称:扩展名为properties
- 文件内容:格式“key=value”,key可自定义,多个英文单词.号隔开,value不支持中文
2、创建配置文件
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/jdbctest
user=root
password=root
3.2. 写入 & 读取 & 遍历角度思考
下面我们从写入、读取、遍历等角度来解析Properties类的常见用法:
写入
Properties类调用setProperty方法将键值对保存到内存中,
此时可以通过getProperty方法读取,propertyNames方法进行遍历,
但是并没有将键值对持久化到属性文件中,故需要调用store方法持久化键值对到属性文件中。
package com.zheng;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Date;
import java.util.Enumeration;
import java.util.Properties;
import junit.framework.TestCase;
public class PropertiesTester extends TestCase {
public void writeProperties() {
Properties properties = new Properties();
OutputStream output = null;
try {
output = new FileOutputStream("config.properties");
properties.setProperty("url", "jdbc:mysql://localhost:3306/");
properties.setProperty("username", "root");
properties.setProperty("password", "root");
properties.setProperty("database", "users");//保存键值对到内存
properties.store(output, "Steven1997 modify" + new Date().toString());
// 保存键值对到文件中
} catch (IOException io) {
io.printStackTrace();
} finally {
if (output != null) {
try {
output.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
读取
下面给出常见的六种读取properties文件的方式:
package com.zheng;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
import java.util.Properties;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
/**
* 读取properties文件的方式
*
*/
public class LoadPropertiesFileUtil {
private static String basePath = "src/main/java/cn/habitdiary/prop.properties";
private static String path = "";
/**
* 一、 使用java.util.Properties类的load(InputStream in)方法加载properties文件
*
* @return
*/
public static String getPath1() {
try {
InputStream in = new BufferedInputStream(new FileInputStream(
new File(basePath)));
Properties prop = new Properties();
prop.load(in);
path = prop.getProperty("path");
} catch (FileNotFoundException e) {
System.out.println("properties文件路径书写有误,请检查!");
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return path;
}
/**
* 二、 使用java.util.ResourceBundle类的getBundle()方法
* 注意:这个getBundle()方法的参数只能写成包路径+properties文件名,否则将抛异常
*
* @return
*/
public static String getPath2() {
ResourceBundle rb = ResourceBundle
.getBundle("cn/habitdiary/prop");
path = rb.getString("path");
return path;
}
/**
* 三、 使用java.util.PropertyResourceBundle类的构造函数
*
* @return
*/
public static String getPath3() {
InputStream in;
try {
in = new BufferedInputStream(new FileInputStream(basePath));
ResourceBundle rb = new PropertyResourceBundle(in);
path = rb.getString("path");
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return path;
}
/**
* 四、 使用class变量的getResourceAsStream()方法
* 注意:getResourceAsStream()方法的参数按格式写到包路径+properties文件名+.后缀
*
* @return
*/
public static String getPath4() {
InputStream in = LoadPropertiesFileUtil.class
.getResourceAsStream("cn/habitdiary/prop.properties");
Properties p = new Properties();
try {
p.load(in);
path = p.getProperty("path");
} catch (IOException e) {
e.printStackTrace();
}
return path;
}
/**
* 五、
* 使用class.getClassLoader()所得到的java.lang.ClassLoader的
* getResourceAsStream()方法
* getResourceAsStream(name)方法的参数必须是包路径+文件名+.后缀
* 否则会报空指针异常
* @return
*/
public static String getPath5() {
InputStream in = LoadPropertiesFileUtil.class.getClassLoader()
.getResourceAsStream("cn/habitdiary/prop.properties");
Properties p = new Properties();
try {
p.load(in);
path = p.getProperty("path");
} catch (IOException e) {
e.printStackTrace();
}
return path;
}
/**
* 六、 使用java.lang.ClassLoader类的getSystemResourceAsStream()静态方法
* getSystemResourceAsStream()方法的参数格式也是有固定要求的
*
* @return
*/
public static String getPath6() {
InputStream in = ClassLoader
.getSystemResourceAsStream("cn/habitdiary/prop.properties");
Properties p = new Properties();
try {
p.load(in);
path = p.getProperty("path");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return path;
}
public static void main(String[] args) {
System.out.println(LoadPropertiesFileUtil.getPath1());
System.out.println(LoadPropertiesFileUtil.getPath2());
System.out.println(LoadPropertiesFileUtil.getPath3());
System.out.println(LoadPropertiesFileUtil.getPath4());
System.out.println(LoadPropertiesFileUtil.getPath5());
System.out.println(LoadPropertiesFileUtil.getPath6());
}
}
其中第一、四、五、六种方式都是先获得文件的输入流,然后通过Properties类的load(InputStream
inStream)方法加载到Properties对象中,最后通过Properties对象来操作文件内容。
第二、三中方式是通过ResourceBundle类来加载Properties文件,然后ResourceBundle对象来操做
properties文件内容。
其中最重要的就是每种方式加载文件时,文件的路径需要按照方法的定义的格式来加载,否则会抛出各种异常,比如空指针异常。
遍历
下面给出四种遍历Properties中的所有键值对的方法:
/**
* 输出properties的key和value
*/
public static void printProp(Properties properties) {
System.out.println("---------(方式一)------------");
for (String key : properties.stringPropertyNames()) {
System.out.println(key + "=" + properties.getProperty(key));
}
System.out.println("---------(方式二)------------");
Set<Object> keys = properties.keySet();//返回属性key的集合
for (Object key : keys) {
System.out.println(key.toString() + "=" + properties.get(key));
}
System.out.println("---------(方式三)------------");
Set<Map.Entry<Object, Object>> entrySet = properties.entrySet();
//返回的属性键值对实体
for (Map.Entry<Object, Object> entry : entrySet) {
System.out.println(entry.getKey() + "=" + entry.getValue());
}
System.out.println("---------(方式四)------------");
Enumeration<?> e = properties.propertyNames();
while (e.hasMoreElements()) {
String key = (String) e.nextElement();
String value = properties.getProperty(key);
System.out.println(key + "=" + value);
}
}
十二、IdentityHashMap 实现类
1. 摘要
咱们了解到,Map 的实现类有 HashMap、LinkedHashMap、TreeMap、IdentityHashMap、
WeakHashMap、Hashtable、Properties 等等。
应该有很多人不知道 IdentityHashMap 的存在,其中不乏工作很多年的 Java 开发者,本文主要从数
据结构和算法层面,探讨 IdentityHashMap 的实现。
2. 简介
IdentityHashMap 从它的名字上可以看出来用于表示唯一的 HashMap,但是分析了其源码,发现其
数据结构与 HashMap 使用的数据结构完全不同。
IdentityHashMap 的数据结构很简单,底层实际就是一个 Object 数组,但是在存储上并没有使用链
表来存储,而是将 K 和 V 都存放在 Object 数组上。
当添加元素的时候,会根据 Key 计算得到散列位置,如果发现该位置上已经有改元素,直接进行新值
替换;
如果没有,直接进行存放。当元素个数达到一定阈值时,Object 数组会自动进行扩容处理。
打开 IdentityHashMap 的源码,
可以看到 IdentityHashMap 继承了 AbstractMap 抽象类,实现了 Map 接口、可序列化接口、可克
隆接口。
public class IdentityHashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, java.io.Serializable, Cloneable
{
/**默认容量大小*/
private static final int DEFAULT_CAPACITY = 32;
/**最小容量*/
private static final int MINIMUM_CAPACITY = 4;
/**最大容量*/
private static final int MAXIMUM_CAPACITY = 1 << 29;
/**用于存储实际元素的表*/
transient Object[] table;
/**数组大小*/
int size;
/**对Map进行结构性修改的次数*/
transient int modCount;
/**key为null所对应的值*/
static final Object NULL_KEY = new Object();
......
}
可以看到类的底层,使用了一个 Object 数组来存放元素;在对象初始化时,IdentityHashMap 容量
大小为64;
public IdentityHashMap() {
//调用初始化方法
init(DEFAULT_CAPACITY);
}
private void init(int initCapacity) {
//数组大小默认为初始化容量的2倍
table = new Object[2 * initCapacity];
}
3. 源码
put
put 方法是将指定的 key, value 对添加到 map 里。该方法首先会对map做一次查找,通过==判断是
否存在key,
如果有,则将旧value返回,将新value覆盖旧value;如果没有,直接插入,数组长度+1,返回null。
源码如下:
public V put(K key, V value) {
//判断key是否为空,如果为空,初始化一个Object为key
final Object k = maskNull(key);
retryAfterResize: for (;;) {
final Object[] tab = table;
final int len = tab.length;
//通过key、length获取数组小编
int i = hash(k, len);
//循环遍历是否存在指定的key
for (Object item; (item = tab[i]) != null;
i = nextKeyIndex(i, len)) {
//通过==判断,是否数组中是否存在key
if (item == k) {
V oldValue = (V) tab[i + 1];
//新value覆盖旧value
tab[i + 1] = value;
//返回旧value
return oldValue;
}
}
//数组长度 +1
final int s = size + 1;
//判断是否需要扩容
if (s + (s << 1) > len && resize(len))
continue retryAfterResize;
//更新修改次数
modCount++;
//将k加入数组
tab[i] = k;
//将value加入数组
tab[i + 1] = value;
size = s;
return null;
}
}
maskNull 函数,判断 key 是否为空
private static Object maskNull(Object key) {
return (key == null ? NULL_KEY : key);
}
hash 函数,通过 key 获取 hash 值,结合数组长度通过位运算获取数组散列下标
private static int hash(Object x, int length) {
int h = System.identityHashCode(x);
// Multiply by -127, and left-shift to use least bit as part of hash
return ((h << 1) - (h << 8)) & (length - 1);
}
nextKeyIndex 函数,通过 hash 函数计算得到的数组散列下标,进行加2;
因为一个 key、value 都存放在数组中,所以一个 map 对象占用两个数组下标,所以加2。
private static int nextKeyIndex(int i, int len) {
return (i + 2 < len ? i + 2 : 0);
}
resize 函数,通过数组长度,进行扩容处理,扩容之后的长度为当前长度的2倍
private boolean resize(int newCapacity) {
//扩容后的数组长度,为当前数组长度的2倍
int newLength = newCapacity * 2;
Object[] oldTable = table;
int oldLength = oldTable.length;
if (oldLength == 2 * MAXIMUM_CAPACITY) { // can't expand any further
if (size == MAXIMUM_CAPACITY - 1)
throw new IllegalStateException("Capacity exhausted.");
return false;
}
if (oldLength >= newLength)
return false;
Object[] newTable = new Object[newLength];
//将旧数组内容转移到新数组
for (int j = 0; j < oldLength; j += 2) {
Object key = oldTable[j];
if (key != null) {
Object value = oldTable[j+1];
oldTable[j] = null;
oldTable[j+1] = null;
int i = hash(key, newLength);
while (newTable[i] != null)
i = nextKeyIndex(i, newLength);
newTable[i] = key;
newTable[i + 1] = value;
}
}
table = newTable;
return true;
}
get
get 方法根据指定的 key 值返回对应的 value。
同样的,该方法会循环遍历数组,通过==判断是否存在key,如果有,直接返回value,
因为 key、value 是相邻的存储在数组中,所以直接在当前数组下标+1,即可获取 value;
如果没有找到,直接返回null。
值得注意的地方是,在循环遍历中,是通过==判断当前元素是否与key相同,如果相同,则返回value。
咱们都知道,在 java 中,==对于对象类型参数,判断的是引用地址,确切的说,是堆内存地址,
所以,这里判断的是key的引用地址是否相同,如果相同,则返回对应的 value;如果不相同,则返回
null。
源码如下:
public V get(Object key) {
Object k = maskNull(key);
Object[] tab = table;
int len = tab.length;
int i = hash(k, len);
//循环遍历数组,直到找到key或者,数组为空为值
while (true) {
Object item = tab[i];
//通过==判断,当前数组元素与key相同
if (item == k)
return (V) tab[i + 1];
//数组为空
if (item == null)
return null;
i = nextKeyIndex(i, len);
}
}
remove
remove 的作用是通过 key 删除对应的元素。
该方法会循环遍历数组,通过 == 判断是否存在 key ,
如果有,直接将 key 、value 设置为 null ,对数组进行重新排列,返回旧 value。
源码如下:
public V remove(Object key) {
Object k = maskNull(key);
Object[] tab = table;
int len = tab.length;
int i = hash(k, len);
while (true) {
Object item = tab[i];
if (item == k) {
modCount++;
//数组长度减1
size--;
V oldValue = (V) tab[i + 1];
//将key、value设置为null
tab[i + 1] = null;
tab[i] = null;
//删除该元素后,需要把原来有冲突往后移的元素移到前面来
closeDeletion(i);
return oldValue;
}
if (item == null)
return null;
i = nextKeyIndex(i, len);
}
}
closeDeletion 函数,删除该元素后,需要把原来有冲突往后移的元素移到前面来,对数组进行重写排列;
private void closeDeletion(int d) {
// Adapted from Knuth Section 6.4 Algorithm R
Object[] tab = table;
int len = tab.length;
Object item;
for (int i = nextKeyIndex(d, len); (item = tab[i]) != null;
i = nextKeyIndex(i, len) ) {
int r = hash(item, len);
if ((i < r && (r <= d || d <= i)) || (r <= d && d <= i)) {
tab[d] = item;
tab[d + 1] = tab[i + 1];
tab[i] = null;
tab[i + 1] = null;
d = i;
}
}
}
4. 总结
- IdentityHashMap 的实现不同于HashMap,虽然也是数组,不过IdentityHashMap中没有用到链表,解决冲突的方式是计算下一个有效索引,并且将数据key和value紧挨着存在map中,即table[i]=key、table[i+1]=value;
- IdentityHashMap 允许key、value都为null,当key为null的时候,默认会初始化一个Object对象作为key;
- IdentityHashMap在保存、删除、查询数据的时候,以key为索引,通过==来判断数组中元素是否与key相同,本质判断的是对象的引用地址,如果引用地址相同,那么在插入的时候,会将value值进行替换;
IdentityHashMap 测试例子:
public static void main(String[] args) {
Map<String, String> identityMaps = new IdentityHashMap<String, String>();
identityMaps.put(new String("aa"), "aa");
identityMaps.put(new String("aa"), "bb");
identityMaps.put(new String("aa"), "cc");
identityMaps.put(new String("aa"), "cc");
//输出添加的元素
System.out.println("数组长度:"+identityMaps.size() + ",输出结果:" + identityMaps);
}
输出结果:
数组长度:4,输出结果:{aa=aa, aa=cc, aa=bb, aa=cc}
十三、EnumMap 实现类
1. 简介
在Java中,Enum类型是一种非常有用的数据类型,它可以用于表示枚举常量。
而EnumMap是Java中一个特殊的Map实现,它只能存储Enum类型的键值对。
在这里,我将介绍EnumMap函数的用法,以及如何在Java中使用EnumMap进行枚举映射操作。
2. 基本用法
EnumMap<K extends Enum, V>是一个Java中的泛型类,其中K表示枚举类型的键,V表示映射到该键的值。
EnumMap创建后,其键必须来自同一个枚举类型,并且所有的值都必须是同一类型。
下面是EnumMap函数的基本用法:
EnumMap<Weekday, String> enumMap = new EnumMap<>(Weekday.class);
enumMap.put(Weekday.MONDAY, "星期一");
enumMap.put(Weekday.TUESDAY, "星期二");
enumMap.put(Weekday.WEDNESDAY, "星期三");
enumMap.put(Weekday.THURSDAY, "星期四");
enumMap.put(Weekday.FRIDAY, "星期五");
enumMap.put(Weekday.SATURDAY, "星期六");
enumMap.put(Weekday.SUNDAY, "星期日");
我们首先创建了一个EnumMap对象enumMap,并通过EnumMap中的put方法将每个星期几和对应的中文名称
存入了该EnumMap中。
这样我们就完成了一个基本的EnumMap的创建和初始化。
3. 初始化
上面的代码示例中,我们使用了EnumMap的默认构造函数,它会自动将所有的值初始化为null。
实际上,我们也可以使用EnumMap的另一个构造函数来进行初始化。
这个构造函数会设置一个初始值,将EnumMap中的所有值都初始化为这个初始值。
下面是EnumMap函数的初始化示例代码:
EnumMap<Weekday, String> enumMap = new EnumMap<>(Weekday.class);
enumMap.put(Weekday.MONDAY, "星期一");
enumMap.put(Weekday.TUESDAY, "星期二");
enumMap.put(Weekday.WEDNESDAY, "星期三");
enumMap.put(Weekday.THURSDAY, "星期四");
enumMap.put(Weekday.FRIDAY, "星期五");
enumMap.put(Weekday.SATURDAY, "星期六");
enumMap.put(Weekday.SUNDAY, "星期日");
// 使用初始化值,将所有键值对的值都设置为"假期"
EnumMap<Weekday, String> defaultEnumMap = new EnumMap<>(Weekday.class);
defaultEnumMap.putAll(Collections.singletonMap(null, "假期"));
EnumMap<Weekday, String> enumMapWithDefaultValue = new EnumMap<>(defaultEnumMap);
enumMapWithDefaultValue.putAll(enumMap);
在上面的示例代码中,我们使用了Collections.singletonMap方法创建了一个只包含一个键值对的Map,它
的键为null,值为"假期"。
然后,我们使用这个Map作为初始值,创建了一个新的EnumMap对象enumMapWithDefaultValue,并且
将前面创建的enumMap中的键值对拷贝到了这个新的EnumMap对象中。
这个示例代码可以让我们了解到如何使用EnumMap的构造函数来进行初始化,以及如何使用另一个Map作
为初始值来创建一个新的EnumMap。
4. 遍历
遍历EnumMap中的所有元素通常是必不可少的操作。我们可以使用Java中的迭代器来实现这个操作。
下面是遍历EnumMap的示例代码:
EnumMap<Weekday, String> enumMap = new EnumMap<>(Weekday.class);
enumMap.put(Weekday.MONDAY, "星期一");
enumMap.put(Weekday.TUESDAY, "星期二");
enumMap.put(Weekday.WEDNESDAY, "星期三");
enumMap.put(Weekday.THURSDAY, "星期四");
enumMap.put(Weekday.FRIDAY, "星期五");
enumMap.put(Weekday.SATURDAY, "星期六");
enumMap.put(Weekday.SUNDAY, "星期日");
// 使用迭代器遍历EnumMap中的所有键值对
Iterator<Map.Entry<Weekday, String>> iterator = enumMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Weekday, String> entry = iterator.next();
Weekday key = entry.getKey();
String value = entry.getValue();
System.out.println(key + ": " + value);
}
// 使用foreach循环遍历EnumMap中的所有值
for (String value : enumMap.values()) {
System.out.println(value);
}
在这个示例代码中,我们使用了Java中的迭代器来遍历EnumMap中的所有键值对。
我们首先获取了该EnumMap的entrySet,然后使用entrySet返回的迭代器来依次遍历所有的键值对。
对于每个键值对,我们使用getKey方法获取键,使用getValue方法获取值,并将它们输出到控制台。
我们也可以使用foreach循环遍历EnumMap中的所有值。
将枚举类型作为键进行取值即可,这种方式可以避免我们频繁地使用getKey方法获取键。
5. 实际应用
除了上面介绍的基本用法之外,EnumMap函数还有很多实际的应用场景。
枚举映射操作
EnumMap最常见的用途是将枚举类型映射到其他值。
比如下面的示例代码中,我们将枚举类型Weekday映射到数字(0-6):
public enum Weekday {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
// 使用EnumMap将Weekday枚举映射到数字
EnumMap<Weekday, Integer> enumMap = new EnumMap<>(Weekday.class);
enumMap.put(Weekday.MONDAY, 0);
enumMap.put(Weekday.TUESDAY, 1);
enumMap.put(Weekday.WEDNESDAY, 2);
enumMap.put(Weekday.THURSDAY, 3);
enumMap.put(Weekday.FRIDAY, 4);
enumMap.put(Weekday.SATURDAY, 5);
enumMap.put(Weekday.SUNDAY, 6);
枚举类型计数器
在某些情况下,我们需要实现一个计数器来统计某个枚举类型的数量。
EnumMap可以很方便地实现这个功能,示例代码如下:
public enum Gender {
MALE, FEMALE
}
// 使用EnumMap实现枚举类型计数器
EnumMap<Gender, Integer> genderCount = new EnumMap<>(Gender.class);
genderCount.put(Gender.MALE, 0);
genderCount.put(Gender.FEMALE, 0);
List<Gender> genderList = Arrays.asList(
Gender.MALE,
Gender.MALE,
Gender.MALE,
Gender.FEMALE,
Gender.FEMALE
);
for (Gender gender : genderList) {
genderCount.put(gender, genderCount.get(gender) + 1);
}
System.out.println("男性数量:" + genderCount.get(Gender.MALE));
System.out.println("女性数量:" + genderCount.get(Gender.FEMALE));
在上面的示例代码中,我们首先创建了一个EnumMap对象genderCount,用于记录Gender类型的数量。
接着,我们使用EnumMap中的put方法将每个Gender类型的数量初始化为0。
然后,我们使用一个List来模拟性别列表,并遍历该列表,统计每个Gender出现的次数。
最后,我们输出了男性和女性的数量。
枚举类型计算器
与枚举类型计数器类似,EnumMap也可以用来实现枚举类型的加法和减法。
比如下面的示例代码中,我们实现了一个简单的计算器,用于统计某个英文字母在某个单词中出现的次数:
public enum Letter {
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z
}
// 使用EnumMap实现枚举类型计算器
EnumMap<Letter, Integer> letterCount = new EnumMap<>(Letter.class);
for (Letter letter : Letter.values()) {
letterCount.put(letter, 0);
}
String word = "Hello, World!";
for (int i = 0; i < word.length(); i++) {
char c = word.charAt(i);
if (c >= 'A' && c <= 'Z') {
Letter letter = Letter.valueOf(String.valueOf(c));
letterCount.put(letter, letterCount.get(letter) + 1);
}
}
for (Letter letter : Letter.values()) {
if (letterCount.get(letter) > 0) {
System.out.println(letter + ": " + letterCount.get(letter));
}
}
在上面的示例代码中,我们首先创建了一个EnumMap对象letterCount,用于记录每个字母出现的次数。
然后,我们使用for循环遍历Letter枚举类型,将每个字母的初始值都设置为0。
接着,我们定义了一个字符串word,用于模拟单词。
我们遍历word中的每个字符,并判断是否为大写字母。
如果是大写字母,我们就用Letter.valueOf方法将其转换为Letter类型,并对letterCount中对应的Letter类型数量
进行累加。
最后,我们遍历Letter枚举类型,并输出出现次数大于0的字母和对应的次数。
6. 总结
在这里,我们介绍了EnumMap函数的基本用法、初始化、遍历、实际应用等方面。
EnumMap是Java中非常实用的Map实现,它可以很好地与Enum类型配合使用,用于实现枚举映射、枚举类型统
计、计算器等应用。
掌握EnumMap的使用方法,有助于提高Java程序的开发效率和代码质量。
以上就是如何使用Java中的EnumMap函数进行枚举映射操作的详细内容!