Java LinkedList

587 阅读5分钟

基于Java8
ArrayList的优点在于对get和set的调用花费常数时间。缺点是新增和删除代价昂贵,除非在ArrayList的端操作。
那需求要经常新增和删除呢?为此Java提供了与之对应的LinkedList。LinkedList是双向链表实现的,它的优点在于,在已知变动位置的情况下新增和删除花费常数时间,缺点在于不利于索引get比较昂贵。

结构

LinkerList是由一个双向链表实现的,其中每个节点都可向上或向下索引。

//标记节点
transient LinkedList.Node<E> first; //头节点
transient LinkedList.Node<E> last; //尾节点
//存储数据的节点
private static class Node<E> {
    E item;
    LinkedList.Node<E> next;
    LinkedList.Node<E> prev;
    Node(LinkedList.Node<E> prev, E e, LinkedList.Node<E> next) {
        this.item = e;
        this.next = next;
        this.prev = prev;
    }
}

LinkedList使用静态内部类Node来存储数据,Node中的prev指向前一个节点的位置,next指向下一个节点的位置。为了利于实现引入了标记节点,其中first为头节点,last为尾节点。

增加节点常用的方法 add(E e),addLast(E e),addFirst(E e)和add(int index, E e).

add(E e)

//新增数据,默认尾部添加
public boolean add(E e) {
    this.linkLast(e);
    return true;
}
// 从尾部开始添加节点
void linkLast(E e) {
    // 把尾节点数据暂存
    LinkedList.Node prev = last;
    // 新建新的节点,初始化入参含义:
    // prev 是新节点的前一个节点,当前值是尾节点值
    // e 表示当前新增节点,当前新增节点后一个节点是 null
    final Node<E> newNode = new Node<>(prev, e, null);
    // 新建节点追加到尾部
    last = newNode;
    //prev是尾节点,prev为空,链表也就是空的,就把新节点赋值给头节点
    if (prev == null)
        first = newNode;
    //不为空则把前尾节点的下一个节点,指向当前尾节点。
    else
        prev.next = newNode;
    //数组大小和结构更改 +1
    size++;
    modCount++;
    //至此元素添加完毕。
}

从尾部追加节点比较简单,头部追加节点也是类似。指定位置添加则麻烦一些,需要遍历查找得到相应节点然后进行操作,其余也和上述方法类似。

//遍历查找节点
LinkedList.Node<E> node(int index) {
    LinkedList.Node node;
    int var3;
    //采用二分法减少运算量,
    //index小于size/2则从头节点往后遍历
    //大于size/2则从尾节点向前遍历
    if (index < this.size >> 1) {
        node = this.first;
        for(var3 = 0; var3 < index; ++var3) {
            node = node.next;
        }
        return node;
    } else {
        node = this.last;
        for(var3 = this.size - 1; var3 > index; --var3) {
            node = node.prev;
        }
        return node;
    }
}

常用的删除节点方法有 remove(int index),removeFirst(),removeLash()和remove(Object obj).删除是把要删除节点的prev,next和item指向null,让GC可以回收它。

//删除指定位置的Node
public E remove(int index) {
    this.checkElementIndex(index);
    //this.node在add方法中分析了。
    return this.unlink(this.node(index));
}
//删除节点
E unlink(LinkedList.Node<E> node) {
    Object item = node.item;
    //得到删除节点的后继节点
    LinkedList.Node next = node.next;
    //得到删除节点的前驱节点
    LinkedList.Node prev = node.prev;
    if (prev == null) {
        this.first = next;
    } else {
        //让前驱节点的next指向删除节点的后继节点
        prev.next = next;
        //让删除节点的prev指向null
        node.prev = null;
    }
    if (next == null) {
        this.last = prev;
    } else {
        //让后继节点的prev指向删除接节点的前区节点
        next.prev = prev;
        //删除节点的next指向null
        node.next = null;
    }
    //删除节点的item指向null
    node.item = null;
    --this.size;
    ++this.modCount;
    return item;
    //至此remvoe完毕
}

