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小记的更多相关文章

  1. effective java读书小记(一)创建和销毁对象

    序言 <effective java>可谓是java学习者心中的一本绝对不能不拜读的好书,她对于目标读者(有一点编程基础和开发经验)的人来说,由浅入深,言简意赅.每一章节都分为若干的条目, ...

  2. java LinkedList(链表)

    LinkedList也像ArrayList一样实现了基本的List接口,但是它执行某些操作(在List的中间插入和移除)时比ArrayList更高效,但在随机访问方面却要逊色一些 LinkedList ...

  3. Java LinkedList add vs push

    Java LinkedList add 是加在list尾部. LinkedList push 施加在list头部. 等同于addFirst.

  4. Java LinkedList【笔记】

    Java LinkedList[笔记] LinkedList LinkedList 适用于要求有顺序,并且会按照顺序进行迭代的场景,依赖于底层的链表结构 LinkedList基本结构 LinkedLi ...

  5. java LinkedList (详解)

    Java 链表(LinkedList) 一.链表简介 1.链表 (Linked List) 是一种常见的基础数据结构,是一种线性表,但是链表不会按线性表的顺序存储数据,而是每个节点里存到下一个节点的地 ...

  6. Java LinkedList 源码剖析

    LinkedList同时实现了List接口和Deque接口,也就是说它既可以看作一个顺序容器,又可以看作一个队列(Queue),同时又可以看作一个栈(Stack).这样看来,LinkedList简直就 ...

  7. java@ LinkedList 学习

    package abc.com; import java.util.LinkedList; public class TestLinkedList { static void prt(Object o ...

  8. JAVA LinkedList和ArrayList的使用及性能分析

    第1部分 List概括List的框架图List 是一个接口,它继承于Collection的接口.它代表着有序的队列.AbstractList 是一个抽象类,它继承于AbstractCollection ...

  9. [Java] LinkedList / Queue - 源代码学习笔记

    简单地画了下 LinkedList 的继承关系,如下图.只是画了关注的部分,并不是完整的关系图.本博文涉及的是 Queue, Deque, LinkedList 的源代码阅读笔记.关于 List 接口 ...

随机推荐

  1. 基本的bash shell命令

    目录 基本的bash shell命令 启动shell shell提示符 基本的bash shell命令 启动shell GNU bash shell 能提供对Linux系统的交互式访问.它是作为普通程 ...

  2. Linux环境下安装java的方法

    linux安装java步骤 方式一:yum方式下载安装 1.查找java相关的列表 yum -y list java* 或者 yum search jdk 2.安装jdk yum install ja ...

  3. 好奇!仅 13kB 大小的游戏,源码长啥样?

    这个马赛克风格的表情正好 13Kb,有人竟然能用一个表情大小的空间,制作个游戏出来.我就不信这么点的地儿,能写出个花来?游戏能好玩吗?因为这些游戏点开就能玩,我抱着试一试的心态把玩了一会. 事实证明是 ...

  4. Flask的环境配置

      Flask django是大而全,提供所有常用的功能 flask是小而精,只提供核心功能 环境配置 为了防止 django和 flask环境相互冲突,可以使用 虚拟环境分割开 pip instal ...

  5. UE4蓝图AI角色制作(六)之行为树

    13.行为树原理 AI最重要的环节就是行为树.我们将解释什么是行为树.为何它如此重要,以及构建行为树需要哪些元素. 借助行为树,我们可以轻松控制并显示AI的决策制定过程.行为树是一种将AI在场景中的决 ...

  6. 最近公共祖先(lca)与树上叉分

    lca的定义不在过多解释, 代码如下: inline void bfs() { queue<int>q; deep[s]=1;q.push(s); while(!q.empty()) { ...

  7. Centos7 升级过内核 boot分区无法挂载修

    参考连接:https://www.cnblogs.com/heqiuyong/p/11186301.html 故障图 挂载系统盘,光盘启动,急救模式, chroot /mnt/sysimage 报错 ...

  8. 【Azure 存储服务】如何把开启NFS 3.0协议的Azure Blob挂载在Linux VM中呢?(NFS: Network File System 网络文件系统)

    问题描述 如何把开启NFS协议的Azure Blob挂载到Linux虚拟机中呢? [答案]:可以使用 NFS 3.0 协议从基于 Linux 的 Azure 虚拟机 (VM) 或在本地运行的 Linu ...

  9. namaspace之pid namespace

    认识Namespace namespace 是 Linux 内核用来隔离内核资源的方式.通过 namespace 可以让一些进程只能看到与自己相关的一部分资源,而另外一些进程也只能看到与它们自己相关的 ...

  10. kafaka高效吞吐量-生产端,服务端,消费端

    零拷贝:sendfile 生产端 生产端:消息压缩,缓存批量发送,异步解耦 多线程并发:防止某一个业务阻塞等待 接收消息缓存 BufferPool设计:不释放 服务端 Reactor模型,顺序写,页缓 ...