【LinkedList】源码笔记

本文详细解析了LinkedList的底层实现,包括其双向链表结构、构造方法、关键操作如添加、删除和获取元素的方法,以及遍历机制。分析了LinkedList在不同场景下的性能表现,如顺序访问的高效性和随机访问的低效性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

LinkedList是基于双向链表的 List接口的实现,在实际开发中十分常用,通过分析源码和解析之后记录一下自己的理解。


底层是双向非循环链表

顺序访问高效,随机访问低效

有序且可重复

插入和删除效率高,查找效率低

LinkedList线程不同步

LinkedList允许元素为空


继承(实现)关系

extends

|---AbstractSequentialList

implements

|---Cloneable
|---List
|---Deque
|---java.io.Serializable


字段

    //元素个数    
    transient int size = 0;

    //头结点
    transient Node<E> first;

    //尾节点
    transient Node<E> last;

双向非循环链表节点定义

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

构造方法

1.无参构造

    public LinkedList() {
    }

解析:LinkedList的无参构造是空的

2.含参构造

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

    public boolean addAll(Collection<? extends E> c) {
        return addAll(size, c);
    }

    //从index位置,将集合c的元素插入新的列表中
    public boolean addAll(int index, Collection<? extends E> c) {

        //检查index位置是否越界
        checkPositionIndex(index);

        //将传入的集合转为数组
        Object[] a = c.toArray();
        //记录数组长度
        int numNew = a.length;
        //如果长度为0,返回false
        if (numNew == 0)
            return false;

        Node<E> pred, succ;
        //如果是插入到末尾位置,后继节点为空,前驱节点为尾节点
        if (index == size) {
            succ = null;
            pred = last;
        } else {
            //如果不是插入到末尾,找到插入的位置
            succ = node(index);        //插入位置
            pred = succ.prev;          //插入位置的前驱
        }

        //遍历数组,将数组中元素一次插入
        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;
        }

        //插入位置为null,说明是在末尾插入
        if (succ == null) {
            last = pred;    
        } else {
            pred.next = succ;
            succ.prev = pred;
        }

        //集合长度增加
        size += numNew;
        //修改次数+1
        modCount++;
        return true;
    }

解析:构建一个包含传入的集合的集合。


关键方法

1.public boolean add(E e)

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

    void linkLast(E e) {

        //记录下尾节点
        final Node<E> l = last;
        //新建一个节点,前驱为l,内容为传入的e,后继为空,
        //相当于 last<—— e ——> null
        final Node<E> newNode = new Node<>(l, e, null);
        //将新插入的节点作为尾节点
        last = newNode;

        //如果l为空,也就是插入之前该列表是空的,那么将新插入的节点作为头结点
        if (l == null)
            first = newNode;
        else
            //否则,让之前的尾节点的指向新插入的节点
            //相当于 l(原尾节点last) <——> newNode(新插入的节点&现尾节点last) ——> null
            l.next = newNode;
        //长度+1
        size++;
        //修改次数+1
        modCount++;
    }

解析:在链表末尾插入指定元素,底层直接调用linkLast方法来实现,主要就是完成末尾插入之后对整个双向链表的头和尾以及节点指向的维护。


2.public void add(int index, E element)

    public void add(int index, E element) {

        //位置合法性检查
        checkPositionIndex(index);

        //如果是在末尾插入,直接调用linkLast插入
        if (index == size)
            linkLast(element);
        else
            //如果不是在末尾插入,调用linkBefore插入
            linkBefore(element, node(index));
    }

    //e:要插入的元素
    //succ:要插入那个位置节点
    void linkBefore(E e, Node<E> succ) {
        
        //记录下要插入位置的前驱节点
        final Node<E> pred = succ.prev;
        //创建新节点 前驱为要插入位置的前驱,内容为传入的e,后继节点为之前插入位置的那个节点
        //即: pred <—— newNode ——>succ
        final Node<E> newNode = new Node<>(pred, e, succ);
        //让之前插入位置的那个节点向前指向新节点
        //即: pred <—— newNode <——> succ
        succ.prev = newNode;

        //如果插入位置前驱节点为空,说明插入的节点是第一个节点,将其作为头结点
        if (pred == null)
            first = newNode;
        else
            //否则将前驱节点指向新节点
            //即:pred <——> newNode
            pred.next = newNode;
        //长度+1
        size++;
        //修改次数+1
        modCount++;
    }

