LinkedList源码分析(JDK1.8)

简介:

   LinkedList底层数据结构为双向链表,与ArrayList相同的是,其是list接口的实现类,意味着其存储元素是连续且可重复的。

   LinkedList继承于AbstractSequentialList类

   LinkedList实现了Deque接口,代表其具备双向队列属性;实现了Cloneable,代表其可克隆;实现了Serializable接口,表示其可序列化

   与ArrayList相比,LinkedList是链表,无需连续存储空间,节点是可以分布在非连续空间的。其添加、删除、插入元素十分方便,只需寻找到其索引,再进行相应操作后,将链连接完整即可(头尾节点重新赋值),时间复杂度只有O(1)。

缺点是ArrayList支持随机查询,查询十分快速,而LinkedList则需遍历整个链表进行数据匹配,时间复杂度为O(n)。

    LinkedList也是线程不安全的,高并发场景下可以考虑List<String> linkedList = Collections.synchronizedList(new LinkedList<String>())这样使用,或者使用ConcurrentLinkedDeque

所以使用两种list还应根据具体情况而定。

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

底层数据结构:

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

可以看到这是典型地双向链表的节点。双向链表支持从头开始和从尾开始两种遍历方式,可以更好解决查询问题。

属性:

transient int size = 0; //元素个数
transient Node<E> first; //头结点
transient Node<E> last; //尾结点

transient表示该变量在一个可序列化的类里不会被序列化。

方法:

构造方法:

public LinkedList() {
}

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

    可以看到linkedList的构造方法非常简单,不会做很多操作。 

    对比ArrayList提供了参数为int(容量)的构造方法,是因为倘若我们知道需要大概多少存储元素,用这个容量去初始化数组,则可以尽可能地避免数组的扩容。(多次的扩容十分损耗性能)。LinkedList为链表结构,添加和删除节点十分高效,不必去设置容量造成有可能的空间浪费。

增加元素(插入元素):

//头插
public void addFirst(E e) {
    linkFirst(e);
}

//尾插
public void addLast(E e) {
    linkLast(e);
}

//默认add是尾插法
public boolean add(E e) {
    linkLast(e);
    return true;
}

可以看到链表添加元素就是头尾插入元素,我们看一下两种插入方法的代码

//头插
private void linkFirst(E e) {
    //先存储未插入前的头结点
    final Node<E> f = first;
    //生成新节点,放入传入的元素
    final Node<E> newNode = new Node<>(null, e, f);
    //将新节点赋值给first,那么链表的头结点就是newNode了
    first = newNode;
    //原头结点为空,证明还没有添加过元素,则让新节点做头结点
    if (f == null)
        last = newNode;
    else
    //将新节点赋值给f的prev,完成链接
        f.prev = newNode;
    size++;
    modCount++;
}

//尾插
void linkLast(E e) {
    //先存储未插入前的尾结点
    final Node<E> l = last;
    //生成新节点,放入传入的元素
    final Node<E> newNode = new Node<>(l, e, null);
    //将新节点赋值给last,那么链表的尾结点就是newNode了
    last = newNode;
    //原尾结点为空,证明还没有添加过元素,则让新节点做头结点
    if (l == null)
        first = newNode;
    else
    //将新节点赋值给l的next,完成链接
        l.next = newNode;
    size++;
    modCount++;//操作数加1
}

以尾插为例,首先创造一个新节点,将值存入。

以新节点作为新的尾结点,此时要判断,如果尾结点为空,则证明没有添加过结点。那么新节点=尾结点=头结点。若不为空,还需要将新节点与旧的尾结点链接起来,即将新节点赋值给尾结点的next成员。

最后size的值加1。

头插法同理。

增加元素到指定位置(插入):

public void add(int index, E element) {
    //检测下标位置
    checkPositionIndex(index);

    //下标同size相等,直接用尾插
    if (index == size)
        linkLast(element);
    else
    //否则调用前插,即linkBefore
        linkBefore(element, node(index));
}

void linkBefore(E e, Node<E> succ) {
    
    //存储succ的前置节点
    final Node<E> pred = succ.prev;
    
    //生成新节点,前置为succ的前置,后置为succ,即链接在succ前
    final Node<E> newNode = new Node<>(pred, e, succ);
    //将新节点赋值给succ的前置节点
    succ.prev = newNode;

    //succ前置节点为空,代表succ为头结点,此时新节点作为头结点
    if (pred == null)
        first = newNode;
    else
    //否则将新节点赋值给succ前置节点,完成链接
        pred.next = newNode;
    //长度加1

    size++;
    modCount++;
}

可以看到add(int index, E element)做的操作比较简单,检测下标位置,不越界的情况下。若index是最后一个元素下标+1,即size,直接用尾插linkLast方法。否则用linkBefore方法。

