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. 回归本心QwQ背包问题luogu1776

    今天在这里说一下多重背包问题 对 之前一直没有怎么彻底理解 首先多重背包是什么?这里就不做过多的赘述了 朴素的多重背包的复杂度是\(O(n*m*\sum s[i])\),其中\(s[i]\)是每一件物 ...

  2. JVM详解(三)——运行时数据区

    一.概述 1.介绍 类比一下:红框就好比内存的运行时数据区,在各自不同的位置放了不同的东西.而厨师就好比执行引擎. 内存是非常重要的系统资源,是硬盘和CPU的中间仓库及桥梁,承载着操作系统和应用程序的 ...

  3. Java(14)面向对象之封装

    作者:季沐测试笔记 原文地址:https://www.cnblogs.com/testero/p/15201610.html 博客主页:https://www.cnblogs.com/testero ...

  4. 【UE4 调试】C++ 几种编译方法和小技巧

    编译方法 Visual Studio 2019 编译 默认编译 UnrealVS 快速编译 Editor 编译 一般 vs 编译完后,Editor会跟着热编译(有声音) 如果发现编译后代码没更新到Ed ...

  5. 大厂面试题系列:重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分

    面试题:重载(Overload)和重写(Override)的区别.重载的方法能否根据返回类型进行区分 面试官考察点猜想 这道题纯粹只是考查基础理论知识,对实际开发工作中没有太多的指导意义,毕竟编辑器都 ...

  6. C++的指针使用心得

    使用C++有一段时间了,C++的手动内存管理缺失很麻烦,一不小心容易产生内存泄漏.自己总结了一点使用原则(不一定对),备注一下,避免忘记. 1.类外部传来的指针不处理 2.Qt对象管理的内存不处理 3 ...

  7. 华为HG255D挂卡中继专用旋风科技固件

    正的挂卡不掉线不掉速,稳定上网看上去好像很NB的样子 挂卡设置教程:http://picimg.lshou.com/pic/clou ... /6/t/1/30247515.mp4 固件链接: htt ...

  8. poj 3417 Network (LCA,路径上有值)

    题意: N个点,构成一棵树.给出这棵树的结构. M条边,(a1,b1)...(am,bm),代表给树的这些点对连上边.这样就形成了有很多环的一个新"树". 现在要求你在原树中断一条 ...

  9. Markdown使用方式

    区块 区块引用在段落开头使用>,后面紧跟一个空格符号 > 区块引用 > XXX > XXX 高级技巧 HTML元素 居中  <center>XXX</cent ...

  10. 正则表达式之grep

    grep 的五个参数,基本的常用的: -a :将 binary 档案以 text 档案的方式搜寻数据 -c :计算找到 '搜寻字符串' 的次数 -i :忽略大小写的不同,所以大小写视为相同 -n :顺 ...