解析:在指定位置插入元素。判断如果是在末尾插入,调用linkLast进行插入,如果不是,调用linkBefore进行插入。


3.public E 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) {
        
        //保存(头)结点的内容
        final E element = f.item;
        //保存(头)节点的后继节点
        final Node<E> next = f.next;

        //(头)节点内容置为空
        f.item = null;
        //断开(头)节点与其后继节点的联系
        f.next = null; 
        //头结点置为它的原头结点的后继节点
        first = next;
        //如果原头结点的后继节点为空,说明现在整个集合都为空,将尾节点置为空
        if (next == null)
            last = null;
        else
            //否则,将哟删除的节点置空
            next.prev = null;
        //长度-1
        size--;
        //修改次数+1
        modCount++;
        return element;
    }

解析:删除头结点。对头结点进行解链操作。


4.public E remove(int index)

    public E remove(int index) {

        //检查要index是否合法
        checkElementIndex(index);
        //获取将要删除的节点解链,并返回删除的元素
        return unlink(node(index));
    }

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

    //将要删除的节点解链
    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    null <—— x
            prev.next = next;
            x.prev = null;
        }

        //如果要删除的节点的后继为空,说明删除的是尾节点
        if (next == null) {
            //将删除的节点的前驱作为尾节点
            last = prev;
        } else {
            //否则,删除的不是尾节点,直接让要删除的节点的后继指向要删除的节点的前驱
            //要删除的节点向后指向空
            //即完成了: prev <——> next     null <—— x ——>null
            next.prev = prev;
            x.next = null;
        }

        //要删除的节点的内容置空
        x.item = null;
        //长度-1
        size--;
        //修改次数+1
        modCount++;
        //返回删除节点的内容
        return element;
    }

解析:删除指定位置的节点。对该位置的节点进行解链操作。要注意一下获取对应下标的节点,使用了一次二分,将集合长度的一半与要查找的下标比较,如果下标小于集合长度一半,则从前往后查找,否则从后往前查找,减少了不必要的遍历。


5.public boolean remove(Object o)

    public boolean remove(Object o) {
        //要删除的元素是否为空
        if (o == null) {
            //从头到尾遍历链表,找到第一个为空的节点,执行解链操作,返回true
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null) {
                    unlink(x);
                    return true;
                }
            }
        } else {
            //要删除的元素不为空
            //从头到尾遍历链表,找到内容与要删除节点内容相同的节点,执行解链操作,返回true
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item)) {
                    unlink(x);
                    return true;
                }
            }
        }
        //如果没有找到,删除失败返回false
        return false;
    }

    //将要删除的节点解链
    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    null <—— x
            prev.next = next;
            x.prev = null;
        }

        //如果要删除的节点的后继为空,说明删除的是尾节点
        if (next == null) {
            //将删除的节点的前驱作为尾节点
            last = prev;
        } else {
            //否则,删除的不是尾节点,直接让要删除的节点的后继指向要删除的节点的前驱
            //要删除的节点向后指向空
            //即完成了: prev <——> next     null <—— x ——>null
            next.prev = prev;
            x.next = null;
        }

        //要删除的节点的内容置空
        x.item = null;
        //长度-1
        size--;
        //修改次数+1
        modCount++;
        //返回删除节点的内容
        return element;
    }

解析:删除指定元素的节点。注意判断元素相同的指标是equals方法,删除对象型元素注意重写equals方法。


6.public E get(int index)

    public E get(int index) {

        //检查index合法性
        checkElementIndex(index);
        
        //获取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;
        }
    }

解析:获取指定位置上的元素。要注意一下获取对应下标的节点,使用了一次二分,将集合长度的一半与要查找的下标比较,如果下标小于集合长度一半,则从前往后查找,否则从后往前查找,减少了不必要的遍历。


遍历

 public E next() {

     //检查并发修改
     checkForComodification();

     //如果没有下一个元素了,抛出异常
     if (!hasNext())
          throw new NoSuchElementException();

     //迭代移动
     lastReturned = next;
     next = next.next;
     nextIndex++;
     return lastReturned.item;
  }

    //判断还有没有下一个元素
    public boolean hasNext() {
            return nextIndex < size;
    }

    //检查并发修改
    final void checkForComodification() {
            //如果修改次数不等于迭代器创建时记录的修改次数,则说明在迭代过程中修改了,抛出并发修改异常
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
    }

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值