3.Map接口
使用键、值映射表来存储
public interface Map<K,V>
Map接口特点:
- Map中的元素由键和值组成,Map 不能有重复的键(覆盖),每个键可以映射到最多一个值
- 允许将映射内容视为一组键、值集合或键值映射集合
- key 不要求有序,不可以重复。 value 也不要求有序,但可以重复
- 当使用对象作为 key 时,要重写 equals 和 hashCode 方法
常用方法
方法 | 返回值 | 描述 |
---|---|---|
clear() | void | 删除所有的映射。 |
containsKey(Object key) | boolean | 判断 Map 中是否包含指定的键。 |
containsValue(Object value) | boolean | 判断 Map 中是否包含指定的值。 |
entrySet() | Set<Map.Entry<K,V>> | 返回包含的映射的 Set。 |
get(Object key) | V | 根据键获取对应的值。 |
isEmpty() | boolean | 判断 Map 是否为空。 |
keySet() | Set | 返回 Map 中所有键的 Set。 |
put(K key, V value) | V | 向 Map 添加映射关系。 |
putAll(Map<? extends K, ? extends V> m) | void | 将指定 Map 中的映射复制到此映射中。 |
remove(Object key) | V | 如果键存在,则删除相应的映射。 |
remove(Object key, Object value) | boolean | 当键值映射存在时,删除对应的映射。 |
replace(K key, V value) | boolean | 当键存在时,替换键对应的值。 |
size() | int | 返回 Map 中映射的数量。 |
values() | Collection | 返回所有值的集合。 |
说明:
- Map 接口是键值对映射表的接口,具有键和值的一一映射关系。
- Map 中不能有重复的键,每个键只能映射唯一的值。
- 当使用对象作为键时,需要重写 equals 和 hashCode 方法。
- Map 提供了多个操作方法,如判断是否包含指定键或值、添加、删除映射等。
- entrySet() 方法返回一个包含所有键值映射的 Set 集合。
- keySet() 方法返回一个包含所有键的 Set 集合。
- values() 方法返回一个包含所有值的集合。
Map的遍历
public class MapIterator {
public static void main(String[] args) {
Map<String,Integer> map = Map.of("a", 97, "b", 98, "A", 65, "c", 97);
//迭代集合
/*
用entrySet
*/
Set<Map.Entry<String,Integer>> entries = map.entrySet();
Iterator<Map.Entry<String,Integer>> iterator = entries.iterator();
while (iterator.hasNext()){
Map.Entry<String,Integer> next = iterator.next();
System.out.println(next.getKey() + " = " + next.getValue());
}
/*
KyeSet
*/
Set<String> strings =map.keySet();
Iterator<String> iterator1 = strings.iterator();
while (iterator1.hasNext()){
String key = iterator1.next();
System.out.println(key + " = " + map.get(key));
}
/*
values
*/
Collection<Integer> values = map.values();
for(Integer value :values){
System.out.println(value);
}
}
}
JDK9
提供了创建不可修改 Map 对象的方法:
- Map.of
- Map.ofEntries
- Map.copyOf
3.1 HashMap
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, Serializable
HashMap
是 Map
接口的一个实现类,它使用哈希表来存储键值对
特点:
-
HashMap
允许键和值为null
。 -
键不允许重复,值可以重复,但是每个键只能映射到一个值。
-
查找、插入和删除操作的时间复杂度接近常数时间
(O(1))
。 -
非线程安全
-
默认容量
16
,默认负载因子0.75
,扩容是2
倍旧的容量- 默认的初始容量为
16
,意味着在创建HashMap
对象时,底层会初始化一个长度为16
的数组用于存储键值对。 - 负载因子(load factor)是一个用于控制 HashMap 扩容的参数。默认的负载因子为0.75。负载因子表示当哈希表中的键值对数量达到容量与负载因子乘积的阈值时,就会进行扩容。例如,在默认情况下,当哈希表中的键值对数量达到了 16 * 0.75 = 12 时,就会进行扩容操作。
- 默认的初始容量为
-
在存储数据时,
key
的hash
计算调用的是HashMap
中的hash
方法 -
内部采用
数组 + 链表实现
,JDK 8
及以后版本增加红黑树的支持。
常用方法:
修饰符和类型 | 方法 | 描述 |
---|---|---|
void | clear() | 删除映射中的所有键值对。 |
Object | clone() | 返回此 HashMap 实例的浅拷贝:键和值本身不会被克隆。 |
V | compute(K key, BiFunction<? super K,? super V,? extends V> remappingFunction) | 尝试为指定的键及其当前映射值(如果没有当前映射值,则为 null)计算一个新的映射值。 |
V | computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction) | 如果指定的键尚未与值关联(或与 null 关联),则尝试使用给定的映射函数计算其值,并将其输入到该映射中,除非 null。 |
V | computeIfPresent(K key, BiFunction<? super K,? super V,? extends V> remappingFunction) | 如果指定键的值存在且非 null,则尝试根据键和当前映射值计算一个新的映射。 |
boolean | containsKey(Object key) | 如果此映射包含指定键的映射关系,则返回 true。 |
boolean | containsValue(Object value) | 如果此映射将一个或多个键映射到指定值,则返回 true。 |
Set<Map.Entry<K,V>> | entrySet() | 返回此映射中包含的映射关系的 Set 视图。 |
V | get(Object key) | 返回指定键所映射的值,如果此映射不包含该键的映射关系,则返回 null。 |
boolean | isEmpty() | 如果此映射不包含键值映射关系,则返回 true。 |
Set | keySet() | 返回此映射中包含的键的 Set 视图。 |
V | merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction) | 如果指定键尚未与值关联或与 null 关联,则将其与给定的非 null 值关联。如果指定键已与某个值关联,则根据给定的 remapping 函数,将其与现有值组合,并将结果放置在该映射中。 |
V | put(K key, V value) | 在此映射中关联指定值与指定键。 |
void | putAll(Map<? extends K,? extends V> m) | 将指定映射中的所有映射关系复制到此映射中。 |
V | remove(Object key) | 如果存在,则从该映射中删除指定键的映射关系。 |
int | size() | 返回此映射中的键值映射关系数。 |
Collection | values() | 返回此映射中包含的值的 Collection 视图。 |
请注意,本接口不保证映射的顺序,如果需要按特定顺序遍历键值对,可以使用
LinkedHashMap
类来代替。
构造方法
构造方法 | 描述 |
---|---|
HashMap() | 使用默认初始容量(16)和默认加载因子(0.75)构造一个空的 HashMap。 |
HashMap(int initialCapacity) | 使用指定的初始容量和默认加载因子(0.75)构造一个空的 HashMap。 |
HashMap(int initialCapacity, float loadFactor) | 使用指定的初始容量和加载因子构造一个空的 HashMap。 |
HashMap(Map<? extends K,? extends V> m) | 使用与指定的 Map 相同的映射关系构造一个新的 HashMap。 |
HashMap 中 put 过程
HashMap
的 put(key, value)
方法用于将指定的键值对插入到哈希表中。下面是 put(key, value)
方法的简化实现过程:
- 判断是否需要进行扩容操作:
- 如果当前键值对数量超过了容量与负载因子乘积的阈值,就会进行扩容操作。
- 扩容操作会创建一个新的更大的数组,并重新计算每个键的哈希值,将键值对重新散列到新的数组中。
- 计算键的哈希值:
- 使用键对象的
hashCode()
方法计算哈希值。 - 这个哈希值会通过一系列位运算来得到最终的存储位置。
- 使用键对象的
- 查找存储位置:
- 根据计算得到的哈希值,找到在数组中对应的索引位置。
- 处理冲突:
- 如果发生了哈希冲突,即多个键的哈希值相同,此时会使用链表或红黑树等数据结构来存储冲突的键值对。
- 如果链表长度较长(默认为
8
,以链表的形式放到后边,长度超过阈值且数组长度大于等于64),会将链表转换为红黑树,以提高查找效率。删除元素时,如果时以红黑树存储的如果节点小于6
个将会变为链表存储
- 插入键值对:
- 如果找到了对应的存储位置,直接插入键值对。
- 如果存在相同的键,则用新的值替换旧的值。
示例:
public V put(K key, V value) {
// 判断是否需要扩容
if (size + 1 > threshold) {
resize();
}
// 计算键的哈希值
int hash = hash(key);
// 查找存储位置
int index = indexFor(hash, table.length);
// 处理冲突
for (Entry<K,V> e = table[index]; e != null; e = e.next) {
if (e.hash == hash && (key == e.key || key.equals(e.key))) {
V oldValue = e.value;
e.value = value;
return oldValue; // 返回旧值
}
}
// 插入新的键值对
addEntry(hash, key, value, index);
return null;
}
这只是 put(key, value)
方法的简化实现,实际上还有更多细节和边界情况需要考虑。以上简化的代码只是为了帮助理解 put
操作的基本原理。实际的 HashMap
类中的代码更加复杂,涉及更多细节和优化。
3.2TreeMap
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, Serializable
特点:
- 继承 AbstractMap ,一个红黑树基于 NavigableMap 实现
- 非线程安全的
- key 不能存 null ,但是 value 可以存 null,允许
null
值,当null
作为键时,由于键是有序的,需要使用自定义比较器 - key 必须是可比较的 (实现 Comparable 接口,传递一个 Comparator 比较器)
使用自定义比较器来处理 null
键
示例代码:
以下是一个示例,演示如何使用自定义比较器来处理 null
键
import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;
public class TreeMapExample {
public static void main(String[] args) {
// 自定义比较器,处理 null 键
Comparator<String> nullComparator = new Comparator<String>() {
@Override
public int compare(String str1, String str2) {
if (str1 == null && str2 == null) {
return 0;
} else if (str1 == null) {
return -1;
} else if (str2 == null) {
return 1;
} else {
return str1.compareTo(str2);
}
}
};
// 使用自定义比较器创建 TreeMap
TreeMap<String, Integer> treeMap = new TreeMap<>(nullComparator);
treeMap.put(null, 1);
treeMap.put("apple", 2);
treeMap.put("banana", 3);
treeMap.put("tangerine", 4);
for (Map.Entry<String, Integer> entry : treeMap.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 获取第一个和最后一个键
System.out.println(treeMap.firstKey()); // null
System.out.println(treeMap.lastKey()); // banana
System.out.println(treeMap.firstEntry()); // null=1
System.out.println(treeMap.lastEntry()); // banana=3
// 获取小于等于指定键的最大值和大于等于指定键的最小值
System.out.println(treeMap.floorKey("c")); // banana
System.out.println(treeMap.ceilingKey("c")); // tangerine
System.out.println(treeMap.floorEntry("c")); // banana=3
System.out.println(treeMap.ceilingEntry("c")); // tangerine=4
// 按照倒叙输出
System.out.println(treeMap.descendingMap()); // {tangerine=4, banana=3, apple=2, null=1}
System.out.println(treeMap); // {null=1, apple=2, banana=3, tangerine=4}
System.out.println(treeMap.descendingKeySet()); // [tangerine, banana, apple, null]
}
}
在这个示例中,我们创建了一个自定义比较器 nullComparator
来处理 null
键。它首先检查两个键是否都是 null
,然后再处理其他情况。这样,即使有 null
键存在,TreeMap 也能够正确地进行排序并保持有序状态。
3.3Hashtable
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, Serializable
Hashtable
是 Java 中的一种哈希表数据结构,它实现了 Map
接口,提供了键值对的储存和检索功能。
特点
- 线程安全(同步的)
- 键值不允许为
null
- 动态扩容:默认初始容量( initialCapacity )为11,默认负载因子( loadFactor )为0.75f,扩容方式:旧容量的2倍 +1
- 为了均匀分布,降低冲突率
- 不保证顺序
数组 + 链表
方式存储实现Hash表存储- 添加元素:
- 如果
hash
一样equals
为false
则将当前值添加到链表头 - 如果
hash
一样equals
为true
则使用当前值替换原来的值 ( key 相同)
- 如果
构造方法
方法 | 描述 |
---|---|
Hashtable() | 构造一个新的,空的散列表,默认初始容量为 11 和负载因子为 0.75。 |
Hashtable(int initialCapacity) | 构造一个新的,空的哈希表,具有指定的初始容量和默认负载因子为 0.75。 |
Hashtable(int initialCapacity, float loadFactor) | 构造一个新的,空的哈希表,具有指定的初始容量和指定的负载因子。 |
Hashtable(Map<? extends K, ? extends V> t) | 构造一个新的哈希表,其映射与给定的 Map 相同。 |
HashMap 与 Hashtable 中 put 过程比较
线程安全性
:Hashtable 是线程安全的,而 HashMap 不是。Hashtable 的所有方法都是同步的,可以在多线程环境下安全使用,但同时也会带来一定的性能开销。而 HashMap 不提供线程安全的保证,需要在多线程环境下自行处理同步。允许键或值为空
:Hashtable 不允许键或值为空(null),如果尝试插入空键或值,会抛出 NullPointerException。而 HashMap 允许键和值都为空。继承关系
:Hashtable 是早期的 Java 类,实现了 Dictionary 接口,并继承自 Dictionary 类。而 HashMap 是 Java Collections Framework 的一部分,实现了 Map 接口,并继承自 AbstractMap 类。同步策略
:Hashtable 使用 synchronized 关键字来实现线程安全。而 HashMap 在默认情况下不进行同步,可以通过 Collections.synchronizedMap 方法将 HashMap 转换为线程安全的版本。
在 put 过程中,Hashtable 和 HashMap 的基本逻辑是相同的,都是根据键的哈希码计算索引位置,解决冲突,并插入键值对。区别主要体现在线程安全性和对空键或值的处理上。因此,在单线程环境下,如果不需要线程安全性,推荐使用 HashMap,它具有更好的性能。
import java.util.Hashtable;
public class HashtableExample {
public static void main(String[] args) {
Hashtable<String, Integer> hashtable = new Hashtable<>();
// 添加键值对
hashtable.put("one", 1);
hashtable.put("two", 2);
hashtable.put("three", 3);
hashtable.put("four", 4);
// 获取键值对
System.out.println(hashtable.get("one")); // 1
// getOrDefault获取值
System.out.println(hashtable.getOrDefault("one", 2)); // 1
System.out.println(hashtable); // {two=2, one=1, three=3, four=4}
// 判断是否包含指定的值或键
System.out.println(hashtable.contains(4)); // true,是否包含值
System.out.println(hashtable.containsKey("one")); // true,是否包含键
System.out.println(hashtable.containsValue(4)); // true,是否包含值
// 遍历键值对
for (String key : hashtable.keySet()) {
System.out.println(key + " : " + hashtable.get(key));
}
}
}