JAVA学习-LinkedList详解

1.定义

实现List接口与Deque接口双向链表,实现了列表的所有操作,并且允许包括null值的所有元素,对于LinkedList定义我产生了如下疑问:

1.Deque接口是什么,定义了一个怎样的规范?

2.LinkedList是双向链表,其底层实现是怎样的,具体包含哪些操作?

下文将围绕这两个问题进行,去探寻LinkedList内部的奥秘,以下源码是基于JDK 1.7.0_79

2.结构

2.1 类结构

LinkedList的类的结构如下图所示

image

通过上图可以看出,LinkedList继承的类与实现的接口如下:

1.Collection 接口: Collection接口是所有集合类的根节点,Collection表示一种规则,所有实现了Collection接口的类遵循这种规则

2.List 接口: List是Collection的子接口,它是一个元素有序(按照插入的顺序维护元素顺序)、可重复、可以为null的集合

3.AbstractCollection 类: Collection接口的骨架实现类,最小化实现了Collection接口所需要实现的工作量

4.AbstractList 类: List接口的骨架实现类,最小化实现了List接口所需要实现的工作量

5.Cloneable 接口: 实现了该接口的类可以显示的调用Object.clone()方法,合法的对该类实例进行字段复制,如果没有实现Cloneable接口的实例上调用Obejct.clone()方法,会抛出CloneNotSupportException异常。正常情况下,实现了Cloneable接口的类会以公共方法重写Object.clone()

6.Serializable 接口: 实现了该接口标示了类可以被序列化和反序列化,具体的 查询序列化详解

7.Deque 接口: Deque定义了一个线性Collection,支持在两端插入和删除元素,Deque实际是“double ended queue(双端队列)”的简称,大多数Deque接口的实现都不会限制元素的数量,但是这个队列既支持有容量限制的实现,也支持没有容量限制的实现,比如LinkedList就是有容量限制的实现,其最大的容量为Integer.MAX_VALUE

8.AbstractSequentialList 类: 提供了List接口的骨干实现,最大限度地减少了实现受“连续访问”数据存储(如链表)支持的此接口所需的工作,对于随机访问数据(如数组),应该优先使用 AbstractList,而不是使用AbstractSequentailList类

2.2 基础属性及构造方法

2.2.1 基础属性
public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    //长度
    transient int size = 0;
    //指向头结点
    transient Node<E> first;
    //指向尾结点
    transient Node<E> last;
}

如上源码中为LinkedList中的基本属性,其中size为LinkedList的长度,first为指向头结点,last指向尾结点,Node为LinkedList的一个私有内部类,其定义如下,即定义了item(元素),next(指向后一个元素的指针),prev(指向前一个元素的指针)

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中的元素为["A","B","C"],其内部的结构如下图所示

image

可以看出一个节点中包含三个属性,也就是上面源码中定义的属性,可以清晰的看出LinkedList底层是双向链表的实现

2.2.2构造方法

在源码中,LinkedList主要提供了两个构造方法,

1.public LinkedList() :空的构造方法,啥事情都没有做

2.public LinkedList(Collection<? extends E> c) : 将一个元素集合添加到LinkedList中

3.底层实现

在2.2.1中的LinkedList内部结构图,可以清晰的看出LinkedList双向链表的实现,下面将通过源码分析如何在双向链表中添加和删除节点的。

3.1 添加节点

通常我们会使用add(E e)方法添加元素,通过源码我们发现add(E e)内部主要调用了以下方法

//在链表的最后添加元素
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++;
}

其实通过源码可以看出添加的过程如下:

1.记录当前末尾节点,通过构造另外一个指向末尾节点的指针l

2.产生新的节点:注意的是由于是添加在链表的末尾,next是为null的

3.last指向新的节点

4.这里有个判断,我的理解是判断是否为第一个元素(当l==null时,表示链表中是没有节点的),
那么就很好理解这个判断了,如果是第一节点,则使用first指向这个节点,若不是则当前节点的next指向新增的节点

5.size增加
例如,在上面提到的LinkedList["A","B","C"]中添加元素“D”,过程大致如图所示

image

LinkedList中还提供如下的方法,进行添加元素,具体逻辑与linkLast方法大同小异,就不在这里一一介绍了。

3.2 删除节点

LinkedList中提供了两个方法删除节点,如下源码所示

//方法1.删除指定索引上的节点
public E remove(int index) {
    //检查索引是否正确
    checkElementIndex(index);
    //这里分为两步,第一通过索引定位到节点,第二删除节点
    return unlink(node(index));
}

