LinkedList

初识LinkedList

LinkedList是基于链表实现的,所以先来说明一下链表的概念。

链表:链表原先是C/C++的概念,是一种线性的存储结构,意思是将要存储的数据存在一个存储单元里面,这个存储单元里面除了存放有待存储的数据之外,还存储有其下一个存储单元的地址(下一个存储单元的地址是必要的,有些存储结构还存放有其前一个存储单元的地址),每次查找数据的时候,通过某个存储单元中的下一个存储单元的地址寻找其后面的那个存储单元。

LinkedList是一种双向链表,双向链表我认为有两个含义:

(1)链表中任意一个存储单元都可以通过向前或者向后寻址的方式获取到其前一个存储单元和其后一个存储单元

(2)链表的尾节点的后一个节点是链表的头节点,链表的头节点的前一个节点是链表的尾节点

LinkedList既然是一种双向链表,那么必然有一个存储单元,看一下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;
        }
    }

可以看出LinkedList中的Node是它真正存储数据的地方,其中 Node<E> next 和 Node<E> prev 分别表示当前节点的后一个节点和前一个节点的存储地址的引用地址(其中JDK1.6和之前的版本Entry<E>表示存储的节点)。

LinkedList的简介

下面是LinkedList的类的签名:

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable

LinkedList继承了AbstractSequentialList双向链表,因此可以被当做堆栈、队列或双端队列进行操作

LinkedList实现了List接口,能对它进行队列操作

LinkedList实现了Deque接口,即能将LinkedList当作双端队列使用

LinkedList实现了Cloneable接口,即覆盖了函数clone(),能克隆

LinkedList实现了java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输

LinkedList是非线程安全的

LinkedList的构造函数

// 默认构造函数
LinkedList()

// 创建一个LinkedList,保护Collection中的全部元素。
LinkedList(Collection<? extends E> collection)

LinkedList的KPI

LinkedList的API
boolean       add(E object)
void          add(int location, E object)
boolean       addAll(Collection<? extends E> collection)
boolean       addAll(int location, Collection<? extends E> collection)
void          addFirst(E object)
void          addLast(E object)
void          clear()
Object        clone()
boolean       contains(Object object)
Iterator<E>   descendingIterator()
E             element()
E             get(int location)
E             getFirst()
E             getLast()
int           indexOf(Object object)
int           lastIndexOf(Object object)
ListIterator<E>     listIterator(int location)
boolean       offer(E o)
boolean       offerFirst(E e)
boolean       offerLast(E e)
E             peek()
E             peekFirst()
E             peekLast()
E             poll()
E             pollFirst()
E             pollLast()
E             pop()
void          push(E e)
E             remove()
E             remove(int location)
boolean       remove(Object object)
E             removeFirst()
boolean       removeFirstOccurrence(Object o)
E             removeLast()
boolean       removeLastOccurrence(Object o)
E             set(int location, E object)
int           size()
<T> T[]       toArray(T[] contents)
Object[]     toArray()

AbstractSequentialList简介

在介绍LinkedList源码之前,先介绍一下AbstractSequentialList。毕竟,LinkedList是AbstractSequentialList的子类。

AbstractSequentialList 实现了get(int index)、set(int index, E element)、add(int index, E element) 和 remove(int index)这些函数。这些接口都是随机访问List的,LinkedList是双向链表;既然它继承于AbstractSequentialList,就相当于已经实现了“get(int index)这些接口”。

此外,我们若需要通过AbstractSequentialList自己实现一个列表,只需要扩展此类,并提供 listIterator() 和 size() 方法的实现即可。若要实现不可修改的列表,则需要实现列表迭代器的 hasNext、next、hasPrevious、previous 和 index 方法即可。

LinkedList的数据结构

LinkedList与Collection关系如下图:

LinkedList的本质是双向链表,从图中我们看出:

(1)LinkedList继承了AbstractSequentialList,并且实现了Dequeue接口;