linkBefore思路:

向非空节点succ前插入节点。有下面几个流程:

1、首先存储succ节点的前置节点,命名为pred。

2、生成新节点,新节点前置节点设置为pred,后置节点设置为succ,那么这时候新节点两端已完成链接。

3、将新节点赋值给succ的前置节点,此时succ与新节点完成了链接。

4、判断pred的值,此时有两种情况,pred为空,则说明succ就是头结点,新节点要插在succ前,则此时将新节点设置为头结点即可。若pred不为空,那么再将新节点赋值给pred的后置节点,此时链接完成。

顺便看一下其中的checkPositionIndex方法

private void checkPositionIndex(int index) {
    if (!isPositionIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

private boolean isPositionIndex(int index) {
    return index >= 0 && index <= size;
}

下标越界会直接抛异常。这里注意index等于size的时候。其实是没有这一元素的,这个下标是最后一个元素下标+1。

与之相对的还有检查是否是链表元素下标的方法:


private boolean isElementIndex(int index) {
    return index >= 0 && index < size;
}

private void checkElementIndex(int index) {
    if (!isElementIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

批量添加:

//集合元素批量添加到链表后
public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}

//集合元素批量添加到index前,这里是前插
public boolean addAll(int index, Collection<? extends E> c) {
    checkPositionIndex(index);

    //集合转化为数组
    Object[] a = c.toArray();
    //获得集合元素个数,即插入数据数
    int numNew = a.length;
    if (numNew == 0)
        return false;

    Node<E> pred, succ;
    //为size的情况就是上面的方法
    if (index == size) {
        succ = null;
        pred = last;
    } else {
    //通过node方法找到index下标的节点,赋值给succ
        succ = node(index);
    //pred作为succ的前置节点
        pred = succ.prev;
    }

    //遍历数组a,通过循环将集合元素插入到pred后面。
    //pred在循环过程中会不断改变,即他是插入的上一个节点,也就是新插入的节点的前置节点
    for (Object o : a) {
        @SuppressWarnings("unchecked") E e = (E) o;
        Node<E> newNode = new Node<>(pred, e, null);
        if (pred == null)
            //pred为空说明succ是头结点,将新节点作为头结点,这只可能在循环第一次出现。
            first = newNode;
        else
        //新节点设置为pred的后置
            pred.next = newNode;
        //插入的新节点赋值给pred,pred成为下一次插入的节点的前置节点
        pred = newNode;
    }

    //经过循环添加后,pred就是插入的最后一个节点
    //倘若succ为null,则插入的最后一个节点应作为尾结点
    if (succ == null) {
        last = pred;
    } else {
    //不为null则将succ与最后一次插入的节点链接上
        pred.next = succ;
        succ.prev = pred;
    }

    //长度增加
    size += numNew;
    modCount++;
    return true;
}

addAll(int index, Collection<? extends E> c)思路:

1、集合c转化为数组

2、若添加的下标为size,则把这些元素添加到链表后。

否则找到插入的下标index对应的节点succ以及它的前置节点pred。之后通过循环将元素一个一个生成节点,添加到pred之后。这里有两次判断。若pred为空,则说明succ是头结点,此时添加的第一个节点应作为头结点。若succ的下一个节点为空,则说明succ是尾结点,此时添加的最后一个元素应作为尾结点。之后完成链接即可。

offer、offerFirst、offerLast方法:

public boolean offer(E e) {
    return add(e);
}

public boolean offerFirst(E e) {
    addFirst(e);
    return true;
}

public boolean offerLast(E e) {
    addLast(e);
    return true;
}

 

索引方法:

//不提供给外部
Node<E> node(int index) {
    //size >> 1即除以二。
    //通过判断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;
    }
}

//找第一次出现的相同节点,正向遍历
public int indexOf(Object o) {
    int index = 0;
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            //元素为空则不需要用equals方法,提高性能
            if (x.item == null)
                return index;
            index++;
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item))
                return index;
            index++;
        }
    }
    return -1;
}

//找最后一次出现的相同节点,逆向遍历
public int lastIndexOf(Object o) {
    int index = size;
    if (o == null) {
        for (Node<E> x = last; x != null; x = x.prev) {
            index--;
            if (x.item == null)
                return index;
        }
    } else {
        for (Node<E> x = last; x != null; x = x.prev) {
            index--;
            if (o.equals(x.item))
                return index;
        }
    }
    return -1;
}

上面的for循环是经典的链表遍历方法。从头(尾)结点开始,通过为一个临时节点x不断赋值为上一个节点的next(prev),则x会被赋值为链表的每一个元素,达到遍历的效果。

 

set方法与、contain方法、size方法、clear方法:

