【Java 集合】LinkedList

LinkedList 一个双向链表。
本身是基于链表进行封装的列表, 所以具备了链表的特性: 变更简单, 容量是无限的, 不必像数组提前声明容量等。
同时 LinkedList 支持存储包括 null 在内的所有数据类型。

1 链表

了解 LinkedList 之前, 我们需要先了解一下双向链的特点

  1. 单链表, 双链表, 循环链表的定义, 可以看一下这个
  2. 链表内存是散乱的, 每一个元素存储本身内存地址的同时还存储下一个元素的地址
  3. 链表具备了增删快, 查找慢的特点
  4. LinkedList 是基于双向链表设计的, 所以具备了链表的特点

2 LinkedList 中每个节点的定义

private static class Node<E> {

  	/** 当前节点的内容 */
    E item;

    /** 下一个节点 */
    Node<E> next;

    /** 上一个节点 */
    Node<E> prev;

    Node(Node<E> prev, E element, Node<E> next) {
        // 当前节点的数据 = element
        this.item = element;
        // 当前节点的后置节点 = next
        this.next = next;
        // 当前节点的前置节点 = prev
        this.prev = prev;
    }
}

Node 节点中除了指向下一个节点的 next, 还包含指向上一个节点的 prev, 所以 LinkedList 是一个双向的链表。

3 LinkedList 中的几个属性

public LinkedList<E> {

    transient int size = 0;

    transient Node<E> first;

    transient Node<E> last;
}

3.1 size

表示当前链表中的节点个数。

3.2 first

整个双向链表的头节点

3.3 last

整个双向链表的尾节点

重要的就这个几个属性了

4 LinkedList 的构造方法

public LinkedList<E> {

    // 构造函数 1: 无参构造函数
    public LinkedList() {
    }

    // 构造函数 2: 给定一个 Collection 的构造
    public LinkedList(Collection<? extends E> c) {
        // 省略
    }
}

4.1 无参的构造函数

public LinkedList() {
}

就是单纯的声明 LinkedList 的实例, 没有其他的逻辑了

4.2 给定一个 Collection 的构造

public LinkedList(Collection<? extends E> c) {

    // 调用自身无参的构造函数
    this();

    // 将集合 c 添加到当前的链表中
    addAll(c);
}

/**
 * 将集合对象全部添加到当前链表中
 */
public boolean addAll(Collection<? extends E> c) {
    // 调用 addAll 重写方法在 size 的位置添加数据
    return addAll(size, c);
}

/**
 * 指定位置的批量插入
 */
public boolean addAll(int index, Collection<? extends E> c) {

    // 判断 0 <= index <= size, 不满足抛出异常
    checkPositionIndex(index);

    // 集合转为数组
    Object[] a = c.toArray();
    int numNew = a.length;

    // 需要插入的集合没有数据, 直接结束
    if (numNew == 0)
        return false;

    // pred: 新增的元素的将要插入的位置的前一个节点  
    // succ: 新增的元素的将要插入的位置的节点
    // 如果新增的元素的节点位置为 a, 那么 pred 就是 a 的前一位的节点, succ 就是 a 节点
    Node<E> pred, succ;
    
    // 需要添加的元素刚好追加在已有的后面的
    if (index == size) {
        succ = null;
        pred = last;
    } else {
        // 获取指定位置的节点
        succ = node(index);
        pred = succ.prev;
    }    

    for (Object o : a) {

        E e = (E) o;
        Node<E> newNode = new Node<>(pred, e, null);
        // 第一次创建时, 那么第一个节点就是 first节点
        if (pred == null)
            first = newNode;
        else
            // 将 pred 的下一个节点指向新创建的节点
            pred.next = newNode;  
          
        // 将 pred 改为当前节点, 方便下一个元素操作
        pred = newNode;    
    }   

    // succ 是为空的, 表示直接在已有的数据后面追加元素的话, 所以将最后节点设置为新增元素的最后一个元素的节点
    if (succ == null) {
        last = pred;
    } else {
        // 原有数据后半部分拼接到现在链表的后半部分
        pred.next = succ;
        succ.prev = pred;
    } 

    // 当前的长度
    size += numNew;
    // 修改次数加1 
    modCount++;
    return true;
}

