本节学习目标:
双列集合指有两列数据的集合,双列集合中的每个数据被称为键值对(Entry)。
键值对由两个数据组成:
K
限制数据类型,键不可重复(唯一),数据结构也只对键有效;V
限制数据类型,值可以重复。Java双列集合框架由Map
接口及其多种实现类构成:
Map
接口为Java双列集合框架中的根接口,它预先定义了双列集合中的基本方法,双列集合中的常用方法如下:
方法 | 返回值类型 | 功能 |
---|---|---|
size() |
int |
返回Map集合中的键值对数量 |
isEmpty() |
boolean |
返回Map集合是否为空 |
containsKey(Object key) |
boolean |
返回Map集合中是否存在指定键 |
containsValue(Object value) |
boolean |
返回Map集合中是否存在指定值 |
get(Object key) |
V |
返回Map集合中指定键的值 |
put(K key, V value) |
V |
向Map集合中添加指定键值对,并返回旧值(如果没有该键的映射则返回null ) |
remove(Object key) |
V |
移除Map集合中的指定键,并返回此键的值 |
putAll(Map extends K, ? extends V> m) |
void |
将指定Map集合的所有键值对添加至当前Map集合中 |
keySet() |
Set |
以Set集合形式返回Map集合的所有键 |
values() |
Collection |
以Collection集合形式返回Map集合的所有值 |
entrySet() |
Set |
以Set集合形式返回Map集合的所有键值对 |
remove(Object key, Object value) |
boolean |
当指定键对应指定值时,移除此键值对,返回是否移除成功 |
replace(K key, V oldValue, V newValue) |
boolean |
当键key 对应值oldValue 时,将值替换为newValue ,返回是否替换成功 |
replace(K key, V value) |
V |
将指定键的值替换为指定值,并返回旧值(如果没有该键的映射则返回null ) |
clear() |
void |
移除Map集合中的所有键值对 |
Entry
接口为Map
接口中的内部接口,它预先定义了双列集合中键值对的基本方法,键值对的常用方法如下:
方法 | 返回值类型 | 功能 |
---|---|---|
getKey() |
K |
返回当前键值对的键 |
getValue() |
V |
返回当前键值对的值 |
setValue(V value) |
V |
将当前键值对的值替换为指定值,并返回旧值 |
编写代码进行测试:
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class Test {
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<>();
System.out.println("集合是否为空:" + map.isEmpty());
map.put(1, "A");
map.put(2, "A");
map.put(3, "C");
System.out.println("集合当前键值对:" + Arrays.toString(map.entrySet().toArray()));
System.out.println("集合中键值对数量:" + map.size());
System.out.println("集合中是否存在键2:" + map.containsKey(2));
System.out.println("集合中是否存在值D:" + map.containsValue("D"));
System.out.println("获取集合中键1的值:" + map.get(1));
map.remove(1);
System.out.println("移除集合中键为1的键值对:" + Arrays.toString(map.entrySet().toArray()));
map.putAll(Collections.singletonMap(4, "D"));
System.out.println("向集合中添加Map集合[4=D]:" + Arrays.toString(map.entrySet().toArray()));
System.out.println("获取集合中的所有键:" + Arrays.toString(map.keySet().toArray()));
System.out.println("获取集合中的所有值:" + Arrays.toString(map.values().toArray()));
map.remove(2, "B");
System.out.println("移除集合中键值对2=B:" + Arrays.toString(map.entrySet().toArray()));
map.replace(2, "A", "B");
System.out.println("将集合中键值对2=A的值替换为B:" + Arrays.toString(map.entrySet().toArray()));
System.out.println("将集合中键为3的键值对的值替换为E,它的旧值:" + map.replace(3, "E"));
Map.Entry<Integer, String> entry = map.entrySet().iterator().next();
System.out.println("获取集合中的第一个键值对:" + entry);
System.out.println("它的键:" + entry.getKey());
System.out.println("它的值:" + entry.getValue());
System.out.println("将它的值替换为C,它的旧值:" + entry.setValue("C"));
map.clear();
System.out.println("移除集合中的所有键值对,此时集合中的键值对数量:" + map.size());
}
}
运行结果:
集合是否为空:true
集合当前键值对:[1=A, 2=A, 3=C]
集合中键值对数量:3
集合中是否存在键2:true
集合中是否存在值D:false
获取集合中键1的值:A
移除集合中键为1的键值对:[2=A, 3=C]
向集合中添加Map集合[4=D]:[2=A, 3=C, 4=D]
获取集合中的所有键:[2, 3, 4]
获取集合中的所有值:[A, C, D]
移除集合中键值对2=B:[2=A, 3=C, 4=D]
将集合中键值对2=A的值替换为B:[2=B, 3=C, 4=D]
将集合中键为3的键值对的值替换为E,它的旧值:C
获取集合中的第一个键值对:2=B
它的键:2
它的值:B
将它的值替换为C,它的旧值:B
移除集合中的所有键值对,此时集合中的键值对数量:0
Map
接口有很多种实现类,这些实现类各自有各自的优缺点,可以根据实际需求挑选使用。
对Map集合结构的理解:
keySet
);values
);HashMap
类是Map
接口的主要实现类之一,并且继承于AbstractMap
抽象类。它是Map
集合的主要实现,一般情况下都使用HashMap集合作为Map集合的实现。
HashMap集合的核心结构为节点(Node
),它实现了Map
接口中的Entry
内部接口,也就是键值对:
// java.util.HashMap.Node
// HashMap.java jdk1.8.0_202 Line:279~317
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // 由键的哈希值(hashCode()返回值)计算得来的值,它不一定等于键的哈希值
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;
}
// 省略其他代码
}
HashSet集合内部使用了HashMap对象提供支持。
HashMap
类的成员变量:
成员变量名 | 类型 | 说明 |
---|---|---|
table |
Node |
键值对数组,数组的长度必须为2的N次幂 |
entrySet |
Set |
所有键值对的Set集合 |
size |
int |
键值对的个数,即集合的长度 |
modCount |
int |
对集合进行操作的次数计数 |
threshold |
int |
扩容临界值(阈值),默认值为当前容量乘以负载因子 |
loadFactor |
float |
负载因子,默认为DEFAULT_LOAD_FACTOR (即0.75F ) |
继承于AbstractMap
类的成员变量:
成员变量名 | 类型 | 说明 |
---|---|---|
keySet |
Set |
所有键的Set集合 |
values |
Collection |
所有值的Collection集合 |
Node
(键值对)类的成员变量:
成员变量名 | 类型 | 说明 |
---|---|---|
hash |
int |
由键的哈希值经过hash() 方法计算得来的值,它不一定等于键的哈希值 |
key |
K |
键值对的键 |
value |
V |
键值对的值 |
next |
Node |
发生哈希冲突时,连接旧键值对的引用 |
HashMap
类的构造方法:
构造方法 | 说明 |
---|---|
HashMap() |
默认构造方法创建集合,键值对数组长度和负载因子均为默认 |
HashMap(int initialCapacity) |
以指定数组长度创建集合,使用默认负载因子 |
HashMap(int initialCapacity, float loadFactor) |
以指定数组长度和负载因子创建集合 |
HashMap(Map extends K, ? extends V> m) |
以某Map集合m转换为HashMap集合 |
调用put()
方法进行添加操作,开始调试,进入put()
方法:
// java.util.HashMap.put
// HashMap.java jdk1.8.0_202 Line:611~613
public V put(K key, V value) {
// 调用hash()方法得出键值对的hash:
// static final int hash(Object key) {
// int h;
// return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
// }
// 传入hash,键和值,进入putVal()方法
return putVal(hash(key), key, value, false, true);
}
继续深入调试,进入putVal()
方法:
// java.util.HashMap.putVal
// HashMap.java jdk1.8.0_202 Line:625~666
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果键值对数组还未初始化(table引用为null)或键值对数组长度为0
if ((tab = table) == null || (n = tab.length) == 0)
// 调用resize()方法对键值对数组进行初始化并获取键值对数组长度
n = (tab = resize()).length;
// 根据下面的计算方式算出新键值对在数组中的存储位置,判断这个存储位置上有没有键值对(即是否为null)
if ((p = tab[i = (n - 1) & hash]) == null)
// 如果为null,则直接添加
tab[i] = newNode(hash, key, value, null);
// 否则获取存储位置上的原键值对引用p
else {
Node<K,V> e; K k;
// 判断原键值对和新键值对的hash以及键是否都相同
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 如果相同,将原键值对的引用赋给e
e = p;
// 如果键值对类型为红黑树节点(TreeNode)类型
else if (p instanceof TreeNode)
// 使用红黑树的添加方法putTreeVal()进行新键值对的添加操作,在此不再深入分析
// 如果e不为null,就为添加后返回的原键值对引用
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 如果键值对类型是节点(Node)类型
else {
// for循环遍历哈希表中键值对数组当前索引的链表
for (int binCount = 0; ; ++binCount) {
// 将原键值对的下一个键值对引用赋给e,同时判断是否为null(即已到达链表末尾)
if ((e = p.next) == null) {
// 将新键值对添加至链表末尾
p.next = newNode(hash, key, value, null);
// 为了加快操作效率
// 如果链表长度大于或等于TREEIFY_THRESHOLD(8)
// 则调用treeifyBin()方法将链表转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果原键值对的下一个键值对和新键值对的hash以及键都相同
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
// 说明是重复,直接跳过
break;
// 将原键值对的下一个键值对引用赋给p,继续循环遍历
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()方法进行扩容
resize();
afterNodeInsertion(evict);
// 返回null,说明没有旧值
return null;
}
链表转换为红黑树的treeifyBin()
方法:
// java.util.HashMap.treeifyBin
// HashMap.java jdk1.8.0_202 Line:755~774
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 如果键值对数组没有初始化或者数组长度还未达到MIN_TREEIFY_CAPACITY(64)
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
// 调用resize()方法进行扩容,而不是转换为红黑树
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
// 具体转换逻辑,在此省略
}
}
总结(简述)HashMap集合在JDK1.8中添加操作的步骤:
resize()
扩容方法进行初始化;null
,结束添加操作。TreeNode
类型):
putTreeVal()
进行添加操作,并返回原值(或者为null
),结束添加操作。null
,结束添加操作。TREEIFY_THRESHOLD
(8)且键值对数组的长度已达MIN_TREEIFY_CAPACITY
(64),则将链表转换为红黑树(treeifyBin()
方法)。table.length
)是否大于或等于阈值(threshold
),如果超了则调用resize()
方法进行扩容。扩容与初始化方法resize()
:
// java.util.HashMap.resize
// HashMap.java jdk1.8.0_202 Line:677~749
final Node<K,V>[] resize() {
// 获取原键值对数组的引用并赋给oldTab
Node<K,V>[] oldTab = table;
// 获取原键值对数组的长度并赋给oldCap,如果没有初始化则为0
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 获取原阈值并赋给oldThr
int oldThr = threshold;
int newCap, newThr = 0;
// 判断原键值对数组的长度是否大于0
if (oldCap > 0) {
// 如果原键值对数组的长度已经超过HashMap集合最大长度MAXIMUM_CAPACITY
if (oldCap >= MAXIMUM_CAPACITY) {
// 将阈值提升到int类型最大值
threshold = Integer.MAX_VALUE;
// 返回原键值对数据引用,方法结束
return oldTab;
}
// 计算扩容后的长度newCap,即原长度的2倍
// 判断扩容后的长度是否小于HashMap集合最大长度MAXIMUM_CAPACITY且小于等于默认长度16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 计算扩容后的阈值newThr,即原阈值的2倍
newThr = oldThr << 1; // double threshold
}
// 如果原阈值oldThr大于0
else if (oldThr > 0) // initial capacity was placed in threshold
// 扩容至原阈值大小
newCap = oldThr;
// 如果原阈值oldThr小于等于0,即还未初始化
else {
// zero initial threshold signifies using defaults
// 扩容至默认长度16
newCap = DEFAULT_INITIAL_CAPACITY;
// 扩容后的阈值为默认负载因子0.75F乘以默认长度16,即12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果原键值对长度小于等于0且原阈值大于0
// 比如将某个Map集合转换为HashMap,这时键值对数组table为null,但阈值threshold可能不为0(在putMapEntries()方法中被设置)
// 即通过了上面的else if语句,此时newCap与原阈值oldThr相等
if (newThr == 0) {
// 计算扩容后的阈值,即原阈值乘以负载因子
float ft = (float)newCap * loadFactor;
// 如果原阈值和扩容后的阈值都小于HashMap集合最大长度MAXIMUM_CAPACITY,则扩容后的阈值就为计算结果
// 否则扩容后的阈值为int类型最大值
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引用指向新键值对数组
table = newTab;
// 如果原键值对数组不为null
if (oldTab != null) {
// 此处省略了将原键值对数组中的键值对重新计算存储位置并放到新键值对数组中的操作
}
// 返回新键值对数组
return newTab;
}
总结(简述)HashMap集合在JDK1.8中扩容操作的步骤:
table
是否已经初始化(即table
引用是否为null
):
table
引用为null
),判断阈值threshold
是否大于0
:
0
,扩容后的长度调整为阈值,阈值调整至原阈值乘以负载因子后的值。0
,扩容后的长度调整为默认长度16,阈值调整至默认阈值12((int) (16 * 0.75F)
)。table
指向新数组并返回。HashMap集合在JDK1.7和JDK1.8中实现方式的不同:
8
将被转换为红黑树以提高遍历效率。HashMap
类提供的常量:
常量名 | 类型 | 值 | 说明 |
---|---|---|---|
DEFAULT_INITIAL_CAPACITY |
int |
1 << 4 (16) |
键值对数组初始化的默认长度(HashMap集合默认容量) |
MAXIMUM_CAPACITY |
int |
1 << 30 (1073741824) |
键值对数组的最大长度(HashMap集合最大容量) |
DEFAULT_LOAD_FACTOR |
float |
0.75F |
默认负载因子 |
TREEIFY_THRESHOLD |
int |
8 |
链表转换为红黑树时所需链表长度阈值 |
UNTREEIFY_THRESHOLD |
int |
6 |
红黑树转换为链表时所需的红黑树长度阈值 |
MIN_TREEIFY_CAPACITY |
int |
64 |
链表转换为红黑树时所需键值对数组长度阈值 |
null
,但只能有一个键值对的键为null
;DEFAULT_INITIAL_CAPACITY
),每次扩容为原来的2倍,扩容后容量一定为2的N次幂;tableSizeFor()
方法重新计算容量),所以会造成指定的容量与实际容量不符的情况。// java.util.HashMap.tableSizeFor
// HashMap.java jdk1.8.0_202 Line:378~386
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
什么是哈希表?什么是哈希冲突(哈希碰撞)?
哈希表是基于数组的一种存储方式,它主要由哈希函数和数组组成。当存储数据时,会先用哈希函数计算出数据的哈希值,
之后根据哈希值存储进数组的指定索引。
哈希冲突是指两个不同的数据由哈希函数计算出的哈希值相同,它们必然会存储进哈希表中的同一个索引,引发冲突(碰撞)。
Java中解决哈希冲突的方式是链地址法,将发生哈希冲突的数据构成一个单链表,并将单链表的头节点存储进数组。
什么是哈希表?什么又是哈希冲突?哈希冲突的解决方法?- JavaShuo
HashMap集合扩容后的容量为什么一定为2的N次幂?
HashMap集合根据哈希值计算存储位置时使用按位与运算代替了取模运算,以提升效率。当数组长度为2的N次幂时,会使数据在哈希表上分布均匀,
减少哈希冲突,减少链表的长度,提升了性能。
为什么hashMap的容量扩容时一定是2的幂次 - 平凡之路无尽路/CSDN
为什么JDK1.8中HashMap集合使用哈希表+红黑树?为什么要在链表长度超过8且容量超过64时才将链表转换为红黑树?
Java使用链地址法来解决哈希冲突,但链表的长度很长时将严重降低效率。由于红黑树的特性使得它的性能比链表更优越,所以在链表很长时会转换为红黑树。
由于红黑树构造效率不是很高,所以要在链表长度超过8时才会转换红黑树,避免大量使用红黑树。如果容量低于64而存在长度超过8的链表,说明发生哈希冲突的概率较高,
这时候会进行扩容操作,将链表中的数据重新计算存储位置,使数据分布更均匀。从而避免使用红黑树。
hashmap中为何链表长度大于8才转换成红黑树 - JerryCode/知乎
什么是负载因子?默认负载因子为什么是0.75?
负载因子决定了阈值的大小,当已用容量超过阈值时会进行扩容,增加容量并消除部分哈希冲突(减少链表的长度)。
负载因子越小,阈值越小,触发扩容越频繁,容量越大,空间利用率越低;哈希冲突越少(链表长度越小),操作效率越高。
负载因子越大,阈值越大,触发扩容次数越少,容量越小,空间利用率越高,哈希冲突越多(链表长度越大),操作效率越低。
可以看出负载因子是用于平衡空间利用率和操作效率。
(源码)作为一般规则,默认负载因子 (.75) 在时间和空间成本之间提供了很好的权衡。 较高的值会减少空间开销,但会增加查找成本(反映在HashMap
类的大多数操作中,包括get
和put
)。// HashMap.java jdk1.8.0_202 Line:68~77 /* As a general rule, the default load factor (.75) offers a good * tradeoff between time and space costs. Higher values decrease the * space overhead but increase the lookup cost (reflected in most of * the operations of the HashMap class, including * get and put). The expected number of entries in * the map and its load factor should be taken into account when * setting its initial capacity, so as to minimize the number of * rehash operations. If the initial capacity is greater than the * maximum number of entries divided by the load factor, no rehash * operations will ever occur. */
HashMap的负载因子为什么默认是0.75 - 火麒马/CSDN
LinkedHashMap
类也是Map
接口的实现类之一,并且继承于HashMap
类。
LinkedHashMap集合相当于HashMap集合加双向链表,由双向链表根据键值对的插入顺序排序。
LinkedHashMap集合的核心结构Entry
(双向链表中的一个节点,继承于HashMap
类中的Node
类):
// java.util.LinkedHashMap.Entry
// LinkedHashMap.java jdk1.8.0_202 Line:192~197
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);
}
}
LinkedHashSet集合内部使用了LinkedHashMap对象提供支持。
LinkedHashMap
类继承于HashMap
类,所以拥有HashMap
类的非私有成员变量,它自身的成员变量:
成员变量名 | 类型 | 说明 |
---|---|---|
accessOrder |
boolean |
遍历顺序,true 则按照哈希表遍历顺序,false 则按照插入顺序 |
head |
Entry |
双向链表的头节点(first) |
tail |
Entry |
双向链表的尾节点(last) |
Entry
类的成员变量:
成员变量名 | 类型 | 说明 |
---|---|---|
before |
Entry |
指向上一个节点的引用(prev) |
after |
Entry |
指向下一个节点的引用(next) |
LinkedHashMap
类的构造方法:
构造方法 | 说明 |
---|---|
LinkedHashMap() |
默认构造方法,accessOrder 为false |
LinkedHashMap(int initialCapacity) |
以指定容量创建集合,accessOrder 为false |
LinkedHashMap(int initialCapacity, float loadFactor) |
以指定容量和负载因子创建集合,accessOrder 为false |
LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) |
以指定容量和负载因子创建集合,遍历顺序为指定顺序 |
LinkedHashMap(Map extends K, ? extends V> m) |
将集合m转换为LinkedHashMap集合 |
null
,但只能有一个键值对的键为null
。Hashtable
类继承于Dictionary
抽象类,是Map
接口的古老实现类(类似ArrayList集合和Vector集合的关系)。
Dictionary
类是Map
接口未出现前用来存储键值对的抽象类,现已过时。
Hashtable
类的成员变量:
成员变量名 | 类型 | 说明 |
---|---|---|
count |
int |
键值对的个数,即集合的长度 |
entrySet |
Set |
所有键值对的Set集合 |
keySet |
Set |
所有键的Set集合 |
values |
Collection |
所有值的Collection集合 |
table |
Entry, ?>[] |
键值对数组 |
threshold |
int |
扩容临界值(阈值),默认值为当前容量乘以负载因子 |
loadFactor |
float |
负载因子,默认为0.75F |
modCount |
int |
对集合进行操作的次数计数 |
Hashtable
类的常用构造方法:
构造方法 | 说明 |
---|---|
Hashtable() |
默认构造方法 |
Hashtable(int initialCapacity) |
以指定容量创建Hashtable集合 |
Hashtable(int initialCapacity, float loadFactor) |
以指定容量和负载因子创建Hashtable集合 |
Hashtable(Map extends K, ? extends V> t) |
将Map集合t 转换为Hashtable集合 |
Hashtable
类提供的常量:
常量名 | 类型 | 值 | 说明 |
---|---|---|---|
MAX_ARRAY_SIZE |
int |
Integer.MAX_VALUE - 8 |
键值对数组的最大长度(Hashtable集合的最大容量) |
Hashtable
类年代久远,优化较差,现已弃用;null
;TreeMap
类为SortedMap
接口的实现类之一,间接实现了Set
接口。TreeMap是一种有序Map集合,按照键值对的键的某一属性进行排序。
TreeMap集合底层使用红黑树存储数据。它的节点Entry
实现:
// java.util.TreeMap.Entry
// TreeMap.java jdk1.8.0_202 Line:2053~2119
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; // 颜色
Entry(K key, V value, Entry<K,V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
// 省略其他代码
}
TreeSet集合内部使用了TreeMap对象提供支持。
TreeMap
类的成员变量:
成员变量名 | 类型 | 说明 |
---|---|---|
comparator |
Comparator super K> |
比较器,如果为null则使用键的自然排序 |
modCount |
int |
对集合进行操作的次数计数 |
root |
Entry |
红黑树的根节点 |
size |
int |
键值对的个数,即集合的长度 |
TreeMap
类的常用构造方法:
构造方法 | 说明 |
---|---|
TreeMap() |
默认构造方法,使用键的自然排序 |
TreeMap(Comparator super K> comparator) |
使用比较器创建一个TreeMap集合,使用比较器的定制排序 |
TreeMap(Map extends K, ? extends V> m) |
将Map集合m转换为TreeMap集合 |
红黑树(Red Black Tree) 是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。
红黑树是在1972年由Rudolf Bayer发明的,当时被称为平衡二叉B树(symmetric binary B-trees)。后来,在1978年被 Leo J. Guibas 和 Robert Sedgewick 修改为如今的“红黑树”。
红黑树是一种特化的AVL树(平衡二叉树),都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。
它虽然是复杂的,但它的最坏情况运行时间也是非常良好的,并且在实践中是高效的: 它可以在O(log n)时间内做查找,插入和删除,这里的n 是树中元素的数目。
红黑树 - 百度百科
红黑树的五个性质:
这些性质使红黑树大致上是平衡的,并且最长路径是最短路径的2倍。
关于实现源码:Java提高篇(二七)-----TreeMap - chenssy/CSDN
null
,但允许键值对的值为null
;Map
接口的四大实现类:HashMap
,LinkedHashMap
,Hashtable
和TreeMap
。
Map集合 | 键可为null |
值可为null |
操作效率 | 是否线程安全 | 存储结构 | 说明 |
---|---|---|---|---|---|---|
HashMap | 是(只有一个) | 是 | 较高 | 否 | 哈希表+红黑树 | Map集合的主要实现,键值对为无序 |
LinkedHashMap | 是(只有一个) | 是 | 较高 | 否 | 哈希表+红黑树+双向链表 | 键有序(按照插入顺序) |
Hashtable | 否 | 否 | 较低 | 是 | 哈希表 | 现已弃用 |
TreeMap | 否 | 是 | 较高 | 否 | 红黑树 | 键有序(按照键的某个属性排序) |
如果在多线程环境中需要使用Map集合,使用Collection
工具类提供的synchronizedMap()
方法获取线程安全的Map集合,或使用ConcurrentHashMap集合。