Java-集合类源码List篇(二)

前言   

      上篇中,我们分析了ArrayList的常用方法及其实现机制。ArrayList是基于内存空间连续的数组来实现的,List中其实还提供了一种基于链表结构的LinkedList来实现集合。同时多线程的操作,还提供了线程安全的Vector实现,以及栈实现的Stack。

3.LinkedList

 看下LinkedList的继承、实现关系:

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

可以看到它与ArrayList是有区别的,继承的不再是AbstractList而是AbstractSequentialList,同时它还实现了Deque接口。

Deque是“double ended queue“的缩写,意为双端队列,它定义了一些FIFO(先进先出)的队列实现方法以及LIFO(后进先出)栈的实现方法。而LinkedList实现了该接口,所以自然而然LinkedList也可以作为队列和栈的实现来使用。

3.1 LinkedList的构造函数

 /**
     * 空的实现
     */
    public LinkedList() {
    }

    /**
     * 以指定的集合构造LinkedList
     */
    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }

LinkedList与ArrayList不同,它不涉及初始化集合大小的操作,所以一般使用都是直接空的实现就可以了。因为没有大小限制,所以LinkedList没有扩容一说,当新添加一个元素直接改变尾结点的指向且将当前结点指定为尾结点即可。

3.2 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.
     * 恒等式 当List为空时 first和last为null,当List不为空时,last结点的next指向null且last结点item不为null
     * Invariant: (first == null && last == null) ||
     *            (last.next == null && last.item != null)
     */
    transient Node<E> last;  /**
  * 结点内部类
  * 
  */
  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;
        }
    }

可以看到,LinkedList定义了三个成员变量,当前链表的size大小,以及双向链表的头结点和尾结点。还有一个是AbstractList的modCount用来计数集合的变化次数的变量。其内部还定义了私有的静态内部类Node,它主要的作用就是描述链表结点。包含一个前驱结点引用,后继结点引用以及自身的item值,这个学过数据结构的都应该清楚,不再赘述。

为什么ArrayList实现的是AbstractList而LinkedList则是要实现AbstractList的子类AbstractSequentialList呢?

我们可以先看下AbstractSequentialList实现:

protected AbstractSequentialList() {
    }
    public E get(int index) {
        try {
            return listIterator(index).next();
        } catch (NoSuchElementException exc) {
            throw new IndexOutOfBoundsException("Index: "+index);
        }
    }
    public E set(int index, E element) {
        try {
            ListIterator<E> e = listIterator(index);
            E oldVal = e.next();
            e.set(element);
            return oldVal;
        } catch (NoSuchElementException exc) {
            throw new IndexOutOfBoundsException("Index: "+index);
        }
    }
    public void add(int index, E element) {
        try {
            listIterator(index).add(element);
        } catch (NoSuchElementException exc) {
            throw new IndexOutOfBoundsException("Index: "+index);
        }
    }

    public E remove(int index) {
        try {
            ListIterator<E> e = listIterator(index);
            E outCast = e.next();
            e.remove();
            return outCast;
        } catch (NoSuchElementException exc) {
            throw new IndexOutOfBoundsException("Index: "+index);
        }
    }

    public boolean addAll(int index, Collection<? extends E> c) {
        try {
            boolean modified = false;
            ListIterator<E> e1 = listIterator(index);
            Iterator<? extends E> e2 = c.iterator();
            while (e2.hasNext()) {
                e1.add(e2.next());
                modified = true;
            }
            return modified;
        } catch (NoSuchElementException exc) {
            throw new IndexOutOfBoundsException("Index: "+index);
        }
    }
    public Iterator<E> iterator() {
        return listIterator();
    }

    public abstract ListIterator<E> listIterator(int index);

如果对上篇中的AbstractList的源码有印象的话,不难发现AbstractSequentialList主要是继续重写了AbstractList的中部分方法。譬如:get(int index)、set(int index,E element)、remove(int index)等方法,它其实是对于基于顺序访问结构的集合再次提供一个骨干实现。如果你需要自己实现一个基于顺序遍历元素的链表实现,可以只需要实现listIterator()方法就可以了,但实际上LinkedList中对于上述方法也是覆写了自己的实现。接下来分析下LinkedList中具体实现。

