ArrayList与LinkedList增加元素对比(源码分析)
ArrayList增加元素
首先在主函数中调用ArrayList
的无参构造方法生成一个List
实例list
,并且发现共有三种增加元素的方法:
List<String> list = new ArrayList<>();
list.add("a");
list.add(0, "b");
list.addAll(Arrays.asList("c", "d"));
add(E e)
点进去第一个add
方法:
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
发现该方法只是通过扩容机制确保容量足够后(ArrayList
扩容机制可参考这篇文章:ArrayList初始化及扩容机制),简单地将元素拼接到ArrayList
的最后;
add(int index, E element)
点进去第二个add
方法:
/**
* Inserts the specified element at the specified position in this
* list. Shifts the element currently at that position (if any) and
* any subsequent elements to the right (adds one to their indices).
*
* @param index index at which the specified element is to be inserted
* @param element element to be inserted
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
首先使用rangeCheckForAdd(index)
来检查index
是否合法,然后通过扩容机制来确保有足够的容量,然后使用System.arraycopy
来将index
及之后的所有元素复制到往后一位(即实现了向后的一位位移),然后将空出来的index
处插入新元素即可;
由于涉及到元素的移动,因此该方法的时间复杂度较高;
addAll(Collection<? extends E> c)
点进去addAll
方法
/**
* Appends all of the elements in the specified collection to the end of
* this list, in the order that they are returned by the
* specified collection's Iterator. The behavior of this operation is
* undefined if the specified collection is modified while the operation
* is in progress. (This implies that the behavior of this call is
* undefined if the specified collection is this list, and this
* list is nonempty.)
*
* @param c collection containing elements to be added to this list
* @return <tt>true</tt> if this list changed as a result of the call
* @throws NullPointerException if the specified collection is null
*/
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
可以看到,首先将集合c
转为一个名为a
的Object[]
,然后将其长度传给newNum
,利用扩容机制确保容量,然后直接将a
复制到以下标为size
开头的、长度为newNum
的位置上,更新size
即可;
三种方法运行所需时间测试
我们分别用这三种增加元素的方法来测试一下插入十万条数据需要多长时间:
int max = 100000;
ArrayList<String> list1 = new ArrayList<>();
long startTime1 = System.currentTimeMillis();
for (int i = 0; i < max; i++) {
list1.add("a");
}
long endTime1 = System.currentTimeMillis();
System.out.println("The time needed using add(element) is: "+(endTime1 - startTime1));
ArrayList<String> list2 = new ArrayList<>();
long startTime2 = System.currentTimeMillis();
for (int i = 0; i < max; i++) {
list2.add(0, "a");
}
long endTime2 = System.currentTimeMillis();
System.out.println(("The time needed using add(index, element) is: "+(endTime2 - startTime2)));
ArrayList<String> list3 = new ArrayList<>();
long startTime3 = System.currentTimeMillis();
list3.addAll(list1);
long endTime3 = System.currentTimeMillis();
System.out.println(("The time needed using addAll is: "+(endTime3 - startTime3)));
运行结果如下:
The time needed using add(element) is: 3
The time needed using add(index, element) is: 402
The time needed using addAll is: 0
结论:
- 使用
add(element)
方法需要大概3ms时间,这是因为在插入数据的过程中,仅仅在尾部插入,无需进行数据的复制操作,仅仅需要扩容操作即可; - 使用
add(index, element)
方法需要的时间高达近400ms,是因为我们在头部插入数据,每次插入都需要将所有数据复制并移动到后一位,再插入数据,还需要不断进行扩容操作;并且随着list
长度的增加,使用一次该方法所需的时间越来越长; - 使用
addAll
方法所需时间最短,接近0ms,这是因为仅在尾部进行了插入数据,无需移动数据,且仅需一次扩容即可;
LinkedList增加元素
首先在主函数中调用LinkedList
的无参构造方法生成一个List
实例list
(若生成LinkedList
实例,则有更多关于队列操作的方法,但在这里我们只对比LinkedList
与ArrayList
在List
上的区别),并且发现共有三种增加元素的方法:
List<String> list = new LinkedList<>();
list.add("a");
list.add(0, "b");
list.addAll(Arrays.asList("c", "d"));
add(E e)
点进去第一个add
方法:
/**
* Appends the specified element to the end of this list.
*
* <p>This method is equivalent to {@link #addLast}.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
linkLast(e);
return true;
}
继续点开linkLast
:
/**
* Links e as last element.
*/
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
发现该方法只是简单地将元素拼接到LinkedList
的最后;
add(int index, E element)
点进去第二个add
方法:
/**
* Inserts the specified element at the specified position in this list.
* Shifts the element currently at that position (if any) and any
* subsequent elements to the right (adds one to their indices).
*
* @param index index at which the specified element is to be inserted
* @param element element to be inserted
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
首先使用checkPositionIndex(index)
来检查index
是否合法,如果index
恰好等于size
,直接将数据拼接到LinkedList
的最后即可,否则使用linkBefore
方法:
/**
* Inserts element e before non-null Node succ.
*/
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
可以看到,在要插入的位置,让新元素的next
指向原位于index
的元素,让新元素的prev
指向位于index
之前一个元素,让原位于index
的元素的prev
指向新元素,若index
之前一个元素为null
,则新元素为头节点,否则让index
之前一个元素的next
指向新元素即可;不涉及元素的复制、移位;
addAll(Collection<? extends E> c)
点进去addAll
方法:
/**
* Appends all of the elements in the specified collection to the end of
* this list, in the order that they are returned by the specified
* collection's iterator. The behavior of this operation is undefined if
* the specified collection is modified while the operation is in
* progress. (Note that this will occur if the specified collection is
* this list, and it's nonempty.)
*
* @param c collection containing elements to be added to this list
* @return {@code true} if this list changed as a result of the call
* @throws NullPointerException if the specified collection is null
*/
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
继续点进去:
/**
* Inserts all of the elements in the specified collection into this
* list, starting at the specified position. Shifts the element
* currently at that position (if any) and any subsequent elements to
* the right (increases their indices). The new elements will appear
* in the list in the order that they are returned by the
* specified collection's iterator.
*
* @param index index at which to insert the first element
* from the specified collection
* @param c collection containing elements to be added to this list
* @return {@code true} if this list changed as a result of the call
* @throws IndexOutOfBoundsException {@inheritDoc}
* @throws NullPointerException if the specified collection is null
*/
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index);
Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false;
Node<E> pred, succ;
if (index == size) {
succ = null;
pred = last;
} else {
succ = node(index);
pred = succ.prev;
}
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null)
first = newNode;
else
pred.next = newNode;
pred = newNode;
}
if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
可以看到,首先使用checkPositionIndex
检查index
,也就是size
是否合法;然后集合c
转为一个名为a
的Object[]
,然后将其长度传给newNum
,判断是否为0;然后确定要插入的位置,使用for-each
循环遍历要插入的元素,挨个插入即可;
三种方法运行所需时间测试
我们分别用这三种增加元素的方法来测试一下插入十万条数据需要多长时间:
int max = 100000;
LinkedList<String> list1 = new LinkedList<>();
long startTime1 = System.currentTimeMillis();
for (int i = 0; i < max; i++) {
list1.add("a");
}
long endTime1 = System.currentTimeMillis();
System.out.println("The time needed using add(element) is: "+(endTime1 - startTime1));
LinkedList<String> list2 = new LinkedList<>();
long startTime2 = System.currentTimeMillis();
for (int i = 0; i < max; i++) {
list2.add(0, "a");
}
long endTime2 = System.currentTimeMillis();
System.out.println(("The time needed using add(index, element) is: "+(endTime2 - startTime2)));
LinkedList<String> list3 = new LinkedList<>();
long startTime3 = System.currentTimeMillis();
list3.addAll(list1);
long endTime3 = System.currentTimeMillis();
System.out.println(("The time needed using addAll is: "+(endTime3 - startTime3)));
运行结果如下:
The time needed using add(element) is: 3
The time needed using add(index, element) is: 3
The time needed using addAll is: 7
结论:
- 使用
add(element)
方法需要大概3ms时间,几乎与ArrayList
使用该方法时间一致,这是因为与ArrayList
的该方法相近,在插入数据的过程中仅仅在尾部插入即可; - 使用
add(index, element)
方法需要的时间仅需3ms,远远小于ArrayList
的近400ms,是因为我们不需要像ArrayList
那样,每次插入都需要将所有数据复制并移动到后一位,再插入数据,还需要不断进行扩容操作,我们只需要在要插入的位置与前后两个元素建立连接即可; - 使用
addAll
方法所需时间接近7ms,该方法相较于前两个方法,所做判断较多;
结论
ArrayList
应尽量避免扩容操作,因为扩容操作需复制一次旧数据,所需时间较长;ArrayList
应尽量避免在非尾部位置插入数据,因为会导致插入点之后的所有数据复制,并向后移位,所需时间非常长;- 对于需要经常在中间新增元素的场景,应尽量使用
LinkedList
来实现,所需时间复杂度较低;