Java LinkedList小记
1. 基本用法
LinkedList实现了List、Deque、Queue接口,可以按照队列、栈和双端队列的方式进行操作。LinkedList有两个构造方法,一个是默认构造,另一个接受Collection:
public LinkedList()
public LinkedList(Collection<? extends E> c)
可以按照List操作:
List<Integer> list = new LinkedList<>();
List<Integer> list1 = new LinkedList<>(Arrays.asList(2, 3, 4, 5));
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的实现可能有。在队列为空时,remove和element会抛出异常NoSuchElementException,而poll和peek返回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());
}
栈是一种和队列特点相反的数据结构,它的特点是先进后出,后进先出。Java中没有单独的栈接口,栈的相关方法包括在了表示双端队列的接口Deque中,主要有三个方法:
// 入栈
void push(E e);
// 出栈
E pop();
// 查看
E peek();
push表示入栈,在头部添加元素,栈的空间可能是有限的,如果栈满了,push会抛出IllegalStateException;pop表示出栈,返回头部元素,并且从栈中删除,如果栈为空会抛出NoSuchElementException;peek查看栈头部元素,不修改栈,如果栈为空,返回特殊值null。使用方法如下:
Deque<Integer> stack = new LinkedList<>();
stack.push(1);
stack.push(2);
stack.push(3);
while (stack.peek() != null) {
System.out.println(stack.pop());
}
/**
* output:
* 3
* 2
* 1
*/
Java中还有一个Stack类,就是栈的意思,它也实现了栈的一些方法,如push、pop、peek等,但它没有实现Deque接口,他是Vector的子类它增加的这些方法也通过synchronized实现了线程安全。由于Vector和Stack内部使用了大量的syncronized做同步操作,效率比较低,已经过时了,具体就不学习了。
栈和队列都是在两端进行操作,栈只操作头部,队列两端都操作,但只在尾部添加、头部只查看和删除元素。有一个更为通用的操作两端的接口Deque。接口定义如下:
public interface Deque<E> extends Queue<E> {
void addFirst(E e);
void addLast(E e);
boolean offerFirst(E e);
boolean offerLast(E e);
E removeFirst();
E removeLast();
E pollFirst();
E pollLast();
E getFirst();
E getLast();
E peekFirst();
E peekLast();
//删除第一次出现的指定元素(从头到尾遍历)
boolean removeFirstOccurrence(Object o);
//删除最后次出现的指定元素(从头到尾遍历)
boolean removeLastOccurrence(Object o);
boolean add(E e);
boolean offer(E e);
E remove();
E poll();
E element();
E peek();
void push(E e);
E pop();
boolean remove(Object o);
// 队列是否包含指定元素
boolean contains(Object o);
public int size();
Iterator<E> iterator();
// 从后往前遍历的迭代器
Iterator<E> descendingIterator();
}
根据方法名很容易知道作用,稍微不太清晰的做了注释,descendingIterator()方法示例如下:
Deque<String> deque = new LinkedList<>(Arrays.asList("a", "b", "c", "d"));
Iterator<String> it = deque.descendingIterator();
while (it.hasNext()) {
System.out.print(it.next() + " ");
}
/**
* output:
* d c b a
*/
下面看下实现原理。
2. 原理
先来看下LinkedList的内部组成,再分析一些主要方法的实现,代码基于JDK8。
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;
size表示链表长度,默认为0,first指向头节点,last指向尾节点,初始值都是null。LinkedList的所有public方法内部操作的就是这三个实例变量,来看下具体方法:
2.2 add方法
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
// 将尾节点赋给l变量
final Node<E> l = last;
// 新建节点,将l赋给新建节点的pre前驱节点,e为当前节点的元素值,新建节点没有后继节点,所以为null
final Node<E> newNode = new Node<>(l, e, null);
// 将新建节点赋给尾节点
last = newNode;
// 如果尾节点不存在,就将新建节点作为头结点赋给first实例变量
if (l == null)
first = newNode;
// 如若尾节点存在,就将新建节点作为尾节点的后继节点赋给l.next
else
l.next = newNode;
// 链表长度加1
size++;
// 修改次数加1
modCount++;
}
代码的基本步骤见代码中注释,modCount变量用来记录修改次数,便于在迭代中检测结构性变化。我们根据图示来理解下。比如如下代码:
List<String> list = new LinkedList<String>();
list.add("a");
list.add("b");

