一、简介
可以先看下官方Api对LinkedList的介绍
从图中可以看出,LinkedList实现的接口有:
- 实现了Serializable是序列化接口,因此它支持序列化,能够通过序列化传输;
- 实现了Cloneable接口,能被克隆;
- 实现了Iterable接口,可以迭代器遍历;
- 实现了Collection,拥有集合操作的方法;
- 实现Deque/Queue可以当做对列/双端队列 使用;
- 实现了List接口,拥有增删改查等方法;
先看下LInkedList的特点,对LinkedList有 一个大体的认识:
- LinkedList底层数据结构时双向链表,但是头结点不存放数据,只有后置节点的引用;
- 集合中的元素允许为null,可以看到源码中在查找和删除时,都划分为该元素为null和不为null两种情况来处理;
- 允许内部元素重复;
- 不存在扩容问题,所以是没有扩容的方法;
- 元素在内部是有序存放的,依次在链表上添加节点;
- 实现了栈和队列的操作方法,因此也可以作为栈、队列和双端队列来使用;
- 由于是链表实现,并且没有实现RandomAccess,虽然在查找的时候,会先判断在前半部分或者后半部分,然后依次从前或者从后查找,但是查找效率还是很低,不过增删效率高,但是查找和修改大部分情况下不如ArrayList;
- 线程不安全,可以用个Collection.synchronizedList(new LinkedList())返回一个线程安全的LinkedList;
二、示例展示
LinkedList<String> list = new LinkedList<String>();
list.add("语文: 1");
list.add("数学: 2");
list.add("英语: 3");
三、源码分析
3.1 属性
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
transient int size = 0;
/**
* Pointer to first node.
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transient Node<E> first;
/**
* Pointer to last node.
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*/
transient Node<E> last;
}
前面讲了,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;
}
}
很明显的双向链表的结构。
3.2 构造方法
/**
* Constructs an empty list.
*/
public LinkedList() {
}
/**
* Constructs a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
*
* @param c the collection whose elements are to be placed into this list
* @throws NullPointerException if the specified collection is null
*/
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
构造方法很简单,没有什么特别的操作 。
3.3 添加方法——add()
不指定插入元素的下标(默认插入尾部)
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
linkLast(e);
return true;
}
/**
* Links e as last element.
*/
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++;
}
在指定位置添加
/**
* Inserts the specified element at the specified position in this list.
* Shifts the element currently at that position (if any) and any
* subsequent elements to the right (adds one to their indices).
*
* @param index index at which the specified element is to be inserted
* @param element element to be inserted
*/
public void add(int index, E element) {
// 检查插入的位置是否合法,及是否比0大,比当前size小
checkPositionIndex(index);
// 如果等于当前大小,就是相当于在尾部再插入一个节点
// 否则就是插入到index 所在位置的节点的前面
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
/**
* 返回指定索引处的一个非空节点
*/
Node<E> node(int index) {
// assert isElementIndex(index);
// 这里LinkedList做了一个优化,先判断索引是在前半部分和后半部分
// 如果在前半部分,从头节点开始找,正序找
// 如果在后半部分,从尾结点开始找,倒序找
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) {
// assert succ != null;
// 取出查找到指定位置的节点
final Node<E> pred = succ.prev;
// 构建新节点,前置节点找到节点的原前置节点,e是元素值,后置节点是根据位置找到的succ
final Node<E> newNode = new Node<>(pred, e, succ);
//原位置的前置节点设置为要插入的节点
succ.prev = newNode;
if (pred == null)
// 如果原位置的前置节点为空,即原位置succ是头节点,即add(0,E)然后把新建节点赋值为头节点
first = newNode;
else
// 不为null,原位置的前置节点的后置节点设置为新节点
pred.next = newNode;
size++;
modCount++;
}
先检查是否在可插入的范围内,不在则抛出异常,如果index和当前size相等,则直接插入到尾节点,如果小于当前size,那么久插入到index节点的前面。
addAll()
// 没有传入位置,直接加到最后
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
/**
* 加入置顶位置
*/
public boolean addAll(int index, Collection<? extends E> c) {
// 检查index合法性
checkPositionIndex(index);
// 传入的Collection转换成数组
Object[] a = c.toArray();
int numNew = a.length;
// 空数组,直接返回插入失败
if (numNew == 0)
return false;
// pred是succ的前置节点,succ指向当前需要插入节点的位置的节点
Node<E> pred, succ;
// index等于size,即在尾节点插入
// 不相等,找到需要插入的位置的节点,以及其前节点,pred可能为空
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);
// 前节点为null,则给first赋值为当前新节点
if (pred == null)
first = newNode;
else
// 不为null,pred后置节点设置为新节点
pred.next = newNode;
// 每次设置完,pred表示刚插入的节点,一次往后插入。
pred = newNode;
}
// 如果是从size位置开始添加,最后添加的节点成了尾节点
if (succ == null) {
last = pred;
} else {
// 如果不是从size开始添加,数组中最后一个元素的后置节点指向为原index位置节点
// 原index位置节点的前置节点置为数组中最后一个元素构建的节点。
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
addFirst 、addLast
/**
* 添加元素到链表头
*/
public void addFirst(E e) {
linkFirst(e);
}
/**
* Links e as first element.
*/
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 void addLast(E e) {
// linkLast()在上面介绍add()方法时已经讲了
linkLast(e);
}
3.4 删除——remove()
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;
}
/**
* Unlinks non-null node 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;
}
这两个删除的方法基本都是先找到要删除元素对应的节点,然后再去执行unlink()方法 对该节点前置节点、后置节点重新进行指向。然后把引用的元素置为null,便于gc回收,最后返回移除的元素。
此外还有移除第一个和最后一个
//移除第一个元素
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
// 移除第一个元素,调整指针指向,并把头部元素置空,便于gc回收
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;
}
// 移除最后一个元素
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
///移除最后一个元素,调整指针指向,并把尾部元素置空,便于 gc 回收
private E unlinkLast(Node<E> l) {
// assert l == last && l != null;
final E element = l.item;
final Node<E> prev = l.prev;
l.item = null;
l.prev = null; // help GC
last = prev;
if (prev == null)
first = null;
else
prev.next = null;
size--;
modCount++;
return element;
}
3.5 修改——set()
/**
* Replaces the element at the specified position in this list with the
* specified element.
*/
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
3.6 查找——get()
/**
* Returns the element at the specified position in this list.
*/
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
// 查找第一个元素
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
// 查找最后一个元素
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
3.7 队列
作为队列使用的一些方法
队列是什么?
- 队列是一种比较特殊的线性结构。它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作。进行插入操作的端称为队尾,进行删除操作的端为队头;
- 队列中最先插入的元素也将最先被删除,对应的最后插入的元素将最后被删除。因此队列又称为“先进先出”(FIFO)的线性表,与栈(FILO)刚好相反;
- 队列的抽象是Queue,而LinedList是实现了Deque接口,Deque又继承了Queue,所以LinkedList是可以当做队列来使用的。
先看下Queue接口:
public interface Queue<E> extends Collection<E> {
// 增加一个元素到队列尾部,如果队列有大小限制,并且队列已满,会抛出异常 IllegalArgumentException
boolean add(E e);
// 增加一个元素到队列尾部,和 add 不同的是:如果队列有大小限制,并且队列已满,则返回 false,不抛出异常
boolean offer(E e);
// 检索到队列头部元素,并且将其移出队列。和 poll 方法不同的是如果队列是空的,那么抛出 NoSuchElementException 异常
E remove();
// 检索到队列头部元素,并且将其移出队列。如果队列是空的,那么返回 null;
E poll();
// 检索队列头部的元素,并不会移除,和 peek 方法不同的是:如果队列是空的,那么抛出 NoSuchElementException 异常;
E element();
// 检索队列头部的元素,并不会移除,如果队列是空的,那么返回 null;
E peek();
}
下面介绍一下对应的方法:
add、offer
public boolean offer(E e) {
return add(e);
}
前面队列中的定义已经写了,在add会在队列满时会抛出异常,但是我们可以看到这个offer()方法也是调了add方法,只是对add的一种包装,实际使用效果是一样的。这是因为LinkedList中并没有设置大小的方法,所以也就不存在超出队列大小的限制。
remove、poll
public E remove() {
return removeFirst();
}
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
这两个方法也不会抛出异常。
remove会直接调用removeFirst从头部移除元素,并且在removeFirst方法移除的过程中可能抛出异常。
poll则先把头部元素取出来,进行判空。
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;
}
3.8 双向链表
双向队列是队列(Queue)的一个子接口Denque,双向队列两端的元素都可以入队列和出队列。可以实现先先进先出或者先进后出的数据结构。
如果把Deque限制为只能从一端入队和出队,那么就可以实现栈的数据结构,遵循先进后出的规则。
如果不对Deque进行限制,用做双线队列,那么就是先进先出。
主要方法如下:
public interface Deque<E> extends Queue<E> {
// 将指定元素插入此双端队列的开头(如果可以直接这样做而不违反容量限制)
void addFirst(E e);
//将指定元素插入此双端队列的末尾(如果可以直接这样做而不违反容量限制)。
void addLast(E e);
//在不违反容量限制的情况下,将指定的元素插入此双端队列的开头。
boolean offerFirst(E e);
// 在不违反容量限制的情况下,将指定的元素插入此双端队列的末尾。
boolean offerLast(E e);
// 获取并移除此双端队列第一个元素。
E removeFirst();
// 获取并移除此双端队列的最后一个元素。
E removeLast();
//获取并移除此双端队列的第一个元素;如果此双端队列为空,则返回 null。
E pollFirst();
//获取并移除此双端队列的最后一个元素;如果此双端队列为空,则返回 null。
E pollLast();
// 获取,但不移除此双端队列的第一个元素。
E getFirst();
// 获取,但不移除此双端队列的最后一个元素。
E getLast();
// 获取,但不移除此双端队列的第一个元素;如果此双端队列为空,则返回 null。
E peekFirst();
//获取,但不移除此双端队列的最后一个元素;如果此双端队列为空,则返回 null。
E peekLast();
//从此双端队列移除第一次出现的指定元素。
boolean removeFirstOccurrence(Object o);
// 从此双端队列移除最后一次出现的指定元素。
boolean removeLastOccurrence(Object o);
// *** Queue methods ***
// 将指定元素插入此双端队列所表示的队列(换句话说,此双端队列的尾部),如果可以直接这样做而不违反容量限制的话;
// 如果成功,则返回 true,如果当前没有可用空间,则抛出 IllegalStateException。
boolean add(E e);
// 将指定元素插入此双端队列所表示的队列(换句话说,此双端队列的尾部),如果可以直接这样做而不违反容量限制的话;
// 如果成功,则返回 true,如果当前没有可用的空间,则返回 false。
boolean offer(E e);
//获取并移除此双端队列所表示的队列的头部(换句话说,此双端队列的第一个元素)。
E remove();
//获取并移除此双端队列所表示的队列的头部(换句话说,此双端队列的第一个元素);如果此双端队列为空,则返回 null。
E poll();
//获取,但不移除此双端队列所表示的队列的头部(换句话说,此双端队列的第一个元素)。
E element();
//获取,但不移除此双端队列所表示的队列的头部(换句话说,此双端队列的第一个元素);如果此双端队列为空,则返回 null。
E peek();
// *** Stack methods ***
// 将一个元素推入此双端队列所表示的堆栈(换句话说,此双端队列的头部),如果可以直接这样做而不违反容量限制的话;
// 如果成功,则返回 true,如果当前没有可用空间,则抛出 IllegalStateException。
void push(E e);
// 从此双端队列所表示的堆栈中弹出一个元素
E pop();
// *** Collection methods ***
// 从此双端队列中移除第一次出现的指定元素
boolean remove(Object o);
// 是否包含一个元素
boolean contains(Object o);
// 队列大小
int size();
// 返回此双端队列的迭代器
Iterator<E> iterator();
// 返回一个迭代器,该迭代器具有此双端队列的相反顺序
Iterator<E> descendingIterator();
}
具体就不做记录。
四、总结
- LinkedList底层数据结构时双向链表,但是头节点不存放数据,只有后置节点的引用;
- 集合中的元素允许为null;
- 允许内部元素重复;
- 不存在扩容问题;
- 元素在内部是有序存放的,依次在链表上添加节点;
- 实现了栈和队列的操作方法,因此也可以作为栈、队列、双联队列来使用;
- 由于是链表实现,并且没有实现RandomAccess,虽然在查找的时候,会先判断是在前半部分或者后半部分,然后依次从前或者从后查找,但是查找效率还是很低,不过增删效率高,但是查找后修改大部分情况不如ArrayList;
- 线程不安全,可以用Collections.synchronizedList(new LinkedList())返回一个线程安全的LinkedList;
ArrayList与LinkedList性能比较ArrayList、LinkedList性能测试与分析」