Java入门记(四):容器关系的梳理(上)——Collection

      目录算法

1、Collection及子类/接口容器继承关系编程

2、List数组

  2.1 ArrayList安全

    2.1.1 序列化的探讨网络

    2.1.2 删除元素数据结构

    2.1.3 调整大小框架

  2.2 Vector和Stack(不建议继续使用)优化

  2.3 抽象类AbstractSequentialListspa

3、Set线程

  3.1 HashSet和LinkedHashSet

  3.2 TreeSet

4、Queue

  4.1 PriorityQueue

  4.2 LinkedList

5、一些琐碎的话题

  5.1 线程安全

  5.2 clone()

  5.3 foreach

  5.4 null对象

 

  Java.util中的容器又被称为Java Collections framework。虽然被称为框架,可是其主要目的是提供一组接口尽可能简单并且相同、而且尽可能高效、以便于开发人员按照场景选用,而不是本身重复实现的类。容器按接口能够分为两大类:Collection和Map。本文主要关注Collection,之后会将Map这块也进行研究。

1、Collection及子类/接口容器继承关系

  先从Collection提及。能够看出:

1.Collection接口并非一个根接口,它的超级接口是Iterator,须要提供其遍历、移除元素(可选操做)的能力。

2.Collection接口定义了基本的容器操做方法。

 

  除此之外,

1.remove()和contains()判断元素是否相等的依据是相似的。

对于remove(Object o),若Collection中包含的元素e,知足(o==null ? e==null : o.equals(e)),移除其中的一个

对于contains(Object o),若Collection中包含至少一个或多个元素e,知足(o==null ? e==null : o.equals(e)),则返回true。

2.AbstractCollection抽象类实现了一部分Collection接口的方法,主要是基于iterator实现的,如remove()、toArray(),以及利用自己的属性size实现的size()。若是读一下源码,能够发现虽然AbstractCollection利用add()实现了addAll(),可是add()自己的实现是直接抛UnsupportedOperationException异常的。实际上add()是一种“可选操做”,目的是延迟到须要时再实现。

 

2、List

  了解了通用的Collection后,接下来,看看三大类的Collection:List、Set、Queue。首先从List提及。List中的元素是有序的,于是咱们能够按序访问List中的元素,以及访问指定位置上的元素。对于“按顺序遍历访问元素”的需求,使用List的超级接口Iterator便可以作到,这也是对应抽象类AbstractList中的实现;而访问特定位置的元素(也即按索引访问)、元素的增长和删除涉及到了List中各个元素的链接关系,并无在AbstractList中提供。

2.1 ArrayList

  ArrayList是最经常使用的List的实现,其包装了一个用于存放元素的数组,并用size属性来标识该容器里的元素个数,而非这个被包装数组的大小。若是对数组有所了解,很容易理解ArrayList的元素是怎么编排的,各个数组的元素如何随机访问(经过索引)、元素之间如何跳转(索引增减)。阅读源码能够发现,这个数组用transient关键字修饰,表示其不会被序列化。固然,ArrayList的元素最终仍是会被序列化的,要否则,这个最经常使用的List之一,不能持久化、不能网络传输,简直不可想象。在序列化/反序列化时,会调用ArrayList的writeObject()/readObject()方法,将该ArrayList中的元素(即0...size-1下标对应的元素)写入流/从流读出。这样作的好处是,只保存/传输有实际意义的元素,最大限度的节约了存储、传输和处理的开销。

2.1.1 序列化的探讨

  提到序列化,有个问题是,ArrayList的writeObject()/readObject()是如何被调用的?它们并不属于ArrayList的任何一个接口,甚至是Serializabe!其实,序列化是ObjectOutputStream对象调用自身的writeObject()方法时,由它经过反射检查入参——也即待序列化的对象——是否有writeObject()方法,并进行调用,这和接口无关,确实很古怪(能够参考《Java编程思想·第四版(中文)》第581页)。

2.1.2 删除元素

  ArrayList在删除元素时,不只要将其余元素前移来占用被移除的元素并缩小size,对于原来位置的元素,如(size-1)位置的元素前移至(size-2)位,那么(size-1)位置是要设置为null的,这样才能让垃圾回收机制发挥做用。这种数据的用法在Java中比较常见,好比利用Vector实现的Stack,也是这样。而在C语言中,一种利用数组实现的栈是能够在pop()后只修改当前栈对应的数组下标而不做清理的。

2.1.3 调整大小

  利用Arrays.copyOf()方法作数组的调整。

2.2 Vector和Stack(不建议继续使用)

   虽然Vector通过了改造,但这么作只是为了兼容Java2以前的代码,不建议继续使用。

  Java1.6的源码中,和ArrayList相似,Vector底层也是数组,可是这个数组并无transient修饰,其序列化要低效很多。

  Stack是继承Vector实现的,而不是包装一个Vector。这并非一个很好的设计,若是要使用栈行为,应该使用LinkedList。Java1.6源码中,Stack每次扩大都须要new新的数组并做拷贝,效率并很差。

  新代码中误用这两个容器的缘由,多是以前在C++中使用过STL的Vector和Stack。我刚接触Java时,总觉得这两个类在Java中的地位相似C++。

2.3 抽象类AbstractSequentialList

  知足“连续访问”数据存储而非“随机访问”需求的List,对于指定index元素的操做,都须要利用抽象方法listIterator()得到一个迭代器。其惟一实现是LinkedList,对其的讨论放在Queue这部分。

