node 存储过程_源码会说话之LinkedList的Node

f6679e79a47a451e7ab8da081c9ea698.png

使用过ArrayList的小伙伴们都知道ArrayList实现了java.util.List接口,间接的实现了java.util.Collection接口,是我们日常开发过程使用频繁的有序数据集合。在java中还提供了一些其他的有序数据集合,今天的主角就是其中的LinkedList集合。

小伙伴们有没有想过,有了ArrayList,为什么还需要LinkedList?

要想回答这个问题,就需要知道ArrayList和LinkedList的本质区别,例如数据结构的区别、操作的区别。

在之前关于ArrayList的文章中,通过源码可以得知ArrayList内部使用静态数组存储数据元素,当容量不足时,会使用新的容量初始化一个数组,然后把原来数组中保存的元素拷贝到新的数组中。

画外音:数组在内存中是连续的一块区域。

通过这些描述,可以推断出ArrayList在使用时的局限:

  1. ArrayList的内存是连续的,如果不满足可能会导致ArrayList初始化失败;

  2. ArrayList中的数据插入、删除操作频繁时,可能会频繁的进行数据拷贝、数组初始化,影响效率。

这些ArrayList的局限在LinkedList中都得到了解决,先从宏观上看下LinkedList与ArrayList的不同:

fe9dea0e34bcf8ffa4d5757e3ed301d6.png

从上图中可以大致了解到LinkedList除了实现java.util.List接口,还实现了java.util.Queue和java.util.Deque两个接口,这两个接口的作用是:

  • java.util.Queue : 队列,在队尾添加元素,在队头读取元素,可以看作是单向链表;

  • java.util.Deque : 双向队列,可以在队尾、对头进行添加、读取的操作,可以看作是双向链表。

换言之,LinkedList内部就不是使用数组存储数据,而是使用队列的形式存储数据元素,而队列中每一个数据元素都是队列中的一个节点,即Node。

在下文中,通过源码介绍:

  1. Node节点的数据结构

  2. LinkedList的内部结构

  3. 与ArrayList操作对比

节点(Node)数据结构

秀才不多说,直接让源码来说:

public class LinkedList<E>  extends AbstractSequentialList<E>  implements List<E>, Deque<E>, Cloneable, java.io.Serializable{     // prev code ......       /**     * LinkedList的内部静态类     * @param      */    private static class Node<E> {        E item; // 当前节点的元素        Node next; // 当前节点的后一个节点        Node prev; // 当前节点的前一个节点        /**         * 使用特定参数,构造一个Node节点         * @param prev         * @param element         * @param next         */        Node(Node prev, E element, Node next) {            this.item = element;            this.next = next;            this.prev = prev;        }    }        // last code ......}

通过以上源码可以得知:Node节点除了存储数据本身,还会使用两个引用来明确自己所处的位置

这也表明LinkedList是一个双向链表,从某一个Node节点触发可以访问到其前一个节点,也可以访问到其后一个节点。

LinkedList的内部结构

想要知道LinkedList的内部结构,就需要查看下LinkedList中维护了哪些成员属性,直接让源码告诉你:

public class LinkedList<E>  extends AbstractSequentialList<E>  implements List<E>, Deque<E>, Cloneable, java.io.Serializable{    // LinkedList中实际存储元素的个数    transient int size = 0;    /**     * Pointer to first node.     * Invariant: (first == null && last == null) ||     *            (first.prev == null && first.item != null)     * 指向LinkedList中第一个元素的引用     */    transient Node first;    /**     * Pointer to last node.     * Invariant: (first == null && last == null) ||     *            (last.next == null && last.item != null)     * 指向LinkedList中最后一个元素的引用     */    transient Node last;    /**     * Constructs an empty list.     * 构造一个空的LinkedList集合     */    public LinkedList() {    }    /**     * Constructs a list containing the elements of the specified     * collection, in the order they are returned by the collection's     * iterator.     *     * @param  c the collection whose elements are to be placed into this list     * @throws NullPointerException if the specified collection is null     *     * 使用特定的Collection集合构造LinkedList,并且把特定的Collection中数据添加到LinkedList中     */    public LinkedList(Collection extends E> c) {        this();        addAll(c);    }      // other code ...... }

看完源码所说的这些,就可以得知:LinkedList中使用Node节点本身来保证元素存储的顺序,并且分别记录了第一个节点和最后一个节点的引用。

其内部结构的理解,可以用下图进行表示:

ff8474523e4dbd6c8ec7db9e074cdfcd.png

结合上图,可以总结得到:

  • LinkedList中的元素在内存中的分布可以是不连续的;