3.3 LinkedList元素的访问

 get(int index)

  /**
     * 获取指定索引的元素
     */
    public E get(int index) {
        //校验索引是否非法
        checkElementIndex(index);
        return node(index).item;
    }

    /**
     * Node结点 内部类方法
     */
    Node<E> node(int index) {
        //index与集合一半进行比较,索引位于前半部分则用first遍历到目标元素
        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;
        }
    }

通过源码发现,当通过get(index)方法来获取某个序列值时,先将index与size的一半进行比较,位于前半部分则用first结点,后半部分则用last结点。然后通过迭代遍历next或者previous结点来寻找目标结点,也正是基于此,所以链表结构的访问目标元素过程耗时要比数组访问的费时。

 

add(E e)

  /**
     * 添加一个指定的元素到链表的尾部
     */
    public boolean add(E e) {
        linkLast(e);
        return true;
    }
    void linkLast(E e) {
        final Node<E> l = last;
        /** 新创建的结点的next为null 
         * 前驱结点引用指向原last,所以印证该链表为双向链表而非双向循环链表
         */
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }

看到它的添加元素方法,就是直接心生成一个结点并且将该结点的前驱结点引用指向原last结点,next为null。故而由此可以印证我们之前的论述:LinkedList内部是基于双向链表来实现的但非双向循环链表

 

remove(Object o)

   /**
     * 将元素移除出链表
     */
    public boolean remove(Object o) {
        //object为null,则直接从first便利第一个为null的元素予以剔除
        if (o == null) {
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null) {
                    unlink(x);
                    return true;
                }
            }
        } else {
            //不为null则直接根据equals找出第一个相等的元素予以剔除
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item)) {
                    unlink(x);
                    return true;
                }
            }
        }
        return false;
    }

删除元素时,首先从first结点开始,根据equals依次遍历到第一个与指定Object对象相等的元素进入删除步骤。看下如何删除后做了什么操作:

 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;
        //prev为null,说明当前结点为头结点
        if (prev == null) {
            first = next;
        } else {
            //改变前驱结点的next指向,同时目标结点前驱置空
            prev.next = next;
            x.prev = null;
        }
        //next,说明当前结点为尾结点
        if (next == null) {
            last = prev;
        } else {
            //改变后继结点的prev指向,同时目标结点后继置空
            next.prev = prev;
            x.next = null;
        }
        //改变size、modCount计数
        x.item = null;
        size--;
        modCount++;
        return element;
    }

在链表结构中,删除某一个元素结点后,需要改变前驱结点的后继指向,后继结点的前驱指向,同时自身元素的item、next、prev都置为null。remove(int index)方法与上述方法类似,无非就是先根据索引找到执行元素进行操作,在此不再赘述。

 3.4 LinkedList序列化

与ArrayList一样,LinkedList中的三个成员变量均被transient修饰,所以需要自身实现readObject()方法和writeObject()方法。

/**
 * 覆写了readObject方法
 * 首先读取size大小,然后调用linkLast重新添加元素结点 构造双向链表
 */
private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
    // Read in any hidden serialization magic
    s.defaultReadObject();

    // Read in size
    int size = s.readInt();

    // Read in all elements in the proper order.
    for (int i = 0; i < size; i++)
        linkLast((E)s.readObject());
}

/**
 * 覆写了writeObject方法
 * 首先写入size大小,然后是item的值
 */
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);
}

可以看到,LinkedList的全部成员变量都是声明为transient类型的,所以进行序列化操作时,都将忽略,在自定义实现中,首先是将size写入,然后把结点中的item值部分按照顺序依次吸入,在反序列中,则是依此调用添加方法,重新生成Node结点类型的数据,达到反序列的目的。

3.5 LinkedList作为队列和栈实现

由于LinikedList实现了List接口和Deque接口,因此LinkedList既可以当做普通的List集合使用,也可以当作用FIFO(先进先出)队列,也可以当作LIFO(后进先出)堆栈。

