上一篇中我们大致介绍了ArrayList的优点和隐藏的,不容易被发现的弊端。但是这一篇,我们还要再对ArrayList批判一番。
又因为它是数组,当我们需要往列表最后丢一个数据的时候很简单,但是如果要往中间丢呢?方法大家肯定都想到了。挪呗!后面的各位同学让让,挤个人进来:
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++;
}
先看一下这个人是不是真的进来的地方对,别跑到很后面去了;再看看进来以后地方还够不够了,不够还得扩容(grow),然后不好意思,这人位置后面的全部copy往后移一位。这个System.arraycopy和Arrays.copyof可不一样,它不会新建一个新的数组对象,但是会挨个去赋值交换,就跟我们自己写for循环,arr[i+1]=arr[i]一样。极端一点的情况,假如我们要往List的头部插一个数据(虽然ArrayList并没有addFirst方法),那就得把后面所有的数据都挨个移位!
而addall是怎么操作的呢?
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
int numMoved = size - index;
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
一样是移,只不过这次不是一个人了,我得算算要进来几个人。怎么算呢,先用toArray,再获取length。这个toArray也是一比开销,如果原来传进来的也是个ArrayList还好,偷懒copyof就行了;如果不是,那就还得先转换成数组,再移位,再复制,是不是头都大了?
同理,如果我想从列表里删掉一个或者几个节点,那么后面的也得统统移位,这个操作量就很大了。所以这里我们隆重推荐ArrayList的一个兄弟:LinkedList。
不难想到,LinkedList的构造函数中不用去指定默认大小了。它里面的数据结构也不是数组了,而是节点(Node)。别误会,这个Node可不是xml的,也跟org.w3c.dom半毛钱关系都没有。这个Node是LinkedList的一个内部类:
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
每个节点记录了:它自己保存的内容,指向它的前一个和后一个节点的引用(或者说,指针)。
对LinkedList的add和remove操作,实际上是通过一系列link和unlink进行操作的,这些方法有:linkFirst、linkLast、linkBefore(Node)、unlinkFirst、unLinkLast、unLink(Node)。他们实际做的,就是修改节点中指向前一个和后一个的指针。
我们用add(E, index)举例子:
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
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++;
}
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;
}
}
首先判断了一下下标是否合法,然后看是不是在队尾(是则视作linkLast),然后调用了一个node方法去取得某个下标的节点。这个方法中实际上也进行了一遍遍历,所以开销其实也是不小的。然后就调用linkBefore的方法,修改了这个插入的节点之前节点的”向后的指针“让它指向自己,修改这个插入的节点之后节点的”向前的“节点让它也指向自己,自己的两个指针则指向前后两个节点,这样这个节点就算插入进去了。
有点绕是不是?鉴于我的绘图水平有限,建议看不懂的去搜一下链表的图,很简单就懂了。
那么有人问了最后那个节点呢?它的往下一个节点的指针指向啥?null呗。
从上面这个例子我们可以看出,对一个LinkedList的头和尾进行数据操作是很高效的,因为只需要改改指针就行了。但是如果要往中间增删节点,由于有一个遍历过程,效率就没那么高了,但是仍然优于ArrayList(因为不需要进行大规模的数据迁徙),而addAll方法需要先把传入的集合变化成数组,再往里插,效率会更加低一些,和ArrayList孰优孰劣我也没验证过,大家有兴趣可以去试一下。
由于LinkedList往头尾增删数据很方便这种特性,我们可以用它模拟栈(stack)这种数据结构,实际上LinkedList也提供了一系列的方法,其中就有栈操作的push和pop:
public void push(E e) {
addFirst(e);
} //往头(栈顶)上插个数据(压栈)
public E pop() {
return removeFirst();
} //删除并返回头上的数据(出栈)
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
} //返回但不删除头上的数据
public E peekFirst() {
final Node<E> f = first;
return (f == null) ? null : f.item;
} //等于peek
public E peekLast() {
final Node<E> l = last;
return (l == null) ? null : l.item;
} //返回但不删除尾巴的数据
public boolean offer(E e) {
return add(e);
} //等同于add,再尾部添加数据
public boolean offerFirst(E e) {
addFirst(e);
return true;
} //addFirst 不多解释了,返回值不同而已
public boolean offerLast(E e) {
addLast(e);
return true;
}//类比上面
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
} //其实就是pop 写法不同而已
public E pollFirst() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}//你懂的
public E pollLast() {
final Node<E> l = last;
return (l == null) ? null : unlinkLast(l);
}//你懂的
public E element() {
return getFirst();
}//getFirst换个名字而已=_= 用来instanceof名字更直观?
其他的不再赘述了,大家可以自己去翻源代码。另外,它的toArray就比较痛苦了:
public Object[] toArray() {
Object[] result = new Object[size];
int i = 0;
for (Node<E> x = first; x != null; x = x.next)
result[i++] = x.item;
return result;
}