Java数据结构之LinkedList、ArrayList的效率分析
前言:
在我们平常开发中难免会用到List集合来存储数据,一般都会选择ArrayList和LinkedList,以前只是大致知道ArrayList查询效率高LinkedList插入删除效率高,今天来实测一下。
先了解一下List
List列表类,顺序存储任何对象(顺序不变),可重复。
List是继承于Collection的接口,不能实例化。实例化可以用:
ArrayList(实现动态数组),查询快(随意访问或顺序访问),增删慢。整体清空快,线程不同步(非线程安全)。数组长度是可变的百分之五十延长
LinkedList(实现链表),查询慢,增删快。
Vector(实现动态数组),都慢,被ArrayList替代。长度任意延长。线程安全(同步的类,函数都是synchronized)
Stack(实现堆栈)继承于Vector,先进后出。
List基本操作
插入:add()
查找:get()
删除:remove(int index)
修改:set()
清空表:clear()
遍历:用Iterator迭代器遍历每个元素
ArrayList、LinkedList性能对比
为了很好的对比效率,直接写个测试程序看下运行结果
代码1:模拟5w条数据指定插入最后一位,然后查询全部,循环从最后一位删除
public class TestList {
public static void main(String[] args) {
// TODO Auto-generated method stub
testArrayList();
testLinkedList();
}
private static void testLinkedList() {
LinkedList<String> list = new LinkedList<>();
int maxTestCount = 50000;
// 测试添加
long start = System.currentTimeMillis();
for (int i = 0; i < maxTestCount; i++) {
list.add(0, String.valueOf(i));
}
long end = System.currentTimeMillis();
System.out.println("LinkedList add cost time :" + (end - start));
// 测试查询
start = System.currentTimeMillis();
for (int i = 0; i < maxTestCount; i++) {
list.get(i);
}
end = System.currentTimeMillis();
System.out.println("LinkedList get cost time :" + (end - start));
// 测试删除
start = System.currentTimeMillis();
for (int i = maxTestCount; i > 0; i--) {
list.remove(0);
}
end = System.currentTimeMillis();
System.out.println("LinkedList remove cost time :" + (end - start));
}
private static void testArrayList(){
ArrayList<String> list=new ArrayList<>();
int maxTestCount=50000;
//测试添加
long start =System.currentTimeMillis();
for(int i =0;i<maxTestCount;i++){
list.add(i,String.valueOf(i));
}
long end =System.currentTimeMillis();
System.out.println("ArrayList add cost time :"+(end-start));
//测试查询
start =System.currentTimeMillis();
for(int i =0;i<maxTestCount;i++){
list.get(i);
}
end =System.currentTimeMillis();
System.out.println("ArrayList get cost time :"+(end-start));
//测试删除
start =System.currentTimeMillis();
for(int i =maxTestCount;i>0;i--){
list.remove(i-1);
}
end =System.currentTimeMillis();
System.out.println("ArrayList remove cost time :"+(end-start));
}
}
结果:
ArrayList add cost time :15
ArrayList get cost time :2
ArrayList remove cost time :3
LinkedList add cost time :10
LinkedList get cost time :2308
LinkedList remove cost time :2
代码2:模拟5w条数据指定插入第一位,然后查询全部,循环从第一位删除
public class TestList {
public static void main(String[] args) {
// TODO Auto-generated method stub
testArrayList();
testLinkedList();
}
private static void testLinkedList() {
LinkedList<String> list = new LinkedList<>();
int maxTestCount = 50000;
// 测试添加
long start = System.currentTimeMillis();
for (int i = 0; i < maxTestCount; i++) {
list.add(0, String.valueOf(i));
}
long end = System.currentTimeMillis();
System.out.println("LinkedList add cost time :" + (end - start));
// 测试查询
start = System.currentTimeMillis();
for (int i = 0; i < maxTestCount; i++) {
list.get(i);
}
end = System.currentTimeMillis();
System.out.println("LinkedList get cost time :" + (end - start));
// 测试删除
start = System.currentTimeMillis();
for (int i = maxTestCount; i > 0; i--) {
list.remove(0);
}
end = System.currentTimeMillis();
System.out.println("LinkedList remove cost time :" + (end - start));
}
private static void testArrayList(){
ArrayList<String> list=new ArrayList<>();
int maxTestCount=50000;
//测试添加
long start =System.currentTimeMillis();
for(int i =0;i<maxTestCount;i++){
list.add(0,String.valueOf(i));
}
long end =System.currentTimeMillis();
System.out.println("ArrayList add cost time :"+(end-start));
//测试查询
start =System.currentTimeMillis();
for(int i =0;i<maxTestCount;i++){
list.get(i);
}
end =System.currentTimeMillis();
System.out.println("ArrayList get cost time :"+(end-start));
//测试删除
start =System.currentTimeMillis();
for(int i =maxTestCount;i>0;i--){
list.remove(0);
}
end =System.currentTimeMillis();
System.out.println("ArrayList remove cost time :"+(end-start));
}
}
结果:
ArrayList add cost time :168
ArrayList get cost time :2
ArrayList remove cost time :138
LinkedList add cost time :8
LinkedList get cost time :2636
LinkedList remove cost time :4
通过上面的运行结果可以大致得出以下结论:
- ArrayList插入、删除效率,在每次都操作第一位元素的时候,明显低于LinkedList
- ArrayList查询效率远远高于LinkedList
通过上面的结构是不是就可以认为插入删除频繁选择LinkedList,追求查询效率就选择ArrayList呢,我们先来分析一下效率差别的原因,这个就跟数据结构有关系了,可以参考一些数据结构中链表的知识,arraylist 顺序表,用数组的方式实现。想想数组要查询那个元素只给出其下标即可,所以才说arraylist随机访问多的场景比较合适。但是如果删除某个元素比如第 i 个元素,则要将 i 之后的元素都向前移一位以保证顺序表的正确,增加也是一样,增加还涉及到扩容的问题,要移动多个元素。要多次删除增加的话是很低效的。而LinkedList是双向链表,注意是链表。要查询只能头结点开始逐步查询,虽然linkedlist也有get(i)的方法,看起来也是根据下标去获取元素,但是它的get(i)底层是这样的:
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
/**
* Returns the (non-null) Node at the specified element index.
*/
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
还是遍历了整个链表去获取元素,所以慢。但是,如果要增加后删除一个元素的话,只需要改变其前后元素的指向即可,不需要像arraylist那样整体移动,所以才说多用于增删多的场合。
List扩容
我们知道List集合的底层实现是数组结构,而数组的大小是不可改变的,因此当其容器内存不足时,需要进行扩容,扩容的方法就是重新分配一个新数组,然后复制元素到新数组中,再将元素添加到数组末位
一、ArrayList
首先便是默认大小的问题
这里我们可以通过ArrayList的源码来分析,首先可以看一下他的构造函数,发现有三个,分别是:无参、一个int型参数、一个集合泛型;
在分析构造函数之前 我们先来看几个参数:
即ArrayList 在内部的表示即为一个Object类型的数组,并非真正的泛型,这里的变量就是数组的真实存储大小
可以理解为设置默认数组大小的常量
1、无参的构造函数
在这里我们可以看到,当不传参数时,数组的大小直接为默认数组大小的常量,而这个大小就为10,至于怎么得到的,这里我们可以先卖个小关子,在后面一起介绍~~
2、有一个int的构造函数
我们可以发现,当传入的值大于0时,数组的大小即为传入的值大小,为0时,则为EMPTY_ELEMENTDATA,小于0则抛出异常。这个EMPTY_ELEMENTDATA又是什么意思呢??
其实就是0(即亦可理解为传入的值);
3、有一个Collection泛型的构造函数
而集合参数的其实与Int的构造函数差不多,只是在初始化容器的同时,还将容器中的数据也初始化了(完成copy)。
虽然说完了构造函数,但实际的扩容是发生在增加元素的时候(容器内存不足或者存储达到加载因子),因此 来到了最为主要的一部分内容:
4、ArrayList 的元素添加
通过上面的代码可以看到,每次添加元素的时候,都会执行一个方法,而这个方法,就是判断是否需要扩容的方法;
进入方法之后,我们看到了一个if语句,发现了么,这个就是我们之前在无参构造函数时卖的关子,当当前的大小为默认数组大小的常量时,minCapacity(即位数组中存储的数据大小即size+1)就为DEFAULT_CAPACITY, minCapacity之间大的那个,而minCapacity的默认值为0,所以从这里就可以知道,无参构造函数产生的数组大小为10~~
但是真正的判断还不在这里,而是下面这个方法:
由此我们可以看出,其实就是判断加一个元素后,数组大小是够超出当前数组的大小,若超出,则执行下面的”扩容“代码~~
从文中我们可看到,新数组的大小其实就是( oldCapacity + (oldCapacity >> 1)),如果你懂得位运算的话,你就知道了,原来就是原数组的长度加上原数组的长度大小的一半(位运算,右移一位相当于整体/2),后面判断数组大小越界这里就不多阐述了,感兴趣的可以自己查看一下源码。
上面的ArrayList是没有涉及加载因子的,但是有些集合类型如Vector等使用到了加载因子,只要把是否需要扩容d的判断条件改一下就可以了。
总结:
通过运行结果和查阅资料基本上验证了ArrayList和LinkedList效率问题,有助于在以后的开发中根据实际场景选择合适的技术方案,所以不能笼统的说arrayList适合查询元素,而linkedList适合增删元素。