LinkedList源码与实现原理

LinkedList简介

在我们平常用LinkedList时,我们肯定都知道LinkedList的一些特点,比如
a、LinkedList是通过链表实现的。
b、如果在频繁的插入,或者删除数据时,就用linkedList性能会更好。
c、linkedList是一个非线程安全的(异步),其中在操作Interator时,如果改变列表结构(add\delete等),会发生fail-fast(快速失败行为)。
至于LinkedList内部是怎样的一种结构,为什么具有这些特点,请看LinkedList的源码实现。

LinkedList源码

1、LinkedList的属性
(1)LinkedList内部定义了一个size属性,用来标记LinkedList存储的元素的数量

//链表元素(节点)的个数
transient int size = 0;

(2)LinkedList内部定义了一个内部类Node,用来存储元素
LinkedList的所有元素都封装在Node内部类的对象(可以称之为节点)的item属性中,每个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;
        }
    }

(3)定义两个属性,储存指向链表第一个节点和最后一个节点的指针

//指向链表第一个节点的指针
transient Node<E> first;
//指向链表最后一个节点的指针
transient Node<E> last;

LinkedList的size、first、last等属性都使用了transient关键字来修饰,表示均不参与序列化。因为LinkedList自定义了writeObject和readObject方法用来序列化和反序列化,其主要特点是并不将组成链表的每个Node对象整体序列化,而且只序列化Node中的封装的本节点的元素。

private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
        // Write out any hidden serialization magic
        s.defaultWriteObject();

        // Write out size
        s.writeInt(size);

        // Write out all elements in the proper order.
        //只将每个节点所实际存储的元素写入输出流,其他属性不写入
        for (Node<E> x = first; x != null; x = x.next)
            s.writeObject(x.item);
    }

2、LinkedList的构造方法
LinkedList提供了两个构造方法,分别是无参构造和有参构造,其中的无法构造不执行任务代码,有参构造会先调用无参构造之后,在将传入的Collection集合失业addAll方法加入链表。备注:在Java语言中,this(参数)方法的作用为调用本类中一种形式的构造函数(必须为构造函数中的第一条语句)。
LinkedList没有初始化链表大小的构造函数,因为链表不像数组。数组创建时必须先定义大小,再去分配内存空间。链表不需要确定大小,只需要通过指针的移动来指向下一个内存地址的分配。

    public LinkedList() {
    }
    public LinkedList(Collection<? extends E> c) {
        //调用自身的无参构造,(在构造方法内调用其他构造方法,一定要写在方法体的第一句)
        this();
        //将传入的Collection集合加入链表
        addAll(c);
    }

3、添加元素
(1)在头部添加元素
在头部添加元素时,会调用其内部定义的linkFirst()方法进行处理。
在linkFirst方法中的处理逻辑为:
①创建一个新的节点,将需要添加的元素为新节点的元素,将新节点中指向其后一个节点的属性指向原链表的第一个节点,新节点指向其前一个节点的属性为null,因为从头部插入的必定是第一个节点,所以新增节点没有前一个节点。
②将新节点的引用赋给链表的标识第一个节点的属性first,因为是从头部插入,所以新增的节点即是LinkedList的first属性指向的对象。
③判断原先的链表是否为空,如果为空的话LinkedList指向最后一个节点的属性last也需要指向新建的节点对象。否则,链表中原先的第一个节点中指向前一个节点的指针就需要指向新增的节点,即把原第一个节点作为新增节点的后一个节点,链表的第二个节点。
④将链表大小size和版本号modCount自增。
这里我们可以看到,如果LinkedList链表为空,则其指向链表中指向第一个和最后一个节点的属性均为null;如果链表中只有一个元素,则指向第一个和最后一个节点的属性均指向链表中的唯一节点。

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

    private void linkFirst(E e) {
        //转储链表原第一个节点
        final Node<E> f = first;
        //创建一个新的节点,新节点的前一个节点为null,后一个节点为链表原第一个节点
        final Node<E> newNode = new Node<>(null, e, f);
        //新节点作为链表的第一个节点
        first = newNode;
        //如果链表原第一个节点为null,说明此链表原为空链表
        if (f == null)
        //如果链表原为空链表,那么现在增加的一个节点为链表的唯一节点,需要将此节点也设为链表的最后一个节点
            last = newNode;
        else
        //如果原链表不为空,需要将原头节点中的指向前一个节点的指针指向新增的节点,原第一个节点作为链表的第二个节点
            f.prev = newNode;
        //链表元素个数自增
        size++;
        //链表版本号自增
        modCount++;
    }

