Java容器专栏: Java容器源码详细解析(面试知识点)
因为部分内容可能已经在本专栏前面的文章提及到,这里只是简单说明,所以建议先看看:ArrayList
详细说明都在源码里,耐心看会发现其实也挺简单的。
(一)LinkedList底层数据结构
双向链表,链表中的每个节点都包含了对前一个和后一个元素的引用。
链表的最大的容量为Integer.MAX_VALUE。
(二)LinkedList继承和实现关系
1、实现的接口:
①List接口:提供了List的相关操作
②Deque接口:提供了双向队列的相关操作
注意,虽然LinkedList实现了此接口,但是LinkedList大部分Deque不太一样:
a) 一般Deque不允许null值,但LinkedList允许null值
b) 一般Deque不限容量,但LinkedList有最大容量:Integer.MAX_VALUE
③Cloneable标志性接口:可克隆
④Serializable标志性接口:可序列化
2、继承的抽象类:
继承了抽象类AbstractSequentialList,最小化实现了支持“顺序访问”的数据结构(比如链表)所需的工作量。而对于支持“随机访问”的数据结构(比如数组),则是需要继承AbstractList类(比如ArrayList就继承了AbstractList)
AbstractSequentialList是AbstractList的子抽象类。
(三)LinkedList源码分析
1、几个重要的成员变量
//链表长度
transient int size = 0;
//指向头节点的指针(最初为null)
transient Node<E> first;
//指向尾节点的指针(最初为null)(注意是最后一个姐的指针,而不是下一个节点的)
transient Node<E> last;
这是LinkedList的成员变量,那么节点Node的成员变量呢?其实Node是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;
}
}
对元素的操作,其实也都是对节点的操作。
下面对链表的添加和删除操作,都需要考虑到操作位置(链表头、链表中间、链表尾),根据不同的位置,会对链表的 first 和 last 指针,以及 新、旧(邻接)节点的 prev 和 next 指针进行不同的操作。(很重要,可以说是LinkedList底层最重要的部分,只要理解了,其他的就很简单了,对双向链表不理解的,可以先去看看其他有关这个数据结构的博客了解学习)
2、两个构造方法
//创建一个空LinkedList
public LinkedList() {
}
//按照容器中的元素创建LinkedList
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
构造方法就不像ArrayList一样复杂了,很清晰,不做过多的说明。
3、获取元素操作
//通过下标获取元素(返回节点的item)
public E get(int index) {
//检查下标是否规范[0~size]
checkElementIndex(index);
//找到对应的节点,并返回其item
return node(index).item;
}
//通过下标查找节点
//这里将链表分前后两半,来优化查找,加快查找速度
Node<E> node(int index) {
//若是在前一半(size右移一位表示“/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;
}
}
4、添加元素操作
//添加单个元素
public boolean add(E e) {
//尾插法插入
linkLast(e);
return true;
}
//尾插法插入传入的元素
//最重要的是处理链表的first和last指针移动
//以及新节点的prev和next和原最后节点的next指针指向
void linkLast(E e) {
//指向链表最后一个节点的指针
final Node<E> l = last;
//创建一个新节点参数为:
//前一节点指针(l)、此节点存放元素、后节点指针(null)
final Node<E> newNode = new Node<>(l, e, null);
//链表的最后节点指针指向新插入的节点
last = newNode;
//如果是第一次插入的,链表的first指针就指向这个新节点
//(之后除非删除了第一个节点,否则first就不动了)
if (l == null)
first = newNode;
else
//若链表已有节点(l!=null),则原链表末节点的next指针指向这个新节点
//将链表和新节点连接起来
l.next = newNode;
//链表长度+1
size++;
//修改次数+1
modCount++;
}
//添加多个元素(传入Collection容器)
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
//AbstractSequentialList的方法
//在指定的index位置上插入容器中的所有元素
public boolean addAll(int index, Collection<? extends E> c) {
//检查index是否规范(0 <= index <= size)
checkPositionIndex(index);
//先用数组存放c容器中的元素,并获取元素的个数
Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false;
//定义两个辅助指针,若将容器中的元素拼接成一个链表A
//pred是这个A链表的第一个节点的prev指针
//succ是这个A链表的最后一个节点的next指针
Node<E> pred, succ;
//如果刚好是在原链表最后插入这个容器元素,则直接拼接在原链表后即可
if (index == size) {
succ = null;
pred = last;
} else {
//否则,则还需要取出原链表index及之后的子链表段,需要在最后插在A链表末尾
//succ设置为index所指的node的指针
succ = node(index);
//pred则是指向index所指的节点的前一个节点
pred = succ.prev;
}
//将容器c中的所有元素和原链表index位置开始,连接起来
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null)
first = newNode;
else
pred.next = newNode;
//后移
pred = newNode;
}
if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
//在指定位置上插入元素
public void add(int index, E element) {
//检查index是否规范
checkPositionIndex(index);
//如果是在最后插入,则直接尾插法
if (index == size)
linkLast(element);
else
//在index之前插入element(其他的后移)
linkBefore(element, node(index));
}
//在指定node之前插入元素(很简单,和前面的思路一致)
void linkBefore(E e, Node<E> succ) {
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
5、删除元素操作
//根据下标删除节点,返回此节点对应的元素item
public E remove(int index) {
//同样是检查下标
checkElementIndex(index);
//先是找到节点,然后unlink此节点
return unlink(node(index));
}
//断开节点
//同样是要考虑节点的位置的,只不过在链表中间这种情况的处理,被分到两个else中
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) {
first = next;
} else {
prev.next = next;
x.prev = null; //置为null
}
//如果是尾节点
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null; //置为null
}
//置为null,到此被删节点的全部属性都是null,即没有被引用了,所以可以被gc回收
x.item = null;
size--;
modCount++;
return element;
}
//根据item值删除节点(仅删除第一次出现的节点),成功删除返回true
public boolean remove(Object o) {
//如果是要删除的对象是null(list允许null值)
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
//是上面的unlink方法,移除节点
unlink(x);
return true;
}
}
} else {
//如果是非null值,则需要使用equals方法来判断是否相等
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
6、Deque相关方法
上面的几个操作,都是作为底层为链表的List的操作,而不是针对队列的操作。若是要将LinkedList作为双向队列使用的话,需要使用下面几个方法而不是上面的方法。
(虽然底层实现原理差不多,也都是对链表的相关操作。只不过若是将其作为双向队列使用的话,可以操作的位置就只能是队列头和队列尾了!)
(也就是对线性列表list操作的特殊情况给提取出来封装成一个新方法,也就是list操作的简单情况,只要上面理解了,这些和队列操作相关的方法也就轻松拿下了)
(使用也不是要如何添加代码,而是在自己写的逻辑代码中自己遵守即可,比如若是作为队列来使用,就不要调用上面类似get(index)等可对队列中间元素获取和操作的方法)
① 双向队列的添加
//头插
public boolean offerFirst(E e) {
addFirst(e);
return true;
}
public void addFirst(E e) {
linkFirst(e);
}
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 boolean offerLast(E e) {
addLast(e);
return true;
}
public void addLast(E e) {
linkLast(e);
}
……//不列了,很简单,可以自己想想再去看源码比较
②双向队列的删除
//移除队列头
public E pollFirst() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
//或
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
//移除操作
return unlinkFirst(f);
}
private E unlinkFirst(Node<E> f) {
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;
}
//移除队列尾
……//同样很简单,上面的first、unlinkFirst换成last、unlinkLast而已
③双向队列的获取
不写了,几行代码,不过是判断后返回first和last指针所指的节点item属性而已。( peekFirst / peekLast )
7、Qeque相关方法
若是要将其作为 queue(而不是上面的 lis t或 deque )来操作,上面的方法就不要使用!使用下面的方法!!(虽然下面的方法只是对上面提到的方法再封装而已)
即:完全遵守 先进先出,中间元素不可知!
(LinkedList的属性中,first指向的是队头,last指向的是队尾)
① 入队
public boolean offer(E e) {
//内部实际调用了list的add方法,是尾插法的
return add(e);
}
②出队
public E pop() {
//移除队头
return removeFirst();
}
③查看队头(不出队)
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
//或
public E element() {
return getFirst();
}
//区分:peek不会抛异常,element调用了getFirst,若first指向null则会抛异常
7、其他方法
注意里面有一个push方法,看起来像是对队列的入队操作,但是它是从队头push的(即头插法)。很另类,最好别用吧比较容易乱。
(洗了个澡突然醒悟,push和pop……这不就是栈了吗!!!原来不是鸡肋啊!!)
(三)线程安全性
不是线程安全的。举例很简单的,无论是插入还是删除,对链表的结构都会造成改变,不同线程不同的顺序改变的结果也会不同。
解决线程安全有下面几个方法:
-
使用Collections.synchronizedList(List list)替换
-
使用ConcurrentLinkedQueue替换
(四)克隆机制
同样是浅克隆。
public Object clone() {
LinkedList<E> clone = superClone();
clone.first = clone.last = null;
clone.size = 0;
clone.modCount = 0;
//循环尾插
for (Node<E> x = first; x != null; x = x.next)
clone.add(x.item);
return clone;
}
(五)序列化机制
(大致和ArrayList一样)在上面的成员变量中可以看到,LinkedList的全部成员变量都加上了transient修饰,即不会被序列化的,但是它继承了Serializable接口,同时实现了readObject和WriteObject方法,序列化允许调用这两个方法进行序列化操作。(若不懂建议看看本专栏的ArrayList博客,里面就有比较详细的解释,当然实现的代码肯定是不同的哈)