上一篇分析了ArrayList的源码部分,基本了解了ArrayList实际是对数组的增删改查进行了包装,而且支持动态扩容。但是ArrayList和数组一样,同样存在查找快,增删慢的特点。
有没有容器更适合用来插入和删除的元素呢,链表就是其中一种适合增删的数据结构,而LinkedList就是链表结构应用之一。
在研读源码之前,先来了解链表这种数据结构,常见的链表有单向链表,双向链表,循环链表,链表的结构包括:头节点,尾节点,和维护节点关联关系的指针。和数组相比:
- 存储空间:数组对于内存的要求比较高,需要一块连续的内存空间来存储,如果你申请的一个100M大小的数组,当内存中没有连续的,足够大的存储空间时,即便剩余内存可用空间大于100MB,申请仍然会是失败的。而链表是一种松散的数据结构,对内存没有连续性要求,节点是通过指针关联的;
- 性能:数组特点查询快,增删慢,查询是根据数据下标,增删可能涉及耗时的数据搬移;链表是查询慢,增删快,查询需要从头来遍历链表,增删之需要修改节点之间的指针关系;
继承体系:
LinkedList继承了抽象类AbstractSequentialList,实现了基本的容器操作方法,实现List接口,Deque接口,Cloneable接口,java.io.Serializable支持序列化
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;//当前链表尾节点,尾节点的特点:前驱指针指向上一个节点,后继指针为null
//transient关键字标记的成员变量不参与序列化过程。
构造方法:相比于ArrayList,LinkedList只有两个构造方法,一个无参的构造方法,另一个是包含集合collection元素的列表。
//无参构造方法
public LinkedList() {
}
//包含指定collection元素的列表
public LinkedList(Collection<? extends E> c) {
this();
//addAll方法先会把Collection转化成数组
addAll(c);
}
在LinkedList中有一个重要的内部类也就是节点类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;
}
}
重点方法分析:
在链表尾部添加元素方法:add(E e);
在链表的尾部添加元素,分两种情况,当尾节点不为空时,说明链表中已存在数据,只需要在将当前链表的尾节点作为新节点的前驱指针,尾节点的后继指针指向新节点,新增节点作为尾节点;尾节点为空,也就是说,链表为空时,新加入的节点即使首节点也是尾节点。
public boolean add(E e) {
linkLast(e);
return true;
}
//如果末尾节点为空,则新增元素为尾节点,否则将原尾节点的下一个元素节点指向新增元素
//尾节点的特点:后继指针为null
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++;//大小增加1
modCount++;//修改次数增加1
}
在指定位置添加元素:add(int index, E element)
首先校验插入的位置是否在链表中;判断插入的位置,如果插入位置在链表的末尾,则在链表末尾追加元素;否则通过二分查找先从首节点开始遍历,如果从未找到元素,则从尾节点开始遍历找倒序查找;将新节点追加到指定位置之前,修改指针指向。
public void add(int index, E element) {
//校验下标的合法性
checkPositionIndex(index);
//如果插入位置等于链表大小,那么就在文章末尾插入元素
if (index == size)
linkLast(element);
else
//在中间节点位置插入元素
linkBefore(element, node(index));
}
//找到指定位置的节点,从这段代码中可以看出,找到指定位置的节点元素,
//是要循环遍历这个链表,不过这里已经采用了二分查找优化了查找的算法
Node<E> node(int 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;
}
}
//插入新元素的方法
void linkBefore(E e, Node<E> succ) {
//获取旧元素的前驱节点
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++;//链表大小加1
modCount++;//修改次数+1
}
获取指定位置的元素:get(int index),校验索引是否越界,通过二分查找遍历链表
public E get(int index) {
//校验索引是否越界
checkElementIndex(index);
return node(index).item;
}
//找到指定位置的节点,从这段代码中可以看出,找到指定位置的节点元素,
//是要循环遍历这个链表,不过这里已经采用了二分查找优化了查找的算法
Node<E> node(int 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;
}
}
修改固定位置的元素:set(int index, E element) 节点位置不变,只修改元素的值。
public E set(int index, E element) {
//校验索引合法性
checkElementIndex(index);
//获取指定位置的元素(同一个方法,不在赘述)
Node<E> x = node(index);
//获取旧的元素值
E oldVal = x.item;
//将新的元素值赋值给节点信息
x.item = element;
return oldVal;
}
清除链表:clear(),循环遍历,直到每个节点的关联关系都被打破。
//编列所有元素,将对于的节点信息置为null,大小修改为0
public void clear() {
for (Node<E> x = first; x != null; ) {
Node<E> next = x.next;
x.item = null;
x.next = null;
x.prev = null;
x = next;
}
first = last = null;
size = 0;
modCount++;
}
默认删除的是首节点,返回删除元素:remove(),默认移除首节点的关联关系
public E remove() {
return removeFirst();
}
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
//移除首节点,将首节点的下驱节点置为首节点
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
还有队列的的方法push(E e)和pop(),其实就是对首节点和尾节点的操纵,先进先出
public void push(E e) {
addFirst(e);
}
public void addFirst(E e) {
linkFirst(e);
}
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
public E pop() {
return removeFirst();
}
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
总结看,LinkedList是一个更适合增删元素的容器,查找元素从需要遍历元素,采用二分查询算法,最坏情况时间复制度是O(logn),新删元素的时间复制度为O(1)。
备注:JDK1.6之前为循环链表,JDK1.7以后取消循环,源码展示为JDK版本:1.8.0_241