基础篇-集合容器深入理解

今天来把集合容器深入了解一番,首先第一步观察集合的模型图

一、模型图第一步

首先使我们熟悉的有序集合,他们的超级接口为Collection

List、Set、Queue也是接口,只不过各自添加了属于自己的方法。

public interface List extends Collection{}
public interface Set extends Collection{}
public interface Queue extends Collection{}

作为超级接口共有15个方法,它涵盖了有序集合常用的方法,因此作为父接口还是比较称职的。

但是在这里发现一个小插曲,不知有没有发现在Collection定义的方法中,例如: public abstract int size();

而我们的List接口继承Collection,按照理解来说,List中不应该再继续拥有该同名方法了,可是List中依旧定义了和Collection中完全相同的方法,当然了不只是这一个方法重复。这里困惑的就是为什么会再次定义一个本可以继承而来的方法呢?

个人找到的原因(不保证准确):查看API定义发现

List中:

Collecton中:

所以我的理解是:

其实只是定义的接口行为不一样。
表示子类如果实现List接口,那就需要遵循List Javadoc的行为。
如果实现Collection那就必须要遵循Collection Javadoc的行为。
不单单只是重写一个方法而已,重写这个方法的时候应该按照Javadoc所描述的一样。

是不是没看到Map有点着急了啊,那就再把Map加上哦

二、模型第二步

加完了,看到上面的图是不是觉得有点怪,没错Map集合本质上无论数据结构还是方法都和Collection没有一毛钱关系,但是你看左上角得标注应该明白,它俩连起来只是表示具有UML中得依赖关系罢了,Produces是生产得意思,就是说Map可以返回其所有键组成的Set和其所有值组成的Collection,或其键值对组成的Set。这下明白了吧。

下面就开始具体得划分了,先从有序集合这面开始

三、模型第三步

多出来得这2个抽象类和2个List集合类介绍下:

1、AbstractCollection

AbstractCollection 是 Java 集合框架中 Collection 接口 的一个直接实现类, Collection 下的大多数子类都继承 AbstractCollection ,比如 List 的实现类, Set的实现类。

它实现了一些方法,也定义了几个抽象方法留给子类实现,因此它是一个抽象类

其中抽象方法为:

public abstract Iterator iterator();

public abstract int size();

注意:

子类必须以自己的方式实现这两个方法。除此外,AbstractCollection 中默认不支持添加单个元素,如果直接调用 add(E) 方法,会报错:

因此,如果子类是可添加的数据结构,需要自己实现 add(E) 方法。

public boolean add(Object obj)
    {
        throw new UnsupportedOperationException();
    }

话说这个方法直接就返回异常挺败家玩意,可能不懂高手的思想吧。

AbstractCollection 的 add(E) 方法默认是抛出异常,这样会不会容易导致问题?为什么不定义为抽象方法?

答案译自 stackoverflow :

  • 如果你想修改一个不可变的集合时,抛出 UnsupportedOperationException 是标准的行为,比如 当你用 Collections.unmodifiableXXX() 方法对某个集合进行处理后,再调用这个集合的 修改方法(add,remove,set…),都会报这个错;
  • 因此 AbstractCollection.add(E) 抛出这个错误是准从标准;

那为什么会有这个标准呢?

在 Java 集合总,很多方法都提供了有用的默认行为,比如:

  • Iterator.remove()
  • AbstractList.add(int, E)
  • AbstractList.set(int, E)
  • AbstractList.remove(int)
  • AbstractMap.put(K, V)
  • AbstractMap.SimpleImmutableEntry.setValue(V)

而之所以没有定义为 抽象方法,是因为可能有很多地方用不到这个方法。

全部的方法为:(具体实现自行查看jdk中源码)

 boolean add(E e)
          确保此 collection 包含指定的元素(可选操作)。
 boolean addAll(Collection<? extends E> c)
          将指定 collection 中的所有元素都添加到此 collection 中(可选操作)。
 void clear()
          移除此 collection 中的所有元素(可选操作)。
 boolean contains(Object o)
          如果此 collection 包含指定的元素,则返回 true。
 boolean containsAll(Collection<?> c)
          如果此 collection 包含指定 collection 中的所有元素,则返回 true。
 boolean isEmpty()
          如果此 collection 不包含元素,则返回 true。
abstract  Iterator<E> iterator()
          返回在此 collection 中的元素上进行迭代的迭代器。
 boolean remove(Object o)
          从此 collection 中移除指定元素的单个实例,如果存在的话(可选操作)。
 boolean removeAll(Collection<?> c)
          移除此 collection 中那些也包含在指定 collection 中的所有元素(可选操作)。
 boolean retainAll(Collection<?> c)
          仅保留此 collection 中那些也包含在指定 collection 的元素(可选操作)。