(2)在尾部添加元素
和在链表头部增加元素类似,在此不做过多阐述。只是创建的新节点中指向其后一个节点的指针需要设为null,指向前一个节点的指针需要指向链表原先的尾节点。

    public void addLast(E e) {
        linkLast(e);
    }
    
    void linkLast(E e) {
        //转储链表原有的尾节点
        final Node<E> l = last;
        //创建新的节点,其指向前一个节点的属性指向链表的原有的尾节点,指向后一个节点的属性为null,
        //因为是在尾部插入所以没有后一个节点
        final Node<E> newNode = new Node<>(l, e, null);
        //将链表的尾节点设为创建的新节点
        last = newNode;
        //如果链表原先的尾节点为空,说明链表为空链表
        if (l == null)
        //如果链表在插入前为空链表,则此插入的节点为链表的唯一节点,链表的头结点也为此插入的节点
            first = newNode;
        else
        //如果链表不为空,将原来的尾节点的下一个节点的指针指向新增的节点
            l.next = newNode;
        size++;
        modCount++;
    }

(2)在指定位置增加元素
先判断传入的索引位置位置是不是链表的末尾,如果是的话将在链表的末尾插入一个节点;否则将先使用node(index)方法查询链表在索引位置的节点,然后使用linkBefore(element, node(index))方法插入新节点。

    public void add(int index, E element) {
        //检查位置的合法性
        checkPositionIndex(index);
        //如果传入的位置等于链表的元素个数,则是在链表的最后添加元素,因为链表中节点的序号是从0开始的,
        //调用上面讲到的linkLast方法在尾部插入新节点接口即可
        if (index == size)
            linkLast(element);
        else
        //先使用node(index)方法查找到索引位置的节点,再将新节点插入到索引位置的节点的后面
            linkBefore(element, node(index));
    }    

其中的node(int index)方法是依据索引位置查找链表的方法,其执行的逻辑为:
先通过移位运算得到当前链表大小的一半,然后判断当前索引在链表的前半段还是后半段。如果当前索引在链表的前半段,则从头开始遍历链表,反之尾开始遍历链表,这是LinkedList对查找性能做出的优化。
由于LinkedList链表的节点本身不带有位置信息,但是却是顺序的。因此能且只能通过第一个节点中存储的后一个节点的指针来找第二个节点,再通过第二个节点中存储的后一个节点的指针来找第三个节点……通过控制查找的次数来确定获取指定位置的节点。查找的过程就像列队报数一样,报到指定的数字为止。比如传入的位置参数是N,则是要找链表的第N+1个节点的元素,此时从第一个节点开始的话,不断的找下一个节点需要连续找N次。因此使用循环来查找的话,把循环的次数控制为N次可以找到该位置的节点。这也就是LinkedList中进行元素查找效率比较低的原因,LinkedList的节点不像数组额元素那样具有下标索引,只能从头都尾或者从尾到头进行逐一查找,直到找到为止。

    Node<E> node(int index) {
        // assert isElementIndex(index);
        //“>>”为右移位运算,右移一位的效果就是将数值除以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;
        }
    }

再看linkBefore(E e, Node succ)方法,再找到索引位置的节点后调用:

void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        //获取索引位置节点的前一个节点的引用
        final Node<E> pred = succ.prev;
        //创建新节点,其指向掐前一个节点的引用设为pred,指向后一个节点的引用设为succ,
        final Node<E> newNode = new Node<>(pred, e, succ);
        //索引位置的前一个节点设为新节点
        succ.prev = newNode;
        //如果索引位置节点的前一个节点为null,说明索引位置的节点为链表的头结点
        if (pred == null)
        //如果在链表头部增加节点,链表的first属性需要设为新增的节点
            first = newNode;
        else
        //如果不是在链表头部增加节点,则原索引位置的前一个节点中指向后一个节点的指针改为指向新增节点。
        //这样的效果就是在索引位置节点和索引位置前一个节点之间插入了一个新的节点。
            pred.next = newNode;
        size++;
        modCount++;
    }

