首页 > 安全资讯 >

日常小结-java-LinkedList源码分析

17-11-06

日常小结-java-LinkedList源码分析,LinkedList是一个基于双向链表的List和Deque的实现。对所有元素(包括null内)实现了所有可选的操作。

LinkedList

API文档

LinkedList是一个基于双向链表的List和Deque的实现。对所有元素(包括null内)实现了所有可选的操作。

所有的操作性能可以从双端队列的list来估计。基于下标的操作需要从list的头或者尾(根据具体位置)遍历到相应的位置才可以。

特别注意这个实现不是同步的。如果多线程访问同时访问一个list,且只是有一个线程结构性的调整了list,这样的操作需要外部特性保持同步。这通常通过在list上自然封装一些同步对象实现。如果没有这样的对象存在,则需要Collections.synchronizedList方法包装。这样操作尽量在创建的同时完成,以避免意外的异步访问list:

   List list = Collections.synchronizedList(new LinkedList(...));

通过这个方法的Iterator和listIterator方法返回的迭代器是fail-fast的。如果在迭代器创建后,不同此迭代器而list实现了结构性调整,这个迭代器会抛出一个ConcurrentModificationException异常,因此在面对同时修改的可能时,迭代器迅速失效,而不是增加风险的随机性、及未来不可预估的行为和时间。

需要说明的是,fail-fast行为的迭代器通常来说不能强制性的保证不发生异步的同时修改。fail-fast迭代器竟可能保证快速抛出异常。因此依赖fail-fast迭代器的抛出异常行为的正确性写出代码是不可靠的。迭代器的fail-fast行为应该只用于debug。


简介下Collection.synchronizedList这个静态方法。
首先这个Collection的一个静态方法,当然不仅仅是List其他的Collection也都有类似的方法。

API文档

返回一个指定list的线程安全版本的List。为了保证串行的访问,关键在于所有的对支持list的访问都需要通过返回list来实现。
需要在使用迭代器遍历它的时候手动的同步。官方的编程模板如下:

  List list = Collections.synchronizedList(new ArrayList());
      ...
  synchronized (list) {
      Iterator i = list.iterator(); // Must be in synchronized block
      while (i.hasNext())
          foo(i.next());
  }

不遵循这样的建议的操作可能会导致不可以预知的的行为。
如果指定的list被序列化的话,那返回的list会被序列化。

文档说明如上所示:
首先如果你使用了synchronizedList这样的类似的方法。那么这个集合是线程安全的。(其实线程安全这个说法并不好,因为所谓线程安全有很多种解释)但是这个线程安全指的是你直接使用.add之类这样的方法是线程安全的,但是在多个方法之间并不是线程安全的。所有如果你的临界区内包含了很多条语句还是需要添加管程来同步他们的。

但是有一点我还没做实验,如果不使用synchronizedList这样的包装,直接使用synchronized管程那单独的.add类似这样的方法还能不能保证线程安全?不过这个不是本文重点。等一会写到同步的部分在详细的说明。

另附一份ArrayList和Vector以及synchronizedList的关系的博客


构造函数

    public LinkedList() {
    }

    public LinkedList(Collection c) {
        this();
        addAll(c);
    }

之前因为基础没打扎实不太理解这里为什么要调用this()。因为它看起来什么都没做。
首先这里得优先说明一下java继承中构造器的机制,如果对这部分比较了解可以略过。

java中的构造器并不存在继承的关系。而只是或显示或隐式的调用基类的构造器,jvm在创建实例的时候会从父类到子类一级一级的调用构造器,如果没有显示的指定构造器则默认自动调用基类同类型的构造器。

比如说基类和子类有一个无参的构造器,那在实例化子类的时候回自动调用一个基类的无参构造器。同理如果基类和子类都有一个有参的构造器,那在实例化子类的时候回自动调用一个基类的有参的构造器。

当然你可在子类的构造器中显式的指定使用父类的何种构造器,比如说如果你在无参的子类构造器中调用了基类的带参的构造器。那就会调用显示指定的基类带参的构造器。

