Java集合系列-LinkedList内部探究|8月更文挑战

539 阅读5分钟

前言

集合是在我们写代码中常见的一个对象、也是我们存储数据的好帮手、但是你真的理解其中的内部实现吗。

Java版本1.8

1610451412355.png

idea查看继承图:Ctrl+Alt+u(光标放在要查看的类上。)

Collection接口

这个接口在我Collection接口简单说明这里简单说明了、这里就不做讲解

LinkedList集合说明

问题:带着问题来探究内部实现。

  • 1、LinkedList存储元素的方式(数据结构)是什么?
  • 2、LinkedList是否有和ArrayList一样的扩容机制?

如何创建一个LinkedList集合?

// 创建一个LinkedList的集合
LinkedList<Integer> list2 = new LinkedList<>();

// 也可以以多态的形式去创建一个LinkedList的集合
List<Integer> list2 = new LinkedList<>(); 

// 注意他们俩创建的对象还是有区别的、后面做说明。

属性

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

transient

这是一个关键字、作用是标记后改属性不被序列化、用于标记属性、不能标记方法以及对象

构造器

// 一个无参构造
public LinkedList() {
}
// 一个无参有构造、将一个某一个集合中的元素放入到LinkedList中
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

对集合CRUD简单说明

回到上面这个问题、区别是什么

// 创建一个LinkedList的集合
LinkedList<Integer> list2 = new LinkedList<>();

// 也可以以多态的形式去创建一个LinkedList的集合 
List<Integer> list2 = new LinkedList<>(); 

// 注意他们俩创建的对象还是有区别的、后面做说明。

image.png

就是少了些LinkedList自己的方法

// 主要是采用里的多态的形式来创建实例对象
List<Integer> list2 = new LinkedList<>(); 

什么是多态:编译看左、运行看右。

下面来说说增删改查时先说一个对象

Node对象 Node:节点的意思

// 这个是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;
    }
}

追加到元素后linkLast(E e)

public boolean add(E e) {
    linkLast(e);  // 一般默认就是添加到元素最后
    return true;
}
// 最后就是调用的这个方法
void linkLast(E e) {
    // 获取LinkedList属性保存的尾节点
    final Node<E> l = last;  // 尾节点
    // 创建一个新节点         上一个节点、当前元素、下一个节点
    final Node<E> newNode = new Node<>(l, e, null);
    // 将新节点赋值给尾节点保存
    last = newNode;
    if (l == null)  // 如果新节点等于null的话就将新节点覆给头节点(一般属于第一次添加数据是成立)
        first = newNode;
    else
        l.next = newNode; // 否则就是新节点赋给尾节点中的下一个节点中。
    size++;  // 元素加一
    modCount++; // 操作加一
}

追加到元素前addFirst(E e)

private void linkFirst(E e) {
    // 获取LinkedList属性保存的头节点
    final Node<E> f = first;
    // 创建一个新节点         上一个节点、当前元素、下一个节点
    final Node<E> newNode = new Node<>(null, e, f);
    // 将新节点赋值给头节点保存
    first = newNode;
    if (f == null) / 如果新节点等于null的话就将新节点覆给尾节点(一般属于第一次添加数据是成立)
        last = newNode;
    else
        f.prev = newNode; // 否则就是新节点赋给头节点中的上一个节点中。
    size++;  // 元素加一
    modCount++; // 操作加一
}

小结:

  • 不管是(头)插还是(尾)插、第一次添加元素都会去验证一下(头)或(尾)的元素属性是不是为空、注意这边不是一个方法中都去验证头和尾是否为null、请自行理解。
  • 问题:
    • 1、LinkedList存储元素的方式(数据结构)是什么?
      • 节点的形式保存起来、简单来说就是假设有3个节点、中间节点的元素有上一个节点和下一个节点的数据、这种数据结构叫做双向链表
    • 2、LinkedList是否有和ArrayList一样的扩容机制?
      • 没有、因为存储的数据是在内存中的、可以存放很多数据、要考虑的就是内存是否能装下这么多数据、数据太多会导致内存不足

