Java核心技术卷I:基础知识(原书第8版):13.3 集合框架

来源:互联网 发布:淘宝新手卖家活动 编辑:程序博客网 时间:2024/05/20 12:22

铁文整理

13.3 集合框架

    框架是一个类的集,它奠定了创建高级功能的基础。框架包含很多超类,这些超类拥有非常有用的功能、策略和机制。框架使用者创建的子类可以扩展超类的功能,而不必重新创建这些基本的机制。例如,Swing就是一种用户界面的机制。

    Java集合类库构成了集合类的框架。它为集合的实现者定义了大量的接口和抽象类(见图13-10),并且对其中的某些机制给予了描述,例如,迭代协议。正如前面几节所做的那样,可以使用这些集合类,而不必了解框架。但是,如果想要实现用于多种集合类型的泛型算法,或者是想要增加新的集合类型,了解一些框架的知识是很有帮助的。

    集合有两个基本的接口:CollectionMap。可以使用下列方法向集合中插入元素:

    boolean add(E e1enent)

    但是,由于映射表保存的是键/值对,所以可以使用put方法进行插入:

    V put(K key, V value)

    要想从集合中读取某个元素,就需要使用迭代器访问它们。然而,也可以用get方法从映射表读取值:

    V get(K key)

    List是一个有序集合。元素可以添加到容器中某个特定的位置。将对象放置在某个位置上可以采用两种方式:使用整数索引或使用列表迭代器。List接口定义了几个用于随机访问的方法:

    void add(int index, E elemet)

    E get(int index)

    void remove(int index)

13-10 集合框架的接口

    如同前面所讨论的,List接口在提供这些随机访问方法时,并不管它们对某种特定的实现是否高效。为了避免执行成本较高的随机访问操作,Java SE 1.4引入了一个标记接口RandomAccess。这个接口没有任何方法,但可以用来检测一个特定的集合是否支持高效的随机访问:

        if (cinstanceof RandomAccess) {

            useRandomAccessAlgorithm();

        } else {

            useSequentialAccessAlgorithm();

        }

    ArrayList类和Vector类都实现了RandomAccess接口。

    注释:从理论上看,有一个独立的Array接口,它扩展于于List接口,并声明了随机访问方法是有很合理的。如果确实有这样一个独立的Array接口,那些需要随机访问的算法就可以使用Array参数,而且也不会无意中将它们应用于随机访问速度很慢的集合了。但是,集合框架的设计者没有选择去定义一个独立的接口,其原因是希望让类库中的接口数量保持在一个较少的水平,此外,不希望对程序员采取一种家长式的作风。这样可以自由地将链表传递给随机访问算法,只是需要清楚这样做会给性能带来什么不良的影响。

    ListIterator接口定义了一个方法,用于将一个元素添加到迭代器所处位置的前面:

    void add(E element)

    要想获取和删除给定位置的元素,只需要调用Iterator接口中的next方法和remove方法即可。

    Set接口与Collection接口是一样的,只是其方法的行为有着更加严谨的定义。集的add方法拒绝添加重复的元素。集的equals方法定义两个集相等的条件是它们包含相同的元素但顺序不必相同。hashCode方法定义应该保证具有相同元素的集将会得到相同的散列码。

    既然方法签名是相同的,为什么还要建立一个独立的接口呢?从概念上讲,并不是所有集合都是集。建立Set接口后,可以让程序员编写仅接受集的方法。

    SortedSetSortedMap接口暴露了用于排序的比较器对象,并且定义的方法可以获得集合的子集视图。下一节将讨论这些视图。

    最后,Java SE 6引入了接口NavigableSetNavigableMap,其中包含了几个用于在有序集和映射表中査找和遍历的方法(从理论上讲,这几个方法已经包含在SortedSetSortedMap的接口中)。TreeSetTreeMap类实现了这几个接口。

    现在,让我们将话题从接口转到实现接口的类上。前面已经讨论过,集合接口有大量的方法,这些方法可以通过更基本的方法加以实现。抽象类提供了许多这样的例行实现:

    AbstractCollection

    AbstractList

    AbstractSequentialList

    AbstractSet

    AbstractQueue

    AbstractMap

    如果实现了自己的集合类,就可能要扩展上面某个类,以便可以选择例行操作的实现。

    Java类库支持下面几种具体类:

    LinkedList

    ArrayList

    ArrayDeque

    HashSet

    TreeSet

    PriorityQueue

    HashMap

    TreeMap

    13-11展示了这些类之间的关系。

