LinkedList源码浅析

155 阅读4分钟

1、LinkedList 概要

LinkedList 是基于链表的数据结构实现的。 链表可分为单向链表和双向链表。

一个单向链表包含两个值: 当前节点的值和一个指向下一个节点的链接。

一个双向链表有三个整数值: 数值、向后的节点链接、向前的节点链接。

我们看源码可以明显看出 LinkedList 是属于双向链表

2、LinkedList 源码

2.1、字段

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;
    

    /**
     * 节点对象
     */
    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 是啥,有何作用?

答:transient 标识不会被序列化(除非方法内手动重写了序列化的方法,然后指定使用相关字段去序列化,例如 arrayList 的writeObject)

问题二:E 代表什么?

答:E 是泛型中通配符, 常用的 T,E,K,V,?本质上这些个都是通配符,没啥区别,只不过是编码时的一种约定俗成的东西。比如上述代码中的 T ,我们可以换成 A-Z 之间的任何一个 字母都可以,并不会影响程序的正常运行,但是如果换成其他的字母代替 T ,在可读性上可能会弱一些。通常情况下,T,E,K,V,?是这样约定的:

  • ?表示不确定的 java 类型
  • T (type) 表示具体的一个java类型
  • K V (key value) 分别代表java键值中的Key Value
  • E (element) 代表Element

2.2、构造方法

/**
 * 没做什么动作
 */
public LinkedList() {
}

/**
  * 构造一个包含指定元素的列表
  * 集合,按照集合返回的顺序
  * 迭代器。
  *
  * @param c 将其元素放入此列表的集合
  * @throws NullPointerException 如果指定的集合为空
  */
public LinkedList(Collection<? extends E> c) {
    // 不知道有啥用
    this();
    addAll(c);
}

问题一:this() 在此处的作用是啥?

猜想可能是为了执行父类的构造方法? 非无参数构造方法不会自动执行父类的构造方法吗?

public class Person {
    public Person() {
        System.out.println("Person.Person()");
    }
}


public class PersonMan extends Person{
    /**
     * 参数的构造方法里啥也没做
     */
    public PersonMan() {

    }

    public PersonMan(String name) {
        this();
        System.out.println("PersonMan.PersonMan(name) --- " + name);
    }

    public PersonMan(int age) {
        // 没有 this() 
        System.out.println("PersonMan.PersonMan(age) -- " + age);
    }
}


public void test2() {
    PersonMan a = new PersonMan("aaa");
    System.out.println("==========");
    PersonMan b = new PersonMan(1);

}

test()的执行结果:

Person.Person()
PersonMan.PersonMan(name) --- aaa
==========
Person.Person()
PersonMan.PersonMan(age) -- 1

可见:
没有 this() 也会执行父类的构造方法
无参数的构造方法 内容如果为空,实际this() 是可以省略的

结论:为了执行父类的构造方法的猜想不成立,this() 在此处仅仅是个编码习惯问题。

2.3、addAll 方法

 /**
  * 往末尾批量添加数据
  */
public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}


/**
  * 往指定位置批量添加数据
  */
public boolean addAll(int index, Collection<? extends E> c) {
    // 检查边界
    checkPositionIndex(index);
    // toArray 将集合转成数组
    Object[] a = c.toArray();
    int numNew = a.length;
    // 为0 则为插入任何元素,返回false
    if (numNew == 0)
        return false;
    // pred 上一个节点, succ 当前节点
    Node<E> pred, succ;
    // index == size 表示从末尾插入
    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);
        // pred == null 说明从头部插入
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        pred = newNode;
    }

    // succ == null 相当于 index == size, 表示从末尾插入 
    // 遍历完数组后pred为数组里的最后一个节点,即整个列表的最后一个节点故赋值给last
    if (succ == null) {
        last = pred;
    } else {
     // 反之则是从开头/中间插入
     // 则遍历完数组后pred为数组里的最后一个节点
     // pred.next = succ , succ.prev = pred  将链表重新衔接了起来
        pred.next = succ;
        succ.prev = pred;
    }

    // 变更size , 记录当前链表长度
    size += numNew;
    modCount++;
    return true;
}

问题一:@SuppressWarnings("unchecked") 的作用是什么?

@SuppressWarnings("unchecked") E e = (E) o;
告诉编译器忽略 unchecked 警告信息: Unchecked cast: 'java.lang.Object' to 'E' 该强制类型转换并未做类型校验,强制转换并不安全,可能会抛出异常导致程序崩溃 因为入参已经决定了 c 一定是继承 E 的, 所以不需要做类型校验

2.4、remove 方法


/*
 * 删除某个项 (只移除匹配到的第一个)
 */
public boolean remove(Object o) {
    // null 单独处理
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
     // 因为 o 和 x.item 都有可能为null
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}


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;

    // prev == null 说明是头部,直接将头部改为下一节点即可 
    if (prev == null) {
        first = next;
    } else {
    // 否则说明是中间/末尾移除,需要将前一个节点next 改为 当前的next 
    // 断开前部当前的链接
        prev.next = next;
        x.prev = null;
    }

    // next == null 说明是从尾部移除,那么尾部需要改为上一个节点即可
    if (next == null) {
        last = prev;
    } else {
    // 否则说明是中间/末尾移除,需要将下一个节点prev 改为 当前的prev 
    // 断开当前的后部链接
        next.prev = prev;
        x.next = null;
    }

   // 当前项置为null, size 减一
    x.item = null;
    size--;
    modCount++;
    return element;
}

image.png

2.5、 get 方法

/**
 * 获取某个位置的节点内容
 */
public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}


/**
 * 根据index 返回对应的节点的对象(分成了两半进行查找,提高效率)
 */
Node<E> node(int index) {
    // assert isElementIndex(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;
    }
}

总结

1、LinkedList 基于链表数据结构实现,属于双向链表
2、LinkedList 增加和删除是通过修改节点的前后指向实现的
3、LinkedList 的查询会将链表分寸两边,进行查找