源码阅读(15):Java中主要的Map结构——概述
(接上文《源码阅读(14):Java中主要的Queue、Deque结构——PriorityQueue集合(下)》)
1、概述
1.1、Map结构和Set集合的关系
为什么要先介绍Java中的主要Map结构呢?如果读者是从本专题第一篇文章开始阅读的话,那么就应该清楚目前我们整个专题还在介绍Java中java.util.Collection接口的注意要实现集合 ,具体来说就应该是List集合、Queue/Deque集合以及Set集合。那么List集合、Queue/Deque集合介绍完后,理所当然就应该开始介绍Set集合。下图为Java中重要的Set集合的构建体系:
是的,从最直观的讲解思路考虑,整个专题是应该按照这样的思路进行介绍。但事实上Java中重要的Set集合内部实现全部基于对应的Map<K , V>结构。举例来说,在早期Java中版本中广泛使用的HashSet集合其内部是一个HashMap,代码片段如下所示:
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable {
// ......
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
/**
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
* default initial capacity (16) and load factor (0.75).
*/
public HashSet() {
map = new HashMap<>();
}
// ......
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
// ......
}
再例如Java中另一个Set集合TreeSet,其内部使用的是TreeMap结构。再例如线程安全的跳跃表结构ConcurrentSkipListSet,内部实际上使用的是ConcurrentSkipListMap。甚至,第三方类org.eclipse.jetty.util.ConcurrentHashSet内部也是使用的java.util.concurrent.ConcurrentHashMap。所以基本上可以说,如果搞清楚了Java中重要的Map结构实现,那么就搞清楚了Java中重要的Set集合实现。
1.2、Map结构概述
1.2.1、基本结构
首先Map结构同样属于Java Collections Framework的知识范畴,但是代表Map结构的顶级接口java.util.Map并没有继承java.util.Collection接口。这是因为Map结构属于映射式容器,既是一个Key键对应一个Value值(所以称为键值对),且同一个容器中不能出现两个相同的Key键信息。
An object that maps keys to values. A map cannot contain duplicate keys;each key can map to at most one value.
上图所示的Map主要结构体系中,我们将重点介绍java.util包中的TreeMap容器、HashMap容器和LinkedHashMap容器。其中TreeMap容器基于红黑树进行构造,HashMap容器和LinkedHashMap容器基于数组+链表+红黑树的复合结构进行构造,而后两中容器的区别仅体现在HashMap容器中的数组被替换成了链表。
另外,ConcurrentHashMap容器和ConcurrentSkipListMap容器也是Map结构体系下重要的线程安全的容器,我们将在本专题后续的专门介绍java.util.concurrent包的文章中专门进行介绍。其中基于跳跃表结构进行构建的ConcurrentSkipListMap容器尤为重要。
1.2.2、键值对定义方式Entry
上文已经提到,Map容器中存储的是键值对,既是一个键信息和一个值信息的映射关系。Map容器中可以有成千上万的键值对信息,每一个键值对都使用Map.Entry<K , V>的定义进行存储——也就是说一个Map容器中可以有成千上万个Map.Entry接口的实例化对象。
Map.Entry<K , V>接口的主要代码如下所示:
public interface Map<K,V> {
// ......
interface Entry<K,V> {
// 获取当前Entry表示的键值对的键信息
K getKey();
// 获取当前Entry表示的键值对的值信息
V getValue();
// 设定当前Entry表示的键值对的值信息
V setValue(V value);
// 比较两个键值对是否相同
boolean equals(Object o);
// 求得当前键值对的hash值
int hashCode();
// ......
}
// ......
}
实际上从JDK1.8+开始,Map.Entry接口中还有一些其它定义,这里为了讲解方便我们暂时不去涉及,后续的内容中会逐步进行说明。一般来说实现了Map接口的具体实现类,都会根据自己的结构特定实现Map.Entry接口。例如AbstractMap类中就有AbstractMap.SimpleEntry类实现了Map.Entry接口;HashMap类中就有HashMap.Node类实现Map.Entry接口;TreeMap类中就有TreeMap.Entry类实现Map.Entry接口……
这些Map.Entry接口的具体实现类,都根据自己存储键值对的特性做了不同的扩展,例如我们可以看一下TreeMap.Entry中的构造定义:
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable {
// ......
/**
* Node in the Tree. Doubles as a means to pass key-value pairs back to
* user (see Map.Entry).
*/
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
Entry<K,V> left;
Entry<K,V> right;
Entry<K,V> parent;
boolean color = BLACK;
/**
* Make a new cell with given key, value, and parent, and with
* {@code null} child links, and BLACK color.
*/
Entry(K key, V value, Entry<K,V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
// ......
}
// ......
}
使用键值对方式存储数据的java.util.TreeMap容器,内部所有的键值对构成一棵红黑树。也就是说代表每一个键值对的TreeMap.Entry需要记录当前树结点的双亲结点(父结点)、左儿子结点和右儿子结点,以及当前树结点的颜色。所以,读者可以在以上代码片段中看到这些属性的定义,我们将在后续章节详细介绍TreeMap容器。
2、Map结构中重要的接口和抽象类
为了便于读者更深入理解java.util包中的TreeMap容器、HashMap容器和LinkedHashMap容器,我们将首先讲解几个重要的上层抽象类和接口:java.util.Map接口、java.util.SortedMap接口、java.util.NavigableMap接口和java.util.AbstractMap抽象类。
2.1、java.util.Map接口
java.util.Map接口是Java Collection Framework(JCF)框架中,Map体系的顶层接口定义。它定义了Map体系最基本的操作功能——针对K-V键值对这种操作结构的最基本操作功能。例如:
- 建立一个指定值与指定键的关联,也就是建立一个新的K-V关联。如果操作之前已经存在这样的键-值关联,那么新的值将会替换调原有的值,并且之前的值将会被返回:
V put(K key, V value);
- 该方法用于清除当前map容器中所有键-值映射关系。
void clear();
- 返回一个指定键映射的指定值,如果当前map容器中不存在这个建,则返回null。
V get(Object key);
- 返回当前map容器中存储的K-V映射数量,如果映射数据大于Integer.MAX_VALUE (也就是231-1),那么就返回Integer.MAX_VALUE
int size();
- 这是一个判定方法,判定当前map容器中是否至少存在一个K-V映射数据。如果存在则返回true;其它情况返回false
boolean isEmpty();
以下java.util.Map接口的方法列表,摘自JDK 1.8版本:
2.2、java.util.SortedMap接口
java.util.Map接口中定义的各种键值对读写方法,并不保证键的顺序。举个例子来说,K1、K2和K3三个键通过Map接口提供的put(K ,V)方法被放入了容器,当它们在容器中不一定按照存入的顺序进行存储。
但很多业务场景下我们却需要存储在map容器中的这些键按照一定的规则进行有序存储,这时我们可能就需要使用实现了SortedMap接口的具体类了。需要注意的是:这里所说的有序存储不一定是线性存储的,例如使用红黑树结构进行的有序存储。SortedMap接口提供了很多和顺序存储有关的方法,例如:
- 既然SortedMap接口下的实现类可以将键信息按照一定规则进行有序存储,那么其中自然就会用到Comparator比较器。通过以下方法可以当前容器使用的比较器。
Comparator<? super K> comparator()
- 既然容器中键信息是有序存储的,那么就可以指定开始的键信息以及结束位的键信息,并返回一个承载前两者之间的键值对引用信息的新的SortedMap容器。注意,由于新的SortedMap容器中存储的是这些键值对信息的引用,所以对新的SortedMap容器中键值对的写操作将会反应在当前容器中,反之亦然。另外,如果指定的开始位置的键信息和指定的结束位置的键信息相同,那么将返回一个空集合。
SortedMap<K,V> subMap(K fromKey, K toKey)
- 指定一个键信息,以下方法将返回一个新的SortedMap容器,后者存储的所有键值对的键信息都小于当前指定的键信息。除此之外,新的SortedMap容器的操作特性和subMap()方法返回的新容器的操作特性一致。需要注意的是,如果指定的键信息并不在当前SortedMap容器中,那么该方法将抛出IllegalArgumentException异常。
SortedMap<K,V> headMap(K toKey);
- 指定一个键信息,以下方法将返回一个新的SortedMap容器,后者存储的所有键值对的键信息都大于或者等于当前指定的键信息。初次之外,新的SortedMap容器的操作特性和headMap()方法返回的新容器的操作特性一致。
SortedMap<K,V> tailMap(K fromKey);
- 以下方法将返回当前SortedMap容器中,经过Comparator比较器比较后,值最小的那个键信息。
K firstKey();
- 以下方法将返回当前SortedMap容器中,经过Comparator比较器比较后,值最大的那个键信息。
K lastKey();
下图展示的java.util.SortedMap接口中完整的方法定义:
下图展示了java.util.Map接口、java.util.SortedMap接口和java.util.NavigableMap接口的继承关系:
2.3、java.util.NavigableMap接口
如果说SortedMap接口为有序的键值对存储定义了基本操作,那么NavigableMap接口就是将和“有序”相关的操作进行细化。它精确定义了诸如返回上一个键/键值对、下一个键/键值对、最小键/键值对、最大键/键值对的一系列操作。
- 以下两个方法返回一个键值映射信息/键信息,这个被返回的信息满足这样的条件:首先它属于一个一定小于当前给定键的集合,其次它是这个集合中键值最大的。如果容器中没有入参key所代表的键值映射信息/键信息,则返回null。
Map.Entry<K,V> lowerEntry(K key);
K lowerKey(K key);
- 以下两个方法返回一个键值映射信息/键信息,这个被返回的信息满足这样的条件:首先它属于一个一定小于当前给定键的集合,其次它是这个集合中键值最大的。如果不存在这样的键值对映射信息/键信息,则返回入参key所代表的键值对映射信息/键信息本身;如果容器中没有入参key所代表的键值映射信息/键信息,则返回null。
Map.Entry<K,V> floorEntry(K key);
K floorKey(K key);
- 以下两个方法返回一个键值映射信息/键信息,这个被返回的信息满足这样的条件:首先它属于一个一定大于当前给定键的集合,其次它是这个集合中键值最小的。如果容器中没有入参key所代表的键值映射信息/键信息,则返回null。
Map.Entry<K,V> higherEntry(K key);
K higherKey(K key);
- 以下两个方法返回一个键值映射信息/键信息,这个被返回的信息满足这样的条件:首先它属于一个一定大于当前给定键的集合,其次它是这个集合中键值最小的。如果不存在这样的键值对映射信息/键信息,则返回入参key所代表的键值对映射信息/键信息本身;如果容器中没有入参key所代表的键值映射信息/键信息,则返回null。
Map.Entry<K,V> ceilingEntry(K key);
K ceilingKey(K key);
下图展示的java.util.NavigableMap接口中完整的方法定义:
============
(接下文 源码阅读(16):Java中主要的Map结构——HashMap集合)