概述
LinkedList
是基于节点实现的 双向 链表的 List,每个节点都指向前一个和后一个节点从形成链表。
类图
从图中我们可以看出, LinkedList
实现了4个接口和继承了1个抽象类,4个接口分别为:
-
List
接口,主要提供添加、删除、修改、迭代遍历等操作。 -
Cloneable
接口,代表LinkedList
支持克隆。 -
Serializable
接口,表示ArrayList支持序列化的功能。 -
Deque
接口,提供 双端 队列的功能,LinkedList
支持快速的在头尾添加和读取元素。
LinedList
所实现的接口相比于 ArrayList
来说,少了一个 RandomAccess
接口,说明 LinkedList
并不支持随机访问;同时多了一个 Deque
接口,新增了双端队列的能力。
继承的抽象类为 AbstactSequentialList
,它是 AbstractList
的子类,实现了本来只能 顺序 访问的数据存储结构(例如:链表)的 get(int index)、 add(int index, E element)
等 随机 操作的方法。这句话可能有点绕,稍加解释一番:链表是一种只能顺序访问的数据存储结构,而 AbstractSequentialList
抽象类对这类只能 顺序访问/操作 的数据存储结构,也提供了类数组般的随机访问/操作 的 API
。感兴趣的朋友可以去看看 AbstractSequentialList
的源代码,其是基于迭代器顺序遍历(说到底还是需要遍历,只不过是它帮我们做了这一步~)实现的。
不过需要留意的是,LinkedList
和 LinkedList
一般,都结合了自身的特性大量重写了抽象类提供的方法实现;ArrayList
重写了 AbstractList
提供的方法实现, LinkedList
重写了 AbstactSequentialList
提供的方法实现。
一般情况下,对于支持随机访问的数据结构会继承 AbstractList
抽象类,不支持随机访问的则会继承 AbstactSequentialList
抽象类。
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
// .....
}
复制代码
属性
LinkedList
只有 3 个属性,分别为: 头节点 first
、 尾节点 last
以及代表数量的 size
。三者的关系如下图所示。
相信朋友们都接触过双向链表的概念,这里就不做过多的展开解释了。
具体源码如下:
/**
* 头节点
*/
transient Node<E> first;
/**
* 尾节点
*/
transient Node<E> last;
/**
* 数量
*/
transient int size = 0;
/**
* 底层实现是 双向链表
*/
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;
}
}
复制代码
构造方法
LinkedList
的构造方法只有2个:无参构造方法 public ListedList()
和指定集合的 public LinkedList(Collection<? extends E> c)
,具体代码如下:
/**
* 无参构造方法
*/
public LinkedList() {
}
/**
* 指定添加集合c的构造方法
* @param c
*/
public LinkedList(Collection<? extends E> c) {
this();
// 批量添加元素,后面在新增元素模块会详细介绍
addAll(c);
}
复制代码
相比于 ArrayList
来说, LinkedList
没有类似指定初始化容量的构造方法;造成这个区别的原因还是因为两者的底层实现方式不一样,LinkedList
是基于节点实现的双向链表,所以每次添加元素的时候直接 new
一个节点即可。
新增元素
新增元素的方法主要有如下4个,分别为:
-
public boolean add(E e)
在链表节点后 顺序 添加一个元素 -
public void add(int index, E element)
在指定index
位置添加元素element
-
public boolean addAll(Collection<? extends E> c)
在链表末尾 顺序 添加多个元素 -
public boolean addAll(int index, Collection<? extends E> c)
在指定index
位置添加多个元素
严格来说,LinkedList
的新增元素的方法还有另外 2 个,分别为: addFirst(E e)
和 addLast(E e)
;这两个方法是 LinkedList
作为实现双端队列 Deque
的产物,后面会对 LinkedList
作为双端队列的方法做专门的介绍。
public boolean add(E e)
继承于 AbstractList
抽象类,在链表节点后 顺序 添加一个元素,源码如下:
/**
* 在链表节点后顺序添加一个元素e
* @param e
* @return boolean 添加成功返回true
*/
@Override
public boolean add(E e) {
// 顺序添加单个元素到链表的末尾
linkLast(e);
return true;
}
/**
* 真实执行添加操作(往链表最后添加新的元素)
* @param e 添加元素 e
*/
void linkLast(E e) {
// 记录原 last 节点
final Node<E> l = last;
// Node类构造方法三个参数: Node<E> prev, E element, Node<E> next
// 所以也很好理解,新节点的前置pre节点是原来的last节点, e代表元素本身 新节点的next节点为null(不存在新的节点)
final Node<E> newNode = new Node<>(l, e, null);
// 更新last节点为最新的newNode
last = newNode;
// 双向链表,相互关联---新节点关联原last节点,原last节点也要关联新的节点
if (l == null) {
// l == null,说明是一个空的情况,上面已经将last指向新节点了,需要将first也指向新节点
first = newNode;
} else {
// 不为空,则说明链表中已有元素,直接将last节点和new节点做一个双向的绑定关系即可
l.next = newNode;
}
size++;
// 操作的次数++,全程似乎就在序列化和非序列化模块有瞅到这个东东被使用到
modCount++;
}
复制代码
代码理解起来不难,往链表末尾新增一个新的节点,可分3步走:
-
创建新节点
new Node<>(last, e, null)
,参数 1 为last
是说新节点的前置节点是原last节点,参数2 代表元素本身e;参数3 为null
说明新节点后面无其他节点 -
原last指针更新为新节点,这里分2种情况:
-
原last节点为空,说明链表还未存储过元素,这时候需要将first节点也指向新节点
-
原last节点不为空,说明链表已存储过元素,直接将原last节点的后置指针
next
指向新节点(因为其为双向链表)
-
-
更新
size
和 操作次数modCount
public void add(int index, E element)
继承于 AbstractSequentialList
抽象类,为以双向链表为底层存储结构的 LinkedList
赋予了类数组般随机访问的能力,具体代码如下:
/**
* 添加单个元素到指定位置
* @param index 下标
* @param element 元素本身
*/
@Override
public void add(int index, E element) {
// 参数index 有效性检查
checkPositionIndex(index);
// index从0开始,所以当index == size 说明是最后的位置,也即是如上面的在链表最后新增元素一样
if (index == size) {
linkLast(element);
} else {
// node(index) 获取index位置的node节点
linkBefore(element, node(index));
}
}
/**
* 添加元素e到 succ节点的前面
*
* pred ---> succ =====变成===> pred --->newNode ---> succ
*
* 步骤:
* 1.获取succ节点的前一个节点 pred
* 2.创建新的待插入节点newNode,指定其前置节点(pred)和后置节点(succ)
* 3.更新succ的前置节点为新节点newNode(双向绑定的体现)
* 4.更新pred的后置节点为新节点newNode(双向绑定的体现)
* 4.1如果pred为空,说明succ本身为头节点,换句话说相当于是在头节点之前插入新的节点,即更新first = newNode即可
* 4.2如果pred不为空,则正常的pred.next= newNoe即可
* @param e
* @param succ
*/
void linkBefore(E e, Node<E> succ) {
// 获取succ节点的前一个节点(因为方法的逻辑是:添加元素e到succ节点的前面)
final Node<E> pred = succ.prev;
// 创建新节点,并且本身关联前后节点(前节点: pred、 后节点:succ)
final Node<E> newNode = new Node<>(pred, e, succ);
// 双向绑定之画个图完事 succ.prev指向新的节点(在其前面)
succ.prev = newNode;
// 如果pred为空,说明succ本身就是为头节点,则更新first指向newNode作为新的首节点
if (pred == null) {
first = newNode;
} else {
// 直接双向绑定
pred.next = newNode;
}
size++;
modCount++;
}
/**
* 找到下标为index的节点
* 骚操作: 通过判断index是否大于size的一半,来判断是从头遍历还是从尾遍历
* @param index
* @return Node<E>
*/
Node<E> node(int index) {
// 不太明白为啥把这个断言注释了(源码注释的,不是我注释的~~), 这相当于是参数的合法性检查,
// assert isElementIndex(index);
Node<E> x;
// index 小于 size的一半,从头遍历更加的便捷
if (index < (size >> 1)) {
x = first;
for (int i = 0; i < index; i++) {
x = x.next;
}
} else {
// index 大于size的一半,从尾遍历更加的便捷
x = last;
for (int i = size - 1; i > index; i--) {
x = x.prev;
}
}
return x;
}
复制代码
代码的逻辑还是比较清晰的,相信小伙伴从代码的注释中都能看懂其逻辑,这里就不过度的展开了;值得一提(比较好玩)的是 node(int index)
找到下标为 index
的节点的方法实现;通过判断 index
是否大于 size
的一半,来判断是从头遍历还是从尾遍历,很细节。
public boolean addAll(Collection<? extends E> c)
public boolean addAll(Collection<? extends E> c)
方法继承于 AbstractCollection
抽象类,往集合后面添加多个元素,源码如下:
/**
* 顺序添加多个元素
* @param c
* @return boolean
*/
@Override
public boolean addAll(Collection<? extends E> c) {
// addAll(int index, c) 在指定index位后添加多个元素
return addAll(size, c);
}
复制代码
这个方法调用的是 public boolean addAll(int index, Collection<? extends E> c)
来实现自己的功能,所以我们直接看真正干活的方法吧~~
public boolean addAll(int index, Collection<? extends E> c)
public boolean addAll(int index, Collection<? extends E> c)
方法继承于 AbstractSequentialList
抽象类,赋予了 LinkedList
类似 ArrayList
一般 随机 添加元素的能力,具体源码如下:
/**
* 在指定index位添加集合元素
* index从0开始算起,并且index位就是新添加的元素了
* 具体步骤:
* 1.前置操作----参数检查等
* 2.找到index位的前置节点prev 和 后置节点succ
* 3.构建子Link链路
* 4.连贯原有链路和子Link链路
*
* @param index
* @param c
* @return boolean
*/
@Override
public boolean addAll(int index, Collection<? extends E> c) {
// 步骤1:各种参数合法性检查
checkPositionIndex(index);
Object[] a = c.toArray();
int numNew = a.length;
// 如果无添加元素,直接返回false 代表未变更
if (numNew == 0) {
return false;
}
// 步骤2:pred 代表index 的前一个节点 succ代表index的后一个节点
Node<E> pred, succ;
// index等于size,说明是往链表的最后添加元素,则index的后一个元素succ为空,index的前一个元素为last
// 注意:这里的index其实是一个空的值,并无节点;所以index的前一个节点prev是last
// 其实这算一种特判
if (index == size) {
succ = null;
pred = last;
} else {
// pred---succ(index位) =======变成======> pred------newNode(index位)----succ
// succ是index的后一个节点---因为index位置即将被新节点所取代,所以原来index位置的Node,变成succ
succ = node(index);
// pred是index 的前一个节点,
pred = succ.prev;
}
// 步骤3:构建子Link链路
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
// pred一直在更新,制作一个内部链接好的子链路(newNode的前置节点为pred)
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null) {
// pred为空,说明插入的位置选择是0,首节点
first = newNode;
} else {
// 双向绑定
pred.next = newNode;
}
// 更新pred指针
pred = newNode;
}
// 步骤4: 连贯原有链路和子Link链路,使其串在一起
// succ为index后的一个节点,如果succ为空,说明插入的位置就是在最后面,则直接设置last为pred(pred一直在更新)
if (succ == null) {
last = pred;
} else {
// 双向链表之双向绑定
pred.next = succ;
succ.prev = pred;
}
// 数量添加
size += numNew;
// 操作次数++
modCount++;
return true;
}
复制代码
代码可能会有点长,但是实际上逻辑十分清晰;可分为4个步骤:
-
前置操作–参数合法性检查
-
找到
index
位的前置节点prev
和后置节点succ
-
构建子Link链路
-
连贯原有链路和子Link链路
至此,LinkedList
的添加元素的方法就介绍完毕了,本质上是对链表的新增Node节点操作。小伙伴在阅读源码的时候可以边阅读边画出相关的链表,加深理解。
删除元素
LinkedList
的删除元素方法主要有2个,分别为:
-
public boolean remove(Object o)
删除首个指定元素o -
public E remove(int index)
删除指定index
位的元素
乍一看,LinkedList
似乎没有批量删除元素的方法,找遍 LinkedList.java
类的所有API方法确实没有找到相关批量删除的方法,但实际上 LinkedList
是有批量删除的功能的。 LinkedList
继承的 AbstractSequentialList
的父类 AbstractCollection
中有提供相关的API,通过迭代器的方式实现。
public boolean remove(Object o)
public boolean remove(Object o)
方法继承于 AbstractCollection
抽象类,删除链表中 首个 指定元素o,具体源码如下:
/**
* 删除首个指定元素o
* @param o
* @return boolean
*/
@Override
public boolean remove(Object o) {
// 说明LinkedList支持存放 null
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;
}
/**
* 真实执行删除操作,并返回被删除元素值
* 做法:将该节点从链路上剔除,并且之节点置null(方便GC)
* @param x
* @return E
*/
E unlink(Node<E> x) {
// 不太知道为啥子注释掉了 参数不做非空判断吗?
// assert x != null;
// element 节点的元素
final E element = x.item;
// 后一个节点
final Node<E> next = x.next;
// 前一个节点
final Node<E> prev = x.prev;
// 如果前置节点为空,说明删除的节点是首节点,first指向next
if (prev == null) {
first = next;
} else {
// prev ---- x --- next ====变成====> prev ----- next
prev.next = next;
// x.prev 设置为null,断开与原有链路的关联,同时也是为了GC
x.prev = null;
}
// 如果后置节点为空,说明删除的节点是尾节点, last直接指向prev
if (next == null) {
last = prev;
} else {
// 本来next.prev指向了 x,但是x要被删除,所以next.prev需要指向 x的前一个节点 也即是prev
next.prev = prev;
// x.next设置为null,断开与原有链路的关联,同时为了GC
x.next = null;
}
// for GC
x.item = null;
size--;
modCount++;
// 返回element
return element;
}
复制代码
从代码中我们可以看出,真实做删除操作的方法是 unlink(Node<E> x)
,其代码不难理解,小伙伴们可以看看注释来理解。
public E remove(int index)
public E remove(int index)
方法继承 AbstractSequentialList
,用以赋能像数组般的 随机 操作的能力,具体源码如下:
/**
* 删除指定index位置的元素
* @param index
* @return E
*/
@Override
public E remove(int index) {
// index参数合法性检查
checkElementIndex(index);
return unlink(node(index));
}
复制代码
可以看出,其实现方式是先找到 index
位置节点的元素,然后调用 unlink(Node e)
方法;这两个方法在上面都有过比较详细的介绍,这里就不做过多的赘述。
查找元素
LinkedList
中查找元素的方法有2个,分别为:
-
public int indexOf(Object o)
方法,查找首个为指定元素 o 的位置 -
public int lastIndexOf(Obejct o )
方法,查找最后一个为指定元素 o 的位置
两个方法的源码都很简单,两者的差别在于一个是从头遍历,一个是从尾遍历(因为LinkedList
底层实现是双向链表,所以很容易实现这点);具体源代码如下:
/**
* 查找首个为指定元素 o 的位置
* @param o
* @return int 如果不存在则返回-1
*/
@Override
public int indexOf(Object o) {
int index = 0;
// 可以看出:LinkedList也是支持存储null值的
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;
}
/**
* 查找最后一个为指定元素 o 下标
* 逆序遍历链表实现
* @param o
* @return int 存在则返回对应index 不存在则返回-1
*/
@Override
public int lastIndexOf(Object o) {
int index = size;
if (o == null) {
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (x.item == null)
return index;
}
} else {
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (o.equals(x.item))
return index;
}
}
return -1;
}
复制代码
从源码中我们可以看出,LinkedList
是支持存储 null
值。
转换成数组
LinkedList
的转换成数组的方法和 ArrayList
一样,也可以转换成数组,具体的方法如下:
-
public Object[] toArray()
将LinkedList
转换为Object[]
数组 -
public <T> T[] toArray(T[] a)
将LinkedList
转换为指定T
泛型的数组
源码如下:
/**
* 转换成数组
* @return Object[]
*/
public Object[] toArray() {
// 遍历链表,获取值 注意:返回值类型是Object[]
Object[] result = new Object[size];
int i = 0;
for (Node<E> x = first; x != null; x = x.next)
result[i++] = x.item;
return result;
}
/**
* 转化成数组(可指定返回泛型)
* @param a
* @return T[]
*/
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
// 如果传入的数组小于size的长度,直接复制一个新的数组返回
if (a.length < size)
a = (T[])java.lang.reflect.Array.newInstance(
a.getClass().getComponentType(), size);
// 顺序遍历链表,复制到a中
int i = 0;
Object[] result = a;
for (Node<E> x = first; x != null; x = x.next) {
result[i++] = x.item;
}
if (a.length > size) {
a[size] = null;
}
return a;
}
复制代码
需要关注的点和 ArrayList
一样,如果希望返回指定类型的数组,则应该使用 #toArray(T[] a)
方法。
其他常见方法
其他比较常见的方法,比如判断集合中是否包含某个元素 contains(Object o)
方法、获取指定位置的元素 get(int index)
、设置指定位置的元素 set(int index, E element)
、清空数组 clear()
、还有一个比较有趣但是不咋常用的方法——创建子数组 subList(int fromIndex, int toIndex)
由于比较简单,一次性上(叶问附体:我要一个打十个!!!)
/**
* 判断元素是否存在
* 依赖indexOf方法
*/
@Override
public boolean contains(Object o) {
return indexOf(o) != -1;
}
/**
* 获取指定index位置的元素
* @param index
* @return E
*/
@Override
public E get(int index) {
// index参数合法性检查
checkElementIndex(index);
// 直接返回~
return node(index).item;
}
/**
* 设置指定位置index位置元素element,并返回旧值
* @param index
* @param element
* @return E
*/
public E set(int index, E element) {
// index参数合法性检查
checkElementIndex(index);
// 获取node节点
Node<E> x = node(index);
// 替换旧值
E oldVal = x.item;
x.item = element;
// 返回旧值
return oldVal;
}
/**
* 清空集合
*/
@Override
public void clear() {
// 顺序遍历链表,设置每个节点前后都指向null 通过这种方式 帮助GC
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++;
}
// AbstractList.java
/**
* [fromIndex, toIndex)
* 根据判断 RandomAccess 接口,判断是否支持随机访问,从而创建 RandomAccessSubList 或 SubList 对象
* 创建子List(注意:和父List共享同一份数据,也即是子List的修改会影响到父List)
* @param fromIndex 可达
* @param toIndex 不可达
*/
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size());
// 根据判断 RandomAccess 接口,判断是否支持随机访问
return (this instanceof RandomAccess ?
new RandomAccessSubList<>(this, fromIndex, toIndex) :
new SubList<>(this, fromIndex, toIndex));
}
复制代码
LinkedList实现栈、队列、双端队列分析
从 LinkedList
的类图我们可以知道,LinkedList
实现了 Deque
双端队列接口,我们来看看Deque
的类图,掰扯掰扯。
从类图来看,Deque
接口继承于 Queue
接口,所以 LinkedList
拥有队列、双端队列的特性;其次JAVA中有一个过时类 Stack
代表栈,JAVA中并没有单独的栈接口,但是栈相关的方法包括在了 Deque
接口中,因此 LinkedList
同时也拥有栈的特性。
双端队列方法
/**
* =======================================双端队列==============================
*/
/**
* 往双端队列 前端添加元素
*/
@Override
public void addFirst(E e) {
linkFirst(e);
}
/**
* 双端队列 末尾添加元素
*/
@Override
public void addLast(E e) {
linkLast(e);
}
/**
* 双端队列前端添加元素
*/
@Override
public boolean offerFirst(E e) {
addFirst(e);
return true;
}
/**
* 双端队列 末尾端添加元素
*/
@Override
public boolean offerLast(E e) {
addLast(e);
return true;
}
/**
* 双端队列 前端移除首个元素
*/
@Override
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
/**
* 双端队列 末尾端 移除首个元素
*/
@Override
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
/**
* 双端队列 前端 弹出首个元素
*/
@Override
public E pollFirst() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
/**
* 双端队列 末尾端 弹出首个元素
*/
@Override
public E pollLast() {
final Node<E> l = last;
return (l == null) ? null : unlinkLast(l);
}
/**
* 双端队列 获取首端 首个元素
*/
@Override
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
/**
* 双端队列 获取末尾端 首个元素
*/
@Override
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
@Override
public E peekFirst() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
@Override
public E peekLast() {
final Node<E> l = last;
return (l == null) ? null : l.item;
}
@Override
public boolean removeFirstOccurrence(Object o) {
return remove(o);
}
@Override
public boolean removeLastOccurrence(Object o) {
if (o == null) {
for (Node<E> x = last; x != null; x = x.prev) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = last; x != null; x = x.prev) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
复制代码
代码都比较的简单,不需要过多的说明,小伙伴看看就行。
队列方法
/**
* ==================================队列方法========================================
*/
@Override
public boolean add(E e) {
// 顺序添加单个元素到链表的末尾
linkLast(e);
return true;
}
@Override
public boolean offer(E e) {
return add(e);
}
@Override
public E remove() {
// 移除第一个元素,Deque接口的方法
return removeFirst();
}
@Override
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
@Override
public E element() {
return getFirst();
}
@Override
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
复制代码
栈方法
/**
* ==================================栈方法========================================
*/
@Override
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
@Override
public void push(E e) {
addFirst(e);
}
@Override
public E pop() {
return removeFirst();
}
复制代码
遍历方式探讨
LinkedList
支持如下几种遍历方式,下面逐个讨论:
- 通过
Iterator
迭代器遍历
Integer value = null;
Iterator it = list.iterator();
while (it.hasNext()) {
value = (Integer)it.next();
}
复制代码
- 通过 快速随机访问 遍历 (注意:并不建议采用这种方式遍历)
Integer value = null;
int size = list.size();
for (int i = 0; i < size; i++) {
value = (Integer)list.get(i);
}
复制代码
- 通过 for循环 遍历
Integer value = null;
for (Integer integ : list) {
value = integ;
}
复制代码
其实经过我们上面的分析,我们都应该明白:不应该使用随机访问的方式去遍历 LinkedList,因为其底层实现上采用的是 双向链表,即使提供了类似数组般随机访问的API,实际上还是通过挨个节点挨个节点遍历来达到随机访问的效果的(具体可看看上面的get(int index)
方法源码解析);官方更推荐使用顺序访问,也即是通过迭代器或者 for循环 的方式来遍历。
总结
本文主要从源码层面讲解了 LinkedList
的实现方式,LinkedList
底层是基于节点实现的双向链表;是一个直线型的链表结构。
LinkedList
只有 3 个属性,分别是代表头节点的 first
、代表尾节点的 last
以及表示数量的 size
。
LinkedList
有 2 个构造方法,分别是无参的构造方法 LinkedList()
和指定元素集的构造方法 LinkedList(Collection<? extends E> c)
,因为其底层实现上采用的是双向链表,因此它并没有像 ArrayList
指定初始化容量的构造方法,也没有需要扩容的操作;每次新增元素直接 new
一个新的节点插入到双向链表中即可。
LinkedList
的新增元素和删除元素操作都是一些链表操作,理解起来相对比较的简单;另外由于LinkedList
继承了AbstactSequentialList
抽象类,因此被赋予了类似数组般的随机访问/操作的能力(实际上还是通过逐个遍历的方式实现的)。- 因为
LinkedList
是基于节点实现的,故而有一定的资源消耗(前后指针),但是如果存储的元素(对象)element
足够大的话,那么这点开销可忽略不记的。 - 最后需要留意的是,我们在分析源码的时候,是发现
LinkedList
是支持存储null
的。