3、Set

  Set接口模仿了数学概念上的set,各个元素不要求重复。除了这一点,几乎和Collection自己是同样的。

  Set接口有一个直接子接口SortedSet,该接口要求Set中全部元素有序。和经过Iterable接口依次访问全部元素的“有序”不一样,这个“有序”要求Set包括一个比较器,能够判断两个元素的大于、小于或等于关系。此外,该接口提供了访问第一个元素、访问最后一个元素、访问必定范围内元素的方法。SortedSet的子接口NavigableSet进一步扩展了这个系列的方法,提供了诸如返回大于/小于元素E的第一个元素的方法。

  因为Set的操做与底层的实现关联性很强,AbstractSet中实现的方法有限,在Java1.6中只有equals()、hashCode()、removeAll()进行了实现。

3.1 HashSet和LinkedHashSet

  HashSet之因此命名中包含了“Hash”,是由于其底层是用HashMap实现的。Map有个特色,各个Key是惟一的,这和Set的元素惟一很相似。对HashSet的元素E进行的操做,其实是对其包装的HashMap中对应的<E,PRESENT>的操做,其中PRESENT是一个private static final的Object。所以,HashSet的原理,放到HashMap那一块来研究。

  HashSet有一个很特别的构造方法:HashSet(int initialCapacity, float loadFactor, boolean dummy)。这个方法第三个参数的惟一做用是,与其余两个参数的构造方法相区分。使用这个构造方法,在底层使用的是HashMap的子类LinkedHashMap。而LinkedHashSet,正是使用了这个构造方法,在内部建立并封装了一个LinkedHashMap而非通常的HashMap。

  假如先有HashSet,后有HashMap,用HashSet实现HashMap,是不是一个好的主意?这也放在HashMap处研究。

   HashSet的包装的HashMap也使用transient关键字修饰,采用了和ArrayList同样的序列化策略。

3.2 TreeSet

  TreeSet是SortedSet的一个实现,也是其子接口NavigableSet的实现。

   与HashSet/LinkedHashSet相似,TreeSet底层封装了一个NavigableMap,一样使用transient修饰,以及序列化策略。

4、Queue

  Queue和List有两个区别:前者有“队头”的概念,取元素、移除元素、均为对“队头”的操做(一般但不老是FIFO,即先进先出),然后者只有在插入时须要保证在尾部进行;前者对元素的一些同一种操做提供了两种方法,在特定状况下抛异常/返回特殊值——add()/offer()、remove()/poll()、element()/peek()。不难想到,在所谓的两种方法中,抛异常的方法彻底能够经过包装不抛异常的方法来实现,这也是AbstractQueue所作的。

  Deque接口继承了Queue,可是和AbstractQueue没有关系。Deque同时提供了在队头和队尾进行插入和删除的操做。

4.1 PriorityQueue

   PriorityQueue用于存放含有优先级的元素,插入的对象必须能够比较。该类内部一样封装了一个数组。与其抽象父类AbstractQueue不一样,PriorityQueue的offer()方法在插入null时会抛空指针异常——null是没法与其余元素比较一般意义下的优先级的;此外,add()方法是直接包装了offer(),没有附加的行为。

  因为其内部的数据结构是数组的缘故,不少操做都须要先把元素经过indexOf()转化成对应的数组下标,再进行进一步的操做,如remove()、removeEq()、contains()等。其实这个数组保持优先级队列的方式,是采用堆(Heap)的方式,具体能够参考任意一本算法书籍,好比《算法导论》等,这里就不展开解释了。和堆的特性有关,在寻找指定元素时,必须从头到尾遍历,而不能使用二分查找。

4.2 LinkedList

  颇有趣的是,LinkedList既是List,也是Queue(Deque),其缘由是它是双向的,内部的元素(Entry)同时保留了上一个和下一个元素的引用。使用头部的引用header,取其previous,就能够得到尾部的引用。经过这一转换,能够很容易实现Deque所须要的行为。也正所以,能够支持栈的行为,天生就有push()和pop()方法。简而言之,是Java中的双向链表,其支持的操做和普通的双向链表同样。

  和数组不一样,根据下标查找特定元素时,只能遍历地获取了,于是在随机访问时效率不如ArrayList。尽管如此,做者仍是尽量地利用了LinkedList的特性作了点优化,尽可能减小了访问次数:

    private Entry<E> entry(int index) {
        if (index < 0 || index >= size)
            throw new IndexOutOfBoundsException("Index: "+index+
                                                ", Size: "+size);
        Entry<E> e = header;
        if (index < (size >> 1)) {
            for (int i = 0; i <= index; i++)
                e = e.next;
        } else {
            for (int i = size; i > index; i--)
                e = e.previous;
        }
        return e;
    }

  LinkedList对首部和尾部的插入都支持,但继承自Collection接口的add()方法是在尾部进行插入。

5、一些琐碎的话题

5.1 线程安全

  ArrayList、HashSet/LinkedHashSet、PriorityQueue、LinkedList是线程不安全的,可使用synchronized关键字,或者相似下面的方法解决:

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

5.2 clone()

  ArrayList、LinkedList、HashMap/LinkedHashMap、TreeSet的clone()是浅拷贝,元素的引用和拷贝前相同;PriorityQueue的clone()继承自Object。

5.3 foreach

  在for(Element e : collection)中:

  collection == null,直接抛异常;

  容器内容为空,即刚刚被new出来,里面什么也没有,直接跳过循环;

  容器中放了null(若是容许的话),则将这个null取出并赋值给e,执行循环中的语句。

5.4 null对象

  List能够放无限多个,set只能放一个。EnumSet、PriorityQueue是不能放null的。这个null也在计数中。因此放进去null用foreach取出来时须要判空。

相关文章
相关标签/搜索