通过上一章的学习,可以知道LinkedList实现了Deque、List等接口,链表的实现主要依靠Entry对象。上一章已经对LinkedList的队列接口方法的进行了分析与了解,本章主要分析LinkedList中List接口方法的实现;还有ArrayList的List接口方法的学习;并且分析两种方法使用的情况。
以下为测试代码
public class LinkList_ArrayList {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
getAddTime(100000);
getAddIndexTime(100000,70000);
}
public static void getAddTime(int count){
ArrayList<String> arr = new ArrayList<String>();//默认长度为10
LinkedList<String> linl = new LinkedList<String>();
Long arr_start = new Date().getTime();
for(int i=1;i<=count;i++){
arr.add(i+"");//当数组空间已满,增加原有长度的1/2
}
Long arr_end = new Date().getTime();
System.out.println("ArrayList 增加"+count+"耗时:"+(arr_end-arr_start));
Long linl_start = new Date().getTime();
for(int i=1;i<=count;i++){
linl.add(i+"");//当数组空间已满,增加原有长度的1/2
}
Long linl_end = new Date().getTime();
System.out.println("LinkedList 增加"+count+"耗时:"+(linl_end-linl_start));
}
public static void getAddIndexTime(int count,int index){
ArrayList<String> arr = new ArrayList<String>();//默认长度为10
LinkedList<String> linl = new LinkedList<String>();
for(int i=1;i<=count;i++){
arr.add(i+"");//当数组空间已满,增加原有长度的1/2
}
Long arr_start = new Date().getTime();
for(int i=1;i<=count;i++){
arr.add(index,i+"");//当数组空间已满,增加原有长度的1/2
}
Long arr_end = new Date().getTime();
System.out.println("ArrayList 在指定位置"+index+"增加"+count+"耗时:"+(arr_end-arr_start));
for(int i=1;i<=count;i++){
linl.add(i+"");//当数组空间已满,增加原有长度的1/2
}
Long linl_start = new Date().getTime();
for(int i=1;i<=count;i++){
linl.add(index,i+"");//当数组空间已满,增加原有长度的1/2
}
Long linl_end = new Date().getTime();
System.out.println("LinkedList 在指定位置"+index+"增加"+count+"耗时:"+(linl_end-linl_start));
}
}
运行结果为
ArrayList 增加100000耗时:78
LinkedList 增加100000耗时:94
ArrayList 在指定位置70000增加100000耗时:7738
LinkedList 在指定位置70000增加100000耗时:192660
为什么同一类调用不同的插入节点方法耗时差距如此之大,为什么LinkedList 在利用索引插入时为什么效率远低于ArrayList (即使在1000W个节点也一样)?我们来看一下ArrayList与LinkedList的 add(E e)方法源码
// ArrayList的源码
public boolean add(E e) {
ensureCapacity(size + 1); // Increments modCount!!此方法用于增加数组大小,如果超出数组大小之后会自动增加1/2+1的长度
elementData[size++] = e;
return true;
}
//LinkedList的源码
public boolean add(E e) {
<span style="white-space:pre"> </span>addBefore(e, header);
return true;
}
private Entry<E> addBefore(E e, Entry<E> entry) {
<span style="white-space:pre"> </span>Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
<span style="white-space:pre"> </span>newEntry.previous.next = newEntry;
<span style="white-space:pre"> </span>newEntry.next.previous = newEntry;
<span style="white-space:pre"> </span>size++;
<span style="white-space:pre"> </span>modCount++;
<span style="white-space:pre"> </span>return newEntry;
}
通过分析源码可以发现,对于这两个方法增加节点的方法时间复杂度都是O(1),所以他们插入相同节点个数的时间相差无几,接下来我们可以在看一下带有索引插入节点的方法
// ArrayList的源码
public void add(int index, E element) {
<span style="white-space:pre"> </span>if (index > size || index < 0)
<span style="white-space:pre"> </span> throw new IndexOutOfBoundsException(
<span style="white-space:pre"> </span>"Index: "+index+", Size: "+size);
<span style="white-space:pre"> </span>ensureCapacity(size+1); // Increments modCount!!
<span style="white-space:pre"> </span>System.arraycopy(elementData, index, elementData, index + 1,
<span style="white-space:pre"> </span> size - index);
<span style="white-space:pre"> </span>elementData[index] = element;
<span style="white-space:pre"> </span>size++;
}
<pre name="code" class="java">//LinkedList的源码
public void add(int index, E element) {
addBefore(element, (index==size ? header : entry(index)));
}
private Entry<E> entry(int index) {
if (index < 0 || index >= size)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+size);
Entry<E> e = header;
if (index < (size >> 1)) {
for (int i = 0; i <= index; i++)
e = e.next;
} else {
for (int i = size; i > index; i--)
e = e.previous;
}
return e;
}
private Entry<E> addBefore(E e, Entry<E> entry) {
<span style="white-space:pre"> </span>Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
<span style="white-space:pre"> </span>newEntry.previous.next = newEntry;
<span style="white-space:pre"> </span>newEntry.next.previous = newEntry;
<span style="white-space:pre"> </span>size++;
<span style="white-space:pre"> </span>modCount++;
<span style="white-space:pre"> </span>return newEntry;
}
通过观察源码可以发现对于ArrayList,在使用索引插入节点元素的时候,其方法的时间复杂度为O(1),影响其效率的主要原因是数组的复制过程,而
LinkedList在使用索引插入元素的时候,其时间复杂度为O(1),但是当他调用entry(index)方法会有一个遍历链表的过程,这个方法的时间复杂度为O((n+2)/4)要远大于O(1),所以在使用索引插入元素的时候,LinkedList效率远低于ArrayList。
在List接口中有如下几个常用方法:
isEmpty()
contains(Object o)
remove(Object o)
containsAll(Collection<?> c)
addAll(int index, Collection<? extends E> c)
removeAll(Collection<?> c)
retainAll(Collection<?> c)
get(int index)
set(int index, E element)
add(int index, E element)
remove(int index)
indexOf(Object o)
通过查看两个类的源码可以发现contains(Object o), containsAll(Collection<?> c)其中在containsAll中调用了contains,而contains有调用了indexOf(Object o)方法,所以indexOf(Object o)的执行效率影响着这三个方法,我们来看下面的代码
public static void getIndexOfTime(int count,int count_con,int index){
ArrayList<String> arr = new ArrayList<String>();//默认长度为10
LinkedList<String> linl = new LinkedList<String>();
ArrayList<String> con = new ArrayList<String>();
for(int i=1;i<=count;i++){
arr.add(i+"");//当数组空间已满,增加原有长度的1/2
}
for(int i=1;i<=count;i++){
linl.add(i+"");//当数组空间已满,增加原有长度的1/2
}
for(int i=1;i<=count_con;i++){
int p = 400000;
con.add((p+i)+"");//当数组空间已满,增加原有长度的1/2
}
Long start_indexof = new Date().getTime();
arr.indexOf(index+"");
Long end_indexof = new Date().getTime();
System.out.println("ArrayList方法indexOf 在"+count+"条数据中,第"+index+"个位置需要耗时"+(end_indexof-start_indexof));
Long startlinl_indexof = new Date().getTime();
linl.indexOf(index+"");
Long endlinl_indexof = new Date().getTime();
System.out.println("LinkedList方法indexOf 在"+count+"条数据中,第"+index+"个位置需要耗时"+(endlinl_indexof-startlinl_indexof));
Long start_contains = new Date().getTime();
arr.contains(index+"");
Long end_contains = new Date().getTime();
System.out.println("ArrayList方法contains 在"+count+"条数据中,第"+index+"个位置需要耗时"+(end_contains-start_contains));
Long startlinl_contains = new Date().getTime();
linl.contains(index+"");
Long endlinl_contains = new Date().getTime();
System.out.println("LinkedList方法contains 在"+count+"条数据中,第"+index+"个位置需要耗时"+(endlinl_contains-startlinl_contains));
Long start_containsAll = new Date().getTime();
arr.containsAll(con);
Long end_containsAll = new Date().getTime();
System.out.println("ArrayList方法containsAll 在"+count+"条数据中,第"+index+"个位置需要耗时"+(end_containsAll-start_containsAll));
Long startlinl_containsAll = new Date().getTime();
linl.containsAll(con);
Long endlinl_containsAll = new Date().getTime();
System.out.println("LinkedList方法containsAll 在"+count+"条数据中,第"+index+"个位置需要耗时"+(endlinl_containsAll-startlinl_containsAll));
}
<pre name="code" class="java"> getIndexOfTime(500000,5,500000)运行结果为
ArrayList方法indexOf 在500000条数据中,第500000个位置需要耗时3
LinkedList方法indexOf 在500000条数据中,第500000个位置需要耗时9
ArrayList方法contains 在500000条数据中,第500000个位置需要耗时4
LinkedList方法contains 在500000条数据中,第500000个位置需要耗时7
ArrayList方法containsAll 在500000条数据中,第500000个位置需要耗时17
LinkedList方法containsAll 在500000条数据中,第500000个位置需要耗时26
和我们预测的一样两个检测包含的方法的效率依赖于indexOf,而ArrayList底层时数组实现所以起indexOf效率要高于LinkedList。接下来我们分析List中三个删除方法在两个链表中的效率。下面为测试代码
public static void getRemoveTime(int count,int element,int index){
ArrayList<String> arr = new ArrayList<String>();//默认长度为10
LinkedList<String> linl = new LinkedList<String>();
ArrayList<String> con = new ArrayList<String>();
for(int i=1;i<=count;i++){
arr.add(i+"");//当数组空间已满,增加原有长度的1/2
}
for(int i=1;i<=count;i++){
linl.add(i+"");
}
for(int i=1;i<=10;i++){
int p = 250000;
con.add((p+i)+"");//当数组空间已满,增加原有长度的1/2
}
Long start_remove = new Date().getTime();
arr.remove(index);
Long end_remove = new Date().getTime();
System.out.println("ArrayList方法remove 在"+count+"条数据中,第"+index+"个位置需删除元素要耗时"+(end_remove-start_remove));
Long startlinl_remove = new Date().getTime();
linl.remove(index);
Long endlinl_remove = new Date().getTime();
System.out.println("LinkedList方法remove 在"+count+"条数据中,第"+index+"个位置需删除元素要耗时"+(endlinl_remove-startlinl_remove));
Long start_remove_OBJ = new Date().getTime();
arr.remove(element+"");
Long end_remove_OBJ = new Date().getTime();
System.out.println("ArrayList方法remove OBJ 在"+count+"条数据中,删除"+element+"元素要耗时"+(end_remove_OBJ-start_remove_OBJ));
Long startlinl_remove_OBJ = new Date().getTime();
linl.remove(element+"");
Long endlinl_remove_OBJ = new Date().getTime();
System.out.println("LinkedList方法remove OBJ 在"+count+"条数据中,删除"+element+"位置需插入元素要耗时"+(endlinl_remove_OBJ-startlinl_remove_OBJ));
Long start_removeAll = new Date().getTime();
arr.removeAll(con);
Long end_removeAll = new Date().getTime();
System.out.println("ArrayList方法removeAll 在"+count+"条数据中,删除容器元素要耗时"+(end_removeAll-start_removeAll));
Long startlinl_removeAll = new Date().getTime();
linl.removeAll(con);
Long endlinl_removeAll = new Date().getTime();
System.out.println("LinkedList方法removeAll 在"+count+"条数据中,删除容器元素要耗时"+(endlinl_removeAll-startlinl_removeAll));
}
getRemoveTime(500000,240000,250000),运行结果(最后一个参数选择250000而不是0或者490000)
ArrayList方法remove 在500000条数据中,第250000个位置需删除元素要耗时0
LinkedList方法remove 在500000条数据中,第250000个位置需删除元素要耗时8
ArrayList方法remove OBJ 在500000条数据中,删除280000元素要耗时3
LinkedList方法remove OBJ 在500000条数据中,删除280000位置需插入元素要耗时5
ArrayList方法removeAll 在500000条数据中,删除容器元素要耗时53
LinkedList方法removeAll 在500000条数据中,删除容器元素要耗时47
接下来将getRemoveTime方法做一下调整
public static void getRemoveTime(int count,int element,int index){
ArrayList<String> arr = new ArrayList<String>();//默认长度为10
LinkedList<String> linl = new LinkedList<String>();
ArrayList<String> con = new ArrayList<String>();
ArrayList<String> con_linl = new ArrayList<String>();
for(int i=1;i<=count;i++){
arr.add(i+"Hellow Word ArrayList!");//当数组空间已满,增加原有长度的1/2
}
for(int i=1;i<=count;i++){
linl.add(i+"Hellow Word LinkedList!");
}
for(int i=1;i<=1;i++){
int p = 2000000;
con.add((p+i)+"Hellow Word ArrayList!");//当数组空间已满,增加原有长度的1/2
}
for(int i=1;i<=1;i++){
int p = 2000000;
con_linl.add((p+i)+"Hellow Word LinkedList!");//当数组空间已满,增加原有长度的1/2
}
Long start_remove = new Date().getTime();
arr.remove(index);
Long end_remove = new Date().getTime();
System.out.println("ArrayList方法remove 在"+count+"条数据中,第"+index+"个位置需删除元素要耗时"+(end_remove-start_remove));
Long startlinl_remove = new Date().getTime();
linl.remove(index);
Long endlinl_remove = new Date().getTime();
System.out.println("LinkedList方法remove 在"+count+"条数据中,第"+index+"个位置需删除元素要耗时"+(endlinl_remove-startlinl_remove));
Long start_remove_OBJ = new Date().getTime();
arr.remove(element+"Hellow Word ArrayList!");
Long end_remove_OBJ = new Date().getTime();
System.out.println("ArrayList方法remove OBJ 在"+count+"条数据中,删除"+element+"元素要耗时"+(end_remove_OBJ-start_remove_OBJ));
Long startlinl_remove_OBJ = new Date().getTime();
linl.remove(element+"Hellow Word LinkedList!");
Long endlinl_remove_OBJ = new Date().getTime();
System.out.println("LinkedList方法remove OBJ 在"+count+"条数据中,删除"+element+"元素要耗时"+(endlinl_remove_OBJ-startlinl_remove_OBJ));
Long start_removeAll = new Date().getTime();
arr.removeAll(con);
Long end_removeAll = new Date().getTime();
System.out.println("ArrayList方法removeAll 在"+count+"条数据中,删除容器元素要耗时"+(end_removeAll-start_removeAll));
Long startlinl_removeAll = new Date().getTime();
linl.removeAll(con_linl);
Long endlinl_removeAll = new Date().getTime();
System.out.println("LinkedList方法removeAll 在"+count+"条数据中,删除容器元素要耗时"+(endlinl_removeAll-startlinl_removeAll));
}
运行getRemoveTime(3000000,1,1000)得到如下运行结果
ArrayList方法remove 在3000000条数据中,第1000个位置需删除元素要耗时2
LinkedList方法remove 在3000000条数据中,第1000个位置需删除元素要耗时0
ArrayList方法remove OBJ 在3000000条数据中,删除1元素要耗时2
LinkedList方法remove OBJ 在3000000条数据中,删除1元素要耗时0
ArrayList方法removeAll 在3000000条数据中,删除容器元素要耗时119
LinkedList方法removeAll 在3000000条数据中,删除容器元素要耗时97
修改运行参数getRemoveTime(3000000,200000,2000000);得到一下运行结果
ArrayList方法remove 在3000000条数据中,第2000000个位置需删除元素要耗时0
LinkedList方法remove 在3000000条数据中,第2000000个位置需删除元素要耗时42
ArrayList方法remove OBJ 在3000000条数据中,删除2000000元素要耗时15
LinkedList方法remove OBJ 在3000000条数据中,删除2000000元素要耗时61
ArrayList方法removeAll 在3000000条数据中,删除容器元素要耗时116
LinkedList方法removeAll 在3000000条数据中,删除容器元素要耗时92
比较后两次的运行结果可以看出,当被移除元素越靠近链表中央LinkedList的效率越低(由于起遍历特点导致的);而ArrayList被移除元素越靠后效率越高(元素越靠后移动元素的个数越少,操作时间也越短)。通过着三个例子也可以看出,ArrayList与LinkedList的效率涉及到链表的遍历耗时与删除节点的操作时间,尤其是后者对于不同的服务器计算能力不同可能会有差异,所以需要根据实际情况来判断使用那种链表(但一般来数如果链表长度越大,ArrayList的删除效率会远低于LinedList,因为数组的移位操作比较耗时)
LinkedList 元素越靠近链表中央,效率越低由于起遍历特点决定的,能够从下面的源码看出来
private Entry<E> entry(int index) {
if (index < 0 || index >= size)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+size);
Entry<E> e = header;
if (index < (size >> 1)) {
for (int i = 0; i <= index; i++)
e = e.next;
} else {
for (int i = size; i > index; i--)
e = e.previous;
}
return e;
}
ArrayList元素越靠近链表尾端,效率越高由于起调用这个方法
System.arraycopy(elementData, index+1, elementData, index,numMoved)。
对于get(int index),set(int index, E element)两个方法由于没有涉及到元素的移动只有遍历和修改节点内容所以ArrayList的效率要高于LinkedList
在两个类中,所有的方法都没有进行同步处理,所以这两个类都是线程非安全的,不支持多线,如果需要同步处理则需要进行外部同步。
Vector为适量队列,他也实现了List接口,并且底层与ArrayList一样也是由动态数组实现,但他内部做了同步处理,线程安全。
Stack继承自Vector,实现先进后出的功能,内部同样做了同步处理,线程安全。