Java(JDK1.9)集合框架归纳总结——Collection接口的第三层——应用层

注:该系列文章总结自JDK1.9源码的文档注释和源码,我对源码的部分中文注释已经同步到Github:https://github.com/Dodozhou/JDK

Collection接口的第三层——应用层&具体实现

应用层就是各个抽象类、底层接口的具体实现,使我们实际使用的各种容器和工具类,如ArrayList、Vector、HashSet、TreeSet等。结构图如下:

AbstractList

AbstractList

AbstractList的结构图如上图所示。接下来对这些子类进行简单概述。

Vector

Vector实现了一个大小可变的数组,它的元素可以通过下标访问(实现了RandomAccess接口)。Vector是线程安全的。在不需要考虑线程安全的情况下,推荐实用ArrayList来替代该类。Vector是通过在需要同步的方法上加Synchronized关键字类进行同步控制的。因此效率较低。
Vector尝试通过capacity和capacityIncrement属性来实现对存储空间的优化。capacity始终比Vector的size大,而capacityIncrement决定了每次Vector容量增加多少。应用在放入大量数据之前可以提前增加容量,以避免插入过程中频繁的扩容和复制带来的时间消耗。

    /**
     * vector自动扩容的增量。如果这个增量小于等于0,那么vector的容量每次会扩容为2倍大小。
     * 该属性默认为0.
     */
    protected int capacityIncrement;

    public Vector(int initialCapacity, int capacityIncrement) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        //直接创建initialCapacity大小的对象数组
        this.elementData = new Object[initialCapacity]; 
        this.capacityIncrement = capacityIncrement;
    }

该类返回的迭代器是“快速失败”的迭代器。当面对并发修改的时候,这个迭代器可以快速失败和清理。以免造成错误的结果。通过elements()返回的Enumerations不是快速失败的。因此在面对并发修改时,该方法的结果将会是未知的。
迭代器的快速失败行为并不能保证一定会成功,它只是尽最大努力来检测和抛出异常。任何非锁定并发修改都不能做出绝对的保证。因此,任何依赖迭代器抛出的异常来进行编程的行为都是错误。迭代器的快速失败行为只应该被用来检查bug。

Stack

Stack是一个继承自Vector的类,它表示数据结构中“后进先出”的栈。它自身值定义了5个方法:

  • push(E):将E压入栈中
  • pop():将栈顶元素出栈
  • peek():返回栈顶元素,但是不出栈
  • empty():检测栈是否为空
  • search(Object):搜索栈中的元素

在该类的文档注释中这样写道:

A more complete and consistent set of LIFO stack operations is
 * provided by the {@link Deque} interface and its implementations, which
 * should be used in preference to this class. 

意思是,Deque接口和它的实现提供了一组更完整、更一致的LIFO堆栈操作,这些操作应该优先于该类。

Deque<Integer> stack = new ArrayDeque<Integer>();

因此,该类并不常使用。

ArrayList

ArrayList是List接口的可序列化实现,允许插入null值。这个集合粗略地等同于Vector,但是ArrayList不是线程安全的。size, isEmpty, get, set,iterator方法花费常数时间。add 花费O(n)时间其他操作粗略认为花费线性时间。ArrayList的常数因子比LinkedList小,常数因子就是不随数据规模增长而增长的时间开销。如做某个操作需要遍历一个数组两遍,那么时间复杂度就是 O(n),常数因子是2.

capacity属性用来表示ArrayList的容量大小,它始终大于或等于size,默认是10:

/**
     * 默认容量为10
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;

底层实现是对象数组:

/**
     * Shared empty array instance used for empty instances.
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

用户可以通过调用ensureCapacity方法来确保ArrayList拥有足够大的空间。并以此来避免频繁的自动扩容所带来的时间开销。

注意,这个实现不是线程安全的。如果多线程要并发地对该集合进行结构化修改,那么必须进行外部同步,或者使用Collections.synchronizedList来包装ArrayList。

该实现也提供了快失败机制的普通迭代器iterator和listIterator迭代器。

AbstractSequentialList

这是一个抽象类,提供了List接口的顺序访问(sequential access)实现框架。对于类似链表方式的类,应该优先考虑继承该类。该类的设计使用了模板方法设计模式,将listIterator留给子类去实现,便于以后扩展。
该类的访问方式与AbstractList不同,AbstractList是通过Random Access方式访问元素,如数组。该类是通过Sequential Access方式访问元素,如链表。

LinkedList

LinkedList是一个实现了List和Deque接口的双向链表。对于使用index定位的方法,它必须由头部或者尾部开始遍历才能得到。
该方式并不是并发安全的。在多线程环境下使用该类,要么进行外部的同步控制,要么使用Collections.synchronizedList对该类进行一个包装。
该类的迭代器是快速失败机制的。

该类可以当做队列使用,它专门定义了一组Queue操作方法:

 public E peek() {
        final Node<E> f = first;
        return (f == null) ? null : f.item;}
    public E element() {return getFirst();}
    public E poll() {final Node<E> f = first;
        return (f == null) ? null : unlinkFirst(f);}
    public E remove() {return removeFirst();}
    public boolean offer(E e) {return add(e);}
    public boolean offerFirst(E e) {addFirst(e);return true;}
    public boolean offerLast(E e) {addLast(e);return true;}
    public E peekFirst() {final Node<E> f = first;return (f == null) ? null : f.item;}
    public E peekLast() {final Node<E> l = last;return (l == null) ? null : l.item;}
    public E pollFirst() {final Node<E> f = first;return (f == null) ? null : unlinkFirst(f);}
    public E pollLast() {final Node<E> l = last;return (l == null) ? null : unlinkLast(l);}

同时,该类也定义了Stack操作方法,如push、pop等:

//栈操作:将输入的值压入栈中。实质:将元素插入链表头部
public void push(E e) {addFirst(e);}
public E pop() {return removeFirst();}

AbstractSet & SortedSet

AbstractSet

HashSet

该类是Set接口的实现,它基于HashMap实现。大部分操作均使用HashMap的相应方法。由于使用了哈希表,因此它不保证元素的顺序,同时也不保证顺序一致不变。这个类允许插入null值。和任何基于Hash表的实现一样,它提供基本操作的线性时间界。
对该Set进行遍历的时间与容量(而不是size)成正比,因此,不应该包初始容量设置的过大。
该类不是线程安全的。如果要在多线程环境下使用,必须进行外部加锁或者使用Collections.SynchronizedSet()方法进行包装。
它返回的迭代器是快速失败机制的。

LinkedHashSet

该类是基于LinkedHashMap实现。它和HashSet的关系就跟LinkedHashMap与HashMap的区别。而且他们都是分别依赖于对应的Map实现。
需要注意的是,LinkedHashSet本身并没有声明LinkedHashMap为容器,而是调用的父类的构造方法:

 /**
     * Constructs a new, empty linked hash set.  (This package private
     * constructor is only used by LinkedHashSet.) The backing
     * HashMap instance is a LinkedHashMap with the specified initial
     * capacity and the specified load factor.
     *
     * @param      initialCapacity   the initial capacity of the hash map
     * @param      loadFactor        the load factor of the hash map
     * @param      dummy             ignored (distinguishes this
     *             constructor from other int, float constructor.)
     * @throws     IllegalArgumentException if the initial capacity is less
     *             than zero, or if the load factor is nonpositive
     */
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }

注释文档也说明了,该构造器是专门提供给LinkedHashSet使用的。

TreeSet

基于TreeMap实现的一个有序的Set。
插入的值都是存在TreeMap的key中的,那么value域用来干什么呢?value用一个final类型的Object来表示:private static final Object PRESENT = new Object();

为什么要这么设计呢?用空间换时间。虽

然付出了O(n)的空间,但是常规操作如add(),remove(),contains()的时间界被优化到了log(n)时间界

TreeSet是怎么保证元素不重复的呢?

是由于map.put(k,v)方法的特性:当k已经存在时,仅仅更新v值。元素根据自然序列或创建Set时传入的比较器进行排序。
对于为什么要用TreeMap来实现TreeSet,这里有一篇详细说明的博文:https://blog.csdn.net/zhoucheng05_13/article/details/79734549

Deque

Deque

Deque是一个双端队列,它的实现类由4个,其中ArrayDeque和LinkedList在java.util包下,而ConcurrentLinkedDeque和LinkedBlockingDeque在java.util.concurrent包下,用于并发情景。

ArrayDeque

ArrayDeque是一个基于可变数组实现的Deque实现。ArrayDeque没有容量限制,它们在需要的时候会自动扩容。
由于ArrayDeque继承自Queue,因此它不允许插入null值。同时,它也不是线程安全的。

LinkedList

LinkedList对Deque的常规方法进行了实现,详情见上面的LinkedList介绍。

ConcurrentLinkedDeque & LinkedBlockingDeque

ConcurrentLinkedDeque和LinkedBlockingDeque是用于多线程环境下的Deque,这里暂不做记录。

总结

发现一个规律,具体的Set实现,如HashSet、TreeSet、LinkedHashSet均是依赖于相应的Map实现,分别对应于HashMap、TreeMap、LinkedHashMap。

猜你喜欢

转载自blog.csdn.net/zhoucheng05_13/article/details/79840591