13-11集合框架中的类

    最后,还有许多Java第一版“遗留”下来的容器类,在集合框架出现就有了,它们是:

    Vector

    Stack

    Hashtable

    Properties

    这些类已经被集成到集合框架中,见图13-12。本章稍后将会讨论这些类。

13.3.1 视图与包装器

    看一下图13-10和图13-11可能会感觉:用如此多的接口和抽象类来实现数量并不多的具体集合类似乎没有太大必要。然而,这两张图并没有展示出全部的情况。通过使用视图可以获得其他的实现了集合接口和映射表接口的对象。映射表类的keySet方法就是一个这样的示例,初看起来,好像这个方法创建了一个新集,并将映射表中的所有键都填进去,然后返回这个集。但是,情况并非如此。取而代之的是:keySet方法返回一个实现Set接口的类对象,这个类的方法对原映射表进行操作。这种集合称为视图。

    视图技术在集框架中有许多非常有用的应用。下面将讨论这些应用。

1. 轻量级集包装器

    Arrays类的静态方法asList将返回个包装了普通Java数组的List包装器。这个方法可以将数组传递给一个期望得到列表或集合变元的方法。例如:

        Card[] cardDeck = new Card[52];

        List<Card> cardList = Arrays.asList(cardDeck);

13-12 集合框架中的遗留类

    返回的对象不是ArrayList。它是一个视图对象,带有访问底层数组的getset方法。改变数组大小的所有方法(例如,与迭代器相关的addremove方法)都会抛出一个UnsupportedOperationException异常。

    Java SE 5.0开始,asList方法声明为一个具有可变数量参数的方法。除了可以传递一个数组之外,还可以将各个元素直接传递给这个方法。例如:

        List<String> names = Arrays.asList("Amy","Bob", "Carl");

    这个方法调用Collections.nCopies(n, anObject),将返回一个实现了List接口的不可修改的对象,并给人一种包含n个元素,每个元素都像是一个anObject的错觉。

    例如,下面的调用将创建一个包含100个字符串的List,每个串都被设置为"DEFAULT"

        List<String> settings = Collections.nCopies(100, "DEFAULT");

    由于字符串对象只存储了一次,所以付出的存储代价很小。这是视图技术的一种巧妙的应用。

    注释:Collections类包含很多实用方法,这些方法的参数和返回值都是集合。不要将它与Collection接口混清起来。

    如果调用下列方法Collections.sinleton(anObject),则将返回一个视图对象。这个对象实现了Set接口(与产生Listncopies方法不同)。返回的对象实现了一个不可修改的单元素集,而不需要付出建立数据结构的开销。sinletonList方法与sinletonMap方法类似。

2. 子范围

    可以为很多集合建立子范围(subrange)视图。例如,假设有一个列表staff,想从中取出第10个~第19个元素。可以使用subList方法来获得一个列表的子范围视图。

        List group2 = staff.subList(10, 20);

    第一个索引包含在内,第二个索引则不包含在内。这与String类的substring操作中的参数情况相同。

    可以将任何操作应用于子范围,并且能够自动地反映整个列表的情况。例如,可以刪除整个子范围:

        group2.clear(); // staff reduction

    现在,元素自动地从staff列表中清除了,并且group2为空。

    对于有序集和映射表,可以使用排序顺序而不是元素位置建立子范围。SortedSet接口声明了3个方法:

    SortedSet<E> subSet(E from, E to)

    SortedSet<E> headSet(E to)

    SortedSet<E> tailSet(E from)

    这些方法将返回大于等于from且小于to的所有元素子集。有序映射表也有类似的方法:

    SortedMap<K, V> subMap(K from, K to)

    SortedMap<K, V> headMap(K to)

    SortedMap<K, V> tailMap(K from)

    返回映射表视图,该映射表包含键落在指定范围内的所有元素.

    Java SE 6引入的NavigableSet接口赋予子范围操作更多的控制能力。可以指定是否包括边界:

    NavigableSet<E> subSet(E from, boolean fromInclusive, E to, boolean toInclusive)

    NavigableSet<E> headSet(E to, boolean toInclusive)

    NavigableSet<E> tailSet(E from, boolean fromInclusive)