当然无论是显示还是隐式的调用基类的构造器都必须将基类的构造器放在第一条语句上,也就是说必须已经构造了基类才能构造子类。如果没有显示的调用基类构造器,而又无法显示的找到隐式调用的同类型构造器,将无法通过编译。
附:子类调用基类构造器的说明

有了这些基本的知识那这里就比较好理解了,记得之前在说到AbstractSequentialList的时候说过AbstractSequentialList只有一个无参的构造器,但是在LinkedList中有两个构造器一个是无参的一个是有参的,无参的构造器当然会自动的隐式调用super(),但是在有参的构造器里面无法隐式的调用基类的有参构造器,因为基类根本没有有参的构造器。

所以这里必须要显示的调用。有两种选择可以实现显示调用super()和this()。当然我觉得这两种是差不多的。但是有些人认为this()更好,因为这样可以保证,以后更改无参的构造器的时候不用重写带参的构造器。
附:关于继承中构造器的讨论

Node

首先需要说明的是Node节点类。这个类是构建LinkedList的基石。

    private static class Node {
        E item;
        Node next;
        Node prev;

        Node(Node prev, E element, Node next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

节点的构建和普通的双向链表的节点的构建并没有什么区别,private和构造器的写法也没有额外需要说明的。唯独有些区别的地方是static修饰符,这也是我见到的第一个静态内部类。
这里简单说明下静态类。

静态类的说明:静态类只能是内部类,外部的静态类无法通过编译。
静态类的特性:

普通的内部类不可以声明静态的方法和变量,而静态类可以 普通的内部类可以访问外部类的全部方法和变量,但是静态内部类只能引用外部类的静态方法和静态变量 普通的内部类在声明的时候必须依赖于一个已存在的外部类,静态内部类不需要依存已存在的外部类

静态类的用途:

通常用于需要内部类访问外部类的信息,但是不需要外部类访问内部类的信息的时候。

不适用jUnit的时候,可以将main方法写入一个静态内部类,然后直接调用。用于测试。

这里贴一个静态类的详细说明 详细的静态类说明

与Node类配合的方法是node方法。没用特别需要说的,node里判断了是从头迭代还是从尾迭代通过if (index < (size >> 1))判断当前传入的index更接近头还是更接近尾。

关于节点的操作

LinkedFirst和LinkedLast、unlinkedFirst和unlinkedLast
LinkedList有3个变量:
一个是int类的size表示当前size的大小
一个是Node类型的first表示当前列表中的第一个节点
一个是Node类型的last表示当前列表中的最后一个节点

而且这三者都没有使用private类型。而是默认的类型,可能是为了包内的类访问,但是这里似乎似违反普遍的原则的。不知道有没有额外的需要考虑的问题。

这里还有提醒一下,默认的访问类型和protected还是有区别的。
protected是同一个包的类可以访问,其他的包的该类的子类也可以访问。
默认的是同一个包的类可以访问

对于first和last有一定的约束条件

(first == null && last == null) ||(first.prev == null && first.item != null)
(first == null && last == null) ||(last.next == null && last.item != null)

很容易看懂,无节点的时候first和last为null,有节点的时候first和last分别指向首尾节点。
对于first和last直接操作的方法有四个:
linkFirst、linkLast和unlinkFirst、unlinkLast。

这里有个比较疑惑的地方。除了linkLast为默认访问权限,其余全部为private,具体原因不明。

这里只看其中一个,其他类似:

    private void linkFirst(E e) {
        final Node f = first;
        final Node newNode = new Node<>(null, e, f);
        first = newNode;
        if (f == null)
            last = newNode;
        else
            f.prev = newNode;
        size++;
        modCount++;
    }

代码很简单意思就是替换 first节点。
先创建一个引用指向first,然后创建一个新节点。插入到first之前,并让first指向新建节点。
然后判断原先的first类型。如果节点为空则说明list为空,则说明当前插入的节点是list的第一个节点将last也指向现在创立的节点。如果节点不为空则说明则将该节点的前向指针指向新建节点。

    private E unlinkFirst(Node f) {
        // assert f == first && f != null;
        final E element = f.item;
        final Node next = f.next;
        f.item = null;
        f.next = null; // help GC
        first = next;
        if (next == null)
            last = null;
        else
            next.prev = null;
        size--;
        modCount++;
        return element;
    }

首先他创建了一个不可更改的final的E类型的引用和node节点引用,并分别指向first的内容和first的后向指针,并将他们全部指向空,注释说明的是帮助垃圾回收机制,应该是为了保证没有引用指向first的内容这样使得gc更快的回收资源,但是我对jvm的垃圾回收机制还不了解,具体细节需要以后补充。
然后根据next的值来判断当前list是否还有元素来进行相对于的指针调整。

另外这里对不可变的变量尽可能的使用了final修饰词。更多关于final的讨论

对于这部分还有比较疑惑的地方:

为何需要使用传入参数而不是直接使用first?既然已经使用了传入参数为何还要专门写一个first?因为这样其实和连接一个普通的节点没有特别大的区别,要说有区别的话可能在于首尾是null以及可以判断list是否为空,但是这些即使放在同一个方法内也是可以的。使用这么多小的程序段难道不会使程序太过臃肿?

但是没有特别需要说明的。是针对普遍的节点使用的link和unlink。唯一的问题在于之前讨论的为何需要这么多的方法。

getFirst和getLast、removeFirst和removeLast、addFirst和addLast、add和remove

这几个类可以看做是对上面的节点操作的类的包装。因为不是所有的list都是链表实现的,这些类作为list普遍实现的方法名。包装后的类隐藏了first和last的实现,而仅仅返回或传入first和last的item即内容。
addFirst和addLast、removeFirst和removeLast、LastgetFirst和getLast。

    public E getFirst() {
        final Node f = first;
        if (f == null)
            throw new NoSuchElementException();
        return f.item;
    }

还有疑惑的问题是:
为什么这里不直接返回first,而是先用一个final的node引用指向first,再返回这个节点的内容?

除此之外还有两个不针对first和last而是针对任意节点的linkBefore和unlink两个方法。同样的默认的访问权限但是假设了传入节点非空。

另外需要说明的是对于remove方法需要对传入的参数判断是否是null进行分别判断。

addAll

addAll有两个方法。主要的是public boolean addAll(int index, Collectionc)另一个addAll是对这个方法的包装。

 public boolean addAll(int index, Collection c) {
        checkPositionIndex(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        if (numNew == 0)
            return false;

        Node pred, succ;
        if (index == size) {
            succ = null;
            pred = last;
        } else {
            succ = node(index);
            pred = succ.prev;
        }

        for (Object o : a) {
            @SuppressWarnings("unchecked") E e = (E) o;
            Node newNode = new Node<>(pred, e, null);
            if (pred == null)
                first = newNode;
            else
                pred.next = newNode;
            pred = newNode;
        }

        if (succ == null) {
            last = pred;
        } else {
            pred.next = succ;
            succ.prev = pred;
        }

        size += numNew;
        modCount++;
        return true;
    }

思路:

先检查index是否符合范围。 然后传入的集合c转换成数组a。 如果数组为空的时候则返回false 定义两个Node类型的引用,一个是pred用来存放集合的前一个节点。succ指当前需要被向后推的元素。 如index和size相等则说明在队尾插入,则将pred设为last即队列的最后一个元素,且当前元素succ为空。如果不是队尾则使用node方法得到当前的需要的节点并将其付给succ。将pred设为succ之前的一个节点 接下来需要对数组a中的元素进行迭代。但是这里需要对一种请求进行判断。就是当前list为空的情况,如果prev为空则说明当前list为空,则将first设为当前数组a中的最后一个。 如果succ为null即当前插入的地方是当前list的尾,则将last指向最后pred。如果是从中间插入则将pred和succ的连接建立起来。

index的判断

关于index的判断一共有4个函数,当然全部是private的内部调用函数,以及一个辅助的方法

private boolean isElementIndex(int index)
private boolean isPositionIndex(int index)
private void checkElementIndex(int index)
private void checkPositionIndex(int index)
private String outOfBoundsMsg(int index)

首先说下前两个方法的直接的区别,也就是Element和position直接的区别。
Element指的是list中的元素是[0,size-1],position指的是迭代器可以存在的位置[0,size]。
但是函数只返回是否是布尔值。比较方便的用于判断if语句等。
这里还对这两个函数分别包装了一下,并且抛出了一个带有index数字的异常,关于异常的抛出调用了第五个辅助函数用以显示index的值。

关于Deque的实现

Deque都是建立在节点上的基础上的额外封装。没有太多详细分析的部分。

迭代器类

这里的迭代器有两类,双向迭代器ListItr和反向迭代器DescendingIterator,分别使用listiterator和descendingiterator来生成。

先说明ListItr类
包含四个私有变量的引用:
lastReturned:用来保存刚刚跳过的节点
next:指向下一个应该跳过的节点,被初始化为当前指向的节点,如果index为size则有可能为null。
nextIndex:指向下一个应该节点的标号,被初始化为传入的index。
expectedModCount:该节点所记录的结构性变化的次数,初始化时被赋予LinkedList的modCount。

这里有些搞清楚这些私有域的问题后,接下来的问题基本上只是修改这些域值来实现方法。和普通的迭代器没有太大的区别这里不详细介绍。

接下来介绍下DescendingIterator类

DescendingIterator类更为简单。只是设定了一个private和final的ListItr类。并且把hasNext和next方法和ListItr中的hasPrevious和Previous的方法。

另外还有有意思的地方是DescendingIterator只实现了Iterator而ListItr而只是实现了ListIterator的方法。这其实很好理解ListItr本身包括了previous方法,而通常DescendingIterator只是为了方便编程人员调用而已因此只实现少量方法足以。

clone的实现

方法和普通的clone方法类似,区别是这里讲方法分成了 两步,
一个是superclone负责完成基类的复制并将其转换成(LinkedList)
一个是调用了superclone的clone,完成LinkedList的初始化,并将元素依次添加进去。

    private LinkedList superClone() {
        try {
            return (LinkedList) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new InternalError(e);
        }
    }
    public Object clone() {
        LinkedList clone = superClone();

        // Put clone into "virgin" state
        clone.first = clone.last = null;
        clone.size = 0;
        clone.modCount = 0;

        // Initialize clone with our elements
        for (Node x = first; x != null; x = x.next)
            clone.add(x.item);

        return clone;
    }

这样做的好处是个人觉得是将任务分离出来,将可能出现问题的基类的初始化单独做成一个类。并可以更改异常的类型,让异常更好的被分析。
而在复制值的部分更不容易出错,如果有子类继承了这些方法只需要更改各自负责的不就好。

比较奇怪的是为什么需要将CloneNotSupportedException转换成InternalError。后者按照API的解释表示的是虚拟机的内部错误,而且是属于Error类的异常。可能是由于LinkedList本身是jre自带的类,如果没办法完成构造只有可能是虚拟机内部的问题,这样的意思。

其他内容

toArray实现
这里的toArray和一篇分析中的内容是一样的。具体省略。 序列化
序列化的实现也没有特别需要说明的先写default,然后是size,然后时各位节点。读取的时候按照相同顺序。 Spliterator()和LLSpliterator类
是1.8新增特性暂不考虑

这里还要一个问题先前没有说明:
就是节点中的内容是null和节点是null是不同的概念。
首先对于一个队列除了first的previous和last的next不可能有null类型的节点,除非list是空的。
但是每个节点都是有可能为空的。这点在很多编程的时候需要额外的注意一下。

相关文章
最新文章
热点推荐