abstract  int size()
          返回此 collection 中的元素数。
 Object[] toArray()
          返回包含此 collection 中所有元素的数组。
<T> T[]
toArray(T[] a)
          返回包含此 collection 中所有元素的数组;返回数组的运行时类型与指定数组的运行时类型相同。
 String toString()
          返回此 collection 的字符串表示形式。

2、AbstractList

看一下API中解释:

此类提供 List 接口的骨干实现,以最大限度地减少实现“随机访问”数据存储(如数组)支持的该接口所需的工作。对于连续的访问数据(如链表),应优先使用 AbstractSequentialList,而不是此类。

要实现不可修改的列表,编程人员只需扩展此类,并提供 get(int)size() 方法的实现。

要实现可修改的列表,编程人员必须另外重写 set(int, E) 方法(否则将抛出 UnsupportedOperationException)。如果列表为可变大小,则编程人员必须另外重写 add(int, E)remove(int) 方法。

按照 Collection 接口规范中的建议,编程人员通常应该提供一个 void(无参数)和 collection 构造方法。

与其他抽象 collection 实现不同,编程人员不必 提供迭代器实现;迭代器和列表迭代器由此类在以下“随机访问”方法上实现:get(int)set(int, E)add(int, E)remove(int)

此类中每个非抽象方法的文档详细描述了其实现。如果要实现的 collection 允许更有效的实现,则可以重写所有这些方法。

此类是 Java Collections Framework 的成员。

个人理解就是这个抽象类基本实现List这个集合的功能,至于再具体的功能就要更进一步实现或重写其中的方法啦

这里又发现了一个和上面类似的问题,该抽象类是ArrayList和Vector的父类,但是呢父类实现了List接口,而子类也同样的实现了List接口,对此查资料显示两种情况:

情况1:

首先接口意义在于规范设计,但接口是没有具体实现的(1.8版本之后的静态方法除外),所有继承它的类都需要一个个实现它的所有方法。但是呢接口可能有些方法具有一定的通用性,也就是说不需要所有实现类都单独实现(实现了代码也是重复的),有一个类实现这几个方法,大家都去继承它好了,剩下的接口大家各自实现,这就是模板类的意义。
现在关键问题来了,为什么要把这个模板类定义成抽象类呢?其实上面已经给出了答案,因为继承接口的实现类必须要实现接口的所有方法,而模板类仅仅是为了实现一些通用方法不需要实现所有接口方法(总不能为了提供这个模板,稀里糊涂的把所有接口方法都实现了吧,没有必要不说,还和可能产生不必要的使用。),这时候抽象类就符合这个需求了。
抽象类实现了通用的接口,剩下的接口谁继承我谁实现,而且抽象类本身又不能实例化,不会产生没有实现的接口滥用。既实现了代码的最大化复用,又实现了代码的最大化精简。

由于抽象类并未对接口中的所有方法都进行实现,而只是对接口中的部分方法进行了实现(为了达到多态解耦的目的),还须由子类重新对接口中剩余的抽象方法进行一次实现来完善。所以再给子类声明了一次这个接口,用来对此作个标记。子类写不写上都是可以的,更多的是体现了一种规范。

情况2:

stackoverflow里面的答案,也就是提问了JDK源码开发人员

I've asked Josh Bloch, and he informs me that it was a mistake. He used to think, long ago, that there was some value in it, but he since "saw the light". Clearly JDK maintainers haven't considered this to be worth backing out later.

用我撇脚的英语翻译一下:

我问过这个Josh Bloch,然后他解释说这是个错误。。。错误。。。错误。。。

他回想,很久以前他觉得这样写有些价值,但是自从看清以后,并且深思熟虑发现这个并没有什么价值。哈哈

其实我现在也是懵逼的。也许真的只是规范化吧。

四、模型第四步

由于线变多了,并且类和接口也多了,所以加上了颜色以及根据线的颜色和类型区分一些特性。

可以看到主要添加了Set集合间的抽象类以及子接口还有常用的Set集合。同时将遗漏的LinkedList添加,那现在就开始解释吧。

1、LinkedList

API定扔上来:

List 接口的链接列表实现。实现所有可选的列表操作,并且允许所有元素(包括 null)。除了实现 List 接口外,LinkedList 类还为在列表的开头及结尾 get、remove 和 insert 元素提供了统一的命名方法。这些操作允许将链接列表用作堆栈、队列双端队列