4、移除元素
(1)移除指定的元素
移除指定的元素分为两步,首先找到元素对应的节点,再删除该节点。
①从链表第一个节点开始,遍历链表的所有节点,直到找到包含该元素的节点为止。
因为此方法在第一次找到指定元素变回使用unlink(Node x)方法进行节点删除,因此此方法只能删除链表从头到尾第一次出现的元素,而不能将所有相同的元素全部删除。

    public boolean remove(Object o) {
        if (o == 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;
                }
            }
        }
        return false;
    }

unlink(Node x)方法的执行逻辑为:
①如果删除节点是链表的头结点,则只需要把链表指向头结点的指针first 改为指向删除节点的下一个节点。
②如果删除节点不是链表的头结点,则需要让删除节点的前一个节点与“分手”,再与删除节点的后一个节点“牵手”。具体做法是将删除节点的上一个节点中指向后一个节点的指针由指向删除节点改为指向删除节点的后一个节点,删除节点中存储其上一个节点的属性设为null。
③如果删除节点是链表的尾节点,则只需要把链表指向尾结点的指针last改为指向删除节点的前一个节点。
④如果删除节点不是链表的尾节点,则需要让删除节点的后一个节点与删除节点“分手”,再与删除节点的前一个节点“牵手”。具体做法是将删除节点的后一个节点中指向上一个节点的指针由指向删除节点改为指向删除节点的前一个节点,删除节点中存储其后一个节点的属性设为null。
⑤为方便JVM垃圾回收,需要将删除节点存储元素的属性置为null。

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

(2)移除指定位置的元素
在链表中移除指定位置的元素分为三步:检查传入位置的合法性,通过索引获取对应位置的节点,使用unlink()方法进行节点删除。

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

这里可以总结一下,为什么LinkedList链表插入和删除的效率比较高,因为LinkedList是通过指针链接了每个元素的节点,在进行插入或删除操作时只需要前后节点改变指针的指向即可。

LinkedList的遍历

1、普通for循环并不适用
初学Java语言时,我们习惯以普通for循环来遍历LinkedList链表,但是实际上这是一种非常浪费性能的遍历方式。

    List<String> list = new LinkedList<String>();
		list.add("A");
		list.add("B");
		list.add("C");
		list.add("D");
		
    for(int i = 0;i<list.size() ; i++) {
	    System.out.println(list.get(i));
    }

这种遍历方式效率非常低,因为LinkedList的get(int index)方法调用node(index)方法来查找节点,因为在前文已经说过,node(index)通过第一个节点中存储的后一个节点的指针来找第二个节点,再通过第二个节点中存储的后一个节点的指针来找第三个节点……通过控制查找的次数来确定获取指定位置的节点。
那么通过以上的普通for循环的话,查找过程是这样的:
第一次查找A,只需要返回链表的头结点的元素,不需要遍历。
第二次查找B,需要从链表的头结点(A元素的节点)开始,然后找到B元素的节点,总共循环1次。
第三次查找C,同样需要从从链表的头结点(A元素的节点)开始,然后找到B元素的节点,再找到C元素的节点,总共循环2次。
第四次查找D,同样需要从从链表的头结点(A元素的节点)开始,然后找到B元素的节点,再找到C元素的节点,最后找到D元素的节点,总共循环3次。
虽然linkedList对查找做了优化,会依据索引位置判断节点在链表的前半段还是后半段,然后选择从头到尾开始查找或者从尾到头开始查找。但是这种遍历方式仍然需要每次都从头或尾开始逐一查找到需要的节点。链表越大,花费的时间越多。

    //LinkedList的get(int index)方法
    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }

2、强大的ListIterator
Iterator是一种对Collection集合进行遍历功能的一个接口,其特点是只能实现从头到尾的单向遍历。而ListIterator是一个更加强大的Iterator子类型,虽然它只能用于各种List类型的访问,但是却可以实现双向遍历,也可以产生相对于迭代器在列表中指向的当前位置的前一个和后一个元素的索引,并且可以使用set()方法替换它访问过的最后一个元素。

Iterator接口的源码为:

public interface Iterator<E> {
    //检查序列中是否还有元素
    boolean hasNext();
    //获取序列的下一个元素
    E next();
    //将迭代器最近返回的元素删除,此次虽然接口中的remove()的方法体中会直接抛出UnsupportedOperationException异常,
    //但是实际上实现Iterator的子类一般会重写remove()方法,用来删除列表中的元素。
    default void remove() {
        throw new UnsupportedOperationException("remove");
    }
    default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }
}