public E set(int index, E element) {
    //检测下标越界
    checkElementIndex(index);
    Node<E> x = node(index);
    E oldVal = x.item;
    x.item = element;
    return oldVal;
}

//indexOf方法查询失败会返回-1
public boolean contains(Object o) {
     return indexOf(o) != -1;
}

public int size() {
     return size;
}

//正向遍历,循环赋空
public void clear() {
   for (Node<E> x = first; x != null; ) {
        Node<E> next = x.next;
        x.item = null;
        x.next = null;
        x.prev = null;
        x = next;
    }
        first = last = null;
        size = 0;
        modCount++;
}

比较简单,没什么可说的。

 

删除元素:

删除头尾元素(无参remove默认删除头元素):

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

public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}

public E removeLast() {
   final Node<E> l = last;
   if (l == null)
       throw new NoSuchElementException();
   return unlinkLast(l);
}

重点看unlinkFirst与unlinkLast方法实现:

private E unlinkFirst(Node<E> f) {
    //记录该节点元素数值作返回值
    final E element = f.item;
    //记录下一个节点
    final Node<E> next = f.next;

    //f的值赋空帮助GC回收
    f.item = null;
    f.next = null;
    first = next;
    //说明f是尾结点,删除后已没有节点了
    if (next == null)
        last = null;
    else
    //next作头结点,前置节点需赋空
        next.prev = null;
    //长度减一
    size--;
    modCount++;
    return element;
}

//与上面方法相反
private E unlinkLast(Node<E> l) {
    
    final E element = l.item;
    final Node<E> prev = l.prev;
    l.item = null;
    l.prev = null;
    last = prev;
    if (prev == null)
        first = null;
    else
        prev.next = null;
    size--;
    modCount++;
    return element;
}

删除指定下标元素:

public E remove(int index) {
    //检测下标越界
    checkElementIndex(index);
    return unlink(node(index));
}

E unlink(Node<E> x) {
    //记录x的元素值作返回值
    final E element = x.item;
    //记录x的前置节点与后置节点
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;

    if (prev == null) {
        first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }

    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }
    
    //赋空方便GC回收
    x.item = null;
    size--;
    modCount++;
    return element;
}

以上的方法大体的思路都是一样的。

都是需要寻找到指定的节点,保存期元素值作为返回值,记录其前后节点。然后再给要删除的节点赋空(前置后置元素值),让GC去回收。然后将其前后置节点进行链接即可。都需要判断其是否是特殊节点(头节点或尾结点)。

通过元素值删除指定节点:

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

public boolean removeFirstOccurrence(Object o) {
    return remove(o);
}

public boolean removeLastOccurrence(Object o) {
    if (o == null) {
        for (Node<E> x = last; x != null; x = x.prev) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        for (Node<E> x = last; x != null; x = x.prev) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

上述方法的实现都是先遍历,通过对比元素值寻找节点,并使用unlink方法进行删除。

poll与pop方法:

public E poll() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}

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

查询:

public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

public E getFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return f.item;
}

public E getLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return l.item;
}

查询也较为简单,查询首尾结点直接返回其元素值。查询一般节点则用node方法遍历即可。

迭代器遍历:

public ListIterator<E> listIterator(int index) {
    checkPositionIndex(index);
    return new ListItr(index);
}

ListItr是LinkedList的内部类,重写了迭代器,提供了更丰富地功能:

private class ListItr implements ListIterator<E>

ListIterator只能用于List及其子类型。

有add()方法,可以往链表中添加对象

可以通过nextIndex()和previousIndex()返回当前索引处的位置

可以通过hasNext()和next()往后顺序遍历,也可以通过hasPrevious()和previous()实现往前遍历

可以通过set()实现当前遍历对象的修改

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
LinkedListJava中提供的一个双向链表实现类,其内部维护了一个first和last节点,分别表示链表的头和尾。以下是LinkedList源码分析: 1. 声明LinkedList类 ```java 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; } ``` 2. 声明Node类 ```java 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; } } ``` 3. 实现LinkedList的方法 - add(E e)方法:将元素添加到链表末尾 ```java 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++; } ``` - add(int index, E element)方法:将元素插入到指定位置 ```java 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) { 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++; } ``` - remove(int index)方法:删除指定位置的元素 ```java public E remove(int index) { checkElementIndex(index); return unlink(node(index)); } 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; } if (next == null) { last = prev; } else { next.prev = prev; x.next = null; } x.item = null; size--; return element; } ``` - get(int index)方法:获取指定位置的元素 ```java public E get(int index) { checkElementIndex(index); return node(index).item; } Node<E> node(int 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; } } ``` 以上就是LinkedList源码分析,通过对其源码分析,我们可以更深入地理解链表的实现。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值