前言
之前已经写了几篇有关Java集合的文章:
今天我们来介绍一下另外一个容器类:LinkedList
。
正文
LinkedList
和ArrayList
一样是集合List的实现类,虽然较之ArrayList,其使用场景并不多,但同样有用到的时候,那么接下来,我们来认识一下它。
public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable{//底层是双向链表
//元素数量
transient int size = 0;
//第一个结点
transient Node<E> first;
//最后一个结点
transient Node<E> last;
.....
其实LinkedList
底层使用双向链表实现的,可以看到上面有first
和last
两个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;
}
很简单,学过链表的同学应该都很清楚。
那首先我们还是来看看构造函数:
public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
addAll(Collection<? extends E> c)
方法用来添加指定的集合数据到LinkedList
中。
辅助函数
在去看LinkedList
的get
、add
等方法前,我们先去看下几个比较重要的辅助函数:
- 首先是第一个辅助函数
linkFirst(E e)
,该方法用于插入元素到链表头部。
/*
* 插入元素到头部
*/
private void linkFirst(E e) {
final Node<E> f = first;
// 设置newNode的前结点为null,后结点为f
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
// 首先链接元素,同时把newNode设为最后一个结点
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
回忆一下内部类 Node
的构造函数:Node(Node<E> prev, E element, Node<E> next)
,三个参数分别前一个结点、元素值以及下一个结点。
从上面可以看出在插入元素到链表头部其实创建一个Node
结点,让其 next 指针指向链表首结点first
。
- 插入元素到链表尾部:
/*
* 插入元素到尾部
*/
void linkLast(E e) {
final Node<E> l = last;
// 设置newNode的前结点为l,后结点为null
final Node<E> newNode = new Node<>(l, e, null);
// 新结点变成最后一个结点
last = newNode;
// 若l == null说明是首次链接元素,将first也指向新结点
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
// 修改次数+1
modCount++;
}
插入到尾部和上面插入到头部的方法也有异曲同工之妙,这里就不再细说。
- 接着我们要给定结点前插入元素
/*
* 在给定结点前插入元素e
*/
void linkBefore(E e, Node<E> succ) {
final Node<E> pred = succ.prev;
// 设置newNode的前结点为pred,后结点为succ
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)//如果succ是头结点,将newNode设置为头结点
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
很简单,就是让给定结点succ
的 prev 指针和前一个结点的 next 指针都指向我们的新结点就可以了。
- 那如何删除一个结点呢?
/*
* 删除链表结点
*/
E unlink(Node<E> x) {
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;//GC回收
}
//判断是否是尾结点
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;//GC回收
}
x.item = null;//GC回收
size--;
modCount++;
return element;
}
注意这里我们删除结点 x 后要把x的前后引用包括自身引用都设为null,这样JVM垃圾回收才会帮我们去回收x,否则会有内存泄漏的隐患。
还有最后一个辅助函数,用来获取指定位置的结点。
/*
*返回指定位置的结点
*/
Node<E> node(int index) {
//根据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;
}
}
这里有一个值得我们借鉴的:就是会根据索引的位置来判读是从头部还是尾部遍历,这也是一个性能的小优化。
有了辅助函数后我们就可以来看看我们平常使用的比较多的API了
首先来看看get
和set
方法:
get(int index)
方法用来获取指定位置上的元素
/*
* 返回指定位置元素
*/
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
可以看到先调用checkElementIndex
方法校验参数,然后调用辅助方法node(int index)
获取元素值。
/*
* 设置元素
*/
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
set(int index, E element)
方法用来设置元素,比较简单不再细说。
接下来看看另外两个方法:
peek
方法用于返回头结点,而pool
用于删除头结点并返回头结点的值。
/*
* 返回头结点值,如果链表为空则返回null
*/
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
/*
* 删除头结点并返回头结点值,如果链表为空则返回null
*/
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
现在来看看如何删除一个结点:
/*
* 默认删除头结点
*/
public E remove() {
return removeFirst();
}
/*
* 删除指定位置结点
*/
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
/*
* 删除指定结点
*/
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
其实只要搞懂上面几个辅助函数后,其它的增删查改就很简单了。
继续来看另外一个index(Object o)
方法,该方法用于返回指定对象在链表中的索引。
/*
* 返回指定对象在链表中的索引(如果没有则返回-1)
* lastIndexOf同理(其实就是从后向前遍历)
*/
public int indexOf(Object o) {
int index = 0;
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
return -1;
}
现在我们如何把LinkedList
转成数组返回呢?来看看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;
}
还有一个toArray(T[] a)
方法,可以返回指定类型的数组:
/*
* 将链表包装成指定类型数组返回
*/
public <T> T[] toArray(T[] a) {
if (a.length < size)//给点的数组长度小于链表长度
//创建一个类型与a一样,长度为size的数组
a = (T[])java.lang.reflect.Array.newInstance(a.getClass().getComponentType(), size);
int i = 0;
Object[] result = a;//定义result指向给定数组,修改result == 修改a
//依次把结点值放入result数组
for (Node<E> x = first; x != null; x = x.next)
result[i++] = x.item;
if (a.length > size)
a[size] = null;
return a;
}
迭代器
LinkedList
和ArrayList
还有一个比较大的区别是LinkedList
除了iterator
方法之外还有一个listIterator
方法,该迭代器除了向后遍历数据外,也可以向前遍历,正是由于底层的双向链表结构才能实现。
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) {
next = (index == size) ? null : node(index);//index默认为0
nextIndex = index;
}
/*判断是否有下一个元素*/
public boolean hasNext() {
return nextIndex < size;
}
/*向后遍历,返回越过的元素*/
public E next() {
checkForComodification();//fail-fast
if (!hasNext())
throw new NoSuchElementException();
lastReturned = next;
next = next.next;
nextIndex++;
return lastReturned.item;
}
/*判断是否有上一个元素*/
public boolean hasPrevious() {
return nextIndex > 0;
}
/*向前遍历,返回越过的元素*/
public E previous() {
checkForComodification();//fail-fast
if (!hasPrevious())
throw new NoSuchElementException();
lastReturned = next = (next == null) ? last : next.prev;//调用previous后lastReturned = next
nextIndex--;
return lastReturned.item;
}
/*返回下一个越过的元素索引*/
public int nextIndex() {
return nextIndex;
}
/*返回上一个越过的元素索引*/
public int previousIndex() {
return nextIndex - 1;
}
/*删除元素*/
public void remove() {
checkForComodification();//fail-fast
if (lastReturned == null)
throw new IllegalStateException();
Node<E> lastNext = lastReturned.next;
unlink(lastReturned);//从链表中删除lastReturned,modCount++(该方法会帮你处理结点指针指向)
if (next == lastReturned)//调用previous后next == lastReturned
next = lastNext;
else
nextIndex--;
lastReturned = null;//GC
expectedModCount++;
}
/*设置元素*/
public void set(E e) {
if (lastReturned == null)
throw new IllegalStateException();
checkForComodification();//fail-fast
lastReturned.item = e;
}
/*插入元素*/
public void add(E e) {
checkForComodification();//fail-fast
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();
}
/*fail-fast*/
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
总结
今天有关LinkedList
的源码分就暂时先到这里,其实如果理解了链表结构那么上面源码应该不是很难,如果有什么不对的地方请多多指教。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。