LinkedList深入源码

Java容器专栏: Java容器源码详细解析(面试知识点)

因为部分内容可能已经在本专栏前面的文章提及到,这里只是简单说明,所以建议先看看:ArrayList

详细说明都在源码里,耐心看会发现其实也挺简单的。

(一)LinkedList底层数据结构

双向链表,链表中的每个节点都包含了对前一个和后一个元素的引用。

链表的最大的容量为Integer.MAX_VALUE

 

(二)LinkedList继承和实现关系

1、实现的接口:

List接口:提供了List的相关操作

Deque接口:提供了双向队列的相关操作

注意,虽然LinkedList实现了此接口,但是LinkedList大部分Deque不太一样:

           a) 一般Deque不允许null值,但LinkedList允许null值

           b) 一般Deque不限容量,但LinkedList有最大容量:Integer.MAX_VALUE

Cloneable标志性接口:可克隆

Serializable标志性接口:可序列化

2、继承的抽象类:

继承了抽象类AbstractSequentialList,最小化实现了支持“顺序访问”的数据结构(比如链表)所需的工作量。而对于支持“随机访问”的数据结构(比如数组),则是需要继承AbstractList类(比如ArrayList就继承了AbstractList)

AbstractSequentialList是AbstractList的子抽象类。

(三)LinkedList源码分析

1、几个重要的成员变量

//链表长度
transient int size = 0;

//指向头节点的指针(最初为null)
transient Node<E> first;

//指向尾节点的指针(最初为null)(注意是最后一个姐的指针,而不是下一个节点的)
transient Node<E> last;

这是LinkedList的成员变量,那么节点Node的成员变量呢?其实Node是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;
    }
}

对元素的操作,其实也都是对节点的操作。

下面对链表的添加和删除操作,都需要考虑到操作位置(链表头、链表中间、链表尾),根据不同的位置,会对链表的 first 和 last 指针,以及 新、旧(邻接)节点的 prev 和 next 指针进行不同的操作。(很重要,可以说是LinkedList底层最重要的部分,只要理解了,其他的就很简单了,对双向链表不理解的,可以先去看看其他有关这个数据结构的博客了解学习)

2、两个构造方法

//创建一个空LinkedList
public LinkedList() {
}

//按照容器中的元素创建LinkedList
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

构造方法就不像ArrayList一样复杂了,很清晰,不做过多的说明。

3、获取元素操作

//通过下标获取元素(返回节点的item)
public E get(int index) {
    //检查下标是否规范[0~size]
    checkElementIndex(index);
    //找到对应的节点,并返回其item
    return node(index).item;
}

//通过下标查找节点
//这里将链表分前后两半,来优化查找,加快查找速度
Node<E> node(int index) {
    //若是在前一半(size右移一位表示“/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;
    }
}

4、添加元素操作

//添加单个元素
public boolean add(E e) {
    //尾插法插入
    linkLast(e);
    return true;
}

//尾插法插入传入的元素
    //最重要的是处理链表的first和last指针移动
    //以及新节点的prev和next和原最后节点的next指针指向
void linkLast(E e) {
    //指向链表最后一个节点的指针
    final Node<E> l = last;
    //创建一个新节点参数为:
        //前一节点指针(l)、此节点存放元素、后节点指针(null)
    final Node<E> newNode = new Node<>(l, e, null);
    //链表的最后节点指针指向新插入的节点
    last = newNode;
    //如果是第一次插入的,链表的first指针就指向这个新节点
    //(之后除非删除了第一个节点,否则first就不动了)
    if (l == null)
        first = newNode;
    else
        //若链表已有节点(l!=null),则原链表末节点的next指针指向这个新节点
        //将链表和新节点连接起来
        l.next = newNode;
    //链表长度+1
    size++;
    //修改次数+1
    modCount++;
}
//添加多个元素(传入Collection容器)
public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}

//AbstractSequentialList的方法
//在指定的index位置上插入容器中的所有元素
public boolean addAll(int index, Collection<? extends E> c) {
    //检查index是否规范(0 <= index <= size)
    checkPositionIndex(index);
    
    //先用数组存放c容器中的元素,并获取元素的个数
    Object[] a = c.toArray();
    int numNew = a.length;
    if (numNew == 0)
        return false;
    
    //定义两个辅助指针,若将容器中的元素拼接成一个链表A
        //pred是这个A链表的第一个节点的prev指针
        //succ是这个A链表的最后一个节点的next指针
    Node<E> pred, succ;
    //如果刚好是在原链表最后插入这个容器元素,则直接拼接在原链表后即可
    if (index == size) {
        succ = null;
        pred = last;
    } else {
        //否则,则还需要取出原链表index及之后的子链表段,需要在最后插在A链表末尾
        //succ设置为index所指的node的指针
        succ = node(index);
        //pred则是指向index所指的节点的前一个节点
        pred = succ.prev;
    }

    //将容器c中的所有元素和原链表index位置开始,连接起来
    for (Object o : a) {
        @SuppressWarnings("unchecked") E e = (E) o;
        Node<E> newNode = new Node<>(pred, e, null);
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        //后移    
        pred = newNode;
    }

    if (succ == null) {
        last = pred;
    } else {
        pred.next = succ;
        succ.prev = pred;
    }

    size += numNew;
    modCount++;
    return true;
}
//在指定位置上插入元素
public void add(int index, E element) {
    //检查index是否规范
    checkPositionIndex(index);

    //如果是在最后插入,则直接尾插法
    if (index == size)
        linkLast(element);
    else
        //在index之前插入element(其他的后移)
        linkBefore(element, node(index));
}