LIFO(后进先出)栈

E peek();

返回栈顶元素,栈为空时返回null

void push(E e);

往栈内添加元素

E pop();

移除栈顶元素,并返回此元素(出栈)

FIFO(先进先出)队列

boolean offer(E e);// 等效boolean add(E e)

往队列中(队尾)添加元素

E poll();//E remove()

移除队列(队首)元素(出队列),返回此元素

E getFirst();

获取队首元素

E getLast();

获取队尾元素

以上方法只有当你声明为Deque的实现类时才可使用,接下来看个示例代码:

package com.ant3;

import java.util.ArrayList;
import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

/** 
 * @author  ant.world
 * @date 2016年5月24日 
 * @version 1.0 
 * @Description  
 */
public class Son {

    
    public static void main(String[] args) {
        /**
         * LinkedList堆栈使用
         */
        Deque<String> stack = new LinkedList<String>();
        stack.push("aa");
        stack.push("bb");
        stack.push("cc");
        Iterator<String> it = stack.iterator();
        System.out.println("堆栈数据");
        while(it.hasNext()){
            System.out.print(it.next()+"\t");
        }
        System.out.println();
        System.out.println("peek查看栈顶元素"+stack.peek());
        System.out.println("pop移除元素"+stack.pop());
        System.out.println("堆栈数据2");
        it = stack.iterator();
        while(it.hasNext()){
            System.out.print(it.next()+"\t");
        }
        
    }

}
}
View Code

以上为栈的实例代码,从栈顶存入元素,出栈操作和入栈操作以及返回栈顶元素。看下队列的相关示例代码:

package com.ant4;

import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedList;

/** 
 * @author  ant.world
 * @date 2016年6月15日 
 * @version 1.0 
 * @Description  
 */
public class MyDeque{

    public static void main(String[] args) {
        Deque<String> queue = new LinkedList<String>();
        queue.offer("aa");//等效 queue.add("aa");
        queue.offer("bb");
        queue.offer("cc");
        
        Iterator<String> it = queue.iterator();
        while(it.hasNext()){
            System.out.print("队列中元素:"+it.next()+"\t");
        }
        System.out.println();
        System.out.println("出队列:"+queue.poll());
        it = queue.iterator();
        while(it.hasNext()){
            System.out.print("队列中元素:"+it.next()+"\t");
        }
        /**
         * 打印结果
         * 
        队列中元素:aa    队列中元素:bb    队列中元素:cc    
        出队列:aa
        队列中元素:bb    队列中元素:cc    
         */
    }

}
View Code

总结

看完了ArrayList和LinkedList,简单总结下它们的之间的异同,及其使用场景:

1.实现方式的不同:ArrayList是基于数组来实现,LinkedList是基于双向(非循环)链表来实现的。

2.寻址空间:ArrayList是连续的存储空间,范围不够时,扩容为原来的1.5倍,LinkedList可在非连续的物理存储空间,通过指针连接起来。

3.访问、修改元素;因为两者存储方式的不同,ArrayList访问元素时只需要根据数组首地址的偏移量就能够找到元素。而LinkedList因为内存空间的不连续,需要通过遍历指针来访问某个元素。所以访问查找元素时,ArrayList明显要优于LinkedList。

4.指定位置新增、删除元素;新增、删除元素时,ArrayList需要保证其存储空间的连续性,需要移动元素。LinkedList则只需要选定元素后,改变其指针的指向,达到新增、删除元素的目的。所以新增、删除元素时,LinkedList要优于ArrayList。

如果是顺序添加时ArrayList则是直接添加到列表size+1位置上的,此时不涉及到移动元素。

 

其实到这里,我们可以有个小疑问,既然集合类元素的可以有基于数组和链表这两种不同的数据结构来来实现,那么栈和队列有没有基于数组来实现的呢?答案肯定是有,那就是Stack。该部分下节再述。

 

posted @ 2017-07-21 14:30  骑着单车的程序猿  阅读(320)  评论(0编辑  收藏  举报