LinkedList源码学习(JDK8)

前面学习了ArrayList与Vector源码中的常用方法,我们也知道了ArrayList与Vector的区别主要在是否同步上,接下来看看LinkedList部分的源码,LinkedList与ArrayList在面试中也常用来比较。我们都知道ArrayList增删慢改查快,因为其增删操作需要移动底层数据,而查改数据可以根据索引直接定位,效率较高;而LinkedList恰恰相反,它是增删快改查慢,因为在增删时只需要改变链表节点的指针即可,而改查过程通过定位时效率就降低了。

在源码描述里,我们可以了解到:List和Deque接口的双链表实现,实现所有可选列表操作,允许所有元素(包含null)。所有操作的执行与双向链表一样。 索引到列表中的操作将从开头或结尾遍历列表,以较接近指定索引为准。

/**
 * Doubly-linked list implementation of the {@code List} and {@code Deque}
 * interfaces.  Implements all optional list operations, and permits all
 * elements (including {@code null}).
 *
 * <p>All of the operations perform as could be expected for a doubly-linked
 * list.  Operations that index into the list will traverse the list from
 * the beginning or the end, whichever is closer to the specified index.

后面的描述跟前面介绍的ArrayList相似,这里就不细说,有需要的可以移步:ArrayList源码学习(基于jdk8)

接下来我们通过源码细节进行学习,首先我们可以看到LinkedList继承了AbstractSequentialList<E>并实现了 List<E>, Deque<E>, Cloneable, java.io.Serializable接口。从图片可以看到AbstractSequentialList<E>实现了list的主要常用接口,降低了其实现List的复杂度。

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为双向连表结构,通过当前节点得到前置和后置节点。 

    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()和LinkedList(Collection<? extends E> c)(实际执行add All()函数)。

    //无参
    public LinkedList() {
    }

    
    //有参,构造一个包含指定集合元素的列表,这些元素将按照collection的迭代器返回的顺序排列。
    public LinkedList(Collection<? extends E> c) {
        this();//先构造一个空的列表
        addAll(c);//将collection集合的数据添加到列表中
    }

以下是addAll()方法 :

    //addAll()

     public boolean addAll(Collection<? extends E> c) {
        //以collection为参数的构造传入size为0,即this()构造的空集合;
        return addAll(size, c);
    }

    public boolean addAll(int index, Collection<? extends E> c) {
        checkPositionIndex(index);//判断下标是否合法

        Object[] a = c.toArray();// 转array
        int numNew = a.length;// 获取插入集合长度(元素个数)
        if (numNew == 0) // 集合为空,返回false
            return false;

        Node<E> pred, succ;//前置节点、后置节点

        if (index == size) {//在集合尾部添加数据(索引位置为队尾size)
            succ = null;//后置节点为空
            pred = last;//前置节点为数据最后一位
        } else {
            succ = node(index);//index索引位置节点作为后置节点
            pred = succ.prev;index节点前一位即为前置节点
        }

        for (Object o : a) {//循环遍历集合数据
            @SuppressWarnings("unchecked") E e = (E) o;
            Node<E> newNode = new Node<>(pred, e, null);//以前置节点和元素e创建新节点
            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;//添加成功
    }
    //判断索引是否合法部分代码:
    private void checkPositionIndex(int index) {
        if (!isPositionIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    
    private boolean isPositionIndex(int index) {
        return index >= 0 && index <= size;//保证索引合法性
    }

然后就是集合中常用的方法,我们就按增、删、改、查的顺序来说吧:

1、添加 : add(E e)(ps :addAll(Collection<? extends E> c)上面已经说过)

    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++;//操作次数加一
    }

2、删除 :remove(int index) / remove(Object o)

    public E remove(int index) {
        checkElementIndex(index);//校验索引合法性
        return unlink(node(index));//删除索引位置节点
    }

    public boolean remove(Object o) {
        if (o == null) {//对象为空
            for (Node<E> x = first; x != null; x = x.next) {//循环集合
                if (x.item == null) {//找到结果为null的节点元素
                    unlink(x);//执行删除
                    return true;
                }
            }
        } else {
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item)) {//找到集合内匹配o的元素
                    unlink(x);//执行删除
                    return true;
                }
            }
        }
        return false;//如果没有匹配数据,则返回false
    }

 删除操作执行的方法:实际原理为将传入的将要删除对象前后节点连接(跳过传入的对象),然后将删除节点位置的元素置为null;供垃圾回收器回收。

    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;
        
        if (prev == null) {// x的前置节点为空(即x为首节点)
            first = next;// 将x的后置节点置为首节点
        } else {
            prev.next = next;//将x的后置节点赋值给x前置节点的后一节点(跳过x节点)
            x.prev = null;//将x的前置节点置为null,即x不指向任何节点
        }

        if (next == null) { //如果x的后置节点为null(即x为最后一位)
            last = prev;//将x的前一节点置为末节点
        } else {
            next.prev = prev;//将x的前置节点置为x后置节点的前一节点
            x.next = null;//将x的后置节点置为null;即x不指向任何节点
        }

        x.item = null;将x的元素置为null
        size--;//节点减一
        modCount++;//操作次数加一
        return element;//返回删除的元素
    }

3、修改:set(int index, E element)   找到对应索引位置的节点,将节点的元素替换为传入元素

    public E set(int index, E element) {
        checkElementIndex(index);//判断索引合法性
        Node<E> x = node(index);//找到索引对应的节点
        E oldVal = x.item;//拿到节点的元素
        x.item = element;//替换元素
        return oldVal;//返回原来的元素
    }

4、查询:get(int index) / getFirst() / getLast()   找到对应节点并获取节点下元素

    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;//返回节点元素
    }

5、迭代:listIterator(int index) ——从列表中的指定位置开始,返回此列表中元素的列表迭代器(按正确顺序)。

    public ListIterator<E> listIterator(int index) {
        checkPositionIndex(index);//检查索引合法性
        return new ListItr(index);//返回迭代器的内部类
    }
    
    private class ListItr implements ListIterator<E> {
        private Node<E> lastReturned;
        private Node<E> next;
        private int nextIndex;
        private int expectedModCount = modCount;

        ListItr(int index) {
            // assert isPositionIndex(index);
            next = (index == size) ? null : node(index);//判断索引是否为集合大小,如果是即下一个元素为null;否则下一个元素为索引对应的节点
            nextIndex = index;
        }

        public boolean hasNext() {//判断是否有下一个
            return nextIndex < size;//下一个索引位置小于集合大小
        }

        public E next() {
            checkForComodification();//判断是否有并发操作,与ArrayList相似
            if (!hasNext())//如果没有下一个,则抛异常
                throw new NoSuchElementException();

            lastReturned = next;//将下一个节点赋值给lastReturned
            next = next.next;//将下一个节点的后置节点赋值给当前的后置节点(即继续向后遍历)
            nextIndex++;//计数+1
            return lastReturned.item;//返回lastReturned的元素
        }   
    }

以上为LinkedList的源码分析内容,若有错误之处,欢迎评论指出,谢谢!

总结:LinkedList与ArrayList的相似之处为它们都是不同步的,如果在多线程下使用,可以使用这种方式处理:List list = Collections.synchronizedList(new LinkedList(...)),它们的不同之处是LinkedList是基于双链表结构,而ArrayList是基于数组结构,这也是他们在增删改查操作上有了效率上的差异,而且LinkedList内部除了存储了数据,同时还记录了其前后节点位置,因此LinkedList相对于ArrayList更消耗内存。

下期预告:HashMap源码学习(基于jdk8)。

 

传送门:

            1、ArrayList源码学习(JDK8)

             2、Vector源码学习(JDK8)

 

如果喜欢本文,请为我点赞,您的支持是我继续下去的动力,您也可以在评论区与我探讨或指正错误,最后别忘了关注一下我哦,谢谢。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值