Java集合(2):LinkedList概述与源码解析

一、LinkedList的概述与特点

LinkedList与ArrayList一样都实现了List接口,但是在底层上却截然不同。也许在平时中它的使用频率不及ArrayList,但是相较于ArrayList,LinkedList也具有独有的特点,因此知道它的实现方式以及在特定场合中使用它是很有必要的。

LinkedList的特点如下:
(1)底层为一个双向链表,存储空间不连续,增删快,查找慢。
(2)属于List,有序存储,存储元素可重复,允许null元素存储。
(3)不支持同步,线程不安全。

二、继承体系

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

LinkedList继承AbstractSequentialList抽象父类,实现了List、Deque、Cloneable、Serializable接口。

其中:
(1)AbstractSequentialList抽象类:提供了顺序访问的数据结构(例如链表)的一些工作。上一篇文章提到,ArrayList实现了随机访问的接口,遍历时使用for循环更快。而LinkedList底层的链表属于顺序存储结构,使用迭代器Iterator更快,所以这个抽象类提供的方法都是使用迭代器操作的一些方法。
(2)List接口:定义了对List的一些操作规范。
(3)Deque接口:Deque是双端队列英文的缩写,支持在两端插入和移除元素。
(4)Cloneable接口:实现此接口可以调用clone()方法,LinkedList的克隆是浅克隆。
(5)Serializable接口:代表可序列化。
LinkedList继承体系

三、重要属性

(1)private static class Node<E>
其实严格的说Node并不是一个属性,而是内部类,但是由于它是LinkedList的底层结构,是一切的基础,所以把它放在了这个位置,看一下它的源码:

  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的底层数据结构是一个双向链表。
(2)transient Node<E> first;
指向第一个节点的指针。
(3)transient Node<E> last;
指向最后一个节点的指针。
(4) transient int size = 0;
链表的长度。

四、 构造方法

LinkedList有两个构造方法:LinkedList()LinkedList(Collection<? extends E> c)。代码:

    /**
     * 创建一个空列表
     */
    public LinkedList() {
    }
    /**
     * 创建一个指定集合中的元素的列表,
     * 这些元素按照它们在迭代器中的顺序返回 
     */
    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }

LinkedList():可以看到无参的构造方法里什么也没做,所以它只是创建了一个空的列表而已。
LinkedList(Collection<? extends E> c):创建包含一个指定集合中的元素的列表。

五、常用方法

LinkedList底层是个双向链表,它的源码也都是在操作这个双线链表,难度不大。
(1)增加方法
LinkedList提供的增加方法有:

public boolean add(E e)	//将指定元素添加到此列表的结尾
public void add(int index, E element)	//将指定的元素插入此列表中的指定位置
public void addFirst(E e)	//将指定的元素插入到列表开头
public void addLast(E e)	//将指定的元素插入到列表结尾
public boolean addAll(Collection<? extends E> c)	//将集合c中的元素添加到此列表的结尾
public boolean addAll(int index, Collection<? extends E> c)	//将集合c中的元素添加到此列表的指定位置

我们重点看一下add(int index, E element)的实现方式:

public void add(int index, E element) {
        checkPositionIndex(index);

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }

整体的思路是:首先检查index是否越界,然后进行判断,如果index等于链表现在的长度,那么就在链表的最后添加,否则在指定位置添加。

checkPositionIndex(index)的代码如下:

    private void checkPositionIndex(int index) {
    	//如果位置越界,则抛异常
        if (!isPositionIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

   	private boolean isPositionIndex(int index) {
   		//index合法的范围是[0,size]
        return index >= 0 && index <= size;
    }

在链表尾部添加元素调用的是linkLast(element)

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

先用节点l将之前的last节点存一下。然后创建一个前指针指向l,后指针为null的新节点,将last指针指向新节点。如果之前的尾部节点为空,说明链表是空的,那么让first节点等于新节点,否则让节点l的后指针指向新节点。最后让size+1,操作次数modCount+1。

如果不是在尾部添加,则需要先拿到index对应的节点,然后再进行添加。根据index找节点调用的是node(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;
        }
    }

这里用了一种对半查找的方法,先判断index是前一半还是后一半,然后再决定从fist开始查找还是从last开始查找。最后将查找到的节点返回。

然后调用的是linkBefore方法,作用是在指定节点之前插入元素:

   void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        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;
        size++;
        modCount++;
    }

