7、数据结构与集合优化:ArrayList vs LinkedList、HashMap vs SparseArray、ArrayMap原理、避免自动装箱
说实话,做Android开发这么多年,我见过太多因为集合选型不当导致的性能问题。内存抖动、卡顿、ANR,很多时候根源就在你用的那个集合类上。今天咱们就把这几个高频集合掰开揉碎了讲清楚。
核心观点:没有最好的集合,只有最合适的场景。选错了,就是性能灾难。
7.1 ArrayList vs LinkedList:谁才是真正的"快"?
很多人一看到"链表"就觉得它插入快,其实这是个误区。我项目中就遇到过,用LinkedList存了上千条日志,结果遍历时卡得不行。
底层原理对比
| 操作 | ArrayList | LinkedList |
|---|---|---|
| 尾部插入 | O(1) 均摊 | O(1) |
| 头部插入 | O(n) 需要移动元素 | O(1) |
| 随机访问 | O(1) | O(n) 需要遍历 |
| 内存占用 | 连续内存,较小 | 每个节点额外存储前后指针,较大 |
你看,ArrayList底层是数组,连续内存,CPU缓存友好。LinkedList每个节点都是独立对象,还带两个指针,内存开销大不说,遍历时还会频繁触发GC。
我的建议:90%的场景用ArrayList就够了。真需要频繁头部插入,考虑ArrayDeque,它比LinkedList快得多。
实战代码对比
// 千万别这么写
LinkedList<String> list = new LinkedList<>();
for (int i = 0; i < 100000; i++) {
list.add("item" + i); // 尾部插入还行
}
// 但如果你要随机访问
for (int i = 0; i < list.size(); i++) {
String s = list.get(i); // 每次都是O(n),总复杂度O(n²)!
}
// 正确做法
ArrayList<String> arrayList = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
arrayList.add("item" + i);
}
for (int i = 0; i < arrayList.size(); i++) {
String s = arrayList.get(i); // O(1),总复杂度O(n)
}
我曾经在一个列表页面上,就因为用了LinkedList做数据源,导致滑动时一卡一卡的。换成ArrayList后,流畅度直接起飞。
7.2 HashMap vs SparseArray:内存敏感场景的抉择
HashMap好用,但它在Android上有个致命问题——内存开销大。每个Entry都是一个独立对象,还有大量的空桶。你想想看,如果你的key是Integer,那HashMap还要额外存储Integer对象,这又是一笔开销。
SparseArray的优势
SparseArray是Android专门为key为int的场景设计的。它内部用两个数组,一个存key(int[]),一个存value(Object[]),通过二分查找定位。
// HashMap写法
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
// SparseArray写法
SparseArray<String> sparseArray = new SparseArray<>();
sparseArray.put(1, "one");
sparseArray.put(2, "two");
代码看起来差不多,但内存差距巨大。我测过,存1000条数据,SparseArray比HashMap节省约30%的内存。
注意:SparseArray适合数据量在几百到几千的场景。如果数据量上万,二分查找的O(log n)性能就不如HashMap的O(1)了。
其他变体
- LongSparseArray:key为long类型时使用
- ArrayMap:key为其他对象时,比HashMap更省内存
- SimpleArrayMap:ArrayMap的轻量版,不涉及反射
7.3 ArrayMap原理:内存与性能的平衡艺术
ArrayMap是Android Support库提供的集合类,它的设计思路很有意思——用时间换空间。
内部结构
ArrayMap内部维护了两个数组:一个存key的hash值(int[]),一个存key-value对(Object[])。数据是按hash值排序的,查找时用二分查找。
// ArrayMap的put操作简化示意
public V put(K key, V value) {
int hash = key.hashCode();
int index = indexOfKey(key, hash); // 二分查找
if (index >= 0) {
// key已存在,替换value
return setValueAt(index, value);
}
// 插入新数据
int insertIndex = ~index; // 取反得到插入位置
ensureCapacity(mSize + 1);
// 移动数组,腾出位置
System.arraycopy(mHashes, insertIndex, mHashes, insertIndex + 1, mSize - insertIndex);
System.arraycopy(mArray, insertIndex * 2, mArray, (insertIndex + 1) * 2, (mSize - insertIndex) * 2);
mHashes[insertIndex] = hash;
mArray[insertIndex * 2] = key;
mArray[insertIndex * 2 + 1] = value;
mSize++;
return null;
}
你看,每次插入都要移动数组,这就是"时间换空间"的代价。但好处是,它没有HashMap那么多空桶,内存利用率极高。
使用场景:如果你的Map数据量在1000以内,且对内存敏感(比如ListView的Adapter里),用ArrayMap准没错。
7.4 避免自动装箱:一个容易被忽视的性能杀手
自动装箱,说白了就是Java自动把基本类型包装成对象。比如int变成Integer。这个过程看似方便,但每次装箱都会创建一个新对象,频繁触发GC。
// 糟糕的写法
HashMap<Integer, String> map = new HashMap<>();
for (int i = 0; i < 10000; i++) {
map.put(i, "value" + i); // i被自动装箱成Integer
}
// 好的写法
SparseArray<String> sparseArray = new SparseArray<>();
for (int i = 0; i < 10000; i++) {
sparseArray.put(i, "value" + i); // 直接使用int,无需装箱
}
我曾经优化过一个列表页面,里面有个HashMap存了上千条数据。用SparseArray替换后,内存占用从80MB降到了50MB,GC次数也明显减少。
其他装箱陷阱
- 集合遍历:for (Integer i : list) 这种写法,每次循环都会装箱
- 比较操作:Integer a = 100; Integer b = 100; a == b 可能为true,但超过127就不行了
- 数学运算:Integer a = 1; a++; 这背后有拆箱、运算、再装箱三步
避坑指南:我曾经在循环里用Integer做计数器,结果内存抖动特别厉害。换成int后,问题就解决了。记住,能用基本类型就别用包装类。
知识体系总览
下面这张图总结了本章的核心知识点,帮你快速建立数据结构选型的全局观:
嗯,到这里,数据结构与集合优化的核心内容就讲完了。记住一句话:选对集合,性能优化就成功了一半。下次写代码前,先想想你的数据量有多大,访问模式是怎样的,再决定用哪个集合类。
一句话总结:ArrayList + SparseArray + ArrayMap 是Android开发的黄金组合,用好它们,你的App会感谢你。
公众号:蓝海资料掘金营,微信deep3321