  • 可以从first向后遍历,也可以从last向前遍历。

与ArrayList操作对比

现在基本明确了LinkedList对内存的要求比ArrayList要小一些,在进行插入、删除操作时的优越性又是怎么表现的呢?

以指定位置插入元素为例:

  • ArrayList向指定位置插入元素的api方法是:add(int index, E element);

  • LinkedList向指定位置插入元素的api方法是:add(int index, E element);

直接让源码来说:

ArrayList#add(int index, E element)
    /**     * Inserts the specified element at the specified position in this     * list. Shifts the element currently at that position (if any) and     * any subsequent elements to the right (adds one to their indices).     *     * @param index index at which the specified element is to be inserted     * @param element element to be inserted     * @throws IndexOutOfBoundsException {@inheritDoc}     */    public void add(int index, E element) {        // 检查指定的索引值,如果小于0 ,或大于当前数组的size值,则抛出异常        rangeCheckForAdd(index);        // 数组扩容操作        ensureCapacityInternal(size + 1);  // Increments modCount!!        // 把elementData数组中指定位置后面的所有元素向后移动一位        // 第一个参数:源数组        // 第二个参数:源数组中移动的起始索引值        // 第三个参数:目标数组        // 第四个参数:目标数据中的起始索引值        // 第五个参数:源数组中移动的元素个数        System.arraycopy(elementData, index, elementData, index + 1,                         size - index);        // 把目标元素添加到elementData数组的指定位置        elementData[index] = element;        // 更新当前ArrayList的元素数量        size++;    }

ArrayList在进行指定位置插入元素的操作时,影响效率的有两个操作:

  1. 自动扩容操作;

  2. 内部元素的移动操作。

LinkedList#add(int index, E element)
    /**     * Inserts the specified element at the specified position in this list.     * Shifts the element currently at that position (if any) and any     * subsequent elements to the right (adds one to their indices).     *     * @param index index at which the specified element is to be inserted     * @param element element to be inserted     * @throws IndexOutOfBoundsException {@inheritDoc}     */    public void add(int index, E element) {        // 检查目标索引值,是否存在下表越界的情况        checkPositionIndex(index);        if (index == size)            // 在LinkedList最后位置插入元素            linkLast(element);        else            // 通过node(int index ) 方法检索到目标索引位置的node节点            // 在目标索引节点之前插入当前元素            linkBefore(element, node(index));    }        /**     * Returns the (non-null) Node at the specified element index.     */    Nodenode(int index) {        // assert isElementIndex(index);        // 判断目标索引值在前半部分,还是在后半部分        if (index < (size >> 1)) {            // 前半部分的情况            Node x = first;            // 从第一个节点开始遍历,直到目标索引位置            for (int i = 0; i < index; i++)                x = x.next;            return x;        } else {            // 后半部分的情况            Node x = last;            // 从最后一个节点开始遍历,直到目标索引位置            for (int i = size - 1; i > index; i--)                x = x.prev;            return x;        }    }    /**     * Links e as last element.     * 在最后位置插入元素     */    void linkLast(E e) {        final Node l = last; // 暂存原来的最后一个节点        final Node newNode = new Node<>(l, e, null);        last = newNode; // 更新当前LinkedList的最后一个节点        if (l == null) // 原来最后一个节点为null,说明在插入目标元素之前LinkedList是空的            // 当前LinkedList只存在目标元素的节点,即是第一个,也是最后一个            first = newNode;        else            // 更新前一个节点与目标节点的关联            l.next = newNode;        // 当前LinkedList元素数量更新        size++;        // 结构变化次数更新        modCount++;    }    /**     * Inserts element e before non-null Node succ.     * 在非空Node节点之前插入目标元素     */    void linkBefore(E e, Node succ) {        // assert succ != null;        // 获取非空节点的前一个节点        final Node pred = succ.prev;        final Node newNode = new Node<>(pred, e, succ);        // 建立非空节点与新节点的关联 newNode         succ.prev = newNode;        if (pred == null)            // 前一个节点为空,说明非空节点是first节点,现在更新为newNode节点为first节点            first = newNode;        else            // 前一节点存在,则建立前一个节点与新节点的关联 pred --> new Node            pred.next = newNode;        // 执行完成之后的情况是:pred --> newNode --> succ        // 更新元素数量        size++;        // 更新结构变化次数        modCount++;    }

LinkedList进行指定位置插入操作时,影响效率的是找到目标索引位置的Node节点,然后修改前后节点的引用。

综合来看在进行指定位置插入操作时,可以延伸到对结构发生变更的操作,LinkedList的效率是优于ArrayList

但各位小伙伴需要注意的是,ArrayList在根据索引进行读取元素时的效率要优于LinkedList

怕,你就输了;只要不放弃,你也可以让源码说话。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值