1. ArrayList、HashSet、HashMap 核心知识点与实战指南
作为Java开发者,ArrayList、HashSet和HashMap这三个集合类是我们日常开发中最常用的工具。它们看似简单,但实际应用中却藏着不少坑。今天我就结合自己多年的开发经验,带大家深入理解这三个核心容器的特性和使用技巧。
1.1 ArrayList:动态数组的智慧
1.1.1 底层实现与扩容机制
ArrayList的底层是一个Object[]数组,这个设计让它拥有了数组的快速随机访问特性。但不同于普通数组的是,ArrayList具备动态扩容能力。默认初始容量是10,当元素数量超过当前容量时,会自动扩容为原来的1.5倍(JDK8+)。
扩容是个昂贵的操作,它需要创建新数组并复制所有元素。所以在已知元素数量的情况下,建议使用带初始容量的构造函数:
java复制// 预计存放1000个元素 List<Integer> list = new ArrayList<>(1000);
1.1.2 性能特点与使用场景
- 查询快:get(int index)操作是O(1)时间复杂度,因为它能直接通过索引定位元素
- 增删慢:
- 尾部add是O(1)
- 中间插入/删除是O(n),因为需要移动后续元素
- 适合场景:
- 需要频繁按索引访问
- 元素允许重复
- 需要保持插入顺序
1.1.3 实战技巧与避坑指南
遍历删除的正确姿势:
java复制// 错误方式 - 会抛出ConcurrentModificationException
for (String item : list) {
if (condition) {
list.remove(item);
}
}
// 正确方式1 - 使用迭代器
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (condition) {
it.remove(); // 关键点:使用迭代器的remove方法
}
}
// 正确方式2 - 倒序遍历
for (int i = list.size()-1; i >= 0; i--) {
if (condition) {
list.remove(i);
}
}
内存优化技巧:
java复制// 如果确定不再添加元素,可以调用trimToSize()释放多余空间
list.trimToSize();
1.2 HashSet:高效去重的秘密
1.2.1 底层实现原理
HashSet的魔法在于它实际上是HashMap的马甲。每个元素作为HashMap的Key,而Value则是一个固定的Object常量(PRESENT)。这种设计让HashSet继承了HashMap的所有优点:
java复制// HashSet的简化版实现
public class HashSet<E> {
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
}
1.2.2 哈希冲突处理
当不同对象产生相同hashCode时,HashSet使用链表+红黑树(JDK8+)解决冲突。当链表长度超过8时转换为红黑树,提高查询效率。
自定义对象去重要点:
java复制class Student {
String id;
String name;
// 必须同时重写hashCode和equals
@Override
public int hashCode() {
return Objects.hash(id, name);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Student)) return false;
Student s = (Student) o;
return id.equals(s.id) && name.equals(s.name);
}
}
1.2.3 实战应用场景
- 快速去重:从10万条数据中去除重复项
- 集合运算:求两个集合的交集、并集等
java复制Set<String> set1 = new HashSet<>(Arrays.asList("A","B","C"));
Set<String> set2 = new HashSet<>(Arrays.asList("B","C","D"));
// 并集
set1.addAll(set2);
// 交集
set1.retainAll(set2);
// 差集
set1.removeAll(set2);
1.3 HashMap:键值对的艺术
1.3.1 核心实现原理
HashMap采用数组+链表+红黑树的结构。通过hash函数将Key映射到数组索引,处理冲突时使用链表,当链表过长时转为红黑树。
重要参数:
- 默认初始容量:16
- 负载因子:0.75(当size > capacity*0.75时触发扩容)
- 树化阈值:链表长度>=8
- 解树阈值:树节点数<=6
1.3.2 线程安全问题全解析
HashMap不是线程安全的,多线程环境下可能出现:
- 扩容时形成循环链表导致CPU 100%
- put操作导致元素丢失
- 并发修改抛出ConcurrentModificationException
解决方案:
java复制// 方法1:使用ConcurrentHashMap(推荐)
Map<String, String> safeMap = new ConcurrentHashMap<>();
// 方法2:使用Collections.synchronizedMap
Map<String, String> safeMap = Collections.synchronizedMap(new HashMap<>());
// 方法3:手动同步(性能较差)
synchronized(map) {
map.put(key, value);
}
1.3.3 高级用法与性能优化
初始化容量计算:
java复制// 预期存储120个元素,计算最佳初始容量
int expectedSize = 120;
float loadFactor = 0.75f;
int initialCapacity = (int) (expectedSize / loadFactor) + 1;
// initialCapacity = 161,但HashMap会取最近的2的幂次方,即256
Map<String, String> map = new HashMap<>(initialCapacity);
三种遍历方式性能对比:
java复制Map<String, Integer> map = new HashMap<>();
// 填充测试数据...
// 1. entrySet遍历(最快)
for (Map.Entry<String, Integer> entry : map.entrySet()) {
entry.getKey();
entry.getValue();
}
// 2. keySet遍历(较慢)
for (String key : map.keySet()) {
map.get(key);
}
// 3. forEach+lambda(JDK8+)
map.forEach((k, v) -> {
// 处理k,v
});
1.4 综合对比与选型指南
1.4.1 核心特性对比
| 特性 | ArrayList | HashSet | HashMap |
|---|---|---|---|
| 底层结构 | 动态数组 | 基于HashMap | 数组+链表+红黑树 |
| 元素特性 | 有序可重复 | 无序不重复 | 键不重复值可重复 |
| get操作时间复杂度 | O(1) | 无get方法 | O(1) |
| contains操作 | O(n) | O(1) | O(1)(仅key) |
| 线程安全 | 不安全 | 不安全 | 不安全 |
| 允许null | 是 | 是(仅1个) | 是(key/value) |
1.4.2 选型决策树
-
是否需要保持元素顺序?
- 是 → ArrayList
- 否 → 是否需要键值对?
- 是 → HashMap
- 否 → HashSet
-
是否需要允许重复元素?
- 是 → ArrayList
- 否 → HashSet
-
是否需要通过key快速访问value?
- 是 → HashMap
1.4.3 线程安全替代方案
- ArrayList → CopyOnWriteArrayList
- HashSet → CopyOnWriteArraySet
- HashMap → ConcurrentHashMap
1.5 真实案例解析
案例1:百万数据去重
java复制// 错误做法:使用ArrayList.contains()去重 - O(n²)时间复杂度
List<String> data = getHugeDataList(); // 100万条
List<String> result = new ArrayList<>();
for (String item : data) {
if (!result.contains(item)) { // 每次都要遍历整个列表
result.add(item);
}
}
// 正确做法:使用HashSet - O(n)时间复杂度
Set<String> tempSet = new HashSet<>(data);
List<String> result = new ArrayList<>(tempSet);
案例2:统计单词频率
java复制String text = "hello world hello java world java";
String[] words = text.split(" ");
// 使用HashMap统计
Map<String, Integer> freqMap = new HashMap<>();
for (String word : words) {
freqMap.put(word, freqMap.getOrDefault(word, 0) + 1);
}
// 输出结果:{java=2, world=2, hello=2}
案例3:缓存实现
java复制class SimpleCache<K, V> {
private final Map<K, V> cache;
private final int maxSize;
public SimpleCache(int maxSize) {
this.maxSize = maxSize;
this.cache = new LinkedHashMap<K, V>(maxSize, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > maxSize;
}
};
}
public synchronized V get(K key) {
return cache.get(key);
}
public synchronized void put(K key, V value) {
cache.put(key, value);
}
}
1.6 性能优化终极技巧
- 预分配容量:对于已知大小的集合,初始化时指定容量避免扩容
- 选择合适的集合类型:
- 频繁按索引访问 → ArrayList
- 需要去重 → HashSet
- 键值映射 → HashMap
- 遍历方式选择:
- ArrayList优先用普通for循环
- HashMap优先用entrySet
- 线程安全选择:
- 读多写少 → CopyOnWriteArrayList/Set
- 高并发写入 → ConcurrentHashMap
- 对象设计:
- 作为HashMap键的对象要实现规范的hashCode和equals
- 避免在hashCode中使用可变字段
记住,没有最好的集合,只有最适合场景的选择。理解它们的底层原理,才能写出更高效的代码。