尾部删除,头部删除和上述类似。

修改方法只有一个 set(int index, E e).

public E set(int index, E e) {
    this.checkElementIndex(index);
    //在add()已分析
    LinkedList.Node node = this.node(index);
    Object item = node.item;
    node.item = e;
    return item;
}

此方法比较简单。

查询方法有get(int index),getFirst(),getLast()和indexOf(Object obj).

//根据索引查找元素
public E get(int index) {
    this.checkElementIndex(index);
    //在add()已分析
    return this.node(index).item;
}

indexOf方法也比较简单先遍历后比较,相等则返回索引。

LinkedList 查询是比较慢的,需要遍历查找。新增和删除比较快的,仅仅把节点的指向位置修改下就好了。

迭代器

LinkedList 要实现双向的迭代访问,但Iterator只支持从头到尾的访问,为此Java新增了一个迭代接口ListIterator,此接口提供了双向迭代方法。

// 迭代器
private class ListItr implements ListIterator<E> {
    private Node<E> lastReturned;//上一次执行 next() 或者 previos() 方法时的节点位置
    private Node<E> next;//下一个Node
    private int nextIndex;//下一个Node的位置
    //expectedModCount:期望结构更改次数;
    //modCount:实际结构更改次数
    private int expectedModCount = modCount;
    ...
}

/**
*从头到尾迭代
*/
//是否有下一个Node
public boolean hasNext() {
    // 下一个Node的位置小于size就有
    return nextIndex < size;
}
// 取下一个Node
public E next() {
    //检查更改次数
    checkForComodification();
    if (!hasNext()){//再次检查
        throw new NoSuchElementException();
    }else{
        lastReturned = next;
        // next 成为下一个节点了,为下次迭代做准备
        next = next.next;
        nextIndex++;
        return lastReturned.item;
    }
}
//检查实际更改次数和期望更改次数是否相同
final void checkForComodification() {
    if (LinkedList.this.modCount != this.expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

/**
*从尾至头迭代
*/
// 若上次节点索引大于 0,则表示还有节点
public boolean hasPrevious() {
    return nextIndex > 0;
}
// 获取前一个节点
public E previous() {
    checkForComodification();
    if (!hasPrevious()){
        throw new NoSuchElementException();
    }else{
        // next 为空:表示第一次迭代,取尾节点,或者上一次操作把尾节点删除掉了
        // next 不为空:表示已经发生过迭代了,取前节点
        lastReturned = next = (next == null) ? last : next.prev;
        // 下一个Node位置变化
        nextIndex--;
        return lastReturned.item;
    }
}
/**
*迭代器删除
*/
public void remove() {
    checkForComodification();
    // lastReturned 本次迭代需要删除的值,分以下空和非空两种情况:
    // lastReturned 为空,说明调用者没有主动执行过 next() 或者 previos(),直接报错
    // lastReturned 不为空,是在上次执行 next() 或者 previos()方法时赋的值
    if (lastReturned == null){
        throw new IllegalStateException();
    }else{
        Node<E> lastNext = lastReturned.next;
        //删除当前节点
        unlink(lastReturned);
        // next == lastReturned :从尾到头递归顺序,并且是第一次迭代,并且要删除最后一个元素的情况下
        // 这种情况下,previous() 方法里面设置了 lastReturned = next = last,所以 next 和 lastReturned会相等
        if (next == lastReturned){
            // 这时候 lastReturned 是尾节点,lastNext 是 null,所以 next 也是 null,这样在 previous() 执行时,发现 next 是 null,就会把尾节点赋值给 next
            next = lastNext;
        }else{
            nextIndex--;
        }
        lastReturned = null;
        //期望结构更改次数 +1;
        expectedModCount++;
    }
}

LinkedList是非线程安全的,官方建议使用“Collections.synchronizedList(new LinkedList(...))”虽然实现了线程安全,但是它的性能比较低,是通过给每个方法加上锁来实现的。
可存储所有元素包括null.