1 HashMap的概念
HashMap集合是Map接口的一个实现类,它用于存储键值映射关系,该集合的键和值允许为空,但键不能重复,且集合中的元素是无序的。
特点
HashMap底层是由哈希表结构组成的,其实就是“数组+链表”的组合体,数组是HashMap的主体结构,链表则主要是为了解决哈希值冲突而存在的分支结构。正因为这样特殊的存储结构,HashMap集合对于元素的增、删、改、查操作表现出的效率都比较高。
结构
在java1.8以后采用数组+链表+红黑树的形势来进行存储,通过散列映射来存储键值对,如下图:
- 在初始化时将会给定默认容量为16
- 对key的hashcode进行一个取模操作得到数组下标
- 数组存储的是一个单链表
- 数组下标相同将会被放在同一个链表中进行存储
- 元素是无序排列的
- 链表超过一定长度(TREEIFY_THRESHOLD=8)会转化为红黑树
- 红黑树在满足一定条件会再次退回链表
2 HashMap的定义
HashMap的构造函数
HashMap共有4个构造函数,如下:
// 默认构造函数。
HashMap()
// 指定“容量大小”的构造函数
HashMap(int capacity)
// 指定“容量大小”和“加载因子”的构造函数
HashMap(int capacity, float loadFactor)
// 包含“子Map”的构造函数
HashMap(Map<? extends K, ? extends V> map)
3 HashMap的使用
3.1 HashMap的基本用法
创建HashMap对象 要创建一个HashMap对象,您可以使用如下的方式:
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
// 创建一个HashMap对象
Map<String, Integer> hashMap = new HashMap<>();
}
}
添加键值对 您可以使用put方法来向HashMap中添加键值对:
hashMap.put("apple", 1);
hashMap.put("banana", 2);
hashMap.put("cherry", 3);
获取值 要从HashMap中获取值,可以使用get方法,并传入键:
int value = hashMap.get("banana"); // 获取键"banana"对应的值,此时value为2
删除键值对 要删除HashMap中的键值对,可以使用remove方法:
hashMap.remove("apple"); // 删除键"apple"对应的键值对
遍历HashMap 遍历HashMap可以使用不同的方法,最常见的是使用forEach方法:
hashMap.forEach((key, value) -> {
System.out.println(key + ": " + value);
});
3.2 HashMap的高级用法
处理碰撞 HashMap在处理哈希碰撞(即两个不同的键映射到了同一个哈希桶中)时,使用了链表和红黑树结构来存储键值对。这使得HashMap在大多数情况下都能提供O(1)的性能。
容量和负载因子 HashMap有两个重要的参数,分别是容量(capacity)和负载因子(load factor)。容量是哈希表中桶的数量,而负载因子是桶的填充程度。当HashMap中的元素数量超过容量与负载因子的乘积时,哈希表会进行扩容,以保持性能。
Map<String, Integer> hashMap = new HashMap<>(16, 0.75f);
遍历键集合或值集合 除了使用forEach方法遍历键值对外,您还可以使用keySet和values方法来分别获取键的集合和值的集合,并进行遍历:
Set<String> keys = hashMap.keySet(); // 获取所有键的集合
Collection<Integer> values = hashMap.values(); // 获取所有值的集合
for (String key : keys) {
System.out.println(key);
}
for (int value : values) {
System.out.println(value);
}
替代默认值 当从HashMap中获取值时,如果键不存在,通常会返回null。如果您希望在键不存在时返回一个默认值,可以使用getOrDefault方法:
int value = hashMap.getOrDefault("orange", 0); // 如果键"orange"不存在,返回默认值0
合并操作 您可以使用merge方法来合并两个HashMap:
Map<String, Integer> anotherMap = new HashMap<>();
anotherMap.put("apple", 5);
anotherMap.put("banana", 6);
anotherMap.forEach((key, value) -> {
hashMap.merge(key, value, (oldValue, newValue) -> oldValue + newValue);
});
这将合并两个HashMap,如果键相同,则将值相加。
3.3 更多操作
当涉及到HashMap的更多操作时,有一些重要的概念和方法可以帮助您更灵活地处理数据。以下是一些HashMap的更多操作:
- 获取HashMap的大小 要获取HashMap中键值对的数量,可以使用size方法:
int size = hashMap.size();
这将返回HashMap中键值对的数量。
- 判断HashMap是否为空 您可以使用isEmpty方法来检查HashMap是否为空:
boolean isEmpty = hashMap.isEmpty();
如果HashMap为空,将返回true,否则返回false。
- 替换值 如果要替换HashMap中的值,可以使用replace方法:
hashMap.replace("apple", 4); // 将键"apple"对应的值替换为4
- 清空HashMap 要清空HashMap中的所有键值对,可以使用clear方法:
hashMap.clear(); // 清空HashMap
- 判断是否包含键或值 您可以使用containsKey方法来检查HashMap是否包含特定键:
boolean containsKey = hashMap.containsKey("apple"); // 检查是否包含键"apple"
同样地,您可以使用containsValue方法来检查HashMap是否包含特定值:
boolean containsValue = hashMap.containsValue(2); // 检查是否包含值2
- 复制HashMap 如果需要创建一个与现有HashMap相同的副本,可以使用clone方法:
Map<String, Integer> copyHashMap = new HashMap<>(hashMap);
这将创建一个与hashMap相同的新HashMap。
- 获取键值对的集合 除了使用keySet和values方法获取键集合和值集合外,您还可以使用entrySet方法来获取键值对的集合:
Set<Map.Entry<String, Integer>> entrySet = hashMap.entrySet();
for (Map.Entry<String, Integer> entry : entrySet) {
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}
这将允许您迭代访问HashMap中的键值对。
- 同步HashMap 如果需要在多个线程之间共享HashMap,并且希望确保线程安全性,可以使用Collections.synchronizedMap方法创建同步的HashMap:
Map<String, Integer> synchronizedHashMap = Collections.synchronizedMap(hashMap);
这将返回一个线程安全的HashMap。
- 获取键或值的集合视图 如果需要获取HashMap中键或值的集合视图,可以使用keySet和values方法。这些集合视图是与原始HashMap关联的,对它们的更改将影响原始HashMap。
- 计算HashMap的哈希码 要计算HashMap的哈希码,可以使用hashCode方法:
int hashCode = hashMap.hashCode();
这将返回HashMap的哈希码值。
- 处理默认值 如果要从HashMap中获取值,如果键不存在,不仅返回默认值,还可以在键不存在时执行某个操作。您可以使用computeIfAbsent方法来实现这一点:
hashMap.computeIfAbsent("orange", key -> {
// 在键"orange"不存在时,执行此操作并返回默认值0
return 0;
});
这将允许您在获取值的同时进行计算或其他操作。
4 注意事项
当使用HashMap时,有一些注意事项需要考虑,以确保您的代码正确且高效地运行。以下是一些重要的注意事项:
- 键的唯一性: HashMap中的键必须是唯一的。如果尝试将相同的键插入HashMap中,新值将覆盖旧值。
- 值可以重复: HashMap中的值可以重复。多个键可以映射到相同的值。
- 空键: HashMap允许使用null作为键,但只能有一个null键。这意味着如果插入多个null键,后续的null键将覆盖前面的。
- 并发性: HashMap不是线程安全的,如果在多个线程之间共享HashMap,请确保适当地同步访问,或者使用ConcurrentHashMap等线程安全的集合。
- 性能: HashMap的性能通常很好,但在某些情况下,可能会发生哈希冲突,导致性能下降。要优化性能,可以考虑调整HashMap的初始容量和负载因子。
- 哈希函数: HashMap使用哈希函数将键映射到存储位置。如果键的哈希码分布不均匀,可能会导致哈希冲突。因此,确保自定义对象的hashCode方法正确实现,以获得更好的性能。
- 遍历顺序: HashMap的遍历顺序不是按照插入顺序或任何特定顺序的。如果需要按特定顺序访问键值对,可以考虑使用LinkedHashMap。
- 容量和负载因子: 考虑根据数据量选择适当的初始容量和负载因子。较大的初始容量可以减少哈希冲突,提高性能。
- 空间复杂度: HashMap的空间复杂度是O(n),其中n是存储的键值对数量。因此,要谨慎使用大型HashMap,以避免内存占用过多。
- 使用泛型: 在创建HashMap时,尽可能使用泛型来指定键和值的类型,以提高类型安全性。
- 及时清理不再需要的键值对: 如果不再需要HashMap中的某个键值对,及时使用remove方法或其他方式删除它们,以释放内存和资源。
- 异常处理: 当使用get方法获取值时,要考虑键不存在的情况,以避免NullPointerException。可以使用containsKey方法或条件语句来检查键是否存在。
- 性能监控: 如果HashMap用于性能关键的应用程序,考虑使用性能监控工具来分析和优化HashMap的使用。
- 备份: 定期备份HashMap中的重要数据,以防止数据丢失或损坏。