最近在看集合这一块,都知道LinkedList是双链表结构,可以用来实现栈、队列、双端队列(因为LinkedList实现了Deque,deque 即双端队列,是一种具有队列和栈的性质的数据结构。双端队列中的元素可以从两端弹出,其限定插入和删除操作在表的两端进行)。LinkedList随机访问效率低,但是插入、随机删除速度快。
而ArrayList是动态数组结构,实现了RandomAccess类。它的随机访问效率高,但是插入、随机删除速度慢。
下面我们来一起分析下为什么(下面的源码是基于JDK1.8)
一、首先看LinkedList:
LinkedList linkedList=new LinkedList<>();
linkedList.add(0,"3");
public void add(int index, E element) {//插入元素的方法
checkPositionIndex(index);
if (index == size)//如果当前插入的元素,是插入到双链表的末尾
linkLast(element);
else//如果当前插入的元素,不是插入到双链表的末尾
linkBefore(element, node(index));
}
重点看一下linkLast和linkBefore
/**
* Links e as last element.
*/
void linkLast(E e) {//如果插入的位置是双链表的末尾
final Node l = last;
final Node newNode = new Node<>(l, e, null);//把元素封装成链表中的一个Node节点对象
last = newNode;//把新节点赋值给last作为最后一个节点
if (l == null)//如果双链表没插入该元素之前的最后一个节点为null,那么插入后,这个新节点就是第一个节点
first = newNode;
else//如果双链表没插入该元素之前最后一个节点不为null,那么之前的末尾节点的next插入的新节点
l.next = newNode;
size++;
modCount++;
}
/**
* Inserts element e before non-null Node succ.
*/
void linkBefore(E e, Node succ) {//如果插入的位置不是双链表的尾部
// assert succ != null;
final Node pred = succ.prev;
final Node newNode = new Node<>(pred, e, succ);//封装新的Node节点对象
succ.prev = newNode;//把新节点赋值给当前节点的前驱节点
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
由此可以看出,在LinkedList进行添加元素时,只是双链表的节点的前驱和后继指向做出了变化,没有过度消耗性能耗时的地方。
在这里需要注意的一点:双向链表查找index位置的节点时,有一个加速动作:若index < 双向链表长度的1/2,则从前向后查找; 否则,从后向前查找。
二、再看ArrayList:
ArrayList arrayList=new ArrayList<>();
arrayList.add(0,"2");
public void add(int index, E element) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
在这里我们重点关注System.arraycopy这个方法以及ensureCapacityInternal(size + 1)。
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
ensureCapacityInternal的作用是“确认ArrayList的容量,若容量不够,则增加容量。”在扩容的时候,用到了Arrays.copyOf方法,数组的拷贝。这个是一个比较耗时的操作。
真正耗时的操作是 System.arraycopy(elementData, index, elementData, index + 1, size - index);
Sun JDK包的java/lang/System.java中的arraycopy()声明如下:
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
arraycopy()是个JNI函数,它是在JVM中实现的。sunJDK中看不到源码。
实际上,我们只需要了解: System.arraycopy(elementData, index, elementData, index + 1, size - index); 会移动index之后所有元素即可。这就意味着,ArrayList的add(int index, E element)函数,会引起index之后所有元素的改变!
数组复制,我们看到了两个方法,Arrays.copyOf和System.arraycopy。下面我们先分析下这俩方法:
(1)System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length):
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
src - 源数组。
srcPos - 源数组中的起始位置。
dest - 目标数组。
destPos - 目标数据中的起始位置。
length - 要复制的数组元素的数量。
该方法是用了native关键字,调用的为C++编写的底层函数,可见其为JDK中的底层函数。
(2)再来看看Arrays.copyOf();该方法对于不同的数据类型都有相应的方法重载:
//复杂数据类型
public static T[] copyOf(U[] original, int newLength, Class newType) {
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
public static T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
original - 要复制的数组
newLength - 要返回的副本的长度
newType - 要返回的副本的类型
//基本数据类型(其他类似byte,short···)
public static int[] copyOf(int[] original, int newLength) {
int[] copy = new int[newLength];
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
观察其源代码发现copyOf(),在其内部创建了一个新的数组,然后调用arrayCopy()向其复制内容,返回出去。
总结:
1.copyOf()的实现是用的是arrayCopy();
2.arrayCopy()需要目标数组,对两个数组的内容进行可能不完全的合并操作。
3.copyOf()在内部新建一个数组,调用arrayCopy()将original内容复制到copy中去,并且长度为newLength。返回copy;
(1)我们先看LinkedList:
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
// 获取双向链表中指定位置的节点
Node node(int index) {
// assert isElementIndex(index);
// 获取index处的节点。
// 若index < 双向链表长度的1/2,则从前先后查找;
if (index < (size >> 1)) {
Node x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
通过get(int index)获取LinkedList第index个元素时。先是在双向链表中找到要index位置的元素;找到之后再返回。
双向链表查找index位置的节点时,有一个加速动作:若index < 双向链表长度的1/2,则从前向后查找; 否则,从后向前查找。
(2)再看ArrayList:
public E get(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
return (E) elementData[index];
}
因为ArrayList底层是一个数组,ArrayList取元素也就是通过索引从数组中取元素。通过get(int index)获取ArrayList第index个元素时。直接返回数组中index位置的元素,而不需要像LinkedList一样进行查找。
由上面的分析可知,LinkedList在元素的插入和删除时,比ArrayList的插入删除效率更高。而在查找元素时,ArrayList比LinkedList效率高。