📚 Android 面试:Collection & Map 核心全攻略
1. 经典 Java 集合框架 (HashMap & ArrayList)
这是所有面试的基础,重点在于扩容和演进。
1.1 HashMap (重点:1.8 的优化)
- 结构:数组 + 链表 + 红黑树。
- 初始化:构造时仅记录容量,第一次
put时才调用resize()分配 16 个空间。 - 索引计算:使用
(n-1) & hash,由于n始终是 的幂次(由tableSizeFor()保证),位运算比取模更快。 - 树化条件:当链表长度 且数组总长度 时,链表转红黑树。
- 扩容:翻倍扩容,数据迁移逻辑为
e.hash & oldCap,结果为 0 留在原位,1 迁移至oldIndex + oldCap。
1.2 ArrayList
- 结构:动态数组。
- 扩容:默认初始大小为 10,当容量不足时,按 1.5 倍 扩容(
newCapacity = oldCapacity + (oldCapacity >> 1))。 - 性能:随机访问(
get)时间复杂度为 ,中间插入或删除需要调用System.arraycopy移动元素,效率较低。
2. 顺序与排序 (LinkedHashMap & TreeMap)
2.1 LinkedHashMap (面试高频:LRU 实现)
-
特性:继承自 HashMap,维护双向链表记录顺序。
-
顺序模式:
accessOrder = false:记录插入顺序。accessOrder = true:记录访问顺序,访问后的节点移至末尾。
-
LRU 缓存:通过重写
removeEldestEntry方法,可以在插入新数据时自动删除最久未使用的头部节点。
2.2 TreeMap
- 特性:基于红黑树,不存在扩容问题。
- 功能:保证 Key 的自然顺序或自定义 Comparator 顺序。
3. Android 特有优化 (SparseArray & ArrayMap)
在 Android 面试中,这部分是体现你“Android 架构理解”的关键。
3.1 SparseArray
- 原理:使用
int[] mKeys和Object[] mValues两个数组。 - 优势:Key 必须是
int,避免了Integer自动装箱,且没有Entry对象开销,极度省内存。 - 算法:采用二分查找(Binary Search)寻找 Key 的下标。
- 扩容:采用
currentSize <= 4 ? 8 : currentSize * 2的步进策略。
3.2 ArrayMap
- 原理:类似 SparseArray,但 Key 可以是任意对象。
- 设计:内部维护
mHashes[](排序后的 hash 值)和mArray[](Key-Value 对)。 - 缓存机制:ArrayMap 内部维护了两个静态缓存池(大小为 4 和 8),减少频繁创建数组带来的 GC 压力。
4. 并发与安全 (ConcurrentHashMap & CopyOnWrite)
4.1 ConcurrentHashMap (1.8)
- 锁粒度:不再使用 Segment 分段锁,而是直接用
synchronized锁住桶的头节点。 - 并发控制:大量使用 CAS 操作(例如在桶为空时插入数据)。
- 读写分离:
get方法完全不加锁,依靠volatile关键字保证内存可见性。
4.2 CopyOnWriteArrayList
- 核心思想:写时复制。
- 逻辑:在添加元素时,先拷贝一份新数组,在旧数组上读,新数组上写,写完后再将引用指向新数组。
- 缺点:极其耗费内存(每次写都复制),且只能保证最终一致性,不保证实时一致性。
5. 总结:面试怎么选?
| 需求场景 | 推荐集合 |
|---|---|
| 通用 Key-Value 存储 | HashMap |
| 需要按访问顺序排序 (LRU) | LinkedHashMap |
| 需要 Key 自动排序 | TreeMap |
| Android 端 Key 为 int 且数据量 < 1000 | SparseArray |
| Android 端内存极度敏感且数据量 < 1000 | ArrayMap |
| 多线程频繁读、极少写 | CopyOnWriteArrayList |
| 多线程高并发读写 | ConcurrentHashMap |
面试官可能会问的“最后一钩”:
“既然
SparseArray这么好,为什么不全部替换掉HashMap?”参考回答:因为
SparseArray的查找基于二分查找,时间复杂度为 ,而HashMap在理想情况下是 。当数据量达到千级以上时,二分查找的性能损耗会非常明显。