这里写自定义目录标题
!!!!感谢 Alector 工程师指导
前言
所有 Java 的童鞋们面试时都会被问过这道题:你说一下集合的特点。
所以 ArrayList 和 LinkedList 的性能问题就一定会被说到:
- 增删时 LinkedList 快,因为 LinkedList 是链表,增删时只需要在该位置断开链接,分别链到最新的那个对象的前后节点就完成了。
- ArrayList 相对慢,因为这是一个数组,还涉及到扩容、元素移动的操作。
- 在 JDK1.8 经过对 ArrayList 优化之后,ArrayList 的增删已经和 LinkedList 的速度不差多少了。
以前我也是这样认为的,并且面试时也是这样说的。但今天,一个笔名为 Alecter 的工程师(我称为廖工)的一番话给我提了个醒,我颠覆了对这个说法的认知。以下是我们的测试结果。
测试代码如下(数据量5w、50w,为了不影响数据,每次运行main只运行一个方法,运行5次main)
1. 中间插入
public class ListMain {
public static void main(String[] args) {
insert_5w(new ArrayList<>();
insert_5w(new LinkedList<>();
insert_50w(new ArrayList<>();
insert_50w(new LinkedList<>();
}
private static void insertTest_5w(List<Object> list) {
insert(list, 50000);
}
private static void insertTest_50w(List<Object> list) {
insert(list, 500000);
}
private static void insert(List<Object> list, int size){
long start = System.currentTimeMillis();
int pos;
Object o = new Object();
for (int i = 0; i < size; i++) {
pos = list.size() / 2;
list.add(pos, o);
}
long end = System.currentTimeMillis();
System.out.println("用时:" + (end - start));
}
}
测试次数 | 一 | 二 | 三 | 四 | 五 |
---|---|---|---|---|---|
ArrayList_5w | 89 | 77 | 86 | 94 | 89 |
LinkedList_5w | 1605 | 1478 | 1525 | 1447 | 1409 |
ArrayList_50w | 7957 | 8441 | 8567 | 7945 | 8318 |
LinkedList_50w | 235089 | 231305 | 我错了大哥 |
这个可以理解,因为 ArrayList 是有序数组,支持随机访问;而 LinkedList 只能一个一个去找位置,所以这种中间插入法对 LinkedList 不友好。
2. 追加法
方法和上面一样,不啰嗦,只写出实际运行代码
public class ListMain {
private static void append(List<Object> list, int size){
long start = System.currentTimeMillis();
Object o = new Object();
for (int i = 0; i < size; i++) {
list.add(pos, o);
}
long end = System.currentTimeMillis();
System.out.println("用时:" + (end - start));
}
}
测试次数 | 一 | 二 | 三 | 四 | 五 |
---|---|---|---|---|---|
ArrayList_5w | 3 | 4 | 4 | 3 | 4 |
LinkedList_5w | 4 | 4 | 3 | 4 | 6 |
ArrayList_50w | 13 | 12 | 13 | 13 | 13 |
LinkedList_50w | 17 | 15 | 16 | 16 | 18 |
ArrayList_500w | 69 | 86 | 74 | 70 | 69 |
LinkedList_500w | 328 | 309 | 318 | 312 | 312 |
ArrayList_5000w | 756 | 746 | 744 | 739 | 799 |
LinkedList_5000w | 19245 | 19050 | 25082 | 18753 | 21347 |
不是说 LinkedList 的增删效率比 ArrayList 更高吗?为什么越往后 ArrayList 的效率比 LinkedList 高得越明显了呢?更有,达到 5000w 时几乎高出 30 倍?
先把剩下的先做完实验再总结
3. 头插法
public class ListMain {
private static void appendFirst(List<Object> list, int size) {
long start = System.currentTimeMillis();
Object o = new Object();
for (int i = 0; i < size; i++) {
list.add(0, o);
}
long end = System.currentTimeMillis();
System.out.println("用时:" + (end - start));
}
}
测试次数 | 一 | 二 | 三 | 四 | 五 |
---|---|---|---|---|---|
ArrayList_5w | 4 | 3 | 6 | 4 | 5 |
LinkedList_5w | 4 | 4 | 5 | 5 | 5 |
ArrayList_50w | 13 | 13 | 13 | 15 | 12 |
LinkedList_50w | 16 | 17 | 16 | 17 | 16 |
ArrayList_500w | 73 | 71 | 70 | 74 | 67 |
LinkedList_500w | 313 | 330 | 351 | 405 | 311 |
ArrayList_5000w | 817 | 785 | 774 | 777 | 787 |
LinkedList_5000w | 18117 | 19016 | 18404 | 18365 | 17957 |
4. 头删法
public class ListMain {
/**
* 此代码复用
*/
private static void beforeRemove(List<Object> list, int size){
Object o = new Object();
for (int i = 0; i < size; i++) {
list.add(0, o);
}
}
private static void removeFirst(List<Object> list, int size) {
beforeRemove(list, size);
long start = System.currentTimeMillis();
for (int i = 0; i < list.size(); i++) {
list.remove(0);
}
long end = System.currentTimeMillis();
System.out.println("用时:" + (end - start));
}
}
测试次数 | 一 | 二 | 三 | 四 | 五 |
---|---|---|---|---|---|
ArrayList_5w | 137 | 148 | 138 | 161 | 149 |
LinkedList_5w | 3 | 4 | 3 | 3 | 7 |
ArrayList_50w | 11835 | 12167 | 11840 | 12031 | 11734 |
LinkedList_50w | 10 | 6 | 8 | 7 | 10 |
5. 尾删法
private static void removeLast(List<Object> list, int size) {
beforeRemove(list, size);
long start = System.currentTimeMillis();
for (int i = list.size() - 1; i >= 0; i--) {
list.remove(0);
}
long end = System.currentTimeMillis();
System.out.println("用时:" + (end - start));
}
测试次数 | 一 | 二 | 三 | 四 | 五 |
---|---|---|---|---|---|
ArrayList_5w | 186 | 174 | 249 | 204 | 200 |
LinkedList_5w | 5 | 4 | 5 | 5 | 4 |
ArrayList_50w | 17317 | 18114 | 16587 | 18520 | 20521 |
LinkedList_50w | 14 | 17 | 9 | 13 | 13 |
6. 中删法
private static void removeMod(List<Object> list, int size) {
beforeRemove(list, size);
long start = System.currentTimeMillis();
while (list != null && list.size() > 0) {
list.remove(list.size() / 2);
}
long end = System.currentTimeMillis();
System.out.println("用时:" + (end - start));
}
测试次数 | 一 | 二 | 三 | 四 | 五 |
---|---|---|---|---|---|
ArrayList_5w | 77 | 128 | 77 | 360 | 107 |
LinkedList_5w | 1212 | 1245 | 1233 | 1394 | 1347 |
ArrayList_50w | 8484 | 7866 | 8197 | 7879 | 8429 |
LinkedList_50w | 133318 | 我错了 |
综述
经测试发现
- LinkedList 是不连续的内存空间,从中间操作需要获取到集合中间的数据再操作,所以这种操作对 LinkedList 不友好。
- LinkedList 只是在做数据删除时效率才比 ArrayList 高,数据量上万时不管是前删还是后删,效率高出几十倍甚至上百倍。
- 对于增加,实验证明是 ArrayList 的效率比 LinkedList 高,并且在我测试的50万阈值时,数据量越大越明显,即使 ArrayList 涉及到扩容、元素移动等操作。
关于源码
回到开头,在1.8时对 ArrayList 做了优化,但真的是这样吗?为此我翻看了 jdk 1.7 和 jdk 1.8 有关于 ArrayList 的方法(增、删),发现两个版本中对于这部分竟然然不差分毫,并且运行的结果也不相上下……
结论
开头的说法几乎是一边倒的说法,那这个结论什么时候被大众熟知并列为公知的?这个结论当时是怎么证明出来的?为什么这么久没有人对这一说法提出异议?jdk 究竟是在什么时候对 ArrayList 进行的优化?如果真的是 1.8 进行的优化,那优化的逻辑会不会在 jdk 的底层 c++ ?还有多少没经过论证的错误结论在横行、在污染大众公知?
由于我没有找到 jdk 1.6 及更前的版本,暂时无法论证以上的问题,经我和廖工讨论,决定对开头的结果持保留意见。
Alector 工程师开玩笑说:“我可能是学了假的 Java ……”。
或许,我们真的是学了假的 Java ……
后言
不忘初心,牢记使命。
让我们携手,互帮互助,取长补短,一起朝各自的目标奋力迈进!!!
由衷感谢您的观看,您的收获和支持就是我的最大动力!!再次感谢!!