ListIterator的源码中增加了一些实用的方法,
previous()方法:用于获取序列的上一个元素;
hasPrevious()方法:用来检查序列是否有上一个元素;
set(E e)方法:替换它访问过的最后一个元素

public interface ListIterator<E> extends Iterator<E> {
    boolean hasNext();
    E next();
    boolean hasPrevious();
    E previous();
    int nextIndex();
    int previousIndex();
    void remove();
    void set(E e);
    void add(E e);
}

在LinkedList的内部,定义了一个ListItr 内部类,继承了ListIterator接口并实现了里面的全部方法。代码很简单,不做解释了。
这里体现了LinkedList也有快速失败机制。迭代了内部有个expectedModCount属性,是迭代器自身记录的链表的modCount值,链表的modCount值表示链表被修改的次数,可以理解为链表的版本号,每次对链表进行插入或删除元素的以后,其modCount值均会自增。expectedModCount的初始值为迭代器实例创建时LinkedList的modCount值。迭代器在每次迭代之前会执行checkForComodification()方法来检查迭代器自身记录的版本号和链表当前实时的版本号(即modCount值)是否相同,以此来判断链表是否被修改,如果链表在迭代过程中被修改,则抛出ConcurrentModificationException异常,即触发快速失败机制。

如何避免LinkedList的快速失败机制了?
在单线程下,可以使用迭代器自带的remove()方法对链表的元素进行删除,因为迭代器自带的remove()方法里在对链表进行修改后,会将自身用来记录链表版本号的expectedModCount属性进行一次自增,以此来保持和链表版本号的一致,避免快速失败机制发生。
迭代器自带的remove()方法。在多线程下,需要使用java.util.concurrent包中的相关类如ConcurrentLinkedQueue来代替LinkedList链表,才能避免快速失败机制。

    private class ListItr implements ListIterator<E> {
        //迭代器上次访问的解答
        private Node<E> lastReturned;
        //迭代器下次访问的节点
        private Node<E> next;
        //迭代的下一个访问的解答的位置索引
        private int nextIndex;
        //迭代器自身所记录的LinkedList的版本号
        private int expectedModCount = modCount;

        //唯一的构造方法,从指定的索引位置开始迭代链表
        ListItr(int index) {
            // assert isPositionIndex(index);
            //如果传入的位置等于链表长度,则没有下一个节点可以访问
            next = (index == size) ? null : node(index);
            nextIndex = index;
        }

        public boolean hasNext() {
            return nextIndex < size;
        }

        public E next() {
            //检查链表版本号是否发生改变,即链表是否被修改过,决定是否触发快速失败机制
            checkForComodification();
            if (!hasNext())
                throw new NoSuchElementException();

            lastReturned = next;
            next = next.next;
            nextIndex++;
            return lastReturned.item;
        }

        public boolean hasPrevious() {
            return nextIndex > 0;
        }

        //返回
        public E previous() {
            checkForComodification();
            if (!hasPrevious())
                throw new NoSuchElementException();
            lastReturned = next = (next == null) ? last : next.prev;
            nextIndex--;
            return lastReturned.item;
        }

        public int nextIndex() {
            return nextIndex;
        }

        public int previousIndex() {
            return nextIndex - 1;
        }

        public void remove() {
            checkForComodification();
            if (lastReturned == null)
                throw new IllegalStateException();

            Node<E> lastNext = lastReturned.next;
            unlink(lastReturned);
            if (next == lastReturned)
                next = lastNext;
            else
                nextIndex--;
            lastReturned = null;
            //更新迭代器自身所记录的链表版本号
            expectedModCount++;
        }

        public void set(E e) {
            if (lastReturned == null)
                throw new IllegalStateException();
            checkForComodification();
            lastReturned.item = e;
        }

        //在链表下一个访问的元素之前,增加一个元素
        public void add(E e) {
            checkForComodification();
            lastReturned = null;
            if (next == null)
                linkLast(e);
            else
                linkBefore(e, next);
            nextIndex++;
            expectedModCount++;
        }

        public void forEachRemaining(Consumer<? super E> action) {
            Objects.requireNonNull(action);
            while (modCount == expectedModCount && nextIndex < size) {
                action.accept(next.item);
                lastReturned = next;
                next = next.next;
                nextIndex++;
            }
            checkForComodification();
        }

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值