比较简单,无非就是:(a)让指定节点的前指针指向要插入的新节点。(b)让新节点的后指针指向当前节点,新节点的前指针指向指定节点的前节点。©最后让指定节点的前节点的后指针指向新节点。

其他的add方法也是类似的过程,可以自行查看源码。

(2)删除方法
LinkedList提供的删除方法有:

public boolean remove(Object o)	//从列表中删除第一次出现的指定元素
public E remove(int index)	//删除列表中指定位置的元素
public E removeFirst()	//从列表中删除并返回第一个元素
public E removeLast()	//从列表中删除并返回最后一个元素
public boolean removeFirstOccurrence(Object o)	//从列表中移除第一次出现的指定元素(从头部到尾部遍历列表时)
public boolean removeLastOccurrence(Object o)	//从此列表中移除最后一次出现的指定元素(从头部到尾部遍历列表时)

我们重点看一下remove(int index)

    public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }

代码也比较简单,checkElementIndex(index)也是检查index是否越界,与前文的逻辑一样。node(index)前面也说过了,重点看一下unlink方法。

 	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;

        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--;
        modCount++;
        return element;
    }

主要逻辑是拿到指定节点的数据、前节点和后节点,让前节点的后指针指向后节点,后节点的前指针指向前节点,这个过程还需要判断一下指定节点是否为first/last节点。最后把指定节点的数据与前后指针都指向null(方便JVM回收)。让链表长度减一,操作数加一,并将数据返回。

(3)修改方法
修改方法比较简单,直接看代码:

    public E set(int index, E element) {
        checkElementIndex(index);
        Node<E> x = node(index);
        E oldVal = x.item;
        x.item = element;
        return oldVal;
    }

检查index是否越界,得到要修改的节点,将它的值改变,并把原先的值返回即可。
(4)查找方法
涉及到查找的方法有:

public E get(int index)	//返回此列表中指定位置的元素。
public E getFirst()	//返回此列表中的第一个元素。
public E getLast()	//返回此列表中的最后一个元素。

这部分代码也比较简单,不再赘述。
(5)Deque方法
前面提到LinkedList实现了Deque接口,提供了一些关于双端队列的方法:

public E peek()	//检索但不删除此列表的头(第一个元素)
public E poll()	//检索并删除此列表的头(第一个元素)
public E peekFirst() //检索但不删除此列表的第一个元素
public E peekLast() //检索但不删除此列表的最后一个元素
public E pollFirst() //检索并删除此列表的第一个元素
public E pollLast() //检索并删除此列表的最后一个元素
public void push(E e) //将元素压入此列表表示的堆栈
public E pop() //从此列表表示的堆栈中弹出一个元素
public boolean offer(E e) //将指定的元素添加为此列表的尾部(最后一个元素)
public boolean offerFirst(E e) //将指定的元素插入此列表的前面。
public boolean offerLast(E e) //将指定的元素插入此列表的末尾。

这些方法的实现大都调用了前文讲述的方法,代码自行阅读即可。

(6)其他方法

public void clear() //从此列表中删除所有元素。
public Object clone() //返回此列表的浅拷贝。(元素本身不会被克隆。)
public Object[] toArray() //以正确的顺序(从第一个元素到最后一个元素)返回包含此列表中所有元素的数组。

看一下toArray()的实现吧,毕竟此方法在平时使用的频率是比较高的:

   public Object[] toArray() {
        Object[] result = new Object[size];
        int i = 0;
        for (Node<E> x = first; x != null; x = x.next)
            result[i++] = x.item;
        return result;
    }

创建一个数组,并for循环遍历链表对数组进行赋值。

总得来说,LinkedList的源码都是在操作双向链表这个数据结构,阅读源码的难度并不大。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值