Java 集合之LinkedList

首先从源码中看一下LinkedList:

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable

LinkedList是基于链表,所以插入和删除操作要比上一篇中说到的ArrayList要高效。但是随机访问没有基于索引的数组高效,这也是它的弊端。从源码看,除了实现AbstractSequentialList(它本身实现了List接口)外,还实现了Deque接口,该接口定义双端队列的相关操作(addFirstaddLastofferFirst等)。

下面看一下它的构造函数:

    /**
     * 初始化一个空集合
     */
    public LinkedList() {
    }

    /**
     * 带有集合参数的方法是:首先初始化一个空集合,然后把传入的集合所有元素添加进去
     */
    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }

那么,LinkedList是如何实现链表的呢?看下面代码:

transient int size = 0;

/**
 * Pointer to first node.
 * Invariant: (first == null && last == null) ||
 *            (first.prev == null && first.item != null)
 */
transient Node<E> first;

/**
 * Pointer to last node.
 * Invariant: (first == null && last == null) ||
 *            (last.next == null && last.item != null)
 */
transient Node<E> last;

自身定义了两个节点first和last,我们注意到他们是什么类型?Node,我们看一下Node是什么?如下:

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;
    }
}

有上面可以看到,Node是LinkedList的一个私有静态内部类!它包含三个属性,那就是 该节点的数据item、以及它的前后继节点。我们就明白了,LinkedList是一个节点一个节点组成的集合列表,只是节点之间靠着前后驱节点进行关联。那也就清晰为什么它在插入元素和删除元素操作上比基于动态数组的ArrayList要高效的原因了!因为只需要改动相关前后节点的关联即可完成操作,而不像ArrayList那样需要对插入或者删除位置之后的数组元素进行顺延。

简单看一下jdk如何实现增删改查

1. add(E e)方法:

//这里调用了linkLast,我们知道插入元素是插入到list的尾部。
public boolean add(E e) {
   linkLast(e);
   return true;
}

然后跟踪代码看一下linkLast方法实现了什么?

/**
  * 添加第一个节点的时候,first = last= Node{prev=null,e,next=null;}
  * 当第二个节点的时候,l=last={prev=null,e,next=null;},
  *                  last=new Node{prev=l,e1,next=null;} ,
  *                  l={prev=null,e,next=last;}
  *                  first=Node{prev=null,e,next=l}   
  *
  *  思路:新添加的元素,加上当前尾节点构成新的尾节点;然后将旧的尾节点的next指向newNode
  * 
  */
void linkLast(E e) {
    final Node<E> l = last;//把当前队尾元素赋给l
    //创建一个新节点newNode,它的前驱节点为l也就是为插入前的队尾节点
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;//新节点赋给last,作为队尾节点
    if (l == null)
        first = newNode;
    else //否则就把插入的节点赋给当前尾节点的后继节点。
        l.next = newNode;
    size++;//然后size做加加
    modCount++;
}

2.add(int index, E element)方法:

public void add(int index, E element) {
    checkPositionIndex(index);//检查 是否超出list长度
    //如果插入的位置等于size,那就插入到尾部。否则调用linkBefore
    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}

刚才我们已经分析了linkLast方法了,那就再看看linkBefore方法实现了什么?在看之前先看一下上面linkBefore第二个参数调用到的node方法:

Node<E> node(int index) {
    //首先判断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;
    }
}

上面的node方法,思路值得去学习,第一个是位移取半的算法小技巧;第二就是分前后两板段,一个从first去遍历,一个从last去遍历。

void linkBefore(E e, Node<E> succ) {
    // 
    final Node<E> pred = succ.prev;//此时的pred,是未来插入元素的前驱节点
    //生成插入的节点,它的前后驱节点即为插入位置的前后节点
    final Node<E> newNode = new Node<>(pred, e, succ);
    //下面的操作就是去更新,插入节点前驱节点的next 和 后继节点的prev 
    succ.prev = newNode;
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}

3.get(int index)方法:

重点是用到了Node<E> node(int index),上面已经说了,这里就不赘述。

//TODO 此处如果有个草图解析一下就好了,如果有时间再来补充上!!!
源码暂时分析道这里,删除等其他操作,也是大同小异。有时间再来补充。上文如有不妥不对之处还请指正!


2018-4-5 简单实现一下LinkedList的add方法

/**
 * @author Kevin 2018-4-5
 * 
 * 实现链表List
 *
 */
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>{
    transient Node<E> head;//首节点
    transient Node<E> last;//尾节点
    int size;//大小

    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;
        }
    }

    //随便写一个遍历方法
    public String foreach() {
        StringBuilder sb = new StringBuilder();
        if(size == 0)
            sb.append("[]");
        sb.append("[");
        for(Node x = head; x!=null ;x = x.prev) {
            if(x.prev == null)
                sb.append(x.item.toString());
            else
                sb.append(x.item.toString()).append(",");
        }
        sb.append("]");
        return sb.toString();

    }

    @Override
    public boolean add(E e) {
        if(head == null) {
            head = new Node<E>(last, e, null);
        } else {
            if(last == null) {
                last =  new Node<E>(null, e, head);
                head.prev = last;
            } else {
                final Node<E> p = last;
                final Node<E> newNode = new Node<E>(null, e, p);
                p.prev = newNode;
                last = newNode;
            }
        }
        size++;
        return true;
    }

    //.......其他方法略去
}

main方法测试一下

public class MyLinkedListDemo {

    public static void main(String[] args) {
        LinkedList<String> l = new LinkedList<>();
        l.add("hello");
        l.add("hehe");
        l.add("hehe22");
        l.add("hehe223");
        System.out.println(l.foreach());

    }

}

测试结果:[hello,hehe,hehe22,hehe223]

同样是实现:按照最常规的一个思路写出来的,没有打磨与jdk提供的实现简洁还是存在差距,需要继续学习jdk代码中的精巧与思路。

void linkLast(E e) {
    final Node<E> l = last;//把当前队尾元素赋给l
    //创建一个新节点newNode,它的前驱节点为l也就是为插入前的队尾节点
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;//新节点赋给last,作为队尾节点
    if (l == null)
        first = newNode;
    else //否则就把插入的节点赋给当前尾节点的后继节点。
        l.next = newNode;
    size++;//然后size做加加
    modCount++;
}
原文地址:https://www.cnblogs.com/Kevin-1992/p/12608401.html