此类实现 Deque 接口,为 add、poll 提供先进先出队列操作,以及其他堆栈和双端队列操作。

所有操作都是按照双重链接列表的需要执行的。在列表中编索引的操作将从开头或结尾遍历列表(从靠近指定索引的一端)。

注意,此实现不是同步的。如果多个线程同时访问一个链接列表,而其中至少一个线程从结构上修改了该列表,则它必须 保持外部同步。(结构修改指添加或删除一个或多个元素的任何操作;仅设置元素的值不是结构修改。)这一般通过对自然封装该列表的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedList 方法来“包装”该列表。最好在创建时完成这一操作,以防止对列表进行意外的不同步访问,如下所示:

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

此类的 iterator 和 listIterator 方法返回的迭代器是快速失败 的:在迭代器创建之后,如果从结构上对列表进行修改,除非通过迭代器自身的 remove 或 add 方法,其他任何时间任何方式的修改,迭代器都将抛出 ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败,而不冒将来不确定的时间任意发生不确定行为的风险。

注意,迭代器的快速失败行为不能得到保证,一般来说,存在不同步的并发修改时,不可能作出任何硬性保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。因此,编写依赖于此异常的程序的方式是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。

简单归类为:

该集合为链表结构,擅长增加删除,不善于查询。

2、AbstractSequentialList

此类提供了 List 接口的骨干实现,从而最大限度地减少了实现受“连续访问”数据存储(如链接列表)支持的此接口所需的工作。对于随机访问数据(如数组),应该优先使用 AbstractList,而不是先使用此类。

从某种意义上说,此类与在列表的列表迭代器上实现“随机访问”方法(get(int index)、set(int index, E element)、add(int index, E element) 和 remove(int index))的 AbstractList 类相对立,而不是其他关系。

要实现一个列表,程序员只需要扩展此类,并提供 listIterator 和 size 方法的实现即可。对于不可修改的列表,程序员只需要实现列表迭代器的 hasNext、next、hasPrevious、previous 和 index 方法即可。

对于可修改的列表,程序员应该再另外实现列表迭代器的 set 方法。对于可变大小的列表,程序员应该再另外实现列表迭代器的 remove 和 add 方法。

按照 Collection 接口规范中的推荐,程序员通常应该提供一个 void(无参数)构造方法和 collection 构造方法。

3、AbstractSet<E>

此类提供 Set 接口的骨干实现,从而最大限度地减少了实现此接口所需的工作。

通过扩展此类来实现一个 set 的过程与通过扩展 AbstractCollection 来实现 Collection 的过程是相同的,除了此类的子类中的所有方法和构造方法都必须服从 Set 接口所强加的额外限制(例如,add 方法必须不允许将一个对象的多个实例添加到一个 set 中)。

注意,此类并没有重写 AbstractCollection 类中的任何实现。它仅仅添加了 equals 和 hashCode 的实现。

4、HashSet

此类实现 Set 接口,由哈希表(实际上是一个 HashMap 实例)支持。它不保证 set 的迭代顺序;特别是它不保证该顺序恒久不变。此类允许使用 null 元素。

此类为基本操作提供了稳定性能,这些基本操作包括 add、remove、contains 和 size,假定哈希函数将这些元素正确地分布在桶中。对此 set 进行迭代所需的时间与 HashSet 实例的大小(元素的数量)和底层 HashMap 实例(桶的数量)的“容量”的和成比例。因此,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。

注意,此实现不是同步的。如果多个线程同时访问一个哈希 set,而其中至少一个线程修改了该 set,那么它必须 保持外部同步。这通常是通过对自然封装该 set 的对象执行同步操作来完成的。如果不存在这样的对象,则应该使用 Collections.synchronizedSet 方法来“包装” set。最好在创建时完成这一操作,以防止对该 set 进行意外的不同步访问:

   Set s = Collections.synchronizedSet(new HashSet(...));

此类的 iterator 方法返回的迭代器是快速失败 的:在创建迭代器之后,如果对 set 进行修改,除非通过迭代器自身的 remove 方法,否则在任何时间以任何方式对其进行修改,Iterator 都将抛出 ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败,而不冒将来在某个不确定时间发生任意不确定行为的风险。

注意,迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。快速失败迭代器在尽最大努力抛出 ConcurrentModificationException。因此,为提高这类迭代器的正确性而编写一个依赖于此异常的程序是错误做法:迭代器的快速失败行为应该仅用于检测 bug。

5、TreeSet

