Java集合学习(三)LinkedList源码学习
前言
侵删,学习记录笔记。
有错请指正
LinkedList
集合List的实现类。与ArrayList不同的是,LinkedList采用链表结构实现,插入和删除性能较好,但查询速度较慢。LinkedList实现了Deque<E>接口,可以当作双向队列来使用,亦或是栈,堆。
结点
这是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;
}
}
属性
//链表实际长度
transient int size = 0;
//首结点
/**
*
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*
*只有当首尾节点都是null或是此结点无父结点并且此节点保存有数据
*时才能被是首结点
*/
transient Node<E> first;
//尾结点
/**
*
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*
*只有当当首尾节点都是null或者此结点没有子结点并且此结点
*保存有数据时才能是尾结点
*/
transient Node<E> last;
构造函数
//无参构造函数,此时size为0,first和last结点均为null
public LinkedList() {
}
//根据集合构造链表,将集合所有元素添加进链表内
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
链表实现的关键函数
链表实现增删改查的关键函数
//将元素e插入到first结点
//如果此时原first结点为null,即长度为0,便让first和last同时引用被Node封装实例好newNode
//如果不为null,则将原first结点的父结点改为引用newNode
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++;
}
//和linkFirst实现类似,不过linkLast是将元素e插入到last结点
//无论是linkLast还是linkFirst都可以看出当链表长度为0时插入元素,元素会被first和last结点同时引用
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
//将元素e插入到succ结点的前面
//由于Node结点保存了父子结点的引用,实现起来很简单,直接改变引用即可。
void linkBefore(E e, Node<E> succ) {
//pred为 succ的父结点
final Node<E> pred = succ.prev;
//设置newNode的父结点为pred,子结点为succ
final Node<E> newNode = new Node<>(pred, e, succ);
//将succ的父结点改为newNode
succ.prev = newNode;
//如果pred为null,即表示此时newNode为first结点,即succ原来是在first位置
if (pred == null)
first = newNode;
else
//否则将pred的子结点改为newNode
pred.next = newNode;
size++;
modCount++;
}
//删除first结点,如果first有子结点,则子结点设置为first结点,并删除原first结点的所有引用,方便JVM的垃圾回收
//参数列表里的f结点必须是first结点
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;
}
//删除last结点,将last结点的父结点变为last结点,并取消所有对原last结点的引用,方便JVM垃圾回收
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;
}
//删除某一结点,并将此结点的父、子结点连接在一起(如果父、子结点不为null)
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;
}
获取函数
//1.检查索引是否超出链表长度或不符合规范
//2.获取相应索引结点的内容
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
//1.判断索引是小于size/2还是大于size/2
//2.小于size/2则顺序遍历获取相应结点
//3.大于size/2则倒序遍历直至找到相应结点并返回
Node<E> node(int index) {
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;
}
}
根据node(index)函数我们可以看出,当链表体量越来越大,而索引越偏向size/2的时候,需要遍历的长度就越长,效率就越低。
添加函数
//直接将元素插入到last位置,效率很高
public boolean add(E e) {
linkLast(e);
return true;
}
//在指定位置插入元素
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
遍历
引用
关于遍历的内容
public static void main(String[] args) {
LinkedList<Integer> list = getLinkedList();
//通过快速随机访问遍历LinkedList
listByNormalFor(list);
//通过增强for循环遍历LinkedList
listByStrengThenFor(list);
//通过快迭代器遍历LinkedList
listByIterator(list);
}
/**
* 构建一个LinkedList集合,包含元素50000个
* @return
*/
private static LinkedList<Integer> getLinkedList() {
LinkedList list = new LinkedList();
for (int i = 0; i < 50000; i++){
list.add(i);
}
return list;
}
/**
* 通过快速随机访问遍历LinkedList
*/
private static void listByNormalFor(LinkedList<Integer> list) {
// 记录开始时间
long start = System.currentTimeMillis();
int size = list.size();
for (int i = 0; i < size; i++) {
list.get(i);
}
// 记录用时
long interval = System.currentTimeMillis() - start;
System.out.println("listByNormalFor:" + interval + " ms");
}
/**
* 通过增强for循环遍历LinkedList
* @param list
*/
public static void listByStrengThenFor(LinkedList<Integer> list){
// 记录开始时间
long start = System.currentTimeMillis();
for (Integer i : list) { }
// 记录用时
long interval = System.currentTimeMillis() - start;
System.out.println("listByStrengThenFor:" + interval + " ms");
}
/**
* 通过快迭代器遍历LinkedList
*/
private static void listByIterator(LinkedList<Integer> list) {
// 记录开始时间
long start = System.currentTimeMillis();
for(Iterator iter = list.iterator(); iter.hasNext();) {
iter.next();
}
// 记录用时
long interval = System.currentTimeMillis() - start;
System.out.println("listByIterator:" + interval + " ms");
}
执行结果如下:
listByNormalFor:1067 ms
listByStrengThenFor:3 ms
listByIterator:2 ms
可以看出如果要遍历链表最好使用迭代器或者for(E e:Listlist),远比使用链表的get()函数快
小结
LinkedList在实现上采用结点相互连接的结构实现,同时保存了first和last结点,使得前插和后插效率很高,时间复杂度为O(1),但涉及不同位置插入时,时间复杂度会变为O(n)
由于LinkedList的get()函数的实现采用根据索引位置是否大于size/2来进行顺序或倒序获取元素,比起每次从头开始遍历查询,可以说时提升了查询效率。但当链表长度过长时,或是索引位置偏中间时,get()函数效率会大幅度降低。
参考资料
LinkedList源码