上文我们查看ArrayList的源码(重拾Java之ArrayList源码阅读),接着我们来瞅瞅LinkedList有什么神奇之处。ArrayList的数据存储方式是数组,LinkedList里面储存数据的方式是链表,什么是链表了?你可以将其理解为一列火车,每一节车厢就是一个节点(Node)。Node是一个类,如下:
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;
}
}
item是存储的数据对象;prev是该节点指向的上一个节点;next是该节点指向的下一个节点。接着我们看看LinkedList的中的几个非常重要的全局变量:
//LinkedList中元素的个数
transient int size = 0;
//LinkedList中链表的头节点
transient Node<E> first;
//LinkedList中链表的尾节点
transient Node<E> last;
一、LinkedList的构造方法
无参数的就一个空方法,没啥好看的,我们看看其有参构造函数:
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
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;
}
addAll这个方法写的很出彩,考虑的非常的全面;这个方法是一个public方法,开发者可以直接外部调用的。首先该方法对添加的索引进行了异常的判断,接着对添加的集合进行了异常的判断。接着代码18行到25行考虑的很全面:当index==size 成立的时候表示新增的集合数据是从原来链表的链表的尾部开始添加的;不成立的时候表示是从链表的中间进行添加的。紧接着27到35行是向链表中添加数据。第30行成立的时候表示时原来的链表是空链表。37行到42行代码;当37行代码成立的时候表示的是向空链表中添加数据的,比如我们用集合初始化LinkedList。modCount和在ArrayList中一样,同样记录的是集合大小改变的次数。
二、LinkedList的增删改查以及遍历方法
1.增加数据 add方法
public boolean add(E e) {
linkLast(e);
return true;
}
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++;
}
可见这个方法非常的单纯的,首先创建了一个新的节点,然后判断last指向节点是不是为空。如果为空的话,那表示之前的链表是空的。
2.删除数据 remove方法
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
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;
}
}
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
remove方法首先检查要移除的节点的索引是否合法;接着使用node方法进行该索引位置节点的查找;node方法中 size>>1表示size/2;如果索引小于该值就从first指向的节点找,大于的话就从last指向的节点查找(有点二分法查找的意思)。然后使用unlink方法进行删除节点(其中该方法需要考虑,要删除的节点是头节点或者尾节点)。
3.修改数据 set方法
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
同样是先检查该索引是否合法;然后使用node方法找到该索引对应的节点。然后直接操作该节点即可。
4.查找数据 get方法
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
同样get方法,没什么亮点,非常的善良。
5.遍历数据 迭代器方法:
public ListIterator<E> listIterator(int index) {
checkPositionIndex(index);
return new ListItr(index);
}
private class ListItr implements ListIterator<E> {
private Node<E> lastReturned;
private Node<E> next;
private int nextIndex;
private int expectedModCount = modCount;
ListItr(int index) {
// assert isPositionIndex(index);
next = (index == size) ? null : node(index);
nextIndex = index;
}
public boolean hasNext() {
return nextIndex < size;
}
public E next() {
checkForComodification();
if (!hasNext())
throw new NoSuchElementException();
lastReturned = next;
next = next.next;
nextIndex++;
return lastReturned.item;
}
public boolean hasPrevious() {
return nextIndex > 0;
}
public E previous() {
checkForComodification();
if (!hasPrevious())
throw new NoSuchElementException();
lastReturned = next = (next == null) ? last : next.prev;
nextIndex--;
return lastReturned.item;
}
public int nextIndex() {
return nextIndex;
}
public int previousIndex() {
return nextIndex - 1;
}
public void remove() {
checkForComodification();
if (lastReturned == null)
throw new IllegalStateException();
Node<E> lastNext = lastReturned.next;
unlink(lastReturned);
if (next == lastReturned)
next = lastNext;
else
nextIndex--;
lastReturned = null;
expectedModCount++;
}
public void set(E e) {
if (lastReturned == null)
throw new IllegalStateException();
checkForComodification();
lastReturned.item = e;
}
public void add(E e) {
checkForComodification();
lastReturned = null;
if (next == null)
linkLast(e);
else
linkBefore(e, next);
nextIndex++;
expectedModCount++;
}
public void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (modCount == expectedModCount && nextIndex < size) {
action.accept(next.item);
lastReturned = next;
next = next.next;
nextIndex++;
}
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
比较简单,没啥亮点.
LinkedList实现了Deque接口,该接口是继承Queue接口的;所以LinkedList可以充当队列Queue的实现类。
public interface Queue<E> extends Collection<E> {
//添加元素
boolean add(E e);
//添加元素
boolean offer(E e);
//返回移除的元素,并将该元素移除
E remove();
//返回队列头部元素并将其删除
E poll();
//返回队列头部元素,不删除队列的头部元素
E element();
//返回队列头部元素,不删除队列的头部元素
E peek();
}
offer方法没啥亮点,其实就是调用add方法。我们看看poll、element、peek这三个方法,首先可以排除poll方法;它返回对头但是会把队头删掉。而element和peek方法不会。下面来看看element和peek方法的源码:
public E element() {
return getFirst();
}
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
可以看出这两个方法并没有太大的区别。仅仅只是element方法如果队列的头部是空的,即如果是空列表的话,会抛出异常!peek方法了?如果队列是空的话,只会返回null而不会抛出异常。类似的LinkedList中含有很多重载的方法;都差不多就不一一的分析,读者可以自己看看;当然还有一些JDK 1.8中新增的方法;这个和ArrayList中的类似,读者可以看我之前的分析ArrayList的源码的文章。
三、小结
到目前为止,我们已经分析完ArrayList和LinkedList的源码;对比之前的ArrayLsit源码,我们不难发现以前的结论:ArrayList做查询很快,但不是适合做删除和插入操作;LinkedList做查询操作比ArrayList慢,但是做插入和删除操作是优于ArrayLsit的。
备注:有什么写的有问题的,欢迎读者在评论区斧正,谢谢。为了我们面试不被面试官按到地上摩擦,我们还是要多看看源码。