前言
在讲完Arraylist之后自然是要讲到LinkedList,因为这两个都实现了List的接口,大家又总是喜欢将其互相比较。本篇文章会介绍其使用,以及深入研究其源码。
介绍
LinkedList是List接口的一个实现类,属于Collection集合。它的底层跟AarrayList可不同,其底层是由双向链表实现。
我们可以看到LinkedList直接实现了四个接口,实现Deque是为了队列的操作,其他三个接口的具体作用可以看我上一篇讲ArrayList的文章。
使用
在使用LinkedList方面大致都和ArrayList差不多,因为它们都实现了List接口。
创建
构造有两个
//无参构造
List list = new LinkedList();
//有参构造
Collection con = null;
List list = new LinkedList(con);
添加
随着LinkedList的添加,size也随着变大。添加的方法用起来还是一样的。
//插入元素
boolean flag=list.add("aa");
list.add(0, "cc");//选择位置进行插入
list.add(0, "bb");//list:[bb, cc, aa]
List list1 = new LinkedList();//构建列表
list1.add(1);
list1.add(2);
//在list中添加集合
list.add(list1);//[bb, cc, aa, [1, 2]]
//addAll是将该集合里面的元素取出加入到列表中
list.addAll(list1);//[bb, cc, aa, 1, 2]
删除
虽然添加和删除方法都是一样的,但是如果是频繁删除还是LinkedList好,我们后面继续分析。
//删除元素
Object remove = list.remove(0); //根据位置进行删除一个元素,并且可以获得自己删除的那个元素
boolean flag = list.remove("aa");//只能删除一个元素需要循环才能删除所有叫“aa”的元素
list.removeAll(list1);//删除list1里面所有的元素
查询
遍历由于和AarrayList一样实现了Iterable所以方法用起来一样。
而查找这方面,LinkedList效率是不如ArrayList的。
//查找元素坐标
int indexOf = list.indexOf("aa");//查询到list中“aa”的坐标位置,从左往右找到第一个
//查找元素
Object object = list.get(0);//根据坐标get到元素
//遍历
Iterator iterator = list.iterator();
while(iterator.hasNext()){
Object next = iterator.next();//获取元素
}
//另外一种遍历
ListIterator listIterator = list.listIterator();
//从左往右遍历
while(listIterator.hasNext()){
Object next = listIterator.next();
}
//从右往左遍历
while(listIterator.hasPrevious()){
Object previous = listIterator.previous();
}
由于LinkedList查找不是靠坐标的所以可以查到头元素和尾元素
//特有的查询
LinkedList list = new LinkedList();//需要特有方法所以不用多态List
Object first = list.getFirst();//获得列表中第一个元素
Object last = list.getLast();//获得列表中的最后一个元素
更改
更改列表中的元素很简单
//更改元素
list.set(0, "ccc");//根据坐标更改元素
Object set = list.set(0, "ccc");//可以获取自己改之前的元素
源码
为何都是实现了List接口的方法,可是不同方面使用起来效率不一样呢,我们需要研究其底层。
成员
先看看这些成员变量和常量。与Arraylist不同,哪些Object数组都不见了,这也证明了LinkedList不是用数组实现的。而定义的Node类则是链表中的节点。
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
transient int size = 0;//元素数量
transient Node<E> first;//头节点
transient Node<E> last;//尾节点
private static final long serialVersionUID = 876323262645176354L;//序列化ID
继续观察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;
}
}
构造
无参构造就是空的。
public LinkedList() {
}
传入集合调用 addAll方法
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
继续调用
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
这里不细讲addAll,后面专门讲,先将构造过一遍。而此时index和size都为0,pred 节点为全局变量 尾节点,然后遍历数组构造newNode 节点。一开始链表为空,所以斗节点是第一个newNode 节点,pred 节点节点不为空了,通过pred节点让newNode节点的next指向下一个循环的newNode节点,最后一个节点的值也是最后一次循环的newNode。
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index);//检查坐标是否在0到size之间
Object[] a = c.toArray();//先将集合转为数组
int numNew = a.length;
if (numNew == 0)//集合长度为0不添加
return false;
Node<E> pred, succ;
if (index == size) {
succ = null;//后节点 为空
pred = last;//前节点
} else {
succ = node(index);
pred = succ.prev;
}
//遍历开始
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;//newNode指向循环中下一个newNode
pred = newNode;
}
if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;//更改次数
return true;
}
其中有个检查方法,后面的方法很多都用到了它
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
其实就是判断index是否是在0到size之间,防止坐标越界
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
添加
来看add方法
public boolean add(E e) {
linkLast(e);
return true;
}
如果尾节点是空的话,头结点就是添加的那个节点。反之,则则将尾节点指向添加的节点。
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++;
}
刚刚的add方法比较简单,也体现不出LinkedList添加的高效率。来看看这个方法,如果插入坐标和size相等,则说明插入的是最后一位,调用上面说明的linkLast方法。反之,则说明插入的是至少是最后一位之前,调用linkBefore方法。
public void add(int index, E element) {
checkPositionIndex(index);//判断是否越位
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
在看linkBefore方法之前我们注意到有一个node方法,这是干什么的?
将size折半进行查找,如果查询坐标在size前半部分,从前往后(0—index)找到坐标对应节点。如果查询坐标在size后半部分,从后往前(size-1—index)找到坐标对应节点。
Node<E> node(int index) {
// assert isElementIndex(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;
}
}
在找到要插入位置的原节点后,找到其前面的节点,然后创造新节点用以代替(原节点)。如果前节点(pred 节点)是空的,说明插入位置为0,那么新节点就是头节点。如果不是的话,让前节点的后驱节点指向新节点,新节点的前驱节点指向前节点,而待处理的原节点则是和新节点连接在一起被分割开来了。
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
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++;
}
演示图:
举例,链表中插入两个String元素“a”,“b”。后来链表又在1位置出入String元素“c”。
再探addAll,看了前面的方法后,了解更深刻了。
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index);//判断index范围是否在0到size之间
Object[] a = c.toArray();//将集合转为数组
int numNew = a.length;//数组长度
if (numNew == 0)//数组长度为0不添加集合
return false;
Node<E> pred, succ;//假设的前节点,后节点
if (index == size) {//size-1位置的节点最后的节点
succ = null;//所以succ(size位置的元素)为0
pred = last;//succ的前节点自然是最后的节点
} else {
succ = node(index);//找到该节点(index坐标对应节点)
pred = succ.prev;//找到succ的前节点
}
//遍历开始
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null);//新建节点
if (pred == null)//pred为0,说明newNode是头结点
first = newNode;
else
pred.next = newNode;//依次插入新节点
pred = newNode;//刷新,pred循环到最后会成为集合对应的最后节点
}
if (succ == null) {
last = pred;
} else {
pred.next = succ;//把succ往后,前驱节点指向原集合最后的节点
succ.prev = pred;
}
size += numNew;//更新size
modCount++;//更改次数
return true;//更改成功
}
删除
删除节点
public E remove(int index) {
checkElementIndex(index);//先检查有没有数组越界
return unlink(node(index));
}
找到删除的原节点,准备将该节点断开联系。先找到原节点的前驱节点和后继节点,让原节点x自断筋骨,跟它们失去联系。
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)变成了原节点的后继节点(x.next)
x.prev = null;//要让x的前驱节点断开
}
if (next == null) {//无后继节点
last = prev;//将x的前驱节点变成尾节点
} else {
next.prev = prev;//原节点的后继节点的前驱节点(原来是x)变成原节点的前驱节点
x.next = null;//让x的后驱节点断开
}
x.item = null;//节点中的元素清空
size--;//size-1
modCount++;//又更改了一次
return element;//返回删除的元素
}
查找
链表的查找是需要不断遍历的,用node方法找到其节点的元素
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
更改
同样用node方法找到节点设置元素
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;//返回旧元素
}
总结
- 由于没有坐标,LinkedList查找效率是很慢的,需要不断遍历节点,为了提高效率源码中Node方法进行了折半查找。而ArrayList是数组所以靠坐标可以方便的查询到元素。
- 由于其数据结构是双向链表所以当对同一位置不断添加和删除的时候,不需要将后面所有的元素进行调整,只需要调整该元素的前驱节点、后驱节点和本节点。所以以后如果频繁的添加或删除是可考虑LinkedList。
- LinkedList与ArrayList一样是不安全的,在多线程环境下还是考虑Vector吧。