Java SE 是什么,包括哪些内容(二十一)?
本文内容参考自Java8标准
顺序访问列表的一个常用类:
LinkedList
类声明:
public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
...
}
源代码图示(图下面有解释说明):
英文注释分别阐述了以下内容(一个段落用一个小标):
⑴、LinkedList(双链列表)同时实现了接口List和Deque(LinkedList实现了这两个接口的所有方法),它既是一个列表,也是一个双端队列。它允许添加任何元素,包括null元素。
⑵、LinkedList所有方法的实现都体现了双链表的特性,所有有关索引的操作都自动从LinkedList的开头或结尾开始遍历(如果索引更接近开头,自动从开头开始遍历,更接近结尾,自动从结尾开始遍历)。
⑶、LinkedList不是线程安全的,如果在多线程环境中涉及结构修改(结构修改是指添加或删除一个或多个元素),必须从外部保证LinkedList的线程安全(通过对持有LinkedList的对象进行同步来完成)。如果不存在持有LinkedList的对象,应该使用类Collections的方法synchronizedList(new LinkedList(…)),具体的代码如下:
List list = Collections.synchronizedList(new LinkedList(...));
最好在创建LinkedList对象时就这么做,以防止任何不安全的操作发生。
⑷、在获取了迭代器或列表迭代器后,除非使用它们自身提供的方法(方法remove()和add())修改LinkedList的结构,否则都将抛出ConcurrentModificationException异常(快速失败机制)。迭代器不会在未来的不确定时间内冒任意不确定行为的风险(如果在多线程环境的迭代过程中修改了LinkedList的结构,可能返回很多种结果)。
⑸、注意,无法保证快速失败机制一定会执行,只能说迭代器会尽最大的努力抛出ConcurrentModificationException异常。因此,通过快速失败机制保证程序的正确性是错误的,它通常只能用于检测错误。
⑹、LinkedList是Java Collections Framework中的重要一员。
在分析类LinkedList的源码之前,必须先分清楚"节点"和"元素",它们是两个不同的概念。
⑺、节点的图示:
LinkedList中的每个节点都是一个单独的存储空间,"元素"存储在节点中,但节点中不仅只有元素。ArrayList的底层是数组,数组是连续的存储空间(通过持有数组的引用+"[ 索引 ]"+访问),所以每个存储空间只需存储元素,不需要存储相互之间的关系。而LinkedList底层是链表(双向),链表不是连续的存储空间,所以每个存储空间(节点)不仅需要存储元素,还需要存储相互之间的关系(prev:上一个节点的位置,如果有。next:下一个节点的位置,如果有)。
⑻、链表的图示:
在链表中,一个独立的存储空间就是一个节点,同时,一个节点就是一个内部类Node的对象实例。链表是由若干个节点构成的,
在下面的描述中,不再使用"存储空间"字眼,统一使用"节点"。
⑼、内部类Node的源码:
//链表节点的类型
//一个对象对应一个节点
//包括元素(item)和节点之间的关系(prev,next)
private static class Node<E> {
//元素的引用
//E表示可以是任何类型的元素
//如果为null,表示没有存储任何元素
//如果不为null,表示存储了某种类型的元素
E item;
//下一个节点的引用
//熟悉Java的都知道,引用代表了
//对象的十六进制地址值,所以
//也可以注释为:下一个节点在内存中的地址
//如果为null,可能是空链表
//也可能是尾节点
Node<E> next;
//上一个节点的引用
//如果为null,可能是空链表
//也可能是首节点
Node<E> prev;
//构造方法
//创建一个节点对象需同时提供:
//①、元素的引用(item)
//②、下一个节点的引用(next)
//③、上一个节点的引用(prev)
Node(Node<E> prev, E element, Node<E> next) {
//元素的引用初始化
this.item = element;
//上一个节点的引用初始化
this.next = next;
//下一个节点的引用初始化
this.prev = prev;
}
}
在接下来的源码分析中,节点都要处理元素和关系两个方面。
在接下来的源码分析中,节点都要处理元素和关系两个方面。
在接下来的源码分析中,节点都要处理元素和关系两个方面。
1、size
//元素的数量,初始化为0
//关键字transient表示它不能被序列化
transient int size = 0;
2、first
//首节点的固定引用(方便节点的创建、添加和链表的查找遍历)
//链表必须先创建添加首节点,才能创建添加第二个节点,以此类推
//查找遍历同理,链表是顺序访问列表,只能从开头或者末尾开始挨个访问
//所以首节点不可能匿名,需要一个固定引用方便后续的使用
//关键字transient表示它不能被序列化
transient Node<E> first;
3、last
//尾节点的固定引用(方便节点的查找遍历)
//关键字transient表示它不能被序列化
transient Node<E> last;
4、LinkedList()
//构造方法,创建一个没有任何节点的空LinkedList
public LinkedList() {
}
5、LinkedList(Collection<? extends E> c)
//构造方法,创建这样一个LinkedList
//①、节点数量与指定集合c的元素数量一致
//②、指定集合c中的每个元素都存储在对应索引值的节点中
//换句话说就是:节点的顺序与指定集合c的元素顺序完全一致
public LinkedList(Collection<? extends E> c) {
//调用无参构造函数,如果它在后期的Java
//升级版本中发生修改,可以实时与它保持一致
//保证了程序的可扩展性,方便了后期的代码维护
this();
//通过方法addAll(Collection<? extends E> c)
//添加c的所有元素
addAll(c);
}
6、linkFirst(E e)
//创建一个包含元素e的新节点
//并链接至链表的首部(linkFirst),实际上
//这个节点就是链表的新首节点
//为什么需要这样一个private方法?
//因为LinkedList同时implement了接口Queue和Deque
//在实现它们的方法时可以有效避免代码重复
private void linkFirst(E e) {
//假设链表不为空,获取并传递首节点的引用
//传递后,引用f和first同时指向首节点
//这一步实际上也是复制首节点的引用
final Node<E> f = first;
//分别提供以下参数创建一个节点对象newNode:
//①、上一个节点的引用null
//②、元素e
//③、下一个节点的引用f
//newNode其实就是新的首节点,两点原因:
//①、在链表中,只有首节点的上一个节点引用为null
//②、原首节点f成为newNode的下一个节点
//③、节点关系的维护放在了构造函数中(参考截图)
final Node<E> newNode = new Node<>(null, e, f);
//将newNode赋值给first,保证first永远都是首节点的引用
first = newNode;
//---------------下面需要考虑一些其它情况
//上面的代码只初始化了first,并未初始化last
//如果首节点f为null,说明链表为空
//所以新创建的节点newNode既是首节点也是尾节点
if (f == null)
//初始化last
last = newNode;
else
//如果首节点f不为空
//需要考虑两方面:元素和关系
//元素由客户端程序员添加
//这里只需考虑关系
//首节点f的prev肯定为null
//现在创建了新的首节点newNode
//f自然就是第二节点
//所以需要通过代码表示:
//f的上一个节点是newNode
//以此保证各个节点的关系正确
f.prev = newNode;
//新增一个节点,长度+1
size++;
//继承自抽象类AbstractList,保证快速失败机制的正常执行
//新增节点是结构性修改
modCount++;
}
截图:
7、linkLast(E e)
//创建一个包含元素e的新节点
//并链接至链表的尾部(linkLast),实际上
//这个节点就是链表的新尾节点
//为什么需要这样一个方法?
//因为LinkedList同时implement了接口Queue和Deque
//在实现它们的方法时可以有效避免代码重复
void linkLast(E e) {
//假设链表不为空,获取并传递尾节点的引用
//传递后,引用l和last同时指向尾节点
//这一步实际上也是复制尾节点的引用
final Node<E> l = last;
//分别提供以下参数创建一个节点对象newNode:
//①、上一个节点的引用l
//②、元素e
//③、下一个节点的引用null
//newNode其实就是新的尾节点,两点原因:
//①、在链表中,只有尾节点的下一个节点引用为null
//②、原尾节点l成为newNode的上一个节点
//③、节点关系的维护放在了构造函数中(参考截图)
final Node<E> newNode = new Node<>(l, e, null);
//将newNode赋值给last,保证last永远都是尾节点的引用
last = newNode;
//---------------下面需要考虑一些其它情况
//上面的代码只初始化了last,并未初始化first
//如果尾节点l为null,说明链表为空
//所以新创建的节点newNode既是尾节点也是首节点
if (l == null)
//初始化first
first = newNode;
else
//如果尾节点l不为空
//需要考虑两方面:元素和关系
//元素由客户端程序员添加
//这里只需考虑关系
//尾节点l的next肯定为null
//现在创建了新的尾节点newNode
//l自然就是倒数第二节点
//所以需要通过代码表示:
//l的下一个节点是newNode
//以此保证各个节点的关系正确
l.next = newNode;
//新增一个节点,长度+1
size++;
//继承自抽象类AbstractList,保证快速失败机制的正常执行
//新增节点是结构性修改
modCount++;
}
截图:
8、linkBefore(E e, Node< E > succ)
//创建一个包含指定元素e的节点
//并将它链接在指定节点succ之前(linkBefore)
void linkBefore(E e, Node<E> succ) {
//获取节点succ的上一个节点
final Node<E> pred = succ.prev;
//分别提供以下参数创建一个节点对象newNode:
//①、上一个节点的引用pred
//②、元素e
//③、下一个节点的引用succ
final Node<E> newNode = new Node<>(pred, e, succ);
//创建后还需要维护节点之间的关系
//以保证newNode是succ的上一个节点
succ.prev = newNode;
//---------------下面需要考虑一些其它情况
//如果pred为null,说明succ是链表的首节点
//(只有首节点的上一个节点为null)
//既然是首节点,那么就还有另一个固定的引用first
if (pred == null)
//newNode代替了succ成为新的首节点
//需要对first重新进行初始化,保证first永远都是首节点的引用
first