当新建list对象后内部结构如图一,头结点和尾节点都是null;当添加“a”后内部结构如图二,size加1,头结点和尾节点都指向同一个Node节点;当添加完“b”后内部结构如图三所示。
2.3 根据索引访问元素
来看下get方法:
public E get(int index) {
// 检查索引位置的有效性,若无效,抛出异常
checkElementIndex(index);
// 索引有效,执行node方法,查找指定索引位置的元素并返回
return node(index).item;
}
private void checkElementIndex(int index) {
if (!isElementIndex(index))
// 抛出未受检异常,索引越界异常
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
Node<E> node(int index) {
// 若索引位置在前半部分,则从头结点开始查找(右移一位相当于除以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;
}
}
与ArrayList不同,ArrayList中数组元素连续存放,可以根据索引直接定位,而在LinkedList中,则必须从头到尾顺着连接查找,效率比较低。
2.4 按内容查找元素
看下indexOf的代码:
public int indexOf(Object o) {
int index = 0;
// 查找元素为null时
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
// 查找元素不为null时
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
// 买找到指定元素返回-1
return -1;
}
代码比较简单,有两种情况,都是从头节点开始找,见代码注释。
2.5 插入元素
add是在尾部添加元素,如果在头部或者中间插入元素可以使用如下重载方法:
public void add(int index, E element) {
checkPositionIndex(index);
// 这就是在尾部添加元素
if (index == size)
linkLast(element);
// 主要看这个
else
linkBefore(element, node(index));
}
void linkBefore(E e, Node<E> succ) {
// succ不为空,就把succ的前驱节点赋给pred
final Node<E> pred = succ.prev;
// 新建节点,将pred指定为新建节点的前驱节点,succ为后继节点
final Node<E> newNode = new Node<>(pred, e, succ);
// 将后的前驱指向新建节点
succ.prev = newNode;
// 将前驱的后继指向新建节点,若前驱为空,修改头结点指向新节点
if (pred == null)
first = newNode;
else
pred.next = newNode;
// 增加长度
size++;
modCount++;
}
下面通过图示来加深理解,比如添加一个元素
list.add(1, "c");

可以看出,在中间插入元素,LinkedList只需按需分配内存,修改前驱和后继节点的链接,而ArrayList则可能需要分配很多的额外空间,且移动、复制所有后继元素。
2.6 删除元素
再来看看删除元素的代码:
public E remove(int index) {
// 同上检查索引是否有效
checkElementIndex(index);
// node方法先查找节点,再执行unlink删除指定节点
return unlink(node(index));
}
E unlink(Node<E> x) {
// x节点不为空,取得节点元素值
final E element = x.item;
// 取得前驱节点
final Node<E> next = x.next;
// 取得后继节点
final Node<E> prev = x.prev;
// 指定前驱的后继为x的后继(不在指向x),若前驱为空,修改头节点指向x的后继
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
// 指定后继的前驱为x的前驱(不再指向x),若后继为空,修改尾节点指向x的前驱
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
// x节点元素值设为null,便于垃圾回收
x.item = null;
// 链表长度减1
size--;
// 修改次数加1
modCount++;
// 返回删除的节点值
return element;
}
分析逻辑见代码注释,基本思路就是让x的前驱和后继直接链接起来,再把x的前驱、后继节点、item都设置为null,便于垃圾回收。下面通过图示加深理解,比如删除一个元素:
list.remove(1);