/**
 * 校验 index 的值满足:  0 <= index <= size
 */
private void checkPositionIndex(int index) {
    if (!isPositionIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

/*
 * 判断 index 是否大于等于 0 并且小于等于 size
 */
private boolean isPositionIndex(int index) {
    return index >= 0 && index <= size;
}

/**
 * 获取指定位置的节点
 */
Node<E> node(int index) {

    // 此次做了一个小优化: 当要查找的位置小于现有数据的一半, 从前往后找, 大于的话, 从后面开始找
    // size >> 1 相当于 size / 2
    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 的数据是通过 first(节点) 和 last(节点) 和 size 三个共同维护的
  3. LinkedList 内部的数据通过泛型, 保持了自己的类型, 没有转为 Object。

5 LinkedList 几个常用的方法

5.1.1 添加数据

直接在链表末尾添加元素

// 直接添加一个元素
public boolean add(E e) {
    // 默认新增到链表的末尾
    linkLast(e);
    return true;
}

/**
 * 将入参的元素添加到链表的末尾
 */
void linkLast(E e) {

    // 获取链表的尾结点
    final Node<E> l = last;
    // 将数据封装为 Node 节点
    final Node<E> newNode = new Node<>(l, e, null);
    // 链表的尾结点等于新的节点
    last = newNode;

    // 如果链表一开始的尾结点为空, 表示链表一开始没有数据
    if (l == null)
        // 链表的头节点也等于当前的新节点
        first = newNode;
    else
        // 一开始的为节点有值, 将原来的尾结点的下一个节点设置为当前新的节点
        l.next = newNode;    
    
    // 链表个数 + 1
    size++;
    // 修改次数 + 1
    modCount++    
}

指定位置的插入

public void add(int index, E element) {

    // 判断 0 <= index <= size, 不满足抛出异常
    checkPositionIndex(index);

    // index == size 要插入的位置刚好在末尾
    if (index == size)
        linkLast(element);
    else 
        // 插入到某个节点的前面
        linkBefore(element, node(index));    
}

/**
 * 将元素插入到某个节点的前面
 */
void linkBefore(E e, Node<E> succ) {

    // 获取某个节点的前置节点
    final Node<E> pred = succ.prev;
    final Node<E> newNode = new Node<>(pred, e, succ);

    // 设置插入位置的节点的前置节点为需要插入的新节点
    succ.prev = newNode;

    // 插入位置的节点的前置节点为空, 表示没有头节点
    if (pred == null)
        first = newNode;
    else
    // 需要插入的位置的前置节点的下一个节点为新节点
        pred.next = newNode;    

    // 个数加 1
    size++;
    
    // 修改次数 + 1
    modCount++;        
}

在头部插入

public void addFirst(E e) {
    linkFirst(e);
}

/**
 * 在链表的头节点前新增元素
 */ 
private void linkFirst(E e) {

    // 获取头节点
    final Node<E> f = first;
    final Node<E> newNode = new Node<>(null, e, f);

    // 头节点等于新节点
    first = newNode;
    // 原本的头节点为空, 表示没有数据
    if (f == null)
        // 设置尾节点为新节点
        last = newNode;
    else
        // 设置原本的头节点的前置节点为当前的节点
        f.prev = newNode;

    size++;
    modCount++;        
}

还有前面看到的

  1. 插入一个 Collection 的 addAll(Collection c)
  2. 指定位置的插入一个 Collection 的 addAll(int index, Collection c)

5.1.2 删除数据

直接删除 (默认删除第一个元素)

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;

    // 如果原本删除元素就是没有后续元素时, 将最后的元素设置为 null
    if (next == null)
        last = null;
    else
    	// 将新的第一个元素的前置节点设置为 null
        next.prev = null;  
    // 元素个数 - 1    
    size--;
    // 修改次数 + 1
    modCount++;
    // 返回数据
    return element;          
}

指定位置的删除

public E remove(int index) {
	// 检测删除的位置是否正确
    checkElementIndex(index);
    return unlink(node(index));
}

private void checkElementIndex(int index) {
    // 如果不满足  0 <= index < size, 就抛出异常
    if (!isElementIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

private boolean isElementIndex(int index) {
    return index >= 0 && index < size;
}

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 = next;
        // 设置删除节点前置节点为 null
        x.prev = null;
    }

    // 如果后置节点为空, 说明移除的为尾节点, 重新设置尾节点为删除节点的前置节点
    if (next == null) {
        last = prev;
    } else {
    	// 将后置节点的前置节点设置为前置节点
        next.prev = prev;
        // 设置删除节点后置节点为 null
        x.next = null;
    }
    
    // 将删除节点的数据设置为 null
    x.item = null;
    size--;
    modCount++;
    return element;
}

指定删除元素

public boolean remove(Object o) {

    // 需要删除的节点为 null
    if (o == null) {
        // 找到第一个为 null 的元素, 进行删除
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            // 找到节点的数据等于要删除的节点
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
}

除了上面几个还有

  1. 删除第一个 removeFirst()
  2. 删除最后一个 removeLast()
  3. 删除第一个符合的元素 removeFirstOccurrence(Object o)
  4. 删除最后一个符合的元素 removeLastOccurrence(Object o)

5.1.3 获取数据

获取指定位置的数据

public E get(int index) {
    // 确保 0 <= index < size
    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.1.4 更新数据

更新指定位置节点的属性值

public E set(int index, E element) {

    // 确保 0 <= index < size
    checkElementIndex(index);
    // 获取指定位置的节点数据
    Node<E> x = node(index);
    // 获取指定节点是旧数据
    E oldVal = x.item;
    // 更新指定节点的数据
    x.item = element;
    // 返回旧数据
    return oldVal;
}

6 LinkedList 补充

1. LinkedList 实现了 Serializable 接口, 但是他的属性都是设置为 transient ?

LinkedList 重写了序列化方法 writeObject 和反序列化方法 readObject。 在序列化中, 重新通过遍历所有节点, 把所有节点数据写入到 ObjectOutputStream。

2. LinkedList 支持 fail-fast 机制 ?

通过继承关系可以指定 LinkedList 也是实现了 AbstractList, 具备了 modCount, 在修改中也会增加 modCount 的值, 所以 LinkedList 也支持 fail-fast 机制。

3. LinkedList不是一个线程安全的集合 ?

LinkedList 是线程不安全的, 如果需要保证线程的安全性, 可以考虑使用 Collections.synchronizedList(Lise l) 函数返回一个线程安全的 LinkedList 类。

4. 不要用 for 遍历 LinkedList ?

遍历的代码

List<String> list2 = new LinkedList<>();
list.add("1");
list.add("2");
list.add("3");

for (int i = 0; i < list2.size(); i++) {
	String item = list2.get(i);
	System.out.println(item);
}

源码中涉及的代码:

public class LinkedList {

    public E get(int index) {
        checkElementIndex(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;
        }
    }
}

如上: 我们通过 for 遍历 LinkedList,

  1. 第一次 get(0), node(0), 就是 first 节点
  2. 第二次 get(1), node(1), 就是 first -> node1
  3. 第三次 get(2), node(2), 就是 first -> node1 -> node2

每次通过 get() 就是需要从第一个节点一直找到对应的位置, 才能找到需要的节点。
所以遍历 LinkedList 可以使用 foreach (foreach 循环的原理就是迭代器) 或者迭代器。

5. LinkedList 还有其他的作用吗

LinkedList 实现了 Deque 接口, 所以 LinkedList 可以作为双端队列, 同时 LinkedList 的双向链表的特点, 还可以作为 Stack 使用, 但是 LinkedList 的这 2 个功能,
如果没有什么特殊的要求的话, 都可以使用 ArrayDeque 替代, 后者的性能更好。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值