引言
Java集合框架是Java编程中最常用的工具类库之一,也是面试中的高频考点。本文将系统地梳理Java集合框架的核心知识点和常见面试题,帮助你深入理解集合框架的底层原理。
一、集合框架概述
1.1 集合框架的层次结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| Collection ├── List (有序,可重复) │ ├── ArrayList │ ├── LinkedList │ └── Vector ├── Set (无序,不可重复) │ ├── HashSet │ ├── LinkedHashSet │ └── TreeSet └── Queue (队列) ├── LinkedList └── PriorityQueue
Map (键值对) ├── HashMap ├── LinkedHashMap ├── TreeMap └── Hashtable
|
1.2 核心接口说明
- Collection:所有单列集合的根接口
- List:有序、可重复的集合
- Set:无序、不可重复的集合
- Map:键值对映射,key不可重复
二、ArrayList相关面试题
2.1 ArrayList的底层实现原理
答案:
ArrayList底层基于动态数组实现:
1 2 3 4 5 6 7 8 9 10
| public class ArrayList<E> extends AbstractList<E> { private static final int DEFAULT_CAPACITY = 10; transient Object[] elementData; private int size; }
|
关键特点:
- 底层是Object数组
- 默认初始容量为10
- 支持动态扩容
- 查询快(O(1)),插入删除慢(O(n))
2.2 ArrayList的扩容机制
扩容流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public boolean add(E e) { ensureCapacityInternal(size + 1); elementData[size++] = e; return true; }
private void grow(int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity < minCapacity) newCapacity = minCapacity; elementData = Arrays.copyOf(elementData, newCapacity); }
|
扩容机制总结:
- 每次扩容为原来的1.5倍
- 使用位运算提高效率:
oldCapacity >> 1 相当于除以2
- 扩容需要复制数组,成本较高
2.3 ArrayList与LinkedList的区别
| 特性 |
ArrayList |
LinkedList |
| 底层结构 |
动态数组 |
双向链表 |
| 随机访问 |
O(1) |
O(n) |
| 插入删除(头部) |
O(n) |
O(1) |
| 插入删除(尾部) |
O(1) |
O(1) |
| 内存占用 |
连续内存 |
非连续,额外指针开销 |
| 适用场景 |
查询多 |
插入删除多 |
代码示例:
1 2 3 4 5 6 7 8 9 10
| List<String> arrayList = new ArrayList<>(); arrayList.add("A"); arrayList.add("B"); String item = arrayList.get(0);
List<String> linkedList = new LinkedList<>(); linkedList.add(0, "New"); linkedList.remove(0);
|
三、HashMap相关面试题
3.1 HashMap的底层实现原理
答案:
HashMap在JDK 1.8中采用数组+链表+红黑树结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class HashMap<K,V> { transient Node<K,V>[] table; static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; } static final int DEFAULT_INITIAL_CAPACITY = 16; static final float DEFAULT_LOAD_FACTOR = 0.75f; static final int TREEIFY_THRESHOLD = 8; }
|
核心机制:
- 计算key的hash值
- 通过hash值定位到数组索引
- 如果发生冲突,采用链地址法(链表或红黑树)
3.2 HashMap的put流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; return oldValue; } } if (++size > threshold) resize(); return null; }
|
3.3 HashMap为什么线程不安全?
主要问题:
- 数据覆盖:多线程同时put时可能覆盖数据
- 扩容死循环(JDK 1.7):并发扩容时可能形成环形链表
- size不准确:并发修改size变量
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| HashMap<String, Integer> map = new HashMap<>();
Thread t1 = new Thread(() -> { for (int i = 0; i < 1000; i++) { map.put("key" + i, i); } });
Thread t2 = new Thread(() -> { for (int i = 0; i < 1000; i++) { map.put("key" + i, i); } });
t1.start(); t2.start();
|
解决方案:
- 使用
ConcurrentHashMap
- 使用
Collections.synchronizedMap()
- 外部加锁
3.4 HashMap的容量为什么是2的幂次方?
原因:
- 高效的取模运算
1 2 3 4 5 6
| int index = hash % length;
int index = hash & (length - 1);
|
- 扩容时元素重新分配更高效
3.5 HashMap的hash函数设计
1 2 3 4 5
| static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
|
设计目的:
- 让hash值的高位也参与到索引计算中
- 减少哈希冲突
- 提高hash的随机性
四、ConcurrentHashMap相关面试题
4.1 ConcurrentHashMap的实现原理
JDK 1.7:Segment分段锁
1 2 3 4 5 6 7 8
| class ConcurrentHashMap<K,V> { final Segment<K,V>[] segments; static class Segment<K,V> extends ReentrantLock { transient volatile HashEntry<K,V>[] table; } }
|
JDK 1.8:CAS + synchronized
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class ConcurrentHashMap<K,V> { transient volatile Node<K,V>[] table; final V putVal(K key, V value, boolean onlyIfAbsent) { for (Node<K,V>[] tab = table;;) { if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; } else { synchronized (f) { } } } } }
|
4.2 JDK 1.7和1.8的区别
| 特性 |
JDK 1.7 |
JDK 1.8 |
| 底层结构 |
Segment数组 + HashEntry数组 |
Node数组 + 链表/红黑树 |
| 锁机制 |
分段锁(ReentrantLock) |
CAS + synchronized |
| 锁粒度 |
Segment级别 |
节点级别 |
| 并发度 |
Segment个数(默认16) |
更高,可达数组长度 |
五、Set相关面试题
5.1 HashSet的实现原理
答案:
HashSet底层基于HashMap实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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; } public boolean contains(Object o) { return map.containsKey(o); } }
|
关键点:
- 元素作为HashMap的key
- value统一使用PRESENT对象
- 利用HashMap的key不重复特性
5.2 TreeSet如何保证有序和不重复?
答案:
TreeSet基于TreeMap(红黑树)实现:
1 2 3 4 5 6 7 8 9 10 11
| public class TreeSet<E> extends AbstractSet<E> { private transient NavigableMap<E,Object> m; public TreeSet() { this(new TreeMap<E,Object>()); } public boolean add(E e) { return m.put(e, PRESENT) == null; } }
|
有序性保证:
使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13
| TreeSet<Integer> set1 = new TreeSet<>(); set1.add(5); set1.add(2); set1.add(8);
TreeSet<String> set2 = new TreeSet<>((a, b) -> b.compareTo(a)); set2.add("apple"); set2.add("banana"); set2.add("cherry");
|
六、面试高频问题汇总
6.1 如何选择合适的集合类?
选择标准:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| if (需要键值对) { if (需要线程安全) return ConcurrentHashMap; if (需要排序) return TreeMap; if (需要保持插入顺序) return LinkedHashMap; return HashMap; }
if (需要去重) { if (需要排序) return TreeSet; if (需要保持插入顺序) return LinkedHashSet; return HashSet; }
if (查询操作多) return ArrayList; if (插入删除操作多) return LinkedList;
|
6.2 fail-fast和fail-safe的区别
fail-fast(快速失败):
1 2 3 4 5 6 7 8
| List<String> list = new ArrayList<>(); list.add("A"); list.add("B");
for (String item : list) { list.remove(item); }
|
fail-safe(安全失败):
1 2 3 4 5 6 7 8
| CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(); list.add("A"); list.add("B");
for (String item : list) { list.remove(item); }
|
6.3 如何实现ArrayList的线程安全?
方法1:使用Collections.synchronizedList()
1
| List<String> list = Collections.synchronizedList(new ArrayList<>());
|
方法2:使用CopyOnWriteArrayList
1
| List<String> list = new CopyOnWriteArrayList<>();
|
方法3:手动加锁
1 2 3 4
| List<String> list = new ArrayList<>(); synchronized(list) { list.add("item"); }
|
性能对比:
CopyOnWriteArrayList:读多写少场景
Collections.synchronizedList:读写均衡场景
- 手动加锁:灵活性高,但容易出错
总结
Java集合框架是Java开发的基础,掌握其底层原理对于编写高效代码和通过面试至关重要。重点需要掌握:
- ArrayList:动态数组,扩容机制
- LinkedList:双向链表,适合插入删除
- HashMap:数组+链表/红黑树,hash算法
- ConcurrentHashMap:线程安全,CAS+synchronized
- HashSet/TreeSet:基于Map实现,去重和排序
参考资料