JavaSE复习之LinkedList源码分析

上一篇分析了ArrayList,ArrayList内部是通过数组实现的,元素在内存中是连续存放的。而LinkedList不同,顾名思义,它的内部是通过链表实现的,准确来说,是双向链表。java里面的容器类,其实就是对各种数据结构进行了封装和实现,所以要分析这些类,其实主要就是分析一下它们各自增删改查的方法都是怎样实现的。

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

既然是链表,那么内部肯定存在一个节点类:

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

节点内部维护了两根指针,分别指向前驱和后继节点。

有了节点类,就可以开始分析链表了。

1. 成员变量

transient int size = 0; //元素个数

transient Node<E> first; //指向头节点

transient Node<E> last; //指向尾结点

三个成员变量都是瞬时的。

2. 构造器

public LinkedList() {
}

public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

相比于ArrayList,LinkedList的构造器十分简单。

3. 成员方法

成员方法主要就是看增删改查。

public boolean add(E e) {
    linkLast(e);
    return true;
}

void linkLast(E e) {
    final Node<E> l = last;
    //新建当前节点,前驱指针指向当前链表中的尾节点,后继指空
    final Node<E> newNode = new Node<>(l, e, null);
    //链表尾指针后移,指向当前节点
    last = newNode;
    if (l == null) //当前链表为空时,头指针指向当前节点
        first = newNode;
    else //当前链表不为空,尾节点后继指针指向当前节点
        l.next = newNode;
    size++; //元素个数+1
    modCount++; //修改次数+1
}
  • 插入
public void add(int index, E element) {
    //检查索引
    checkPositionIndex(index);

    if (index == size) //索引等于链表长度,直接加在尾部
        linkLast(element);
    else //否则,先找到索引对应位置的节点,再进行插入
        linkBefore(element, node(index));
}
//根据索引查找结点
Node<E> node(int index) {     
    Node<E> x;
    if (index < (size >> 1)) { 
        //如果索引小于链表长度的一半,就从头开始找
        x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
    } else {
        //否则,就从尾部开始找
        x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
    }
    return x;
}
//通过传入节点对象的值进行删除
public boolean remove(Object o) {
    if (o == null) { //节点对象的值为null
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else { //节点对象的值不为null
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

//通过索引删除
public E remove(int index) {
    //检查索引
    checkElementIndex(index);
    return unlink(node(index));
}

//移除元素
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;
        //该节点的前驱指针指空
        x.prev = null;
    }

    if (next == null) { //该节点为尾节点
        last = prev; //尾指针前移
    } else {
        //该节点的后继节点的前驱指针指向该节点的前驱节点
        next.prev = prev;
        //该节点的后继指针指空
        x.next = null;
    }

    //节点元素指空,方便垃圾回收
    x.item = null;
    size--; //链表长度-1
    modCount++; //修改次数+1
    return element; //返回被移除的元素
}
public E set(int index, E element) {
    //检查索引
    checkElementIndex(index);
    //根据索引查找结点
    Node<E> x = node(index);
    E oldVal = x.item;
    x.item = element;
    return oldVal;
}
//查询元素
public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

//查询索引
public int indexOf(Object o) {
    int index = 0;
    if (o == null) { //节点对象的值为null
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null)
                return index;
            index++;
        }
    } else { //节点对象的值不为null
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item))
                return index;
            index++;
        }
    }
    return -1;
}

对于增删改查,LinkedList提供了一系列的xxxFirst()xxxLast()方法,来对头和尾进行操作,实现原理也都大同小异,这里就不赘述了。

值得一提的是,LinkedList不仅实现了List接口,还实现了Deque接口

Deque是一个双端队列,实现了Queue接口,在日常的使用中,我们通常是将Deque当作栈或队列来使用,那么在LinkedList中,理所应当的实现了对应的方法。

当我们将LinkedList当作栈来使用时,可以使用以下方法:

  • public E pop() 出栈
  • public void push(E e) 进栈
  • public E peek() 查看栈顶元素

当我们将LinkedList当作队列来使用时,可以使用以下两组方法:

  • public boolean add(E e) / public boolean offer(E e)入队
  • public E remove() / public E poll() 出队
  • public E element() / public E peek() 查看队头元素

这两组方法的区别是:

throw Exception返回false或null
入队add(E e)boolean offer(E e)
出队E remove()E poll()
查看队头元素E element()E peek()

4. 特点

了解了LinkedList的内部结构和实现原理,我们能够发现,它有以下特点:

  1. 按需分配空间,不需要预先分配很多空间。
  2. 不可以随机访问,按照索引位置访问效率比较低,必须从头或尾顺着链接找,效率为O(N/2)。
  3. 不管列表是否已排序,只要是按照内容查找元素,效率都比较低,必须逐个比较,效率为O(N)。
  4. 在两端添加、删除元素的效率很高,为O(1)。
  5. 在中间插入、删除元素,要先定位,效率比较低,为O(N),但修改本身的效率很高,效率为O(1)。

可以看出来,LinkedList和ArrayList特点几乎完全相反。但是个人感觉,在日常开发中,如果只是使用List的话,基本上ArrayList就完全够用了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值