1. Java集合框架中的Map与Set概述
在Java开发中,数据结构的选择直接影响着程序的性能和可维护性。Map和Set作为Java集合框架(Java Collections Framework)中的两大核心接口,虽然经常被开发者使用,但很多人对其底层实现和适用场景的理解仍停留在表面。实际上,这两者的关系比想象中更为紧密——从JDK源码中可以看到,HashSet内部就是通过HashMap来实现的,而TreeSet则是基于TreeMap构建。
Map是一种键值对(Key-Value)映射结构,它不允许键重复,每个键最多映射到一个值。而Set则是不允许元素重复的集合,它本质上可以看作只包含键的Map。这种设计哲学体现了Java集合框架的巧妙之处:通过组合和复用减少代码冗余。在日常开发中,HashMap和HashSet因其出色的平均时间复杂度(O(1))成为最常用的实现类,但在线程安全、排序需求等特定场景下,我们还需要考虑ConcurrentHashMap、TreeMap、LinkedHashMap等变体。
理解Map和Set的底层实现机制,不仅能帮助我们在面试中应对各种"八股文"问题,更重要的是能在实际开发中做出合理的选择。比如,当我们需要保持插入顺序时,LinkedHashSet比HashSet更合适;当需要自然排序时,TreeSet会自动对元素进行排序,但代价是更高的时间复杂度(O(log n))。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Map接口的核心实现类与底层机制
2.1 HashMap的工作原理与优化策略
HashMap作为最常用的Map实现,其核心是一个数组+链表+红黑树的结构。在JDK8之前,HashMap采用纯数组+链表的方式实现,当哈希冲突严重时,查询性能会退化到O(n)。JDK8对此进行了优化,当链表长度超过阈值(默认为8)时,会将链表转换为红黑树,将最坏情况下的时间复杂度改善为O(log n)。
HashMap的初始化容量和负载因子是两个关键参数。默认初始容量为16,负载因子为0.75。当元素数量达到容量与负载因子的乘积时,HashMap会进行扩容操作,这是一个相对耗时的过程。因此,如果我们能预估元素数量,最好通过构造函数指定初始容量:
java复制// 预估有100个元素,计算初始容量避免频繁扩容
int initialCapacity = (int) Math.ceil(100 / 0.75);
Map<String, String> map = new HashMap<>(initialCapacity);
注意:HashMap是非线程安全的。在多线程环境下,推荐使用ConcurrentHashMap或Collections.synchronizedMap()包装器。
2.2 TreeMap的有序特性与比较器机制
TreeMap基于红黑树实现,能够按照键的自然顺序或自定义比较器顺序维护键值对。与HashMap相比,TreeMap提供了更多与顺序相关的方法,如firstKey()、lastKey()、headMap()、tailMap()等。
java复制TreeMap<Integer, String> treeMap = new TreeMap<>();
treeMap.put(3, "Three");
treeMap.put(1, "One");
treeMap.put(2, "Two");
// 输出 {1=One, 2=Two, 3=Three}
System.out.println(treeMap);
// 获取第一个键值对
Map.Entry<Integer, String> first = treeMap.firstEntry();
当使用自定义对象作为键时,必须确保对象实现了Comparable接口或在构造函数中提供Comparator。否则会抛出ClassCastException。
2.3 LinkedHashMap的访问顺序模式
LinkedHashMap继承自HashMap,通过维护一个双向链表来保持插入顺序或访问顺序。这在实现LRU(最近最少使用)缓存时特别有用:
java复制// 创建一个基于访问顺序的LinkedHashMap,容量为3,负载因子0.75,当size超过3时自动删除最久未访问的条目
Map<String, String> lruCache = new LinkedHashMap<String, String>(3, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
return size() > 3;
}
};
lruCache.put("1", "One");
lruCache.put("2", "Two");
lruCache.put("3", "Three");
// 访问"1"会使它成为最近使用的条目
lruCache.get("1");
// 添加新条目会导致"2"被移除
lruCache.put("4", "Four");
// 输出 {3=Three, 1=One, 4=Four}
System.out.println(lruCache);
2.4 ConcurrentHashMap的并发优化
ConcurrentHashMap是线程安全的HashMap实现,在JDK8中进行了重大改进,放弃了分段锁的设计,改为使用CAS+synchronized实现更细粒度的锁控制。其并发性能比Hashtable和Collections.synchronizedMap()包装的Map要好得多。
java复制ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
// 线程安全的putIfAbsent操作
concurrentMap.putIfAbsent("key", 1);
// 原子性更新
concurrentMap.compute("key", (k, v) -> v == null ? 1 : v + 1);
ConcurrentHashMap的size()方法在JDK8中是一个估计值,因为精确计算在并发环境下代价太高。对于需要精确计数的场景,可以考虑使用AtomicLong配合ConcurrentHashMap实现。
3. Set接口的实现与内部机制
3.1 HashSet的快速查找特性
HashSet是最常用的Set实现,它内部使用HashMap来存储元素,所有元素都作为HashMap的key存储,而value则统一使用一个静态的Object对象填充。这种实现方式使得HashSet具有与HashMap相似的性能特征。
java复制// HashSet的构造方法实际上创建了一个HashMap
public HashSet() {
map = new HashMap<>();
}
// 添加元素实际上是向map中添加key
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
HashSet的contains()操作时间复杂度为O(1),非常适合用于快速查找是否存在某个元素的场景。但要注意,HashSet不保证迭代顺序,如果需要保持插入顺序,应该使用LinkedHashSet。
3.2 TreeSet的排序功能
TreeSet基于TreeMap实现,能够按照元素的自然顺序或自定义比较器顺序维护元素。与HashSet相比,TreeSet提供了更多与顺序相关的方法,如first()、last()、headSet()、tailSet()等。
java复制TreeSet<String> treeSet = new TreeSet<>();
treeSet.add("Banana");
treeSet.add("Apple");
treeSet.add("Orange");
// 输出 [Apple, Banana, Orange]
System.out.println(treeSet);
// 获取第一个和最后一个元素
String first = treeSet.first(); // "Apple"
String last = treeSet.last(); // "Orange"
TreeSet的contains()操作时间复杂度为O(log n),比HashSet慢,但在需要有序遍历或范围查询时更有优势。
3.3 LinkedHashSet的插入顺序保持
LinkedHashSet继承自HashSet,通过维护一个双向链表来保持元素的插入顺序。它的性能略低于HashSet,因为需要维护链表结构,但在需要保持插入顺序的场景下非常有用。
java复制LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add("First");
linkedHashSet.add("Second");
linkedHashSet.add("Third");
// 输出顺序与插入顺序一致 [First, Second, Third]
System.out.println(linkedHashSet);
LinkedHashSet的迭代性能比HashSet好,因为它只需要遍历链表而不是遍历整个哈希表。在需要频繁迭代且希望保持插入顺序的场景下,LinkedHashSet是最佳选择。
4. 高级应用与性能优化
4.1 选择合适的Map/Set实现
在实际开发中,选择哪种Map或Set实现取决于具体需求:
- 快速查找、不关心顺序:HashMap/HashSet
- 需要自然排序或自定义排序:TreeMap/TreeSet
- 保持插入顺序或访问顺序:LinkedHashMap/LinkedHashSet
- 线程安全需求:ConcurrentHashMap/Collections.synchronizedSet()
- 内存敏感场景:可以考虑第三方实现如Trove的THashMap
4.2 对象作为键的注意事项
当自定义对象作为Map的键或Set的元素时,必须正确重写hashCode()和equals()方法。根据约定:
- 如果两个对象equals()返回true,它们的hashCode()必须相同
- hashCode()应该尽可能均匀分布,减少哈希冲突
- equals()和hashCode()应该基于相同的字段计算
java复制class Person {
private String name;
private int age;
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Person)) return false;
Person other = (Person) obj;
return age == other.age && Objects.equals(name, other.name);
}
}
4.3 内存优化技巧
对于存储大量数据的Map/Set,内存占用可能成为问题。以下是一些优化技巧:
- 合理设置初始容量:避免频繁扩容,但也不宜过大浪费内存
- 使用原始类型专用集合:如Trove、Eclipse Collections等第三方库提供的专门处理原始类型的集合
- 考虑弱引用或软引用:WeakHashMap使用弱引用作为键,适合做缓存
- 及时清理不再使用的集合:特别是静态集合容易造成内存泄漏
4.4 并发场景下的最佳实践
在多线程环境下使用Map/Set时:
- 读多写少:使用ConcurrentHashMap或CopyOnWriteArraySet
- 写操作频繁:考虑使用锁或并发度更高的数据结构
- 避免在迭代时修改集合:这会抛出ConcurrentModificationException
- 使用原子性复合操作:如ConcurrentHashMap的computeIfAbsent()方法
java复制ConcurrentHashMap<String, AtomicInteger> counterMap = new ConcurrentHashMap<>();
// 线程安全的计数
public void increment(String key) {
counterMap.computeIfAbsent(key, k -> new AtomicInteger()).incrementAndGet();
}
4.5 Java 8+的新特性应用
Java 8为Map接口添加了许多有用的方法:
java复制Map<String, Integer> map = new HashMap<>();
map.put("A", 1);
map.put("B", 2);
// 如果键不存在则计算
map.computeIfAbsent("C", k -> 3);
// 合并值
map.merge("A", 1, Integer::sum);
// 遍历
map.forEach((k, v) -> System.out.println(k + ": " + v));
// 获取或默认值
int value = map.getOrDefault("D", 0);
对于Set,Java 9引入了方便的工厂方法:
java复制Set<String> immutableSet = Set.of("A", "B", "C");
5. 常见问题排查与性能调优
5.1 内存泄漏问题
Map/Set使用不当可能导致内存泄漏,特别是在使用静态集合或缓存时。一个典型场景是将对象作为Map的键后,修改了参与hashCode计算的字段:
java复制Map<Person, String> map = new HashMap<>();
Person p = new Person("Alice", 25);
map.put(p, "Data");
// 修改键对象的字段
p.setAge(26);
// 现在无法通过原对象获取值
String data = map.get(p); // 返回null
// 但对象仍在Map中
map.containsKey(p); // 可能返回false或true,取决于hashCode变化
解决方案是:要么使用不可变对象作为键,要么确保键对象被修改后从Map中移除。
5.2 哈希冲突性能退化
当HashMap中哈希冲突严重时,性能会显著下降。可以通过以下方式诊断和解决:
- 监控HashMap的链表长度和树化情况
- 检查键对象的hashCode()实现是否产生均匀分布
- 考虑使用更好的哈希函数或调整初始容量和负载因子
java复制// 检查HashMap的内部结构(反射实现,仅用于调试)
public static void analyzeHashMap(HashMap<?, ?> map) throws Exception {
Field tableField = HashMap.class.getDeclaredField("table");
tableField.setAccessible(true);
Object[] table = (Object[]) tableField.get(map);
int total = 0, buckets = 0, maxChain = 0;
for (Object entry : table) {
if (entry != null) {
buckets++;
int chainLength = 0;
Object current = entry;
while (current != null) {
chainLength++;
Field nextField = current.getClass().getDeclaredField("next");
nextField.setAccessible(true);
current = nextField.get(current);
}
maxChain = Math.max(maxChain, chainLength);
total += chainLength;
}
}
System.out.printf("Buckets: %d, Elements: %d, Load factor: %.2f, Max chain: %d%n",
buckets, map.size(), (float)map.size()/buckets, maxChain);
}
5.3 并发修改异常
在使用迭代器遍历集合时修改集合会抛出ConcurrentModificationException。解决方案包括:
- 使用迭代器的remove()方法而不是集合的remove()
- 先收集要修改的元素,遍历完成后再统一修改
- 使用并发集合如ConcurrentHashMap
- Java 8+可以使用removeIf()方法
java复制List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
// 错误方式 - 抛出ConcurrentModificationException
for (String s : list) {
if (s.equals("B")) {
list.remove(s);
}
}
// 正确方式1 - 使用迭代器
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().equals("B")) {
it.remove();
}
}
// 正确方式2 - Java 8+ removeIf
list.removeIf(s -> s.equals("B"));
5.4 性能调优实战
当Map/Set成为性能瓶颈时,可以考虑以下优化策略:
- 选择合适的哈希函数:确保键的hashCode()分布均匀
- 调整初始容量:减少扩容操作,特别是对于已知大小的集合
- 考虑加载因子:更高的加载因子减少内存使用但增加冲突
- 使用专用集合:如针对原始类型的Trove集合
- 并行处理:对于大型集合,使用并行流或分片处理
java复制// 并行处理大型Map示例
ConcurrentHashMap<String, Integer> wordCount = new ConcurrentHashMap<>();
List<String> documents = // 大量文本数据
documents.parallelStream()
.flatMap(doc -> Arrays.stream(doc.split("\\s+")))
.forEach(word -> wordCount.merge(word, 1, Integer::sum));
6. 实际应用案例分析
6.1 使用Map实现缓存系统
基于LinkedHashMap实现简单的LRU缓存:
java复制public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int maxSize;
public LRUCache(int maxSize) {
super(maxSize, 0.75f, true);
this.maxSize = maxSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > maxSize;
}
public static void main(String[] args) {
LRUCache<String, String> cache = new LRUCache<>(3);
cache.put("1", "One");
cache.put("2", "Two");
cache.put("3", "Three");
// 访问"1"使其成为最近使用的
cache.get("1");
// 添加新元素会淘汰"2"
cache.put("4", "Four");
// 输出: {3=Three, 1=One, 4=Four}
System.out.println(cache);
}
}
6.2 使用Set实现数据去重
处理大型数据集时,HashSet可用于高效去重:
java复制public List<String> removeDuplicates(List<String> data) {
if (data == null || data.isEmpty()) {
return Collections.emptyList();
}
// 使用LinkedHashSet保持原始顺序
Set<String> uniqueSet = new LinkedHashSet<>(data);
return new ArrayList<>(uniqueSet);
}
对于原始类型数据,可以考虑使用Trove的THashSet减少内存开销:
java复制TIntHashSet intSet = new TIntHashSet();
intSet.add(1);
intSet.add(2);
intSet.add(1); // 重复元素不会被添加
6.3 多级索引查询实现
复杂查询可以使用多级Map构建索引:
java复制class Employee {
String department;
String name;
int salary;
// 构造方法、getters等
}
public class EmployeeIndex {
// 部门 -> 姓名 -> 员工对象
private Map<String, Map<String, Employee>> deptNameIndex = new HashMap<>();
// 部门 -> 工资范围 -> 员工列表
private Map<String, TreeMap<Integer, List<Employee>>> deptSalaryIndex = new HashMap<>();
public void addEmployee(Employee emp) {
// 更新部门-姓名索引
deptNameIndex.computeIfAbsent(emp.getDepartment(), k -> new HashMap<>())
.put(emp.getName(), emp);
// 更新部门-工资索引
int salaryRange = emp.getSalary() / 1000 * 1000; // 按千分组
deptSalaryIndex.computeIfAbsent(emp.getDepartment(), k -> new TreeMap<>())
.computeIfAbsent(salaryRange, k -> new ArrayList<>())
.add(emp);
}
public Employee getEmployee(String dept, String name) {
return deptNameIndex.getOrDefault(dept, Collections.emptyMap()).get(name);
}
public List<Employee> getEmployeesInSalaryRange(String dept, int min, int max) {
TreeMap<Integer, List<Employee>> salaryMap = deptSalaryIndex.get(dept);
if (salaryMap == null) return Collections.emptyList();
List<Employee> result = new ArrayList<>();
for (List<Employee> employees : salaryMap.subMap(min, true, max, true).values()) {
result.addAll(employees);
}
return result;
}
}
6.4 使用TreeMap实现范围查询
TreeMap的subMap()方法非常适合实现范围查询:
java复制public class ProductPriceTracker {
private TreeMap<Double, List<Product>> priceMap = new TreeMap<>();
public void addProduct(Product product) {
priceMap.computeIfAbsent(product.getPrice(), k -> new ArrayList<>())
.add(product);
}
public List<Product> getProductsInPriceRange(double min, double max) {
List<Product> result = new ArrayList<>();
for (List<Product> products : priceMap.subMap(min, true, max, true).values()) {
result.addAll(products);
}
return result;
}
public List<Product> getCheapestProducts(int count) {
List<Product> result = new ArrayList<>();
for (List<Product> products : priceMap.values()) {
for (Product p : products) {
if (result.size() >= count) return result;
result.add(p);
}
}
return result;
}
}
7. Java集合框架的演进与新特性
7.1 Java 8的Stream API与集合交互
Java 8引入的Stream API为集合操作提供了更强大的功能:
java复制Map<String, List<Employee>> employeesByDept = // 初始化数据
// 使用流计算每个部门的平均工资
Map<String, Double> avgSalaryByDept = employeesByDept.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> e.getValue().stream()
.mapToInt(Employee::getSalary)
.average()
.orElse(0.0)
));
// 使用流过滤并转换Map
Map<String, String> nameTitleMap = employeesByDept.values().stream()
.flatMap(List::stream)
.filter(e -> e.getSalary() > 100000)
.collect(Collectors.toMap(
Employee::getName,
Employee::getTitle,
(existing, replacement) -> existing
));
7.2 Java 9的集合工厂方法
Java 9引入了方便的集合工厂方法,可以简洁地创建小型不可变集合:
java复制Set<String> immutableSet = Set.of("A", "B", "C");
Map<String, Integer> immutableMap = Map.of(
"One", 1,
"Two", 2,
"Three", 3
);
// 超过10个元素使用Map.ofEntries
Map<String, Integer> largerMap = Map.ofEntries(
entry("One", 1),
entry("Two", 2),
// ...更多条目
entry("Twenty", 20)
);
7.3 Java 10的不可变集合拷贝方法
Java 10为集合接口添加了copyOf()方法,可以方便地创建不可变集合拷贝:
java复制List<String> original = new ArrayList<>();
original.add("A");
original.add("B");
List<String> copy = List.copyOf(original);
// copy是不可变的,任何修改尝试都会抛出UnsupportedOperationException
7.4 Java 17的新特性对集合的影响
Java 17作为长期支持版本,进一步增强了集合API:
- 密封接口:集合接口可以限制实现类,增强类型安全
- 模式匹配:简化集合元素的条件检查和类型转换
- 增强的Stream API:新增如mapMulti()等中间操作
java复制// 使用模式匹配简化集合处理
if (collection instanceof Set<String> set) {
// 直接使用set变量
if (set.contains("target")) {
// ...
}
}
// 使用mapMulti替代flatMap某些场景
List<String> result = strings.stream()
.mapMulti((String str, Consumer<String> consumer) -> {
if (str.length() > 5) {
consumer.accept(str.substring(0, 5));
consumer.accept(str.substring(5));
}
})
.toList();
8. 性能对比与基准测试
8.1 不同Map实现的性能特征
通过JMH基准测试比较不同Map实现的性能:
java复制@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class MapPerformanceBenchmark {
@State(Scope.Thread)
public static class MyState {
Map<Integer, String> hashMap = new HashMap<>();
Map<Integer, String> treeMap = new TreeMap<>();
Map<Integer, String> linkedHashMap = new LinkedHashMap<>();
Map<Integer, String> concurrentHashMap = new ConcurrentHashMap<>();
@Setup(Level.Trial)
public void setup() {
IntStream.range(0, 10000).forEach(i -> {
hashMap.put(i, "Value" + i);
treeMap.put(i, "Value" + i);
linkedHashMap.put(i, "Value" + i);
concurrentHashMap.put(i, "Value" + i);
});
}
}
@Benchmark
public String hashMapGet(MyState state) {
return state.hashMap.get(ThreadLocalRandom.current().nextInt(10000));
}
@Benchmark
public String treeMapGet(MyState state) {
return state.treeMap.get(ThreadLocalRandom.current().nextInt(10000));
}
// 其他基准测试方法...
}
典型测试结果可能显示:
- HashMap的get()操作最快,接近O(1)
- TreeMap的get()较慢,为O(log n)
- LinkedHashMap略慢于HashMap,因为需要维护链表
- ConcurrentHashMap在并发环境下性能最好
8.2 不同Set实现的性能对比
类似地,比较Set实现的性能:
java复制@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class SetPerformanceBenchmark {
@State(Scope.Thread)
public static class MyState {
Set<Integer> hashSet = new HashSet<>();
Set<Integer> treeSet = new TreeSet<>();
Set<Integer> linkedHashSet = new LinkedHashSet<>();
@Setup(Level.Trial)
public void setup() {
IntStream.range(0, 10000).forEach(i -> {
hashSet.add(i);
treeSet.add(i);
linkedHashSet.add(i);
});
}
}
@Benchmark
public boolean hashSetContains(MyState state) {
return state.hashSet.contains(ThreadLocalRandom.current().nextInt(10000));
}
// 其他基准测试方法...
}
测试结果通常显示:
- HashSet的contains()最快
- TreeSet较慢但有排序功能
- LinkedHashSet介于两者之间,保持插入顺序
8.3 实际应用中的选择建议
基于性能测试和实际经验,给出以下建议:
-
单线程环境:
- 优先选择HashMap/HashSet,除非需要特殊排序或顺序保持
- 对于小型集合(小于100元素),差异不明显,可根据代码清晰度选择
-
多线程环境:
- 首选ConcurrentHashMap/ConcurrentSkipListSet
- 读多写少场景可考虑CopyOnWriteArraySet
- 避免使用Collections.synchronizedXXX包装器,除非有特殊需求
-
内存敏感场景:
- 考虑第三方库如Trove、Eclipse Collections
- 对于原始类型数据,避免使用包装类集合
-
持久化场景:
- 考虑使用LinkedHashMap/LinkedHashSet保持顺序
- 或使用TreeMap/TreeSet确保可预测的排序
9. 面试常见问题解析
9.1 HashMap的工作原理详解
HashMap是Java面试中最常被问到的数据结构之一。深入理解其工作原理需要掌握以下要点:
-
哈希函数:HashMap使用键的hashCode()计算哈希值,然后通过扰动函数减少碰撞
java复制// JDK 8中的哈希扰动函数 static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } -
数组+链表+红黑树结构:HashMap内部维护一个Node数组(table),每个Node可以是一个链表节点或红黑树节点
-
扩容机制:当元素数量超过容量×负载因子时,HashMap会扩容为原来的2倍,并重新哈希所有元素
-
树化条件:当链表长度超过TREEIFY_THRESHOLD(8)且数组长度超过MIN_TREEIFY_CAPACITY(64)时,链表会转换为红黑树
-
退化条件:当红黑树节点数小于UNTREEIFY_THRESHOLD(6)时,会退化为链表
9.2 ConcurrentHashMap的并发策略演进
ConcurrentHashMap的线程安全实现经历了重大变化:
-
JDK7及之前:采用分段锁(Segment)机制,将数据分成多个段,每段独立加锁
-
JDK8及之后:
- 放弃分段锁,改用CAS+synchronized实现更细粒度的锁控制
- 锁的粒度从段级别降低到单个链表或树节点
- 引入ForwardingNode处理扩容时的并发访问
- 使用TreeBin管理红黑树,保证树操作的线程安全
java复制// JDK8中的putVal方法关键部分
final V putVal(K key, V value, boolean onlyIfAbsent) {
// ...
synchronized (f) {
if (tabAt(tab, i) == f) {
// 链表或树节点的插入操作
}
}
// ...
}
9.3 HashSet与HashMap的关系问题
这是一个常见的陷阱问题:HashSet实际上是通过HashMap实现的:
java复制// HashSet的部分源码
private transient HashMap<E, Object> map;
// 虚拟值用于填充HashMap的value
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
这种设计有几个好处:
- 代码复用,减少维护成本
- 自动获得HashMap的所有优化
- 实现简单,性能有保障
9.4 对象作为键的最佳实践
当自定义对象作为Map键或Set元素时,必须注意:
-
不可变性:理想情况下键对象应该是不可变的,或者至少不修改参与equals/hashCode计算的字段
-
equals和hashCode契约:
- 一致性:对象相等时hashCode必须相同
- 稳定性:hashCode在对象生命周期内不应改变
- 分散性:不同对象应尽量有不同的hashCode
-
实现建议:
- 使用IDE自动生成equals和hashCode方法
- 优先使用final字段参与计算
- 考虑使用java.util.Objects的hash()和equals()方法
java复制class GoodKey {
private final String id;
private final int version;
public GoodKey(String id, int version) {
this.id = id;
this.version = version;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof GoodKey)) return false;
GoodKey goodKey = (GoodKey) o;
return version == goodKey.version && id.equals(goodKey.id);
}
@Override
public int hashCode() {
return Objects.hash(id, version);
}
}
9.5 Java集合框架的设计模式
Java集合框架中运用了多种设计模式:
- 迭代器模式:通过Iterator接口统一集合的遍历方式
- 工厂方法模式:Collections类中的各种工厂方法
- 适配器模式:Arrays.asList()将数组适配为List
- 装饰器模式:Collections.synchronizedXXX()等方法返回装饰后的集合
- 策略模式:Comparator作为排序策略参数
理解这些设计模式有助于更好地使用和扩展集合框架。例如,自定义集合实现时,可以考虑实现Iterable接口支持for-each循环,或提供特定的Comparator实现不同的排序策略。