3. LinkedList特点总结
用法上LinkedList是一个List,有序有重复元素,也实现了Deque接口,可以作为队列、栈和双端队列使用。实现原理上,LinkedList内部是一个双向链表,并维护了长度、头结点和尾节点。有如下特点:
1. 按需分配空间,不需要预先分配很多空间。
2. 不可以随机访问,按照索引位置访问效率比较低,必须从头或尾顺着链接找,效率为O(N/2)。
3. 不管列表是否已排序,只要按照内容查找元素,效率都比较低,必须逐个比较,效率为O(N)。
---------- I love three things in this world. Sun, moon and you. Sun for morning, moon for night , and you forever .
Java LinkedList小记的更多相关文章
- effective java读书小记(一)创建和销毁对象
序言 <effective java>可谓是java学习者心中的一本绝对不能不拜读的好书,她对于目标读者(有一点编程基础和开发经验)的人来说,由浅入深,言简意赅.每一章节都分为若干的条目, ...
- java LinkedList(链表)
LinkedList也像ArrayList一样实现了基本的List接口,但是它执行某些操作(在List的中间插入和移除)时比ArrayList更高效,但在随机访问方面却要逊色一些 LinkedList ...
- Java LinkedList add vs push
Java LinkedList add 是加在list尾部. LinkedList push 施加在list头部. 等同于addFirst.
- Java LinkedList【笔记】
Java LinkedList[笔记] LinkedList LinkedList 适用于要求有顺序,并且会按照顺序进行迭代的场景,依赖于底层的链表结构 LinkedList基本结构 LinkedLi ...
- java LinkedList (详解)
Java 链表(LinkedList) 一.链表简介 1.链表 (Linked List) 是一种常见的基础数据结构,是一种线性表,但是链表不会按线性表的顺序存储数据,而是每个节点里存到下一个节点的地 ...
- Java LinkedList 源码剖析
LinkedList同时实现了List接口和Deque接口,也就是说它既可以看作一个顺序容器,又可以看作一个队列(Queue),同时又可以看作一个栈(Stack).这样看来,LinkedList简直就 ...
- java@ LinkedList 学习
package abc.com; import java.util.LinkedList; public class TestLinkedList { static void prt(Object o ...
- JAVA LinkedList和ArrayList的使用及性能分析
第1部分 List概括List的框架图List 是一个接口,它继承于Collection的接口.它代表着有序的队列.AbstractList 是一个抽象类,它继承于AbstractCollection ...
- [Java] LinkedList / Queue - 源代码学习笔记
简单地画了下 LinkedList 的继承关系,如下图.只是画了关注的部分,并不是完整的关系图.本博文涉及的是 Queue, Deque, LinkedList 的源代码阅读笔记.关于 List 接口 ...
随机推荐
- Python内置高阶函数map()
map()函数map()是 Python 内置的高阶函数,它接收一个函数 f 和一个 list,并通过把函数 f 依次作用在 list 的每个元素上,得到一个新的 list 并返回. 例如,对于lis ...
- TypeScript中将函数中的局部变量“导出”的方法
首先是在模块a.js中声明一个可导出(export)的数据结构,例如: export class ModelInfo{ id: string; name:string; } 其次是在模块b中声明可导出 ...
- javascript-原生-结构
1.获取用户输入内容的方法 window.prompt("提示信息","默认值"); 获取用户输入内容(字符串类型),返回用户输入内容. 2.顺序结构:所有语句 ...
- python解释器和Pycharm编辑器安装使用完整详细教程
一.官网下载或软件管家公众号下载 二.安装Python解释器 1.选择自定义安装并添加到环境变量 2.检验Python是否安装成功 三.安装pycharm编辑器 1.点击安装,修改安装路径,建议安装C ...
- Flink sql 之 join 与 StreamPhysicalJoinRule (源码解析)
源码分析基于flink1.14 Join是flink中最常用的操作之一,但是如果滥用的话会有很多的性能问题,了解一下Flink源码的实现原理是非常有必要的 本文的join主要是指flink sql的R ...
- the Agiles Scrum Meeting 7
会议时间:2020.4.15 21:00 1.每个人的工作 根据项目进度,我们将原先的完善组和debug组合并,成为团队项目增量开发组,原增量组成为个人结对项目增量开发组. 今天已完成的工作 个人结对 ...
- Beta阶段第一次会议
Beta阶段第一次例会 时间:2020.5.16 完成工作 姓名 完成任务 难度 完成度 lm 1.修订网页端信息编辑bug2.修订网页端登录bug(提前完成,相关issue已关闭) 中 100% x ...
- 大闸蟹的 O O 第三单元日子——中测与强测的惨烈修罗场
第三单元是大闸蟹体验及其差的一单元,鬼知道从一开始的自信慢慢到最后的自暴自弃我都经历了什么,我已经感觉到分数与gpa与头发都在渐渐和我说再见了 JML基础梳理及工具链 JML(Java Modelin ...
- 2021.5.24考试总结 [NOIP模拟3]
带着爆0的心态考的试,没想到整了个假rk2 (炸鱼大佬wtz忒强了OTZ T1 景区路线规划 这题对刚学完概率期望的我来说简直水爆了好吗.. 因为存在时间限制,不好跑高斯消元,就直接跑dp就完了. 令 ...
- 实验5:开源控制器实践——POX
一.实验目的 1.能够理解 POX 控制器的工作原理: 2.通过验证POX的forwarding.hub和forwarding.l2_learning模块,初步掌握POX控制器的使用方法: 3.能够运 ...