基于 TreeMapNavigableSet 实现。使用元素的自然顺序对元素进行排序,或者根据创建 set 时提供的 Comparator 进行排序,具体取决于使用的构造方法。

此实现为基本操作(addremovecontains)提供受保证的 log(n) 时间开销。

注意,如果要正确实现 Set 接口,则 set 维护的顺序(无论是否提供了显式比较器)必须与 equals 一致。(关于与 equals 一致 的精确定义,请参阅 ComparableComparator。)这是因为 Set 接口是按照 equals 操作定义的,但 TreeSet 实例使用它的 compareTo(或 compare)方法对所有元素进行比较,因此从 set 的观点来看,此方法认为相等的两个元素就是相等的。即使 set 的顺序与 equals 不一致,其行为也 定义良好的;它只是违背了 Set 接口的常规协定。

注意,此实现不是同步的。如果多个线程同时访问一个 TreeSet,而其中至少一个线程修改了该 set,那么它必须 外部同步。这一般是通过对自然封装该 set 的对象执行同步操作来完成的。如果不存在这样的对象,则应该使用 Collections.synchronizedSortedSet 方法来“包装”该 set。此操作最好在创建时进行,以防止对 set 的意外非同步访问:

   SortedSet s = Collections.synchronizedSortedSet(new TreeSet(...));

此类的 iterator 方法返回的迭代器是快速失败 的:在创建迭代器之后,如果从结构上对 set 进行修改,除非通过迭代器自身的 remove 方法,否则在其他任何时间以任何方式进行修改都将导致迭代器抛出 ConcurrentModificationException。因此,对于并发的修改,迭代器很快就完全失败,而不会冒着在将来不确定的时间发生不确定行为的风险。

注意,迭代器的快速失败行为无法得到保证,一般来说,存在不同步的并发修改时,不可能作出任何肯定的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。因此,编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测 bug。

6、LinkedHashSet

具有可预知迭代顺序的 Set 接口的哈希表和链接列表实现。此实现与 HashSet 的不同之外在于,后者维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,即按照将元素插入到 set 中的顺序(插入顺序)进行迭代。注意,插入顺序 受在 set 中重新插入的 元素的影响。(如果在 s.contains(e) 返回 true 后立即调用 s.add(e),则元素 e 会被重新插入到 set s 中。)

此实现可以让客户免遭未指定的、由 HashSet 提供的通常杂乱无章的排序工作,而又不致引起与 TreeSet 关联的成本增加。使用它可以生成一个与原来顺序相同的 set 副本,并且与原 set 的实现无关:

     void foo(Set s) {
         Set copy = new LinkedHashSet(s);
         ...
     }

如果模块通过输入得到一个 set,复制这个 set,然后返回由此副本决定了顺序的结果,这种情况下这项技术特别有用。(客户通常期望内容返回的顺序与它们出现的顺序相同。)

此类提供所有可选的 Set 操作,并且允许 null 元素。与 HashSet 一样,它可以为基本操作(add、contains 和 remove)提供稳定的性能,假定哈希函数将元素正确地分布到存储段中。由于增加了维护链接列表的开支,其性能很可能会比 HashSet 稍逊一筹,不过,这一点例外:LinkedHashSet 迭代所需时间与 set 的大小 成正比,而与容量无关。HashSet 迭代很可能支出较大,因为它所需迭代时间与其容量 成正比。

链接的哈希 set 有两个影响其性能的参数:初始容量加载因子。它们与 HashSet 中的定义极其相同。注意,为初始容量选择非常高的值对此类的影响比对 HashSet 要小,因为此类的迭代时间不受容量的影响。

注意,此实现不是同步的。如果多个线程同时访问链接的哈希 set,而其中至少一个线程修改了该 set,则它必须 保持外部同步。这一般通过对自然封装该 set 的对象进行同步操作来完成。 如果不存在这样的对象,则应该使用 Collections.synchronizedSet 方法来“包装”该 set。最好在创建时完成这一操作,以防止意外的非同步访问:

     Set s = Collections.synchronizedSet(new LinkedHashSet(...));

此类的 iterator 方法返回的迭代器是快速失败 的:在迭代器创建之后,如果对 set 进行修改,除非通过迭代器自身的 remove 方法,其他任何时间任何方式的修改,迭代器都将抛出 ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败,而不冒将来不确定的时间任意发生不确定行为的风险。

注意,迭代器的快速失败行为不能得到保证,一般来说,存在不同步的并发修改时,不可能作出任何强有力的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。因此,编写依赖于此异常的程序的方式是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。

7、NavigableSet<E>

