1 Overview
This article mainly describes the similarities and differences between ArrayList
and LinkedList
, as well as the underlying implementation of the two (environment OpenJDK 11.0.12
).
2 The difference between the two
Before introducing the underlying implementation of the two in detail, let's take a brief look at the similarities and differences between the two.
2.1 Similarities
- Both implement the
List
interface, and both inheritAbstractList
(LinkedList
indirect inheritance,ArrayList
direct inheritance) - Both are thread unsafe, both are asynchronous
- Both have addition, deletion, search and modification methods
2.2 Differences
- The underlying data structures are different:
ArrayList
based onObject[]
array,LinkedList
based onLinkedList.Node
doubly linked list - The random access efficiency is different:
ArrayList
random access can achieveO(1)
(implementRandomAccess
), because the element can be found directly through the subscript, whileLinkedList
needs to be traversed from the head pointer, and the time isO(n)
- The initialization operation is different:
ArrayList
is initialized to an empty array when initialized with no parameters, whileLinkedList
does not need - Expansion:
ArrayList
will expand when the length is not enough to accommodate new elements, whileLinkedList
will not - Memory space occupation: The memory space occupation of
ArrayList
is mainly reflected in that a certain capacity space will be reserved at the end of the array, while the space cost ofLinkedList
is mainly reflected in that each element needs to consume more space thanArrayList
3 ArrayList
bottom layer
3.1 Basic structure
The bottom layer is implemented using Object[]
array, and the member variables are as follows:
private static final long serialVersionUID = 8683452581122892189L;
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = new Object[0];
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];
transient Object[] elementData;
private int size;
private static final int MAX_ARRAY_SIZE = 2147483639;
The default initialized capacity is 10, followed by two empty arrays for use by the default constructor and constructors with initialized capacity:
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else {
if (initialCapacity != 0) {
throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
}
this.elementData = EMPTY_ELEMENTDATA;
}
}
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
Here are some more important methods, including:
add()
remove()
indexOf()/lastIndexOf()/contains()
3.2 add()
add()
There are four methods:
add(E e)
add(int index,E e)
addAll(Collection<? extends E> c)
addAll(int index, Collection<? extends E> c
3.2.1 Single element add()
Let's take a look at add(E e)
and add(int index,E eelment)
first:
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length) {
elementData = this.grow();
}
elementData[s] = e;
this.size = s + 1;
}
public boolean add(E e) {
++this.modCount;
this.add(e, this.elementData, this.size);
return true;
}
public void add(int index, E element) {
this.rangeCheckForAdd(index);
++this.modCount;
int s;
Object[] elementData;
if ((s = this.size) == (elementData = this.elementData).length) {
elementData = this.grow();
}
System.arraycopy(elementData, index, elementData, index + 1, s - index);
elementData[index] = element;
this.size = s + 1;
}
add(E e)
actually calls a private method. After judging whether expansion is required, add it directly to the end. And add(int index,E element)
will first check whether the subscript is legal, and if it is legal, then determine whether it needs to be expanded, then call System.arraycopy
to copy the array, and finally assign the value and add 1 to the length.
Regarding System.arraycopy
, the official documents are as follows:
There are a total of 5 parameters:
- The first parameter: the original array
- The second parameter: the position where the original array needs to start copying
- The third parameter: the target array
- Fourth parameter: copy to the beginning of the target array
- The fifth parameter: the number of copies to be copied
That is:
System.arraycopy(elementData, index, elementData, index + 1, s - index);
The function is to "move backwards" the elements of the original array behind index
, freeing up a position for index
to insert.
3.2.2 addAll()
Let's take a look at the two addAll()
:
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
++this.modCount;
int numNew = a.length;
if (numNew == 0) {
return false;
} else {
Object[] elementData;
int s;
if (numNew > (elementData = this.elementData).length - (s = this.size)) {
elementData = this.grow(s + numNew);
}
System.arraycopy(a, 0, elementData, s, numNew);
this.size = s + numNew;
return true;
}
}
public boolean addAll(int index, Collection<? extends E> c) {
this.rangeCheckForAdd(index);
Object[] a = c.toArray();
++this.modCount;
int numNew = a.length;
if (numNew == 0) {
return false;
} else {
Object[] elementData;
int s;
if (numNew > (elementData = this.elementData).length - (s = this.size)) {
elementData = this.grow(s + numNew);
}
int numMoved = s - index;
if (numMoved > 0) {
System.arraycopy(elementData, index, elementData, index + numNew, numMoved);
}
System.arraycopy(a, 0, elementData, index, numNew);
this.size = s + numNew;
return true;
}
}
In the first addAll
, first determine whether expansion is required, and then directly call the target set to add to the tail. In the second addAll
, due to an additional subscript parameter, the processing steps are slightly more:
- First determine whether the subscript is legal
- Then decide if you need to expand
- Then calculate whether it is necessary to "move backward" the original array elements, that is,
if
inSystem.arraycopy
- Finally, copy the target array to the specified
index
location
3.2.3 Expansion
The above add()
methods all involve expansion, which is the grow
method. Let's take a look:
private Object[] grow(int minCapacity) {
return this.elementData = Arrays.copyOf(this.elementData, this.newCapacity(minCapacity));
}
private Object[] grow() {
return this.grow(this.size + 1);
}
private int newCapacity(int minCapacity) {
int oldCapacity = this.elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity <= 0) {
if (this.elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(10, minCapacity);
} else if (minCapacity < 0) {
throw new OutOfMemoryError();
} else {
return minCapacity;
}
} else {
return newCapacity - 2147483639 <= 0 ? newCapacity : hugeCapacity(minCapacity);
}
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) {
throw new OutOfMemoryError();
} else {
return minCapacity > 2147483639 ? 2147483647 : 2147483639;
}
}
grow()
first calculates the capacity to be expanded through newCapacity
, then calls Arrays.copyOf
to copy the old elements, and overwrites the return value to the original array. And in newCapacity
, there are two variables:
newCapacity
: The new capacity, the default is 1.5 times the old capacity, that is, the default expansion is 1.5 timesminCapacity
: Minimum required capacity
If the minimum capacity is greater than or equal to the new capacity, it is one of the following:
- Array initialized by default construction: returns
minCapacity
with the max of 10 - Overflow: throw
OOM
directly - Otherwise return the minimum capacity value
If not, judge whether the new capacity has reached the maximum value (here is a little curious why MAX_ARRAY_SIZE
is not used, guess it is a problem of decompilation), if it does not reach the maximum value, return the new capacity, if the maximum value is reached, call hugeCapacity
.
hugeCapacity
will also first judge whether the minimum capacity is less than 0, and throw OOM
if it is less than, otherwise it will be Integer.MAX_VALUE
with the maximum value ( MAX_ARRAY_SIZE
MAX_ARRAY_SIZE
.
3.3 remove()
remove()
contains four methods:
remove(int index)
remove(Object o)
removeAll()
removeIf()
3.3.1 Single element remove()
That is, remove(int index)
and remove(Object o)
:
public E remove(int index) {
Objects.checkIndex(index, this.size);
Object[] es = this.elementData;
E oldValue = es[index];
this.fastRemove(es, index);
return oldValue;
}
public boolean remove(Object o) {
Object[] es = this.elementData;
int size = this.size;
int i = 0;
if (o == null) {
while(true) {
if (i >= size) {
return false;
}
if (es[i] == null) {
break;
}
++i;
}
} else {
while(true) {
if (i >= size) {
return false;
}
if (o.equals(es[i])) {
break;
}
++i;
}
}
this.fastRemove(es, i);
return true;
}
Among them, the logic of remove(int index)
is relatively simple. First check the validity of the subscript, then save the value that needs remove
, and call fastRemove()
to remove it. In remove(Object o)
, the array is traversed directly, and it is judged whether there is a corresponding element, if not Return false
directly, if it exists, call fastRemove()
, and return true
.
Take a look at fastRemove()
below:
private void fastRemove(Object[] es, int i) {
++this.modCount;
int newSize;
if ((newSize = this.size - 1) > i) {
System.arraycopy(es, i + 1, es, i, newSize - i);
}
es[this.size = newSize] = null;
}
First, increase the number of modifications by 1, then decrease the length of the array by 1, and judge System.arraycopy
the new length is the last one. A value that comes out is set to null
.
3.3.2 removeAll()
public boolean removeAll(Collection<?> c) {
return this.batchRemove(c, false, 0, this.size);
}
boolean batchRemove(Collection<?> c, boolean complement, int from, int end) {
Objects.requireNonNull(c);
Object[] es = this.elementData;
for(int r = from; r != end; ++r) {
if (c.contains(es[r]) != complement) {
int w = r++;
try {
for(; r < end; ++r) {
Object e;
if (c.contains(e = es[r]) == complement) {
es[w++] = e;
}
}
} catch (Throwable var12) {
System.arraycopy(es, r, es, w, end - r);
w += end - r;
throw var12;
} finally {
this.modCount += end - w;
this.shiftTailOverGap(es, w, end);
}
return true;
}
}
return false;
}
removeAll
actually calls batchRemove()
. In batchRemove()
, there are four parameters with the following meanings:
Collection<?> c
: target setboolean complement
: If the value istrue
, it means that the elements contained in the target setc
in the array are retained, and if it isfalse
, it means that the elements contained in the target setc
in the array are deletedfrom/end
: Interval range, left closed and right open
So the passed (c,false,0,this.size)
means to delete the element in the target set c
in the array. The following is a brief description of the steps to perform:
- First perform the empty operation
- Then find the first element that meets the requirements (here is to find the first element that needs to be deleted)
- After finding it, continue to search backward from the element, and record the subscript
w
of the last element in the deleted array. try/catch
is a protective behavior, becausecontains()
will useAbstractCollection
in the implementation ofIterator
. Here,catch
still callsSystem.arraycopy
after an exception, so that the processed elements are "moved" to the front- Finally, the number of modifications will be increased and
shiftTailOverGap
will be called, which will be explained in detail later
3.3.3 removeIf()
public boolean removeIf(Predicate<? super E> filter) {
return this.removeIf(filter, 0, this.size);
}
boolean removeIf(Predicate<? super E> filter, int i, int end) {
Objects.requireNonNull(filter);
int expectedModCount = this.modCount;
Object[] es;
for(es = this.elementData; i < end && !filter.test(elementAt(es, i)); ++i) {
}
if (i < end) {
int beg = i;
long[] deathRow = nBits(end - i);
deathRow[0] = 1L;
++i;
for(; i < end; ++i) {
if (filter.test(elementAt(es, i))) {
setBit(deathRow, i - beg);
}
}
if (this.modCount != expectedModCount) {
throw new ConcurrentModificationException();
} else {
++this.modCount;
int w = beg;
for(i = beg; i < end; ++i) {
if (isClear(deathRow, i - beg)) {
es[w++] = es[i];
}
}
this.shiftTailOverGap(es, w, end);
return true;
}
} else if (this.modCount != expectedModCount) {
throw new ConcurrentModificationException();
} else {
return false;
}
}
In removeIf
, to delete the elements that meet the conditions, the null operation will be performed first, and then the subscript of the first element that meets the conditions will be found. If it is not found ( i>=end
), it will be judged whether there is a concurrent operation problem. If not, return false
. i<end
, that is to officially enter the deletion process:
- Record start subscript
beg
deathRow
is a marker array with a length of(end-i-1)>>6 + 1
. Starting frombeg
, if a qualified element is encountered, the subscript will be marked (callsetBit
)- Delete after marking. The so-called deletion is actually moving the elements that do not meet the conditions to the positions after
beg
one by one. - Call
shiftTailOverGap
to process the elements at the end - Returns
true
, indicating that there is an element that meets the conditions and has been deleted
3.3.4 shiftTailOverGap()
The above removeAll()
and removeIf()
both involve shiftTailOverGap()
. Let's take a look at the implementation:
private void shiftTailOverGap(Object[] es, int lo, int hi) {
System.arraycopy(es, hi, es, lo, this.size - hi);
int to = this.size;
for(int i = this.size -= hi - lo; i < to; ++i) {
es[i] = null;
}
}
This method moves the elements in the es
array forward by hi-lo
bits, and sets the extra elements at the end after the move to null
.
3.4 indexOf()
series
include:
indexOf()
lastIndexOf()
contains()
3.4.1 indexOf
public int indexOf(Object o) {
return this.indexOfRange(o, 0, this.size);
}
int indexOfRange(Object o, int start, int end) {
Object[] es = this.elementData;
int i;
if (o == null) {
for(i = start; i < end; ++i) {
if (es[i] == null) {
return i;
}
}
} else {
for(i = start; i < end; ++i) {
if (o.equals(es[i])) {
return i;
}
}
}
return -1;
}
indexOf()
is actually a packaged method. It will call the internal indexOfRange()
to search. The logic is very simple. First, judge whether the value to be searched is empty. If it is not empty, use equals()
==
judge. mark, otherwise return -1
.
3.4.2 contains()
contains()
is actually a indexOf()
:
public boolean contains(Object o) {
return this.indexOf(o) >= 0;
}
Call indexOf()
method, and judge whether it is greater than or equal to 0 according to the returned subscript.
3.4.3 lastIndexOf()
The implementation of lastIndexOf()
is similar to that of indexOf()
, except that the traversal starts from the tail, and the internal call is lastIndexOfRange()
:
public int lastIndexOf(Object o) {
return this.lastIndexOfRange(o, 0, this.size);
}
int lastIndexOfRange(Object o, int start, int end) {
Object[] es = this.elementData;
int i;
if (o == null) {
for(i = end - 1; i >= start; --i) {
if (es[i] == null) {
return i;
}
}
} else {
for(i = end - 1; i >= start; --i) {
if (o.equals(es[i])) {
return i;
}
}
}
return -1;
}
4 LinkedList
bottom layer
4.1 Basic structure
First look at the member variables inside:
transient int size;
transient LinkedList.Node<E> first;
transient LinkedList.Node<E> last;
private static final long serialVersionUID = 876323262645176354L;
One for the length, a head pointer and a tail pointer.
Among them, LinkedList.Node
implemented as follows:
private static class Node<E> {
E item;
LinkedList.Node<E> next;
LinkedList.Node<E> prev;
Node(LinkedList.Node<E> prev, E element, LinkedList.Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
It can be seen that LinkedList
is actually implemented based on a double linked list.
Here are some more important methods, including:
add()
remove()
get()
4.2 add()
add()
methods include 6:
add(E e)
add(int index,E e)
addFirst(E e)
addLast(E e)
addAll(Collection<? extends E> c)
addAll(int index, Collection<? extends E> c)
4.2.1 linkFirst
implemented by add()
/ linkLast
/ linkBefore
Let's take a look at the relatively simple four add()
:
public void addFirst(E e) {
this.linkFirst(e);
}
public void addLast(E e) {
this.linkLast(e);
}
public boolean add(E e) {
this.linkLast(e);
return true;
}
public void add(int index, E element) {
this.checkPositionIndex(index);
if (index == this.size) {
this.linkLast(element);
} else {
this.linkBefore(element, this.node(index));
}
}
It can be seen that the above four add()
do not perform any operation of adding elements, add()
is only the encapsulation of adding elements, the real implementation of add
operation is linkLast()
, linkFirst()
and linkBefore()
, these methods as the name suggests is to link elements , or the front of a node in the linked list:
void linkLast(E e) {
LinkedList.Node<E> l = this.last;
LinkedList.Node<E> newNode = new LinkedList.Node(l, e, (LinkedList.Node)null);
this.last = newNode;
if (l == null) {
this.first = newNode;
} else {
l.next = newNode;
}
++this.size;
++this.modCount;
}
private void linkFirst(E e) {
LinkedList.Node<E> f = this.first;
LinkedList.Node<E> newNode = new LinkedList.Node((LinkedList.Node)null, e, f);
this.first = newNode;
if (f == null) {
this.last = newNode;
} else {
f.prev = newNode;
}
++this.size;
++this.modCount;
}
void linkBefore(E e, LinkedList.Node<E> succ) {
LinkedList.Node<E> pred = succ.prev;
LinkedList.Node<E> newNode = new LinkedList.Node(pred, e, succ);
succ.prev = newNode;
if (pred == null) {
this.first = newNode;
} else {
pred.next = newNode;
}
++this.size;
++this.modCount;
}
The implementation is roughly the same, one is added to the tail, one is added to the head, and one is inserted to the front. In addition, all three have the following operations at the end of the method:
++this.size;
++this.modCount;
The first one indicates that the number of nodes is increased by 1, while the second one indicates that the number of modifications to the linked list is increased by 1.
For example, at the end of the unlinkLast
method, there is the following code:
--this.size;
++this.modCount;
The operation of unlinkLast
is to remove the last node. When the number of nodes is reduced by 1, the number of modifications to the linked list is increased by 1.
On the other hand, in general, the linked list insertion operation needs to find the position of the linked list, but in the three link
methods, there is no code for for
to loop to find the insertion position. Why?
Since the head and tail pointers are saved, linkFirst()
and linkLast()
do not need to traverse to find the insertion position, but for linkBefore()
, it is necessary to find the insertion position, but linkBefore()
does not have parameters such as "insertion position/insertion subscript", and is only one element value and one successor node. In other words, this successor node is the insertion position obtained through the loop, for example, the calling code is as follows:
this.linkBefore(element, this.node(index));
It can be seen that in this.node()
, a subscript is passed in and a successor node is returned, that is, the traversal operation is completed in this method:
LinkedList.Node<E> node(int index) {
LinkedList.Node x;
int i;
if (index < this.size >> 1) {
x = this.first;
for(i = 0; i < index; ++i) {
x = x.next;
}
return x;
} else {
x = this.last;
for(i = this.size - 1; i > index; --i) {
x = x.prev;
}
return x;
}
}
Here, we first judge which side the subscript is on. If it is close to the head, traverse backwards from the head pointer. If it is close to the tail, traverse backwards from the tail pointer.
4.2.2 Traversal implemented addAll()
public boolean addAll(Collection<? extends E> c) {
return this.addAll(this.size, c);
}
public boolean addAll(int index, Collection<? extends E> c) {
this.checkPositionIndex(index);
Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0) {
return false;
} else {
LinkedList.Node pred;
LinkedList.Node succ;
if (index == this.size) {
succ = null;
pred = this.last;
} else {
succ = this.node(index);
pred = succ.prev;
}
Object[] var7 = a;
int var8 = a.length;
for(int var9 = 0; var9 < var8; ++var9) {
Object o = var7[var9];
LinkedList.Node<E> newNode = new LinkedList.Node(pred, o, (LinkedList.Node)null);
if (pred == null) {
this.first = newNode;
} else {
pred.next = newNode;
}
pred = newNode;
}
if (succ == null) {
this.last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
this.size += numNew;
++this.modCount;
return true;
}
}
First of all, you can see that the two addAll
actually call the same method. The steps are briefly described as follows:
- First judge whether the subscript is legal by
checkPositionIndex
- Then convert the target collection to
Object[]
array - Perform some special judgment processing to determine whether the range of
index
is inserted in the middle or at the end for
loop through the target array and insert into the linked list- Modify the node length and return
4.3 remove()
Similar to add()
, remove
includes:
remove()
remove(int index)
remove(Object o)
removeFirst()
removeLast()
removeFirstOccurrence(Object o)
removeLastOccurrence(Object o)
Of course, there are actually two removeAll
and removeIf
, but they are actually the methods of the parent class, so I won't analyze them here.
4.3.1 unlinkFirst()
implemented by remove()
/ unlinkLast()
remove()
, removeFirst()
, removeLast()
are actually removed by calling unlinkFirst()
/ unlinkLast()
, where remove()
is just an alias for removeFirst()
:
public E remove() {
return this.removeFirst();
}
public E removeFirst() {
LinkedList.Node<E> f = this.first;
if (f == null) {
throw new NoSuchElementException();
} else {
return this.unlinkFirst(f);
}
}
public E removeLast() {
LinkedList.Node<E> l = this.last;
if (l == null) {
throw new NoSuchElementException();
} else {
return this.unlinkLast(l);
}
}
The logic is very simple, call unlinkFirst()
/ unlinkLast()
after it is empty:
private E unlinkFirst(LinkedList.Node<E> f) {
E element = f.item;
LinkedList.Node<E> next = f.next;
f.item = null;
f.next = null;
this.first = next;
if (next == null) {
this.last = null;
} else {
next.prev = null;
}
--this.size;
++this.modCount;
return element;
}
private E unlinkLast(LinkedList.Node<E> l) {
E element = l.item;
LinkedList.Node<E> prev = l.prev;
l.item = null;
l.prev = null;
this.last = prev;
if (prev == null) {
this.first = null;
} else {
prev.next = null;
}
--this.size;
++this.modCount;
return element;
}
In these two unlink
, since the positions of the head pointer and the tail pointer have been saved, they can be removed directly in O(1)
, and finally the length of the node is decreased by 1, the number of modifications is increased by 1, and the old element is returned.
4.3.2 unlink()
implemented by remove()
Let's take another look at remove(int index)
, remove(Object o)
, removeFirstOccurrence(Object o)
, removeLastOccurrence(Object o)
:
public E remove(int index) {
this.checkElementIndex(index);
return this.unlink(this.node(index));
}
public boolean remove(Object o) {
LinkedList.Node x;
if (o == null) {
for(x = this.first; x != null; x = x.next) {
if (x.item == null) {
this.unlink(x);
return true;
}
}
} else {
for(x = this.first; x != null; x = x.next) {
if (o.equals(x.item)) {
this.unlink(x);
return true;
}
}
}
return false;
}
public boolean removeFirstOccurrence(Object o) {
return this.remove(o);
}
public boolean removeLastOccurrence(Object o) {
LinkedList.Node x;
if (o == null) {
for(x = this.last; x != null; x = x.prev) {
if (x.item == null) {
this.unlink(x);
return true;
}
}
} else {
for(x = this.last; x != null; x = x.prev) {
if (o.equals(x.item)) {
this.unlink(x);
return true;
}
}
}
return false;
}
These methods actually call unlink
to remove elements, of which removeFirstOccurrence(Object o)
equivalent to remove(Object o)
, let's talk about remove(int index)
first, the logic of this method is relatively simple, first check the validity of the subscript, then find the node by subscript and perform unlnk
.
In remove(Object o)
, it is necessary to first judge whether the value of the element is null
. The two loops are actually equivalent, and the first encountered element with the same target value will be removed. In removeLastOccurrence(Object o)
, the code is roughly the same, except that remove(Object o)
traverses from the head pointer, and removeLastOccurrence(Object o)
traverses from the tail pointer.
It can be seen that these remove
methods actually find the node to be deleted, and finally call unlink()
to delete it. Let's take a look at unlink()
:
E unlink(LinkedList.Node<E> x) {
E element = x.item;
LinkedList.Node<E> next = x.next;
LinkedList.Node<E> prev = x.prev;
if (prev == null) {
this.first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
this.last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
--this.size;
++this.modCount;
return element;
}
The implementation logic is similar to unlinkFirst()
/ unlinkLast()
. The deletion is performed in O(1)
. There are only some simple special judgment operations. Finally, the length of the node is decreased by 1, and the number of modifications is increased by 1, and finally the old value is returned.
4.4 get()
get
method is relatively simple and provides three external methods:
get(int index)
getFirst()
getLast()
Among them, getFirst()
and getLast()
save the head and tail pointers. After the special judgment, O(1)
returns directly:
public E getFirst() {
LinkedList.Node<E> f = this.first;
if (f == null) {
throw new NoSuchElementException();
} else {
return f.item;
}
}
public E getLast() {
LinkedList.Node<E> l = this.last;
if (l == null) {
throw new NoSuchElementException();
} else {
return l.item;
}
}
And get(int index)
undoubtedly takes O(n)
time:
public E get(int index) {
this.checkElementIndex(index);
return this.node(index).item;
}
get(int index)
judges the subscript, the actual operation is this.node()
. Since this method finds the corresponding node by subscript, the source code is also written before, so it will not be analyzed here, and it will take O(n)
time.
5 Summary
ArrayList
based onObject[]
,LinkedList
based on double linked listArrayList
random access efficiency is higher thanLinkedList
LinkedList
provides more insertion methods thanArrayList
, and the head and tail insertion efficiency is higher thanArrayList
- The two methods of removing elements are not exactly the same,
ArrayList
provides uniqueremoveIf()
, whileLinkedList
provides uniqueremoveFirstOccurrence()
andremoveLastOccurrence()
- The
ArrayList
method ofget()
is alwaysO(1)
, while theLinkedList
has onlygetFirst()
/getLast()
isO(1)
- The two core methods in
ArrayList
aregrow()
andSystem.arraycopy
, the former is the expansion method, the default is 1.5 times the expansion, the latter is the copy array method, which is anative
method, and operations such as insertion, deletion, etc. need to be used - Many methods in
LinkedList
require special judgment on the head and tail, which is simpler to create thanArrayList
, does not require initialization, and does not involve expansion issues
6 Appendix: An Experiment on Insertion and Deletion
Regarding insertion and deletion, it is generally believed that the efficiency of LinkedList
is higher than that of ArrayList
, but this is not the case. The following is a small experiment to test the insertion and deletion time.
6.1 Test Environment
JDK
:OpenJDK 11.0.12
JMH
:JMH 1.33
- System:
Manjaro 21.1.2
- Test array length:
5w
,50w
,500w
- Test method:
List.add(int,E),list.remove(int)
- Insertion and deletion of subscripts: randomly generated
- Test Metrics: Average Time
- with preheat
- single thread
6.2 Code
package com.company;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Threads(1)
@State(Scope.Benchmark)
public class Main {
private static final Random random = new Random();
private static final List<Integer> data_50_000 = Stream.generate(random::nextInt).limit(50_000).collect(Collectors.toList());
private static final List<Integer> data_500_000 = Stream.generate(random::nextInt).limit(500_000).collect(Collectors.toList());
private static final List<Integer> data_5_000_000 = Stream.generate(random::nextInt).limit(5_000_000).collect(Collectors.toList());
private static final String ARRAY_LIST_50_000 = "1";
private static final String ARRAY_LIST_500_000 = "2";
private static final String ARRAY_LIST_5_000_000 = "3";
private static final String LINKED_LIST_50_000 = "4";
private static final String LINKED_LIST_500_000 = "5";
private static final String LINKED_LIST_5_000_000 = "6";
@Param({ARRAY_LIST_50_000, ARRAY_LIST_500_000, ARRAY_LIST_5_000_000, LINKED_LIST_50_000, LINKED_LIST_500_000, LINKED_LIST_5_000_000})
private String type;
private List<Integer> list;
private int size;
@Setup
public void setUp() {
switch (type) {
case ARRAY_LIST_50_000:
list = new ArrayList<>(data_50_000);
size = 50_000;
break;
case ARRAY_LIST_500_000:
list = new ArrayList<>(data_500_000);
size = 500_000;
break;
case ARRAY_LIST_5_000_000:
list = new ArrayList<>(data_5_000_000);
size = 5_000_000;
break;
case LINKED_LIST_50_000:
list = new LinkedList<>(data_50_000);
size = 50_000;
break;
case LINKED_LIST_500_000:
list = new LinkedList<>(data_500_000);
size = 500_000;
break;
case LINKED_LIST_5_000_000:
list = new LinkedList<>(data_5_000_000);
size = 5_000_000;
break;
default:
throw new IllegalArgumentException("Illegal argument exception:" + type);
}
}
@Benchmark
public void add() {
int index = random.nextInt(size);
int element = random.nextInt();
list.add(index, element);
}
@Benchmark
public void remove() {
int n = list.size();
if (n > 0) {
list.remove(random.nextInt(n));
} else {
switch (type) {
case ARRAY_LIST_50_000:
list = new ArrayList<>(data_50_000);
break;
case ARRAY_LIST_500_000:
list = new ArrayList<>(data_500_000);
break;
case ARRAY_LIST_5_000_000:
list = new ArrayList<>(data_5_000_000);
break;
case LINKED_LIST_50_000:
list = new LinkedList<>(data_50_000);
break;
case LINKED_LIST_500_000:
list = new LinkedList<>(data_500_000);
break;
default:
list = new LinkedList<>(data_5_000_000);
break;
}
list.remove(random.nextInt(list.size()));
}
}
public static void main(String[] args) throws RunnerException {
new Runner(new OptionsBuilder().include(Main.class.getSimpleName()).build()).run();
}
}
Test Results:
Benchmark (type) Mode Cnt Score Error Units
Main.add 1 avgt 5 0.147 ± 0.083 ms/op
Main.add 2 avgt 5 0.159 ± 0.117 ms/op
Main.add 3 avgt 5 1.949 ± 0.102 ms/op
Main.add 4 avgt 5 0.274 ± 0.010 ms/op
Main.add 5 avgt 5 0.916 ± 0.507 ms/op
Main.add 6 avgt 5 4.199 ± 0.477 ms/op
Main.remove 1 avgt 5 0.001 ± 0.001 ms/op
Main.remove 2 avgt 5 0.010 ± 0.001 ms/op
Main.remove 3 avgt 5 1.844 ± 0.213 ms/op
Main.remove 4 avgt 5 0.010 ± 0.001 ms/op
Main.remove 5 avgt 5 0.107 ± 0.097 ms/op
Main.remove 6 avgt 5 3.934 ± 0.124 ms/o
Among them, type
of 1-6
corresponds to 5w
and LinkedList
of ArrayList
, 50w
, 500w
, respectively.
Although this experiment has certain limitations, it also proves that the insertion and deletion performance of ArrayList
is not worse than that of LinkedList
. In fact, from the source code (see the above analysis), we can know that the main time-consuming of ArrayList
insertion and deletion is System.arraycopy
, while the main time-consuming of LinkedList
is this.node()
. In fact, both require O(n)
time.
As for why the insertion and deletion speed of ArrayList
is faster than that of LinkedList
, the author guesses that the speed of System.arraycopy
is faster than the loop traversal speed of LinkedList
in for
, because the position of insertion/deletion in LinkedList
is found through this.node()
for
is implemented in a loop (of course, the bottom layer is to first determine which side it is on, if it is close to the head, it will start from the head, and if it is close to the tail, it will start from the tail). The native C++
method implementation relative to System.arraycopy
may be slower than C++
, thus causing the speed difference.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。