一.LinkedList的關係依賴
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
繼承了一個類,實現了四個接口
AbstractSequentialList抽象類,是AbstractList的子類。此類提供了List接口的基本實現,以最大程度地減少由順序訪問數據存儲(例如鏈表)支持的實現此接口所需的工作。對於隨機訪問數據(例如數組),應優先使用AbstractList。
List接口,有序集合。允許重複的元素,也允許多個空元素,擁有List集合的常用方法。
Deque接口,是Queue的子接口,支持在兩端插入和刪除元素的線性集合。
Cloneable接口和Serializable接口都是標記接口,言外之意這兩個接口沒有內容。分別進行拷貝和序列化,這裏不做詳細概述。
注:不過需要注意的是,要使用Object的clone()方法必須實現Cloneable接口,否則會報java.lang.CloneNotSupportedException的異常。
類圖
二.屬性
/**
* 集合元素的個數
*/
transient int size = 0;
/**
* 指向第一個節點的指針
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transient Node<E> first;
/**
* 指向最後一個節點的指針
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*/
transient Node<E> last;
三.構造方法
/**
* 無參構造
*/
public LinkedList() {
}
/**
* 構造一個包含特定集合的list,其元素按照迭代器的順序返回。
*
* @param c 集合的元素都要放到list中
* @throws NullPointerException 如果這個集合爲空,拋出空指針異常
*/
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
四.代碼分析
1.節點類
/**
* 節點類
*
* @param <E>
*/
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.添加元素
(1).添加元素
/**
* 將元素插入到list尾部
* <p>
* 此方法與addLast方法調用的是一個方法
*/
@Override
public boolean add(E e) {
linkLast(e);
return true;
}
LinkedList默認將元素添加到列表尾部,所以add方法與addLast方法的作用是一樣的。下面來看一下linkLast方法。
/**
* 在鏈表尾部添加數據
*/
private void linkLast(E e) {
//獲取當前尾部節點
final Node<E> l = last;
//聲明新節點
final Node<E> newNode = new Node<>(l, e, null);
//使新節點成爲新的尾部節點
last = newNode;
//如果l爲空,那麼是空鏈表。新節點即是鏈表的頭部節點,也是鏈表的尾部節點
if (l == null)
{
first = newNode;
} else
//否則,將新節點指向之前尾部元素的後繼節點
{
l.next = newNode;
}
//元素個數和鏈表修改次數進行計數
size++;
modCount++;
}
因爲想在鏈表的尾部添加新元素,所以需要先拿到尾部節點的信息,存在l中。然後創建要添加的元素的節點,其中前驅節點就是剛纔的l,元素爲e,後繼節點爲null,因爲在鏈表的尾部。接着把新節點變成鏈表的尾部元素。剩下最後一個步驟,將二者進行關聯。判斷之前的尾部節點是否爲空,如果爲空那麼新節點的元素是空鏈表的唯一元素,否則,將新節點與之前的尾部節點進行關聯即可。隨後更新元素個數和鏈表修改的次數。
(2).在鏈表頭部插入新元素
/**
* 將元素插入到鏈表頭部
*/
@Override
public void addFirst(E e) {
linkFirst(e);
}
linkFirst具體內容如下:
/**
* 在鏈表頭部添加數據
*/
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++;
}
與上面的添加元素的方法類似
(3).在鏈表的特定位置插入元素
/**
* 在鏈表的特定位置插入元素
* 之前該位置的元素及後面的元素均要右移
*
* @param index 指定鏈表的位置
* @param element 元素
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
@Override
public void add(int index, E element) {
//校驗索引位置是否合法,即index >= 0 && index <= size。不合法會報出索引越界的異常。
checkPositionIndex(index);
//如果索引位置爲鏈表的size,那麼添加的位置便是鏈表尾部
if (index == size)
{
linkLast(element);
} else
//在非空節點succ之前插入新節點
{
linkBefore(element, node(index));
}
}
在鏈表的指定位置添加元素,首先需要判斷索引的位置是否合法,即在0和size之間,否則報出越界的異常。然後判斷索引位置是否爲size,如果爲size,只需調用linkLast方法,在鏈表尾部添加新節點即可;如果不爲size,還需調用linkBefore方法。下面來看一下linkBefore方法。
/**
* 在非空節點succ之前插入新元素
*/
void linkBefore(E e, Node<E> succ) {
//獲取succ的前驅節點
final Node<E> pred = succ.prev;
//聲明新節點
final Node<E> newNode = new Node<>(pred, e, succ);
//使新節點成爲succ節點的前驅節點
succ.prev = newNode;
//判空
if (pred == null)
{
first = newNode;
} else
{
pred.next = newNode;
}
//更新計數
size++;
modCount++;
}
這幾個添加方法本質上是一樣的,理解其一,其他的也就都懂了。
(4).將集合作爲參數,添加到鏈表中
/**
*
* 內容有些深奧,尚未消化
*
* @param c 集合
* @return {@code true} if this list changed as a result of the call
* @throws NullPointerException 如果集合爲空,空指針異常
*/
@Override
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
3.刪除元素
(1).刪除元素
默認刪除鏈表的頭結點,與removeFirst方法作用一樣。
/**
* 刪除鏈表頭元素
*
* @return 鏈表頭元素
* @throws NoSuchElementException 如果鏈表爲空
* @since 1.5
*/
@Override
public E remove() {
return removeFirst();
}
不進行傳參的remove方法,默認刪除鏈表的頭結點,不過需要進行向上轉型。ArrayList的remove方法必須傳參。
/**
* 從列表中刪除並返回第一個元素
*
* @return 被刪除的節點
* @throws NoSuchElementException 如果鏈表爲空
*/
@Override
public E removeFirst() {
//將鏈表的頭指針傳給f
final Node<E> f = first;
//如果節點爲空
if (f == null) {
throw new NoSuchElementException();
}
return unlinkFirst(f);
}
unlinkFirst方法具體內容如下:
/**
* 刪除非空頭結點
*/
private E unlinkFirst(Node<E> f) {
// 確定f爲非空頭結點
//獲取被刪除頭結點的元素
final E element = f.item;
//獲取頭結點的下一個節點
final Node<E> next = f.next;
//清空該節點的元素和後繼指針
f.item = null;
f.next = null;
//f後繼節點成爲新的頭節點
first = next;
//後繼節點爲空,那麼該鏈表是空鏈表
if (next == null) {
last = null;
} else {
//否則,新的前驅節點置空
next.prev = null;
}
//更新計數
size--;
modCount++;
//返回被刪除的元素
return element;
}
(2).刪除指定位置的元素
/**
* 刪除並返回指定位置的元素
*
* @param index 索引位置
* @return 被刪除的元素
* @throws IndexOutOfBoundsException 索引越界
*/
@Override
public E remove(int index) {
//是否越界
checkElementIndex(index);
return unlink(node(index));
}
/**
* 刪除非空節點
*/
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;
}
(3).刪除鏈表中第一次出現的元素
/**
* 刪除鏈表中第一次出現的元素,如果該元素存在。如果該元素不存在,那麼鏈表沒有變化
*
* @param o 要被刪除的鏈表元素。
* @return {@code true} 刪除成功返回真,否則返回假
*/
@Override
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;
}
判斷要刪除的元素是否爲空,然後遍歷鏈表,找到爲空的元素後調用unlink方法就行刪除;如果不爲空,遍歷列表,如果與鏈表中的元素相等,那麼就行刪除。總的來說都用調用unlink方法,那麼這個判斷有什麼作用呢?我還不是很懂。希望有大佬指點一二。
(4).清空鏈表中的全部元素
/**
* 清空鏈表的全部元素
*/
@Override
public void clear() {
//遍歷鏈表
for (Node<E> x = first; x != null; ) {
//先獲取x的後繼節點,便於清空x後進行操作
Node<E> next = x.next;
//將x節點置空
x.item = null;
x.next = null;
x.prev = null;
//指向x的後繼節點,繼續遍歷
x = next;
}
//清空鏈表之後,頭結點,尾節點清空
first = last = null;
//更新計數
size = 0;
modCount++;
}
4.替換鏈表特定位置的元素
/**
* 替換指定位置的元素
*
* @param index 索引
* @param element 要替換的元素
* @return 被替換的元素
* @throws IndexOutOfBoundsException 索引越界
*/
@Override
public E set(int index, E element) {
//校驗是否越界
checkElementIndex(index);
//獲取索引對應位置的節點信息
Node<E> x = node(index);
//替換元素
E oldVal = x.item;
x.item = element;
//返回被替換的舊元素
return oldVal;
}
核心方法是node(index):
/**
* 返回索引位置的節點
*/
Node<E> node(int index) {
//>>位運算符,右移1位,即除2
//如果索引小於鏈表容量的一半
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;
}
}
根據JDK8的源碼進行解讀,依照添刪改查的順序簡單的就行了梳理,方便自己的理解和學習。由於部分代碼還不是很明白,後期還會就行更深入的解析。
前路漫漫,編程作伴 --by mirror6