1. Java集合框架概述
作为Java开发者,每天打交道最多的除了对象就是集合了。记得刚入行时,我总把ArrayList和LinkedList搞混,直到有次在百万级数据遍历时用了LinkedList,性能直接崩盘才真正明白它们的区别。Java集合框架(Java Collections Framework)就像是我们编程工具箱里的瑞士军刀,List、Set、Map这三样核心容器,每种都有其独特的适用场景和实现原理。
在实际项目中,集合的选择直接影响着代码的性能和可维护性。比如用HashMap存储有序数据导致展示错乱,或者用Vector在并发场景下仍出现线程安全问题,这些都是我踩过的坑。今天我们就来深入剖析这些集合类型的特点、实现原理和使用技巧,让你在面试和实际开发中都能游刃有余。
2. List接口及其实现类
2.1 ArrayList动态数组实现
ArrayList底层采用Object[]数组实现,这个设计决定了它的特性。初始化时如果不指定容量,默认会创建一个空数组,第一次添加元素时才分配10个单位的初始容量。这个懒加载策略在1.8版本引入,减少了不必要的内存占用。
java复制// 典型扩容代码
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5倍扩容
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
elementData = Arrays.copyOf(elementData, newCapacity);
}
扩容是个昂贵的操作,涉及数组拷贝。我有次处理百万级数据时没预分配大小,导致频繁扩容,性能下降了近40%。最佳实践是在已知数据量时通过构造函数预设容量:
java复制List<User> userList = new ArrayList<>(1000000);
注意:ArrayList的size()方法返回的是实际元素个数而非数组长度,trimToSize()可以释放多余空间
2.2 LinkedList双向链表实现
LinkedList的每个节点都是这样的结构:
java复制private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
//...
}
这种结构使得它在头尾操作上表现出色,比如实现队列时:
java复制// 作为队列使用
Queue<String> queue = new LinkedList<>();
queue.offer("first");
queue.poll();
但在随机访问时,性能会急剧下降。我曾做过测试,访问第100万个元素时,LinkedList比ArrayList慢了近5000倍。不过它的迭代器性能很好,因为不需要像ArrayList那样检查并发修改。
2.3 Vector与CopyOnWriteArrayList
虽然Vector是线程安全的,但由于其同步粒度太粗(方法级同步),在竞争激烈时性能很差。更现代的替代方案是CopyOnWriteArrayList,它采用写时复制策略:
java复制// 添加元素时的实现
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
这种实现读操作完全无锁,适合读多写少的场景。但要注意两点:1) 写操作性能较差 2) 迭代器看到的是创建时的快照,可能不是最新数据。
3. Set接口及其实现类
3.1 HashSet与哈希表原理
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;
}
}
哈希冲突解决采用链地址法,Java8之后当链表长度超过8时会转为红黑树。这个转换过程我曾在性能测试中观察到明显的停顿,说明树化是有成本的。
重写hashCode()时有几个原则:
- 同一对象多次调用应返回相同值
- equals相等的对象hashCode必须相等
- 尽量让不同对象返回不同hash值
- 不要用可变字段参与计算
3.2 TreeSet与红黑树
TreeSet基于TreeMap实现,元素必须实现Comparable或提供Comparator。它的contains操作时间复杂度是O(log n),比HashSet的O(1)慢,但能保持元素有序。
java复制// 自定义排序示例
Set<Employee> staff = new TreeSet<>(Comparator
.comparing(Employee::getDepartment)
.thenComparing(Employee::getSalary));
警告:不要在TreeSet/TreeMap中使用可变排序字段,否则会导致结构破坏
3.3 LinkedHashSet的有序性
LinkedHashSet继承自HashSet,但通过维护双向链表保留了插入顺序。这个特性在需要去重又保持顺序的场景非常有用,比如最近访问记录:
java复制Set<String> recentViews = new LinkedHashSet<>(100);
// 新访问时
recentViews.remove(url);
recentViews.add(url);
if(recentViews.size() > 100) {
Iterator<String> it = recentViews.iterator();
it.next();
it.remove();
}
4. Map接口及其实现类
4.1 HashMap深度解析
HashMap的容量总是2的幂次方,这样可以通过位运算快速计算桶位置:
java复制static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 计算桶索引
(n - 1) & hash
负载因子(默认0.75)决定了扩容时机。我曾调整这个参数到0.9以减少内存使用,结果哈希冲突激增导致性能下降30%。建议只在特别清楚后果时才修改。
Java8的优化包括:
- 链表转红黑树阈值=8
- 红黑树退化为链表阈值=6
- 扩容时保持原有顺序
4.2 ConcurrentHashMap并发实现
ConcurrentHashMap在Java8进行了重大改进,采用Node数组+链表/红黑树+CAS+synchronized的实现:
java复制final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//...其他情况处理
}
}
这种设计使得读操作完全无锁,写操作只在特定桶上加锁,大大提升了并发度。
4.3 LinkedHashMap与访问顺序
LinkedHashMap可以通过accessOrder参数开启访问顺序模式,这是实现LRU缓存的基础:
java复制public class LRUCache<K,V> extends LinkedHashMap<K,V> {
private final int maxCapacity;
public LRUCache(int maxCapacity) {
super(maxCapacity, 0.75f, true);
this.maxCapacity = maxCapacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return size() > maxCapacity;
}
}
5. 集合使用的高级技巧
5.1 选择合适的集合类型
选择集合类型的决策树:
- 需要键值对?→ 选择Map
- 需要排序?→ TreeMap
- 需要插入顺序?→ LinkedHashMap
- 需要线程安全?→ ConcurrentHashMap
- 只需要值?
- 允许重复?→ List
- 随机访问多?→ ArrayList
- 频繁增删?→ LinkedList
- 不允许重复?→ Set
- 需要排序?→ TreeSet
- 需要插入顺序?→ LinkedHashSet
- 只需要去重?→ HashSet
- 允许重复?→ List
5.2 集合的线程安全策略
常见的线程安全方案对比:
| 方案 | 示例 | 适用场景 | 性能影响 |
|---|---|---|---|
| 同步包装 | Collections.synchronizedList | 低并发 | 高 |
| 写时复制 | CopyOnWriteArrayList | 读多写少 | 写操作昂贵 |
| 分段锁 | ConcurrentHashMap(Java7) | 中等并发 | 中等 |
| CAS优化 | ConcurrentHashMap(Java8+) | 高并发 | 低 |
| 不可变集合 | Collections.unmodifiableList | 配置数据 | 无 |
5.3 性能优化实战经验
- HashMap初始化优化:
java复制// 已知有1000个元素,负载因子0.75
int capacity = (int) Math.ceil(1000 / 0.75);
Map<String, User> map = new HashMap<>(capacity);
- 避免在循环中调用size():
java复制// 反例 - 每次循环都检查size()
for(int i=0; i<list.size(); i++) {...}
// 正例 - 缓存size值
int size = list.size();
for(int i=0; i<size; i++) {...}
- 使用Arrays.asList注意事项:
java复制List<String> list = Arrays.asList("a", "b", "c");
list.add("d"); // 抛出UnsupportedOperationException
因为返回的是固定大小的列表,如果需要可变列表应该:
java复制new ArrayList<>(Arrays.asList("a", "b", "c"));
6. 常见面试问题解析
6.1 HashMap与HashTable的区别
关键区别点:
- 线程安全:HashTable全部方法同步,HashMap不同步
- null值:HashTable不允许null键值,HashMap允许
- 迭代器:HashTable使用Enumeration,HashMap使用Iterator
- 继承关系:HashTable继承Dictionary,HashMap继承AbstractMap
- 性能:HashTable由于同步开销,性能通常较差
6.2 ConcurrentHashMap分段锁实现
Java7的实现采用Segment分段锁:
java复制final Segment<K,V>[] segments;
static final class Segment<K,V> extends ReentrantLock {
transient volatile HashEntry<K,V>[] table;
}
每个Segment相当于一个小的HashMap,锁定一个Segment不影响其他Segment的访问。默认并发级别是16,意味着最多支持16个线程并发写。
6.3 fail-fast与fail-safe机制
fail-fast示例:
java复制List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
Iterator<String> it = list.iterator();
list.add("d"); // 修改结构
it.next(); // 抛出ConcurrentModificationException
原理是通过modCount计数器检测并发修改。而CopyOnWriteArrayList这类fail-safe集合会在迭代时使用原数组的快照,不会抛出异常。
7. Java8对集合的增强
7.1 Stream API与集合操作
java复制// 统计单词频率
Map<String, Long> wordCount = files.stream()
.flatMap(file -> Arrays.stream(file.getWords()))
.collect(Collectors.groupingBy(
Function.identity(),
Collectors.counting()
));
Stream操作分为中间操作(filter, map等)和终止操作(collect, forEach等),只有终止操作才会触发实际计算。
7.2 Lambda表达式简化集合处理
java复制// 传统方式
Collections.sort(list, new Comparator<User>() {
public int compare(User u1, User u2) {
return u1.getName().compareTo(u2.getName());
}
});
// Lambda方式
Collections.sort(list, (u1, u2) -> u1.getName().compareTo(u2.getName()));
7.3 computeIfAbsent等新方法
java复制Map<String, List<Order>> customerOrders = new HashMap<>();
// 传统方式
if(!customerOrders.containsKey(customerId)) {
customerOrders.put(customerId, new ArrayList<>());
}
customerOrders.get(customerId).add(order);
// 新方式
customerOrders.computeIfAbsent(customerId, k -> new ArrayList<>())
.add(order);
这些方法让代码更简洁且线程安全(对于ConcurrentHashMap)。
8. 实际应用案例分析
8.1 电商购物车实现
典型的购物车需要:
- 快速根据商品ID查找商品信息 → HashMap
- 保持商品添加顺序 → LinkedHashMap
- 线程安全 → ConcurrentHashMap
java复制public class ShoppingCart {
private final Map<String, CartItem> items = new ConcurrentLinkedHashMap<>();
public void addItem(Product product, int quantity) {
items.compute(product.getId(), (id, existing) -> {
return existing == null ?
new CartItem(product, quantity) :
existing.addQuantity(quantity);
});
}
public List<CartItem> getItemsInOrder() {
return new ArrayList<>(items.values());
}
}
8.2 最近联系人列表
需要:
- 去重 → Set特性
- 保持访问顺序 → LinkedHashSet
- 固定大小 → 移除最旧元素
java复制public class RecentContacts {
private static final int MAX_SIZE = 20;
private final LinkedHashSet<Contact> contacts = new LinkedHashSet<>();
public void addContact(Contact contact) {
if(contacts.contains(contact)) {
contacts.remove(contact);
} else if(contacts.size() >= MAX_SIZE) {
Iterator<Contact> it = contacts.iterator();
it.next();
it.remove();
}
contacts.add(contact);
}
}
8.3 多级缓存实现
java复制public class MultiLevelCache<K,V> {
private final ConcurrentHashMap<K,V> L1 = new ConcurrentHashMap<>();
private final ConcurrentSkipListMap<K,V> L2 = new ConcurrentSkipListMap<>();
private final LinkedHashMap<K,V> L3 = new LinkedHashMap<>(100, 0.75f, true) {
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return size() > 100;
}
};
public V get(K key) {
V value = L1.get(key);
if(value == null) {
value = L2.get(key);
if(value != null) {
L1.put(key, value);
} else {
value = L3.get(key);
if(value != null) {
L2.put(key, value);
L1.put(key, value);
}
}
}
return value;
}
}
这个设计利用了不同集合的特性:L1作为最快速缓存,L2提供排序能力,L3作为LRU后备存储。