3. 不可修改的视图

    Collections还有几个方法,用于产生集合的不可修改视图。这些视图对现有集合增加了一个运行时的检査。如果发现试图对集合进行修改,就抛出一个异常,同时这个集合将保持未修改的状态。

    可以使用下面6种方法获得不可修改视图:

    Collections.unmodifiableCollection

    Collections.unmodifiableList

    Collections.unmodifiableSet

    Collections.unmodifiableSortedSet

    Collections.unmodifiableMap

    Collections.unmodifiableSortedMap

    每个方法都定义干一个接口。例如,Collections.unmodifiableListArrayListLinkedList或者任何实现了List接口的其他类一起协同工作。

    例如,假设想要査看某部分代码,但又不触及某个集合的内容,就可以迸行下列操作:

        List<String> staff = new LinkedList<String>();

        lookAt(new Collections.unmodifiableList(staff));

    Collections.unmodifiableList方法将返回一个实现List接口的类对象。其访问器方法将从staff集合中获取值。当然,lookAt方法可以调用List接口中的所有方法,而不只是访问器。但是所有的更改器方法(例如,add)已经被重新定义为抛出一个UnsupportedOperationException异常,而不是将调用传递给底层集合。

    不可修改视图并不是集合本身不可修改。仍然可以通过集合的原始引用(在这里是staff)对集合进行修改。并且仍然可以让集合的元素调用更改器方法。

    由于视图只是包装了接口而不是实际的集合对象,所以只能访问接口中定义的方法。例如,LinkedList类有一些非常方便的方法,addFirstaddLast,它们都不是List接口的方法,不能通过不可修改视图进行访问。

    警告:unmodifiableCollection方法(与本节稍后讨论的synchronizedCollectioncheckedCollection方法一样)将返回一个集合,它的equals方法不调用底层集合的equals方法。相反,它继承了Object类的equals方法,这个方法只是检测两个对象是否是同一个对象。如果将集或列表转换成集合,就再也无法检测其内容是否相同了。视图就是以这种方式运行的,因为内容是否相等的检测在分层结构的这一层上没有定义妥当。视图将以同样的方式处理hashCode方法。

    然而,unmodifiableSet类和unmodifiableList类却使用底层集合的equals方法和hashCode方法。

4. 同步视图

    如果由多个线程访问集合,就必须确保集不会被意外地破坏。例如,如果一个线程试图将元素添加到散列表中,同时另一个线程正在对散列表进行再散列,其结果将是灾难性的。

    类库的设计者使用视图机制来确保常规集合的线程安全,而不是实现线程安全的集合类。例如,ColIections类的静态synchronizedMap方法可以将任何一个映射表转换成具有同步访问方法的Map

           Map<String, Employee> map = Collections.synchronizedMap(new HashMap<String, Employee>());

    现在,就可以由多线程访问map对象了。像getput这类方法都是串行操作的,即在另一个线程调用另一个方法之前,刚才的方法调用必须彻底完成。第14章将会详细地讨论数据结构的同步访问。

5. 被检验视图

    Java SE 5.0增加了一组“被检验"视图,用来对泛型类型发生问题时提供调试支持。如同第12章中所述,实际上将错误类型的元素私自带到泛型集合中的问题极有可能发生。例如:

        ArrayList<String> strings = new ArrayList<String>();

        ArrayList rawList = strings; // get warning only, not an error, for

                                        // copatibility with legacy code

        rawList.add(new Date());// now Strings contains a Date object!

    这个错误的add命令在运行时检测不到。相反,只有在稍后的另一部分代码中调用get方法,并将结果转化为String时,这个类才会抛出异常。被检验视图可以探测到这类问题。下面定义了一个安全列表:

        List<String> safeStrings = Collections.checkedList(strings, String.class);

    视图的add方法将检测插入的对象是否属于给定的类。如果不属于给定的类,就立即抛出一个ClassCastException。这样做的好处是错误可以在正确的位置得以报告:

        ArrayList rawList = safeStrings;

        rawList.add(new Date());// Checked list throws a ClassCastException

    警告:被检测视图受限于虚拟机可以运行的运行时检查。例如,对于ArrayList<Pair<String>>,由于虚拟机有一个单独的“原始"Pair类,所以,无法阻止插入Pair<Dale>