(2)LinkedList包含了两个重要的成员:headersize(1.6以后的版本是firstsize

  header:双向链表的表头,它是双向链表节点所对应的类的Entry(Node)的实例

  size:是双向链表中节点的个数

四个关注点在LinkedList上的答案

关  注  点 结      论
LinkedList是否允许空 允许
LinkedList是否允许重复数据 允许
LinkedList是否有序 有序
LinkedList是否线程安全 非线程安全

添加元素

下面一段代码:

1 public static void main(String[] args){
2         List<String> linkedList = new LinkedList<String>();
3         linkedList.add("111");
4         linkedList.add("222");
5     }

逐行分析上述代码中的3行代码是如何执行的,首先是看第2行,来看LinkedList的源码:

 1 // 链表的表头,表头不包含任何数据。Entry是个链表类数据结构。
 2     private transient Entry<E> header = new Entry<E>(null, null, null);
 3 
 4     // LinkedList中元素个数
 5     private transient int size = 0;
 6 
 7     // 默认构造函数:创建一个空的链表
 8     public LinkedList() {
 9         header.next = header.previous = header;
10     }

从源码中可以看出,new了一个Entry出来名为header,Entry里面的previous、element、next都为null,执行构造函数的时候,将previous和next的值都设置为header的引用地址,还是用画图的方式表示。32位JDK的字长为4个字节,而目前64位的JDK一般采用的也是4个字节,所以就以4个字长为单位,header引用地址的字长就是4个字节,假设为0x00000000,那么执行完List<String> linkedList = new LinkedList<String>();之后可以这样表示:

接着看代码第4行,add了一个字符串“111”:

// 将元素(E)添加到LinkedList中
    public boolean add(E e) {
        // 将节点(节点数据是e)添加到表头(header)之前。
        // 即,将节点添加到双向链表的末端。
        addBefore(e, header);
        return true;
    }
1 private Entry<E> addBefore(E e, Entry<E> entry) {
2 Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
3 newEntry.previous.next = newEntry;
4 newEntry.next.previous = newEntry;
5 size++;
6 modCount++;
7 return newEntry;
8 }

上述第二段代码的第2行:new了一个新的Entry,这句代码可能不太好理解,“翻译”一下:

(1)newEntry.element = e;

(2)newEntry.next = header.next;

(3)newEntry.previous = header.previous;

header.next和header.previous上图中已经看到了,都是0x00000000,那么假设new出来的这个Entry的地址是0x00000001,继续画图表示:

一共有5步,每一步的操作步骤都用数字表示出来了:

(1)新的entry的element赋值为111;

(2)新的entry的next是header的next,header的next是0x00000000,所以新的entry的next即为0x00000000;

(3)新的entry的previous是header的previous,header的previous是0x00000000,所以新的entry的next即为0x00000000;

(4)“newEntry.previous.next = newEntry”,首先是newEntry的previous,由于newEntry的previous为0x00000000,所以newEntry.previous表示的是header,header的next为newEntry,即header的next为0x00000001;

(5)“newEntry.next.previous = newEntry”,和步骤4一样,把header的previous设置为0x00000001;

为什么奥这样做?还记得双向链表的两个特点吗?一个是任意节点都可以向前和向后寻址,另外一个是整个链表头的previous表示的是链表的尾Entry,链表尾的next表示的是链表头的Entry。现在链表头就是0x00000000这个Entry,链表尾就是0x00000001,从上图可以看出,完全符合这两个条件。

最后一个步骤是add了一个字符串“222”,假设新new出来的Entry的地址是0x00000002,画图表示:

还是上述的5个步骤,可以看出还是符合双向链表的两个特征的,这里就不再赘述步骤了,可以照着上述步骤进行模拟。

查看元素

 看一下源码是怎样写的:

public E get(int index) {
    return entry(index).element;
}
 1 private Entry<E> entry(int index) {
 2     if (index < 0 || index >= size)
 3         throw new IndexOutOfBoundsException("Index: "+index+
 4                                             ", Size: "+size);
 5     Entry<E> e = header;
 6     if (index < (size >> 1)) {
 7         for (int i = 0; i <= index; i++)
 8             e = e.next;
 9     } else {
10         for (int i = size; i > index; i--)
11             e = e.previous;
12     }
13     return e;
14 }

这段代码就体现出了双向链表的好处了。双向链表增加了一点点的空间消耗(每个Entry里面需要维护它的前后置Entry的引用),同时也增加了一定的编程复杂度,却大大提升了效率。

由于LinkedList是双向链表,所以LinkedList既可以向前查找,也可以向后查找,上述代码第6~12行的作用就是:当index小于链表大小一半的时候(size >> 1表示size/2,使用位移运算提升代码运行的效率),向后查找;否则,就向前查找(也叫做二分查找法)。

这样,假如在我的链表结构中有10000个元素,刚巧查找的元素在第10000时,就不用从头遍历10000次了,向前遍历即可,一次就能找到需要的元素。

删除元素

删除元素也画图来说明,比如说有下面的一段代码:

public static void main(String[] args)
{
    List<String> list = new LinkedList<String>();
    list.add("111");
    list.add("222");
    list.remove(0);
}

代码的最后一行,也就是要删除元素“111”,看一下源码是怎么样执行的:

public E remove(int index) {
     return remove(entry(index));
 }
 1 private E remove(Entry<E> e) {
 2 if (e == header)
 3     throw new NoSuchElementException();
 4 
 5         E result = e.element;
 6 e.previous.next = e.next;
 7 e.next.previous = e.previous;
 8        e.next = e.previous = null;
 9        e.element = null;
10 size--;
11 modCount++;
12        return result;
13 }

 上述代码与get方法基本一致,都是先定位出元素在那里,画图说明:

上述步骤也比较简单,每一步操作都标注在图上了,就不再赘述了。

但注意一点,图中的3、4、5步骤将待删除的Entry的previous、element、next都设置为null,这三步的作用是让虚拟机可以尽快的回收Entry。

再继续扩展深入一点:按照Java虚拟机HotSpot采用的垃圾回收算法-------根节点搜索算法来说,即使previous、element、next不设置为null也是可以回收这个Entry的,因为此时这个Entry已经没有任何地方会指向它了,header和tail的previous与header的next都已经变掉了,所以这块Entry会被当做“垃圾”对待。之所以还要将previous、element、next设置为null,我认为可能是为了兼容另外一种算法-------引用计数法,这种垃圾回收算法,只要对象之间存在相互引用,那么这块内存就不会被当作“垃圾”对待。

LinkedList和ArrayList的对比

(1)顺序插入速度ArrayList会比较快,因为ArrayList是基于数组实现的,数组是事先new好的,只要往指定位置塞一个数据就好了;LinkedList则不同,每次顺序插入的时候LinkedList将new一个对象出来,如果对象比较大,那么new的时间势必会长一点,再加上一些引用赋值的操作,所以顺序插入LinkedList必然慢于ArrayList;

(2)基于上一点,因为LinkedList里面不仅维护了待插入的元素,还维护了Entry的前置Entry和后继Entry,如果一个LinkedList中的Entry非常多,那么LinkedList将比ArrayList更耗费一些内存;

(3)使用各自遍历效率最高的方式,ArrayList的遍历效率会比LinkedList的遍历效率高一些;

(4)有的说法认为LinkedList做插入和删除更快,这种说法其实是不正确的:

  1.LinkedList做插入、删除的时候,慢在寻址,快在只需要改变前后Entry的引用地址

  2.ArrayList做插入、删除的时候,慢在数组元素的批量copy,快在寻址

所以,如果待插入、删除的元素是在数据结构的前半段尤其是非常靠前的位置的时候,LinkedList的效率将大大快过ArrayList,因为ArrayList将批量copy大量的元素;越往后,对于LinkedList来说,因为它是双向链表,所以在第2个元素后面插入一个数据和在倒数第二个元素后面插入一个元素在效率上基本没有差别,但是ArrayList由于要批量copy的元素越来越少,操作速度必然追上乃至超过LinkedList。

从上述分析来看,如果你十分确定插入、删除的元素是在前半段,那么就使用LinkedList;如果你十分确定删除、插入的元素是在比较靠后的位置,那么可以考虑使用ArrayList,如果你不确定你要做的插入、删除是在那里?那还是建议你使用LinkedList,因为LinkedList整体插入、删除的执行效率比较的稳定,没有ArrayList这种越往后越快的情况;同时插入元素的时候,弄不好ArrayList还要进行一次扩容,ArrayList底层数组扩容是一个既消耗时间又消耗空间的操作。

对于LinkedList和ArrayList的遍历,可以参考我的文章切勿用普通的for循环遍历LinkedList

参考:https://www.cnblogs.com/xrq730/p/5005347.html

posted on 2019-02-18 21:14  AoTuDeMan  阅读(186)  评论(0编辑  收藏  举报

导航