本文介绍
1、LinkedList相关面试题。
2、LinkedList基本信息。
3、LinkedList属性和方法。
4、LinkedList最佳实践。
5、LinkedList的总结。
相关面试题
ArrayList与LinkedList有哪些区别?
(todo待收集面试题)
概述
LinkedList 是一种变长的集合类。底层数据结构是双向链表,是非连续的内存空间。
LinkedList 允许空值和重复元素,因为add方法不做空值和重复元素校验。
LinkedList 是非线程安全类,并发环境下,多个线程同时操作 LinkedList,会引发不可预知的错误。
LinkedList 继承AbstractSequentialList,实现了List, Deque, Cloneable, java.io.Serializable接口。
LinkedList 实现List接口,即提供相关的添加、删除、修改、遍历功能。
LinkedList 实现Cloneable接口,即覆盖了函数clone(),能被克隆。
LinkedList 实现Serializable接口,这意味着LinkedList 支持序列化。
下面让我们翻开LinkedList 的源代码,看看一些常用的方法属性,以及一些需要注意的地方。
数据结构
LinkedList最最最核心的就是节点的数据结构。组成链表的节点由 prev:指向上一个节点的引用、next:指向下一个节点的引用、item:存储的元素组成。
// 静态内部类形式的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;
}
}
下图是由Node节点组成双向链表,LindedList底层的数据存储基本上就是酱紫了。
属性
LinkedList 属性主要有当前链表长度size,first 指向第一个节点 这里我称为首节点,last 指向最后一个节点简称尾节点 。除此之外还有一个经常用到的属性就是从 AbstractList 继承过来的 modCount 属性,代表 LinkedList 集合的修改次数。
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;
加入first属性和last属性后的双向链表
构造函数
无参构造函数
如果不传入参数,则使用默认无参构建方法创建LinkedList对象,如下:
public LinkedList() {
}
带collection类型的构造函数
// 创建LinkedList
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
// 添加 collection 中所有元素
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
// 在index位置添加 collection 所有元素
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index); // 范围插入范围,[0 , size]位置为正常
Object[] a = c.toArray(); // 将集合转成Object数组
int numNew = a.length;
if (numNew == 0) // 如果新增元素数量为0,直接返回false
return false;
Node<E> pred, succ; // pred与succ是两个临时变量,为将c中的元素构建成Node再将其连接到链表上的临时变量,(具体理解该的方法:先要理解链表是什么,也就是先搞清楚不懂的概念。再Debug进来具体的代码)
if (index == size) {
succ = null;
pred = last;
} else {
succ = node(index);
pred = succ.prev;
}
// 此处for循环主要把a中的元素链接到已有的元素上
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null); // 创建一个新节点,新节点的prev 指向 pred,item 为e,next 为null
if (pred == null)
first = newNode; // 设置首节点
else
pred.next = newNode; // pred.next 指向新的节点
pred = newNode; // pred 指向新的节点
}
if (succ == null) {
last = pred; // 设置尾节点
} else {
pred.next = succ;
succ.prev = pred;
}
size += numNew; // 添加元素后数据大小
modCount++;
return true;
}
/**
* 将集合转成Object数组
* 1、创建一个size长度的数组
* 2、循环将链表节点的元素赋值给新数组中并返回。
*/
public Object[] toArray() {
Object[] result = new Object[size];
int i = 0;
for (Node<E> x = first; x != null; x = x.next)
result[i++] = x.item;
return result;
}
add方法
add(E e) 方法
添加单个元素方法
public boolean add(E e) {
linkLast(e); // 在尾部添加元素
return true;
}
/**
* 添加元素到链表最后
* 1、生成新节点
* 2、插入到链表尾部
* 3、更新 last/first 节点。
*/
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null); // 创建pre指向l,item赋值为e,next赋值为null
last = newNode; // 设置last引用
if (l == null)
first = newNode; // 空链表情况设置first
else
l.next = newNode; // 设置尾节点指向新节点
size++;
modCount++;
}
add(int index, E element) 方法
添加元素到指定位置方法
public void add(int index, E element) {
checkPositionIndex(index); // 检查插入点的范围 [0,size]
if (index == size)
linkLast(element); // 添加到链表最后
else
linkBefore(element, node(index));
}
/**
* Returns the (non-null) Node at the specified element index.
* 返回指定元素索引处的(非空)节点。下标从0开始
*/
Node<E> node(int index) {
if (index < (size >> 1)) { // index是否小于size的一半,小于则从首节点往后移
Node<E> x = first;
for (int i = 0; i < index; i++) // 从首节点开始,循环往后移动,直到 i=index
x = x.next;
return x; // 返回下标为index处的节点(下标从0开始)
} else { // 反之大于从尾节点往前移
Node<E> x = last;
for (int i = size - 1; i > index; i--) // 从尾节点开始,循环往前移动,直到 i=index
x = x.prev;
return x; // 返回下标为index处的节点(下标从0开始)
}
}
/**
* 将 e元素连接到 succ节点之前
*/
void linkBefore(E e, Node<E> succ) {
final Node<E> pred = succ.prev; // pred指向succ的上一个节点
final Node<E> newNode = new Node<>(pred, e, succ); // 创建新的节点,新节点的prev指向pred,新节点的item为e,新节点的nextr指向succ.
succ.prev = newNode; // succ节点的prev指向新节点
if (pred == null) // 如果pred是null,说明succ.prev是第一个节点,则新的节点设置为首节点
first = newNode;
else // 修改pred指向的下一个节点为新节点
pred.next = newNode;
size++;
modCount++;
}
get方法
get(int index)
获取指定位置节点元素方法
public E get(int index) {
checkElementIndex(index); // 检查索引元素范围,[0 , size)位置为正常。注意:不包括size
return node(index).item; // 返回节点中具体的值
}
/** 检查元素的索引位置 */
private void checkElementIndex(int index) {
if (!isElementIndex(index)) // 是否是元素的索引,如果不是则抛出异常
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/** 是否是元素的索引,[0,size) 包括0不包括size*/
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
getFirst()
获取第一个节点元素方法
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
getLast()
获取最后一个元素方法
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
remove方法
remove(int index)
移除指定位置元素方法
public E remove(int index) {
checkElementIndex(index); // 检查元素索引位置,如不是则抛出异常
return unlink(node(index)); // 找到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) { // x是否为首节点,如果是则修改首节点指向
first = next;
} else { // 不是首节点
prev.next = next; // 修改x前一个节点的next指向x后一个节点
x.prev = null; // 修改x节点的prev指向null
}
if (next == null) { // x是否为尾节点,如果是则修改尾节点指向
last = prev;
} else { // 不是尾节点
next.prev = prev; // 修改x后一个节点的prev指向x前一个节点。
x.next = null; // 修改x节点的next指向null
}
x.item = null;
size--;
modCount++;
return element;
}
remove(Object o)
根据对象移除元素方法
public boolean remove(Object o) {
if (o == null) { // o是否为空,为空。循环遍历链表,找到一个就移除,并返回,假如有两个null只会移除第一个。
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else { // 不为空,循环遍历链表,找到一个就移除,并返回,假如有两个o对象只会移除第一个。
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
removeFirst()
移除第一个节点方法
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
/** 将第一个元素移除,将移除元素置空,first指向下一个节点,size减1,修改次数加1,返回旧值 */
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;
}
removeLast()
移除最后一个节点方法
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
/** 将最后一个元素移除,last指向上一个节点,将移除元素置空,size减1,修改次数加1,返回旧值 */
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;
}
set方法
set(int index, E element)
设置指定位置元素方法
public E set(int index, E element) {
checkElementIndex(index); // 检查元素索引位置,如不是则抛出异常
Node<E> x = node(index); // 获取index位置的节点
E oldVal = x.item; // 保存旧值
x.item = element; // 设置item为新值
return oldVal; // 返回旧值
}
clear方法
clear()
清除链表方法
/** 将所有链表元素置空,然后将链表长度修改成0,修改次数加1 */
public void clear() {
// Clearing all of the links between nodes is "unnecessary", but:
// - helps a generational GC if the discarded nodes inhabit
// more than one generation
// - is sure to free memory even if there is a reachable Iterator
for (Node<E> x = first; x != null; ) { // 从首节点开始,循环遍历链表,将节点只有item、next、prev设置为null
Node<E> next = x.next;
x.item = null;
x.next = null;
x.prev = null;
x = next;
}
first = last = null; // 首尾节点设置为null
size = 0; // 设置大小为0
modCount++;
}
push和pop方法
最后说下两个栈操作的方法,这也是链表的另一种用法,把LinkedList当做栈来使用。push其实就是调用addFirst(e)方法,pop调用的就是removeFirst()方法。
/** 出栈 */
public E pop() {
return removeFirst();
}
/** 入栈 */
public void push(E e) {
addFirst(e);
}
总结
LinkedList基于双向链表实现,无容量的限制。
LinkedList下标从0开始,最大下标为size-1。
LinkedList当作栈使用,可以调用push和pop方法。
LinkedList移除对象时,如果有多个相同对象只会移除第一个。
LinkedList根据下标和对象获取元素的值时都会循环遍历链表,时间复杂度为都为O(n),而ArrayList根据下标获取时直接可取到值,时间复杂度为O(1),而根据对象查询是需要遍历数据,时间复杂度为O(n)。
最佳实践
1、适合用于增加删除多,查询修改少的业务中,与ArrayList对比刚好互补。但是目前对于这个容器的使用还是极少,以至于找不到一个好点的例子(后续补上)。
引文
面试必备:LinkedList源码解析(JDK8)
https://blog.csdn.net/zxt0601/article/details/77341098
LinkedList源码分析(基于JDK8)
https://blog.csdn.net/fighterandknight/article/details/61476335