6. 关于可选操作的说明

    通常,视图有一些局限性,即可能只可以读、无法改变大小、只支持删除而不支持插入,这些与映射表的键视图情况相同。如果试图进行不恰当的操作,受限制的视图就会抛出一个UnsupportedOperationException

    在集合和迭代器接口的API文档中,许多方法描述为“可选操作"。这看起来与接口的概念有所抵触。毕竟,接口的设计目的难道不是负责给出一个类必须实现的方法吗?确实,从理论的角度看,在这里给出的方法很难令人满意。一个更好的解决方案是为每个只读视图和不能改变集合大小的视图建立各自独立的两个接口。不过,这将会使接口的数目成倍增长,这让类库设计者无法接受。

    是否应该将“可选”方法这一技术扩展到用户的设计中呢?我们认为不应该,尽管集合被频繁地使用,其实现代码的风格也未必适用于其他问题领域。集合类库的设计者必须解决一组特别严格且又相互冲突的需求。用户希望类库应该易于学习、使用方便,彻底泛型化,面向通用性,同时又与手写算法一样高效,要同时达到所有目标的要求,或者尽量兼顾所有目标完全是不可能的。但是,在自己的编程问题中,很少遇到这样极端的局限性。应该能够找到一种不必依靠极端衡量“可选的”接口操作来解决这类问题的方案。

APIjava.util.Collections 1.2

  • static <E> Collection unmodifiableCollection(Collection<E> c)

  • static <E> List unmodifiableList(List<E> c)

  • static <E> Set unmodifiableSet(Set<E> c)

  • static <E> SortedSet unmodifiableSortedSet(SortedSet<E> c)

  • static <E> Map unmodifiableMap(Map<K, V> c)

  • static <K, V> SortedMap unmodifiableSortedMap(SortedMap<K, V> c):构造一个集合视图,其更改器方法将抛出一个UnsupportedOperationException。

  • static <E> Collection<E> synchronizedCollection(Collect1an<E> c)

  • static <E> List synchronizedList(List<E> c)

  • static <E> Set synchronizedSet(Set<E> c)

  • static <E> SortedSet synchron1zedSortedSet(SortedSet<E> c)

  • static <K, V> Map<K, V> synchronizedMap<Map<K, V> c)

  • static <K, V> SortedMap<K, V> synchronizedSortedMap(SortedMap<K, V> c):构造一个集合视图,其方法都是同步的。

  • static< E> Collection<E> checkedCollection(Collect1an<E> c)

  • static <E> List checkedList(List<E> c)

  • static< E> Set checkedSet(Set<E> c)

  • static< E> SortedSet synchron1zedSortedSet(SortedSet<E> c)

  • static< K, V> Map<K, V> checkedMap<Map<K, V> c)

  • static <K, V> SortedMap<K, V> checkedSortedMap(SortedMap<K, V> c):构造一个集合视图。如果插入一个错误类型的元素,将抛出一个ClassCastException。

  • static <E> List<E> nCopies(int n, E value)

  • static <E> Set<E> singleton(E value):构造一个对象视图,它既可以作为一个拥有n个相同元素的不可修改列表,又可以作为一个拥有单个元素的集。

APIjava.util.Arrays 1.2

  • static <E> IList<E> asList(E.,.. array):返回一个数组元素的列表视图。这个数组是可修改的,但其大小不可变。

APIjava.util.List<E> 1.2

  • List<E> subList(int firstIncluded, int firstExcluded):返回给定位置范围内的所有元素的列表视图。

