Java——LinkedList
LinkedList
1、用法
LinkedList的构造方法与ArrayList类似,有两个:一个是默认构造方法,另外一个可以接受一个已有的Collection,如下所示:
public LinkedList()
public LinkedList(Collection<? extends E> c)
比如,可以这么创建:
List<String> list = new LinkedList<>();
List<String> list2 = new LinkedList<>(
Arrays.asList(new String[]{"a", "b", "c"}));
LinkedList与ArrayList一样,同样实现了List接口,而List接口扩展了Collection接口,Collection又扩展了Iterable接口,所有这些接口的方法都是可以使用的。LinkedList还实现了队列接口Queue,所谓队列就类似于日常生活中的各种排队,特点就是先进先出,在尾部添加元素,从头部删除元素,它的接口定义为:
public interface Queue<E> extends Collection<E> {
boolean add(E e);
boolean offer(E e);
E remove();
E poll();
E element();
E peek();
}
Queue扩展了Collection,它的主要操作有三个:
- 在尾部添加元素(add、offer);
- 查看头部元素(element、peek),返回头部元素,但不改变队列;
- 删除头部元素(remove、poll),返回头部元素,并且从队列中删除。
每种操作都有两种形式,有什么区别呢?区别在于,对于特殊情况的处理不同。特殊情况是指队列为空或者队列为满,为空容易理解,为满是指队列有长度大小限制,而且已经占满了。LinkedList的实现中,队列长度没有限制,但别的Queue的实现可能有。在队列为空时,element和remove会抛出异常NoSuchElementException,而peek和poll返回特殊值null;在队列为满时,add会抛出异常IllegalStateException,而offer只是返回false。
把LinkedList当作Queue使用也很简单,比如,可以这样:
Queue<String> queue = new LinkedList<>();
queue.offer("a");
queue.offer("b");
queue.offer("c");
while(queue.peek()! =null){
System.out.println(queue.poll());
}
输出有三行,依次为a、b和c。
栈也是一种常用的数据结构,与队列相反,它的特点是先进后出、后进先出,类似于一个储物箱,放的时候是一件件往上放,拿的时候则只能从上面开始拿。Java中没有单独的栈接口,栈相关方法包括在了表示双端队列的接口Deque中,主要有三个方法:
void push(E e);
E pop();
E peek();
解释如下。
- push表示入栈,在头部添加元素,栈的空间可能是有限的,如果栈满了,push会抛出异常IllegalStateException。
- pop表示出栈,返回头部元素,并且从栈中删除,如果栈为空,会抛出异常NoSuch-ElementException。
- peek查看栈头部元素,不修改栈,如果栈为空,返回null。
把LinkedList当作栈使用也很简单,比如,可以这样:
Deque<String> stack = new LinkedList<>();
stack.push("a");
stack.push("b");
stack.push("c");
while(stack.peek()! =null){
System.out.println(stack.pop());
}
输出有三行,依次为c、b和a。
Java中有一个类Stack,单词意思是栈,它也实现了栈的一些方法,如push/pop/peek等,但它没有实现Deque接口,它是Vector的子类,它增加的这些方法也通过synchronized实现了线程安全,具体就不介绍了。不需要线程安全的情况下,推荐使用LinkedList。
栈和队列都是在两端进行操作,栈只操作头部,队列两端都操作,但尾部只添加、头部只查看和删除。有一个更为通用的操作两端的接口Deque。Deque扩展了Queue,包括了栈的操作方法,此外,它还有如下更为明确的操作两端的方法:
void addFirst(E e);
void addLast(E e);
E getFirst();
E getLast();
boolean offerFirst(E e);
boolean offerLast(E e);
E peekFirst();
E peekLast();
E pollFirst();
E pollLast();
E removeFirst();
E removeLast();
xxxFirst操作头部,xxxLast操作尾部。与队列类似,每种操作有两种形式,区别也是在队列为空或满时处理不同。为空时,getⅩⅩⅩ/removeⅩⅩⅩ会抛出异常,而peekⅩⅩⅩ/pollⅩⅩⅩ会返回null。队列满时,addⅩⅩⅩ会抛出异常,offerⅩⅩⅩ只是返回false。
栈和队列只是双端队列的特殊情况,它们的方法都可以使用双端队列的方法替代,不过,使用不同的名称和方法,概念上更为清晰。
Deque接口还有一个迭代器方法,可以从后往前遍历:
Iterator<E> descendingIterator();
比如,看如下代码:
Deque<String> deque = new LinkedList<>(
Arrays.asList(new String[]{"a", "b", "c"}));
Iterator<String> it = deque.descendingIterator();
while(it.hasNext()){
System.out.print(it.next()+" ");
}
// c b a
简单总结下:LinkedList的用法是比较简单的,与ArrayList用法类似,支持List接口,只是,LinkedList增加了一个接口Deque,可以把它看作队列、栈、双端队列,方便地在两端进行操作。如果只是用作List,那应该用ArrayList还是LinkedList呢?我们需要了解LinkedList的实现原理。
2、实现原理
我们先来看LinkedList的内部组成,然后分析它的一些主要方法的实现,代码基于Java 7。
2.1、内部组成
我们知道,ArrayList内部是数组,元素在内存是连续存放的,但LinkedList不是。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;
}
}
Node类表示节点,item指向实际的元素,next指向后一个节点,prev指向前一个节点。
LinkedList内部组成就是如下三个实例变量:
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
我们暂时忽略transient关键字,size表示链表长度,默认为0, first指向头节点,last指向尾节点,初始值都为null。
LinkedList的所有public方法内部操作的都是这三个实例变量,具体是怎么操作的?链接关系是如何维护的?我们看一些主要的方法,先来看add方法。
2.2、add方法
add方法的代码为:
public boolean add(E e) {
linkLast(e);
return true;
}
add方法的代码为:
public boolean add(E e) {
linkLast(e);
return true;
}
主要就是调用了linkLast,它的代码为:
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++;
}
代码的基本步骤如下。
- 创建一个新的节点newNode。l和last指向原来的尾节点,如果原来链表为空,则为null。代码为:
Node<E> newNode = new Node<>(l, e, null);
- 修改尾节点last,指向新的最后节点newNode。代码为:
last = newNode;
- 修改前节点的后向链接,如果原来链表为空,则让头节点指向新节点,否则让前一个节点的next指向新节点。代码为:
if(l == null)
first = newNode;
else
l.next = newNode;
- 增加链表大小。代码为:
size++
modCount++的目的与ArrayList是一样的,记录修改次数,便于迭代中间检测结构性变化。
我们通过一些图示来进行介绍。比如,代码为:
List<String> list = new LinkedList<String>();
list.add("a");
list.add("b");
执行完第一行后,内部结构如图所示。
添加完“a”后,内部结构如图所示。
可以看出,与ArrayList不同,Linked-List的内存是按需分配的,不需要预先分配多余的内存,添加元素只需分配新元素的空间,然后调节几个链接即可。
2.3、根据索引访问元素get
添加了元素,如何根据索引访问元素呢?我们看下get方法的代码:
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
checkElementIndex检查索引位置的有效性,如果无效,则抛出异常,代码为:
private void checkElementIndex(int index) {
if(! isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
如果index有效,则调用node方法查找对应的节点,其item属性就指向实际元素内容,node方法的代码为:
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;
}
}
size>>1等于size/2,如果索引位置在前半部分(index<(size>>1)),则从头节点开始查找,否则,从尾节点开始查找。可以看出,与ArrayList明显不同,ArrayList中数组元素连续存放,可以根据索引直接定位,而在LinkedList中,则必须从头或尾顺着链接查找,效率比较低。
2.4、根据内容查找元素
我们看下indexOf的代码:
public int indexOf(Object o) {
int index = 0;
if(o == null) {
for(Node<E> x = first; x ! = null; x = x.next) {
if(x.item == null)
return index;
index++;
}
} else {
for(Node<E> x = first; x ! = null; x = x.next) {
if(o.equals(x.item))
return index;
index++;
}
}
return -1;
}
代码也很简单,从头节点顺着链接往后找,如果要找的是null,则找第一个item为null的节点,否则使用equals方法进行比较。
2.5、插入元素
add是在尾部添加元素,如果在头部或中间插入元素呢?可以使用如下方法:
public void add(int index, E element)
它的代码是:
public void add(int index, E element) {
checkPositionIndex(index);
if(index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
如果index为size,添加到最后面,一般情况,是插入到index对应节点的前面,调用方法为linkBefore,它的代码为:
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++;
}
参数succ表示后继节点,变量pred表示前驱节点,目标是在pred和succ中间插入一个节点。插入步骤是:
- 新建一个节点newNode,前驱为pred,后继为succ。代码为:
Node<E> newNode = new Node<>(pred, e, succ);
- 让后继的前驱指向新节点。代码为:
succ.prev = newNode;
- 让前驱的后继指向新节点,如果前驱为空,那么修改头节点指向新节点。代码为:
if (pred == null)
first = newNode;
else
pred.next = newNode;
- 增加长度。
我们通过图示来进行介绍。还是上面的例子,比如,添加一个元素:
list.add(1, "c");

可以看出,在中间插入元素,LinkedList只需按需分配内存,修改前驱和后继节点的链接,而ArrayList则可能需要分配很多额外空间,且移动所有后续元素。
3.6、删除元素
我们再来看删除元素,代码为:
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
通过node方法找到节点后,调用了unlink方法,代码为:
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;
}
if(next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
删除x节点,基本思路就是让x的前驱和后继直接链接起来,next是x的后继,prev是x的前驱,具体分为两步。
- 让x的前驱的后继指向x的后继。如果x没有前驱,说明删除的是头节点,则修改头节点指向x的后继。
- 让x的后继的前驱指向x的前驱。如果x没有后继,说明删除的是尾节点,则修改尾节点指向x的前驱。
通过图示进行说明。还是上面的例子,如果删除一个元素:
list.remove(1);

3、特点分析
用法上,LinkedList是一个List,但也实现了Deque接口,可以作为队列、栈和双端队列使用。实现原理上,LinkedList内部是一个双向链表,并维护了长度、头节点和尾节点,这决定了它有如下特点。
- 按需分配空间,不需要预先分配很多空间。
- 不可以随机访问,按照索引位置访问效率比较低,必须从头或尾顺着链接找,效率为O(N/2)。
- 在两端添加、删除元素的效率很高,为O(1)。
- 在中间插入、删除元素,要先定位,效率比较低,为O(N),但修改本身的效率很高,效率为O(1)。
- 在中间插入、删除元素,要先定位,效率比较低,为O(N),但修改本身的效率很高,效率为O(1)。
更多推荐


所有评论(0)