扩展的 SortedSet,具有了为给定搜索目标报告最接近匹配项的导航方法。方法 lowerfloorceilinghigher 分别返回小于、小于等于、大于等于、大于给定元素的元素,如果不存在这样的元素,则返回 null。可以按升序或降序访问和遍历 NavigableSetdescendingSet 方法返回 set 的一个视图,该视图表示的所有关系方法和方向方法都是逆向的。升序操作和视图的性能很可能比降序操作和视图的性能要好。此外,此接口还定义了 pollFirstpollLast 方法,它们返回并移除最小和最大的元素(如果存在),否则返回 nullsubSetheadSettailSet 方法与名称相似的 SortedSet 方法的不同之处在于:可以接受用于描述是否包括(或不包括)下边界和上边界的附加参数。任何 NavigableSet 的 Submap 必须实现 NavigableSet 接口。

导航方法的返回值在允许 null 元素的实现中可能是不确定的。不过,即使在这种情况下,也可以通过检查 contains(null) 来明确结果。为了避免这样的问题,建议在此接口的实现中 允许插入 null 元素。(注意,Comparable 元素的有序集本身不允许 null。)

subSet(E, E)headSet(E)tailSet(E) 方法被指定为返回 SortedSet,以允许现有 SortedSet 实现能相容地改进为实现 NavigableMap,但鼓励此接口的扩展和实现重写这些方法以返回 NavigableSet

8、SortedSet<E>

进一步提供关于元素的总体排序Set。这些元素使用其自然顺序进行排序,或者根据通常在创建有序 set 时提供的 Comparator 进行排序。该 set 的迭代器将按元素升序遍历 set。提供了一些附加的操作来利用这种排序。(此接口是 SortedMap 的 set 对应接口)。

插入有序 set 的所有元素都必须实现 Comparable 接口(或者被指定的比较器所接受)。另外,所有这些元素都必须是可互相比较的:对于有序 set 中的任意两个元素 e1 和 e2,执行 e1.compareTo(e2)(或 comparator.compare(e1, e2))都不得抛出 ClassCastException。试图违反此限制将导致违反规则的方法或者构造方法调用抛出 ClassCastException。

注意,如果有序 set 要正确实现 Set 接口,则有序 set 所维持的顺序(无论是否提供了明确的比较器)都必须与 equals 一致。(有关与 equals 一致 的精确定义,请参阅 Comparable 接口或 Comparator 接口。)这是因为 Set 接口是按照 equals 操作定义的,但有序 set 使用它的 compareTo(或 compare)方法对所有元素进行比较,因此从有序 set 的角度来看,此方法认为相等的两个元素就是相等的。即使顺序与 equals 不一致,有序 set 的行为仍然 定义良好的,只不过没有遵守 Set 接口的常规协定。

所有通用有序 set 实现类都应该提供 4 个“标准”构造方法:1) void(无参数)构造方法,它创建一个空的有序 set,按照元素的自然顺序进行排序。2) 带有一个 Comparator 类型参数的构造方法,它创建一个空的有序 set,根据指定的比较器进行排序。3) 带有一个 Collection 类型参数的构造方法,它创建一个新的有序 set,其元素与参数相同,按照元素的自然顺序进行排序。4) 带有一个 SortedSet 类型参数的构造方法,它创建一个新的有序 set,其元素和排序方法与输入的有序 set 相同。无法保证强制实施此建议,因为接口不能包含构造方法。

注:一些方法返回具有受限范围的子集。这些范围区间是半开的,也就是说,它们包括低端点,但不包括高端点(如果适用)。如果需要一个闭区间(同时包含两个端点),且元素类型允许计算给定值的后继值,则只需要请求从 lowEndpoint 到 successor(highEndpoint) 的子区间。例如,假设 s 是一个字符串有序 set。下面的语句将得到一个包含 s 中从 low 到 high(包括)所有字符串的视图:

   SortedSet<String> sub = s.subSet(low, high+"\0");

可使用类似的技术生成 开区间(两个端点都不包括)。下面的语句得到包含 s 中从 low 到 high(不包括)所有字符串的视图:

   SortedSet<String> sub = s.subSet(low+"\0", high);

五、模型第五步

第五部分主要是添加了Map集合部分。

可以看出Map就是超级接口,抽象类以及类和接口大部分都要实现Map接口。

值得关注的是HashTable继承的Dictionary抽象类已过时,不建议使用。

六、模型第六步

模型先放在此,后期继续添加各个接口和集合不同实现

猜你喜欢

转载自blog.csdn.net/ysj4428/article/details/81324133