APIjava.util.SortedSet<E> 1.2

  • SortedSet<E> subSet(E firstIncluded, E firstExcluded)

  • SortedSet<E> headSet(E firstExcluded)

  • SortedSet<E> tailSet(E firstIncluded):返回给定范围内的元素视图。

APIjava.util.NavigableSet<E> 6

  • NavigableSet<E> subSet(E from, boolean fromIncluded, E to, boolean toIncluded)

  • NavigableSet<E> headSet(E to, boolean toIncluded)

  • NavigableSet<E> tailSet(E from, boolean fromIncluded):返回给定范围内的元素视图。boolean标志决定视图是否包含边界。

APIjava.util.SortedMap<E> 1.2

  • SortedMap<E> subMap(E firstIncluded, E firstExcluded)

  • SortedMap<E> headMap(E firstExcluded)

  • SortedMap<E> tailMap(E firstIncluded):返回给定范围内的键条目的映射表视图。   

APIjava.util.NavigableMap<E> 6

  • NavigableMap<E> subMap(E from, boolean fromIncluded, E to, boolean toIncluded)

  • NavigableMap<E> headMap(E to, boolean toIncluded)

  • NavigableMap<E> tailMap(E from, boolean fromIncluded):返回给定范围内的键条目的映射表视图。boolean标志决定视图是否包含边界。

13.3.2 批操作

    到现在为止,列举的绝大多数示例都采用迭代器遍历集合,一次遍历一个元素。然而,可以使用类库中的批操作(bulk operations)避免频繁地使用迭代器。

    假设希望找出两个集的交(intersection),即两个集中共有的元素。首先,要建立一个新集,用于存放结果。

        Set<String> result = new HashSet<String>(a);

    这里利用了这样一个事实:每一个集合有一个构造器,其参数是保存初始值的另一个集合。

    接着,调用retainAll方法:

    result.retainAll(b);

    result中保存了既在a中出现,也在b中出现的元素。这时已经构成了交集,而且没有使用循环。

    可以将这个思路向前推进一步,将批操作应用于视图。例如,假如有一个映射表,将员工的ID映射为员工对象,并且建立了一个将要结束聘用期的所有员工的ID集。

        Map<String, Employee> staffMap = ...;

        Set<String> terminatedIDs = ...;

    直接建立一个键集,并删除终止聘用关系的所有员工的ID即可。

        staffMap.keySet().removeAll(terminatedIDs);

    由于键集是映射表的一个视图,所以,键与对应的员工名将会从映射表中自动地删除。

    通过使用一个子范围视图,可以将批操作限制于子列表和子集的操作上。例如,假设希望将一个列表的前10个元素添加到另一个容器中,可以建立一个子列表用于选择前10个元素:

        relocated.addAll(staff.subList(0, 10));

    这个子范围也可以成为更改操作的对象。

        staff.subList(0, 10).clear();

13.3.3 集合与数组之间的转换

    由于Java平台API中的大部分内容都是在集合框架创建之前设计的,所以,有时候需要在传统的数组与现代的集合之间进行转换。

    如果有一个数组需要转换为集合,Arrays.asList的包装器就可以实现这个目的。例如:

        String[] values = ...;

        HashSet<String> staff = new HashSet<String>(Arrays.asList(values));

    反过来,将集合转换为数组就有点难了。当然,可以使用toArray方法:

    Object[] values = staff.toArray();

    但是,这样做的结果是产生一个对象数组。即使知道集合中包含一个特定类型的对象也不能使用类型转换:

        String[] values = (String[]) staff.toArray(); // ERROR

    toArray方法返回的数组是一个Object数组,无法改变其类型。相反必须使用另外一种toArray方法,并将其设计为所希望的元素类型且长度为0的数组。随后返回的数组将与所创建的数组一样:

        String[] values = staff.toArray(new String[0]);

    如果愿意的话,可以构造一个指定大小的数组:

        String[] values = staff.toArray(new String[staff.size()]);

    在这种情况下,没有创建任何一个新数组。

    注释:为什么不直接将一个Class对象(例如,String.class)传递給toArray方法。其原因是这个方法具有“双重职责”,不仅要填充已有的数组(如果足够长)还要创建一个新数组。
0 0
原创粉丝点击