//在指定node之前插入元素(很简单,和前面的思路一致)
void linkBefore(E e, Node<E> succ) {
    final Node<E> pred = succ.prev;
    final Node<E> newNode = new Node<>(pred, e, succ);
    succ.prev = newNode;
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}

5、删除元素操作

//根据下标删除节点,返回此节点对应的元素item
public E remove(int index) {
    //同样是检查下标
    checkElementIndex(index);
    //先是找到节点,然后unlink此节点
    return unlink(node(index));
}

//断开节点
//同样是要考虑节点的位置的,只不过在链表中间这种情况的处理,被分到两个else中
E unlink(Node<E> x) {
    //获取到节点的属性
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;

    //如果是头节点
    if (prev == null) {
        first = next;
    } else {
        prev.next = next;
        x.prev = null;  //置为null
    }
    
    //如果是尾节点
    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null; //置为null
    }
    
    //置为null,到此被删节点的全部属性都是null,即没有被引用了,所以可以被gc回收
    x.item = null;  
    size--;
    modCount++;
    return element;
}
//根据item值删除节点(仅删除第一次出现的节点),成功删除返回true
public boolean remove(Object o) {
    //如果是要删除的对象是null(list允许null值)
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                //是上面的unlink方法,移除节点
                unlink(x);
                return true;
            }
        }
    } else {
        //如果是非null值,则需要使用equals方法来判断是否相等
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

6、Deque相关方法

上面的几个操作,都是作为底层为链表的List的操作,而不是针对队列的操作。若是要将LinkedList作为双向队列使用的话,需要使用下面几个方法而不是上面的方法。

(虽然底层实现原理差不多,也都是对链表的相关操作。只不过若是将其作为双向队列使用的话,可以操作的位置就只能是队列头和队列尾了!)

(也就是对线性列表list操作的特殊情况给提取出来封装成一个新方法,也就是list操作的简单情况,只要上面理解了,这些和队列操作相关的方法也就轻松拿下了)

(使用也不是要如何添加代码,而是在自己写的逻辑代码中自己遵守即可,比如若是作为队列来使用,就不要调用上面类似get(index)等可对队列中间元素获取和操作的方法)

① 双向队列的添加

//头插
public boolean offerFirst(E e) {
    addFirst(e);
    return true;
}
public void addFirst(E e) {
    linkFirst(e);
}
private void linkFirst(E e) {
    final Node<E> f = first;
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    size++;
    modCount++;
}

//尾插
public boolean offerLast(E e) {
    addLast(e);
    return true;
}
public void addLast(E e) {
    linkLast(e);
}
……//不列了,很简单,可以自己想想再去看源码比较

②双向队列的删除

//移除队列头
public E pollFirst() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}
//或
public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    //移除操作
    return unlinkFirst(f);
}
private E unlinkFirst(Node<E> f) {
    final E element = f.item;
    final Node<E> next = f.next;
    f.item = null;
    f.next = null; // help GC
    first = next;
    if (next == null)
        last = null;
    else
        next.prev = null;
    size--;
    modCount++;
    return element;
}

//移除队列尾
……//同样很简单,上面的first、unlinkFirst换成last、unlinkLast而已

③双向队列的获取

      不写了,几行代码,不过是判断后返回first和last指针所指的节点item属性而已。( peekFirst / peekLast )

7、Qeque相关方法

若是要将其作为 queue(而不是上面的 lis t或 deque )来操作,上面的方法就不要使用!使用下面的方法!!(虽然下面的方法只是对上面提到的方法再封装而已)

即:完全遵守 先进先出,中间元素不可知!

(LinkedList的属性中,first指向的是队头,last指向的是队尾)

① 入队

public boolean offer(E e) {
    //内部实际调用了list的add方法,是尾插法的
    return add(e); 
}

②出队

public E pop() {
    //移除队头
    return removeFirst();
}

③查看队头(不出队)

public E peek() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
}
//或
public E element() {
    return getFirst();
}
//区分:peek不会抛异常,element调用了getFirst,若first指向null则会抛异常

7、其他方法

注意里面有一个push方法,看起来像是对队列的入队操作,但是它是从队头push的(即头插法)。很另类,最好别用吧比较容易乱。

(洗了个澡突然醒悟,push和pop……这不就是栈了吗!!!原来不是鸡肋啊!!)

 

(三)线程安全性

不是线程安全的。举例很简单的,无论是插入还是删除,对链表的结构都会造成改变,不同线程不同的顺序改变的结果也会不同。

 

解决线程安全有下面几个方法:

  • 使用Collections.synchronizedList(List list)替换

  • 使用ConcurrentLinkedQueue替换

 

 

(四)克隆机制

同样是浅克隆。

public Object clone() {
    LinkedList<E> clone = superClone();

    clone.first = clone.last = null;
    clone.size = 0;
    clone.modCount = 0;

    //循环尾插
    for (Node<E> x = first; x != null; x = x.next)
        clone.add(x.item);

    return clone;
}

(五)序列化机制

(大致和ArrayList一样)在上面的成员变量中可以看到,LinkedList的全部成员变量都加上了transient修饰,即不会被序列化的,但是它继承了Serializable接口,同时实现了readObject和WriteObject方法,序列化允许调用这两个方法进行序列化操作。(若不懂建议看看本专栏的ArrayList博客,里面就有比较详细的解释,当然实现的代码肯定是不同的哈)

 

 

 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值