LinkedList源码分析总结

LinkedList源码分析总结

前言

本文基于JDK8,如有谬误请各位大佬指正。

概述

  • LinkedList是一个有序集合,底层结构是一个双向链表,它允许元素为null

  • 增删节点只需要修改节点指针,效率很高;而查改都需要遍历链表,效率较低

  • 线程不安全。使用迭代器遍历时如果进行add,remove等增删元素的操作,就会抛出ConcurrentModificationException。

    如须在多线程下使用,可以通过Collections.synchronizedList(List<T> list)方法将其转化为线程安全的列表,或直接使用ConcurrentLinkedQueue

    • **Collections.synchronizedList(List list)**的原理其实就是创建了一个SynchronizedList类,其中有一个成员变量mutex作为锁对象,它就是传入列表的class类对象,其中的所有方法都被synchronized关键字来修饰。
    • ConcurrentLinkedQueue底层是一个单链表,只有在方法内涉及增删等操作时,才会调用unsafe类中的方法通过CAS操作来解决并发问题。(关于CAS问题,这里不再赘述,感兴趣的同学可以自行百度)

源码分析

LinkedList中,我们经常使用的API还是增删查改等操作,下面我们来看看这些方法。

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

  • 是AbstractSequentialList的子类,实现了List接口,表明它是一个有序集合。
  • 实现了Deque接口,而Deque是Queue的子接口,所以LinkedList具有双端增删元素的能力,可以作为队列 和 双向队列的实现类使用
  • 实现了Cloneable接口,具有复制对象的能力。Object的native方法clone() 会复制一个对象,它和原对象的内存地址是不同的
  • 实现了java.io.Serializable序列化接口,具有序列化和反序列化的能力

2.构造方法

public LinkedList() {
}

public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

3.增

add(E e)

创建一个新的节点,并插入链表表尾

public boolean add(E e) {
    linkLast(e);
    return true;
}

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
}

内部的Node节点类

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;
    }
}

add(int index, E element)

在指定位置index插入一个元素

步骤

  • 对index进行越界检查
  • 如果index等于当前链表节点个数(也就是说插入位置为链表尾结点之后),就将它插入表尾。
  • 否则遍历链表,将它插入链表指定位置
public void add(int index, E element) {
    checkPositionIndex(index);

    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}

//通过遍历链表找到index位置的节点,遍历前做了优化来提升效率:
//如果index在链表前半部分,就从前往后变量;否则从后往前遍历。
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;
    }
}

void linkBefore(E e, Node<E> succ) {	//succ为原来index位置的节点
    // assert succ != null;
    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++;			//修改次数+1
}

addAll(Collection<? extends E> c)

向链表中加入集合c中所有的元素。插入链表后,这些节点在链表中的排列顺序就是迭代集合c的顺序。

步骤

  • 对插入位置index进行越界检查
  • 将集合转化为Object数组
  • 通过循环将Object数组中的元素全部插入链表中
public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}

//向指定位置index
public boolean addAll(int index, Collection<? extends E> c) {
    checkPositionIndex(index);		//index越界检查

    Object[] a = c.toArray();		//先将集合c转为Object数组
    int numNew = a.length;
    if (numNew == 0)				//集合为空直接返回false,表示添加元素失败
        return false;

    Node<E> pred, succ;
    //先确定index位置的节点succ和它前一个节点pred
    if (index == size) {		//如果插入链表表尾节点后一个位置
        succ = null;
        pred = last;
    } else {					//插入链表表头或中间
        succ = node(index);		//node方法上面说过,通过优化的遍历来找到index位置的元素
        pred = succ.prev;
    }

    //遍历集合c转换成的数组a
    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) {		//说明集合c中元素是append到链表表尾的,只需要修改last成员变量
        last = pred;
    } else {				//说明集合c中元素是插入到链表中间的,需要修改下面两个节点的指针
        pred.next = succ;
        succ.prev = pred;
    }

    size += numNew;		//当前节点个数 = 原链表节点个数+插入的集合中元素个数
    modCount++;			//修改次数+1
    return true;
}

4.删

remove()

这个无参构造方法用于移除链表头节点

public E remove() {
    return removeFirst();
}

public E removeFirst() {
    final Node<E> f = first;
    if (f == null)		//头结点为空则抛出异常
        throw new NoSuchElementException();
    return unlinkFirst(f);
}

//删除头结点的方法,很简单
private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    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;
}

remove(int index)

移除链表指定位置的节点

步骤

  • 检查index是否越界
  • 获取index位置的节点
  • 修改指针域将index位置的节点删除
public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}

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;

    //移除index位置节点x的前驱引用
    if (prev == null) {
        first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }

    //移除后继引用
    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }

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

remove(Object o)

通过遍历链表,移除第一个和指定元素o相等的节点

public boolean remove(Object o) {
    if (o == 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;
}

5.改

set(int index, E element)

public E set(int index, E element) {
    checkElementIndex(index);	//越界检查
    Node<E> x = node(index);	//遍历获取index位置节点
    E oldVal = x.item;
    x.item = element;			//修改该节点存储内容
    return oldVal;
}

6.查

get(int index)

移除指定位置的元素。

public E get(int index) {
    checkElementIndex(index);	//越界检查
    return node(index).item;	//遍历获取
}

toArray方法

再看看toArray的两个构造方法。

两个方法非常类似,都是先创建一个size大小的数组,再通过遍历链表将每个节点存储的内容赋给数组,只不过下面的那个是通过反射创建数组而已。

public Object[] toArray() {
    Object[] result = new Object[size];
    int i = 0;
    for (Node<E> x = first; x != null; x = x.next)
        result[i++] = x.item;
    return result;
}

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        a = (T[])java.lang.reflect.Array.newInstance(
        a.getClass().getComponentType(), size);
    int i = 0;
    Object[] result = a;
    for (Node<E> x = first; x != null; x = x.next)
        result[i++] = x.item;

    if (a.length > size)
        a[size] = null;

    return a;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值