类图
一, 要分析 LinkedList, 我们最好先了解一些背景知识.
1, LinkedList 和ArrayList类似, 是 List的一个实现. 因此, 我们可以大概知道. LinkedList实现的功能和ArrayList 是类似的, 底层的结构不同
2, 链表的实现必须要有类似指针的对象, 指向下一个对象, 通过进入一个get方法, 可知. get首先获取的是一个 Node对象, 再从node对象获取我们存进去的元素, 所以, LinkedList每一个元素实际上是包装了我们存储的元素.
3, 从Node类中, 我们可以知道, Node有 next 和 prev , 所以 LinkedList实现的是一个 双向链表. 而链表还有一个特性, 需要指针来指明位置. (此处使用 first和last)否则无从访问
4, 当只有一个节点时, header即是头节点也是尾节点.即指明位置的指针first和last指向同一个元素.
二, 源码.
1, 构造函数. LinkedList基本是空构造, 没什么说的了. 要注意的是 LinkedList的节点Node. 创建一个新节点, 必须指定, 上一个节点和先一个节点.
/**
* 创建LinkedList
*/
public LinkedList() {
}
/**
* 创建包含集合c的 LinkedList
*/
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
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;
}
}
2, 主要属性
transient int size = 0; //元素个数
/**
* 头指针
*/
transient Node<E> first;
/**
* 尾指针
*/
transient Node<E> last;
和ArrayList类似, LinkedList 的父类继承了AbstractList, 因此也有一个 modCount. 来进行快速失败.
3, 方法
add方法
/**
* 将元素添加到链表尾部,该方法实际上和 addLast相同.
*/
public boolean add(E e) {
linkLast(e);
return true;
}
/**
* 将 e 包装为Node添加到链表尾部.
*/
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null); //创建新的Node, 将原先的尾部节点作为该节点的前节点. 下一个节点为null
last = newNode; //尾部节点修改为节点.
if (l == null) //如果尾部节点是空的,即没有尾部节点. 说明. 添加前链表是空的. 因此把 first 指针 指向 该节点.
first = newNode;
else //如果不为空. 建立双向连接, 把前节点的next指向新节点.(新节点指向前节点在new Node时就建立了)
l.next = newNode;
size++; //长度+1
modCount++; // 修改次数+1
}
我们再来看看 addFirst方法.
/**插入一个元素到链表头部
*/
public void addFirst(E e) {
linkFirst(e);
}
/**
* 把 e 作为头节点插入到链表.
* 基本逻辑. 把原链表.头节点的 prev 指针指向新节点.
*/
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f); //创建新节点, 并把新节点的 next指针指向. 原连表的头结点.
first = newNode; // 头指针指向新节点.
if (f == null) // 如果原头指针是null, 即没有头部节点, 说明, 添加元素前链表是空.
last = newNode; // 把尾指针也指向 新节点.
else //否则,说明原链表不为空, 那么. 建立原节点到新节点的指针,(新节点到原节点的指针在创建 new Node时就建立了)
f.prev = newNode;
size++; //长度+1
modCount++; //修改次数+1
}
我们再来看看, add(int index, E e) 方法, 和addFirst和类似的, 也是建立连接即可. 和 ArrayList不同的是. 由于通过指针而非数组(游标)建立连接.因此 linkedList无须考虑扩容.
public void add(int index, E element) {
checkPositionIndex(index); //检查位置是否合法. 即index>=0和 index<=size
if (index == size) // 如果index =size , 那么即addLast
linkLast(element);
else
linkBefore(element, node(index));
}
/**
* 返回一个 index 位置下的链表节点.
*/
Node<E> node(int index) {
if (index < (size >> 1)) { //优化效率. 按照逻辑, 如果 index 小于 size的 1/2, 那么从头指针开始取这个节点比较快, 否则, 从尾指针开始取这个节点比较快.
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插入到 succ几点前.和addFirst类似的.
*/
void linkBefore(E e, Node<E> succ) {
final Node<E> pred = succ.prev; //取的succ节点的 前一个节点.
final Node<E> newNode = new Node<>(pred, e, succ); // 创建新节点, prev指针指向 原succ的前节点, next指针指向 succ节点.
//下面的代码就是 重建 succ的 prev指针和 和 prev(原链表succ的前节点)的 next指针, 不赘述
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
再看看 remove(int index)方法.
public E remove(int index) {
checkElementIndex(index); //和add(index)类似, 检查index是否合法
return unlink(node(index)); //node(index)返回某个位置的节点, 详细看add(index)
}
/**
* 基本逻辑: 把 x 的前节点的next指向 x的后节点, 把x的后节点next指向前节点 . 并删除 x 的prev 和next指向(等待gc回收)
*/
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) { // 说明x是头节点
first = next;
} else { //不是头结点,则把x前节点的 next指向, x的next. 并把x.prev断开.
prev.next = next;
x.prev = null;
}
if (next == null) { // 说明x是尾节点
last = prev;
} else { //不是尾结点,则把x后节点的 pre指向, x的prev. 并把x.next断开.
next.prev = prev;
x.next = null;
}
x.item = null; //item置为null
size--; //长度减-1
modCount++; //修改次数+1
return element; //返回删除元素.
}
indexOf方法也是通过遍历的方式.
public int indexOf(Object o) {
if (o == null) { //null时无法通过equal来比较.
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
contains(Object o) 和 indexOf方法, 以及 add和remove的重载方法. 基本大同小异,不再说明. 迭代器和ArrayList基本是一样的道理. 也比较简单, 此处也不再赘述.
简单总结.
- 理解LinkedList最关键的一点其实就是理解**双向链表**,理解了双向链表, linkedList就基本没有什么问题。
- LinkedList 通过指针连接.添加元素时没有扩容这个说法。 无容量限制.
- 很多操作需要遍历链表. 并且通过指针取下一节点。 因此,查找来说效率比ArrayList较低,但是添加和删除时。 只需要断开指针并不需要移动数据, 效率比ArrayList较高。因此 对于大量的删除和修改,LinkedList比较合适。
- get(int index) 和 remove(int index) 至多需要遍历1/2的链表长度,contains(E e)、 remove(E e)可能需要遍历全部。