//方法2.删除指定值的节点
public boolean remove(Object o) {
    //判断删除的元素是否为null
    if (o == null) {
        //若是null遍历删除
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        //若不是遍历删除 
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

通过源码可以看出两个方法都是通过unlink()删除,在方法一种有个方法要介绍下,就是node(index)该方法的作用就是根据下标找到对应的节点,要是本人去写这个方法肯定是遍历到index找到对应的节点,而JDK提供的方法如下所示

1.首先确定index的位置,是靠近first还是靠近last

2.若靠近first则从头开始查询,否则从尾部开始查询,可以看出这样避免极端情况的发生,也更好的利用了LinkedList双向链表的特征

Node<E> node(int index) {
    // assert isElementIndex(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;
    }
}

下面会详细介绍unlink()方法的源码,这是删除节点最核心的方法

E unlink(Node<E> x) {
    // assert x != null;
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;
    
    //删除的是第一个节点,first向后移动
    if (prev == null) {
        first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }
    
    //删除的是最后一个节点,last向前移
    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }

    x.item = null;
    size--;
    modCount++;
    return element;
}

1.获取到需要删除元素当前的值,指向它前一个节点的引用,以及指向它后一个节点的引用。

2.判断删除的是否为第一个节点,若是则first向后移动,若不是则将当前节点的前一个节点next指向当前节点的后一个节点

3.判断删除的是否为最后一个节点,若是则last向前移动,若不是则将当前节点的后一个节点的prev指向当前节点的前一个节点

4.将当前节点的值置为null

5.size减少并返回删除节点的值

至此介绍了LinkedList添加、删除元素的内部实现。

4.对比

ArrList详解中讲解了ArrayList的相关的内容,下面将对ArrayList与LinkedList进行对比,主要从以下方面进行

4.1 相同点

1.接口实现:都实现了List接口,都是线性列表的实现

2.线程安全:都是线程不安全的

4.2 区别

1.底层实现:ArrayList内部是数组实现,而LinkedList内部实现是双向链表结构

2.接口实现:ArrayList实现了RandomAccess可以支持随机元素访问,而LinkedList实现了Deque可以当做队列使用

3.性能:新增、删除元素时ArrayList需要使用到拷贝原数组,而LinkedList只需移动指针,查找元素 ArrayList支持随机元素访问,而LinkedList只能一个节点的去遍历

4.3 性能比较

下面通过代码去比较下ArrayList与LinkedList在性能方面的差别,代码如下

public class ListPerformance {

    private static ArrayList<String> arrayList= new ArrayList<String>();

    private static LinkedList<String> linkedList = new LinkedList<String>();

    /**
     * 插入数据
     * @param list
     * @param count
     */
    public static void insertElements(List<String> list, int count){
        Long startTime = System.currentTimeMillis();
        for (int i = 0; i < count; i++) {
            list.add(String.valueOf(i));
        }
        Long endTime =  System.currentTimeMillis();
        System.out.println("insert elements use time: " +(endTime-startTime) + " ms");
    }

    /**
     * 删除元素
     * @param list
     * @param count
     */
    public static void removeElements(List<String> list, int count){
        Long startTime = System.currentTimeMillis();
        for (int i = 0; i < count; i++) {
            list.remove(0);
        }
        Long endTime =  System.currentTimeMillis();
        System.out.println("remove elements use time: " +(endTime-startTime) + " ms");
    }

    /**
     * 获取元素
     * @param list
     * @param count
     */
    public static void getElements(List<String> list, int count){
        Long startTime = System.currentTimeMillis();
        for (int i = 0; i < count; i++) {
            list.get(i);
        }
        Long endTime =  System.currentTimeMillis();
        System.out.println("get elements use time: " +(endTime-startTime) + " ms");
    }
    /**
     * 删除元素第二种实现
     * @param list
     * @param count
     */
    public static void removeElements2(List<String> list, int count){
        Long startTime = System.currentTimeMillis();
        for (int i = count-1; i > 0; i--) {
            list.remove(i);
        }
        Long endTime =  System.currentTimeMillis();
        System.out.println("remove elements use time: " +(endTime-startTime) + " ms");
    }
    public static void main(String[] args){
        System.out.println("arrayList test");
        insertElements(arrayList,100000);
        getElements(arrayList,100000);
        removeElements(arrayList,100000);

        System.out.println("linkedList test");
        insertElements(linkedList,100000);
        getElements(linkedList,100000);
        removeElements(linkedList,100000);


        System.out.println("arrayList test2");
        insertElements(arrayList,100000);
        getElements(arrayList,100000);
        removeElements2(arrayList,100000);

        System.out.println("linkedList test2");
        insertElements(linkedList,100000);
        getElements(linkedList,100000);
        removeElements2(linkedList,100000);
    }

结果如下图所示,可以看出

image

1.LinkedList下插入、删除是性能优于ArrayList,这是由于插入、删除元素时ArrayList中需要额外的开销去移动、拷贝元素(但是使用removeElements2方法所示去遍历删除是速度异常的快,这种方式的做法是从末尾开始删除,不存在移动、拷贝元素,从而速度非常快)

2.ArrayList在查询元素的性能上要由于LinkedList

5.总结

本文主要介绍了LinkedList的内部实现,主要从添加元素、删除元素入手分析LinkedList实现的细节,后面也将LinkedList与ArrayList进行了对比。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容