删除最后一个元素E removeLast()返回最后一个元素数据

public E removeLast() {
    final Node<E> l = last;
    if (l == null) // 如果没有数据则抛出异常
        throw new NoSuchElementException();
    return unlinkLast(l);
}
// 具体操作内容
private E unlinkLast(Node<E> l) { // 传递过来是集合最后一个元素(尾节点)
    // assert l == last && l != null;
    final E element = l.item;  // 获取尾节点的当前元素
    final Node<E> prev = l.prev; // 将尾节点中的上一个节点赋值给prev
    l.item = null;  // 将尾节点的当前元素置为null
    l.prev = null; // help GC  为了帮助垃圾回收机制
    last = prev;  // 将尾节点的上一个节点赋值给LinkedList属性尾节点中
    if (prev == null) // 如果尾节点的上一个节点等于null、就将LinkedList属性头节点置为null
        first = null; // 一般执行这里都是集合中就有一个元素的时候
    else
        prev.next = null;  // 否则就将尾节点中的下一个节点置为null
    size--; // 元素减一
    modCount++; // 操作次数加一
    return element; // 返回当前元素
}

删除第一个元素E 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;  // 将头节点中的下一个节点赋值给next
    f.item = null; // 将头节点的当前元素置为null
    f.next = null; // help GC 为了帮助垃圾回收机制
    first = next; // 将头节点的下一个节点赋值给LinkedList属性尾节点中
    if (next == null) // 如果头节点的下一个节点等于null、就将LinkedList属性尾节点置为null
        last = null; // 一般执行这里都是集合中就有一个元素的时候
    else
        next.prev = null; // 否则就将头节点中的上一个节点置为null
     size--; // 元素减一
    modCount++; // 操作次数加一
    return element; // 返回当前元素
}

这种都是基本头(尾)删除元素、你们可以去探究一下remove(int index)remove(Object o)remove()这些方法的实现原理。

public E set(int index, E element) { // 返回之前被改元素
    // 校验数据越界异常、主要还是通过size来做判断
    checkElementIndex(index);
    // 返回指定索引的当前节点
    Node<E> x = node(index);
    // 将旧节点保存到oldVal
    E oldVal = x.item;
    // 当数据赋值给当前节点中的当前元素
    x.item = element;
    return oldVal; // 返回之前被改元素
}
// 通过下标获取对应节点
Node<E> node(int index) {
     // assert isElementIndex(index);
     // >> 是右位移计算
     /*
         公式:size >> n²
     */
    // 这种写法就类似于二分查找、有叫对半查找
    // 如 8 < (15 >> 1) = 7  我要找第八个元素、总共有15元素位移1的话就是7不成立就执行下面的else
    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;
    }
}

查询第一个getFirst()

public E getFirst() {
    final Node<E> f = first;
    // 判断当前是否有元素
    if (f == null)
        throw new NoSuchElementException();
    return f.item; // 返回头节点的当前元素
}

查询最后一个getLast()

public E getLast() {
    final Node<E> l = last;
     // 判断当前是否有元素
    if (l == null)
        throw new NoSuchElementException();
    return l.item;// 返回尾节点的当前元素
}

根据下标查询get(int index)

public E get(int index) {
     // 校验数据越界异常、主要还是通过size来做判断
    checkElementIndex(index);
    //  这个方法上面解释了。
    return node(index).item;
}

图解

image.png

总结:

  • LinkedList底层实现是链表结构、而链表的结构为双向链表结构、总体思想为第一个节点会保留下一个节点的位置、而下一个节点会保留下一个节点的位置、没有上一个节点或下一个节点就为null值
  • LinkedList可以存放null值、非线程安全集合、可以存放重复元素
  • LinkedList为链表结构所以不存在容量不足的问题、所以不存在扩容。

集合探究上一篇: ArrayList的简单源码探究欢迎前去阅读:Java集合系列-ArrayList内部探究

附赠鸡汤

不能否认你的以后、但是在我眼里你正在和我说的以后只是不过是一时兴起罢了。不能否认你的以后、但是在我眼里你正在和我说的以后只是不过是一时兴起罢了。