如果一个程序只包含固定数量的且其生命期都是已知的对象,那么这是一个非常简单的程序。
通常,程序总是根据运行时才知道的某些条件去创建新对象。在此之前,不会知道所需对象的数量,甚至不知道确切的类型。
为解决这个普遍的编程问题,需要在任意时刻和任意位置创建任意数量的对象。所以,不能依靠创建命名的引用来持有每一个对象:MyType aReference;因为你不知道实际上会需要多少这样的引用。
Java有多种方式保存对象(对象的引用),例如:数组。数组是保存一组对象最有效的方式,若想保存一组基本类型数据,推荐使用数组。数组具有固定的尺寸。
但是很多时候,并不知道需要多少个对象,或者是否需要更复杂的方式来存储对象,Java为解决这个问题,提供了一套相当完整的容器类来解决这个问题:
基本的容器类包括:List,Set,Queue和Map。这些对象类型也称为集合类。但是Java中使用了Collection这个名字来指代该类库的一个特殊子集,所以作者使用了“容器”这个名字来指代该类库的一个特殊子集。
- 泛型和类型安全的容器
- 基本概念
- 添加一组元素
- 容器的打印
- List
- 迭代器
- LinkedList
- Stack
- Set
- Map
- Queue
- Collection和Iterator
- Foreach与迭代器
1.泛型和类型安全的容器
使用Java SE5之前的容器的一个主要问题就是编译器允许你向容器中插入不正确的类型。例如,考虑一个Apple对象的容器,我们使用最基本最可靠的容器ArrayList。现在,可以把ArrayList当做“可以自动扩充自身尺寸的数组”来看待。
使用ArrayList很简单:创建一个实例,用add()插入对象;用get()访问这些对象,需要索引,像数组一样,但是不使用方括号;size()方法,可以查看有多少对象在里面;
下面这个例子中,Apple和Orange都放置在了容器中,然后将它们取出。正常情况下,编译器会给警告信息:这个范例没有使用泛型。可以使用@SuppressWarnings注解及其参数表示“不受检查的异常”。
Apple和Orange类是有区别的,它们除了都是Object之外没有任何共性。因为ArrayList保存的是Object,因此不仅可以通过ArrayList的add()方法将Apple对象放进这个容器,还可以添加Orange对象,且编译器不会报错;
当使用ArrayList的get()方法来取出你认为是Apple的对象时,你得到的只是Object引用,必须将其转型为Apple,因此需要将整个表达式括起来,在调用Apple的id()方法之前,强制类型转换;否则将得到语法错误。
在运行时,当你试图将Orange对象转型为Apple时,你就会以前面提及的异常的形式得到一个错误。
使用Java泛型来创建类会非常复杂,但是应用预定义的泛型通常会很简单;
例如,要想定义用来保存Apple对象的ArrayList,可以声明ArrayList<Apple>,而不仅仅只是ArrayList,其中尖括号括起来的是类型参数(可以有多个),它指定了这个容器实例可以保存的类型。
通过使用泛型,就可以在编译期防止将错误类型的对象放置到容器中,下面是这样的例子:
现在编译器可以组织你将Orange放置到apples中,因此它变成了一个编译期错误,而不再是运行时错误。
在将元素从List中取出时,类型转换已经不再需要了,因为List知道它保存的是什么类型,因此它会在调用get()时替你执行转型。
如果不需要使用每个元素的索引,可以使用foreach语法来选择List中的每个元素。
当你指定了某个类型作为泛型参数时,你并不仅限于只能将确切类型的对象放置到容器中。向上转型也可以像作用于其他类型一样作用于泛型。
因此,可以将Apple的子类型添加到被指定为保存Apple对象的容器中。
2.基本概念
Java容器类类库的用途是“保存对象”,并将其划分为两个不同的概念:
1).Collection。一个独立元素的序列,这些元素都服从一条或多条规则。List必须按照插入的顺序保存元素,而Set不能有重复元素,Queue按照排队规则来确定对象产生的顺序(通常与它们被插入的顺序相同)
2).Map。一组成对的“键值对”对象,允许你使用键来查找值。ArrayList允许你使用数字来查找值,因此在某种意义上将,它将数字与对象关联在了一起。映射表允许我们使用另一个对象来查找对象,它也被称为“关联数组”,因为它将某些对象与另外一些对象关联在了一起;或者被称为“字典”,因为你可以使用键对象来查找值对象,就像在字典中使用单词来定义一样。Map是强大的编程工具。
理想情况下,编写的代码都是在与这些接口打交道,并且你唯一需要指定所使用精确类型的地方就是在创建的时候。下面创建一个List:
这里,ArrayList被向上转型为List,这与前一个示例中的处理方式正好相反。使用接口的目的在于如果你决定去修改你的实现,所需做的只是在创建处修改它,就像下面这样:
因此,你应该创建一个具体类的对象,将其转型为对应的接口,然后在其余的代码中都使用这个接口。
但是,这种方式并非总是有效的,因为某些类具有额外的功能,例如:LindedList具有在List接口中未包含的额外方法,而TreeMap也具有在Map接口中未包含的方法。如果需要使用这些方法,就不能将它们向上转型为更通用的接口。
Collection接口概括了序列的概念--一种存放一组对象的方式,下面示例用Integer对象填充了一个Collection(这里用ArrayList表示),然后打印所产生的容器中的所有元素:
任何继承自Collection的类的对象都可以正常工作,但是ArrayList是最基本的序列类型。
在使用ArrayList,或者任何种类的List时,add()总是表示把它放进去,因为List不关心它是否存在重复。
所有的Collection都可以用foreach语法遍历,就像这里所展示的。
3.添加一组元素
在java.util的Arrays和Collection类中都有很多实用方法,可以在一个Collection中添加一组元素。Arrays.asList()方法接受一个数组或是一个用逗号分隔的元素列表(使用可变参数),并将其转换为一个List对象。
Collection.addAll()方法接受一个Collection对象,以及一个数组或是一个用逗号分割的列表,并将元素添加到Collection中。
Collection的构造器可以接受另一个Collection,用它来将自身初始化,因此可以使用Arrays.List()来为这个构造器产生输入。
Collection.addAll()方法运行起来要快好多,而且构建一个不包含元素的Collection,然后调用Collections.addAll()这种方式很方便,因此它是首选方式。
Collection.addAll()成员方法只能接受另一个Collection对象作为参数,因此它不如Arrays.asList()或Collections.addAll()灵活,这两个方法使用的都是可变参数列表。
也可以直接使用Arrays.asList()的输出,将其当作List,在这种情况下,其底层表示的是数组,因此不能调整尺寸。如果试图用add()或delete()方法在这种列表中添加或删除元素,就要可能会引发改变数组尺寸的尝试,因此会得到“UNsupported Operation”错误。
Arrays.asList()方法的限制是它对所产生的List的类型做出了最理想的假设,而并没有注意对它会赋予什么样的类型。这样就会引发问题:
当试图创建snow2时,Arrays.asList()中只有Powder类型,因此它会创建List<Powder>而不是List<Snow>,尽管Collections.addAll()工作的很好,因为它从第一个参数中了解到了目标类型是什么。
正如创建snow4的操作中所看到的,可以在Arrays.asList()中间插入一条“线索”,以告诉编译器对于由Arrays.asList()产生的List类型,实际的目标应该是什么。这称为显式类型参数说明。
总结:
若使用Arrays.asList()给一个List赋值,需要使用显式类型参数说明,否则会自动寻找它们的共同最浅的基类,这样如果向上转型至根类,则会报错;
但是若使用,Collections.addAll()则不存在这个问题。
所谓显式类型参数,就是在右边用尖括号告诉编译器这是一个什么类型的参数。
4.容器的打印
必须使用Arrays.toString()来产生数组的可打印表示,但是容器不需要,例子如下:
这里展示了Java容器类库中的两种主要类型,它们的区别在于容器中每个"槽"保存的元素个数。
Collection在每个槽中只能保存一个元素。此类容器包括:List,它以特定的顺序保存一组元素;Set,元素不能重复;Queue,只允许在容器的一“端”插入对象,并从另外一“端”移除对象。
Map在每个槽内保存了两个对象,即键和与之相关联的值。
在结果中,都是使用默认的打印行为(使用容器提供的toString()方法)即可生成可读性很好的结果。Collection打印出来的内容用方括号括住,每个元素由逗号分隔。Map则用大括号括住,键与值由等号联系(键在等号左边,值在等号右边)。
第一个fill()方法可以作用于所有类型的Collection,这些类型都实现了用来添加新元素的add()方法。
ArrayList和LinkedList都是List类型,从输出可以看出,它们都按照被插入的顺序保存元素。两者的不同之处不仅在于执行某些类型的操作时的性能,而且LinkedList包含的操作也多于ArrayList。
HashSet、TreeSet和LinkedList都是List类型,输出显示在Set中,每个相同的项只保存一次,但是输出也显示了不同的Set实现存储元素的方式也不同。HashSet使用的是相当复杂的方式来存储元素的,存储的顺序并无实际意义;如果存储顺序很重要,可以使用TreeSet,它按照比较结果的升序保存对象;或者使用LinkedList,它按照被添加的顺序保存对象。
Map(也称为关联数组)使得你可以通过键来查找对象,就像一个简单的数据库。键所关联的对象称为值。
Map.put(key, value)方法将增加一个值,并将它与某个键关联起来。Map.get(key)方法将产生与这个键相关联的值。
不必指定Map的尺寸,因为它会自己自动地调整尺寸。Map还知道如何打印自己,它会显示相关联的键和值。
本例使用三种基本风格的Map:HashMap,TreeMap和LinkedMap。与HashSet一样,HashMap也提供了最快的查找技术,存储顺序没有任何实际意义。TreeMap按照比较结果的升序保存键,而LinkedHashMap则按照插入顺序保存键,同时还保留了HashMap的查询速度。
5.List
List承诺可以将元素维护在特定的序列中。List接口在Collection的基础上添加了大量的方法,使得可以在List的中间插入和移除元素。
有两种类型的List:
1).基本的ArrayList;它长于随机访问元素,但是在List的中间插入和移除元素时较慢;
2).LinkedList;它通过代价较低的在List中间插入和删除操作,提供了优化的顺序访问。LinkedList在随机访问方面相对比较慢,但是它的特性集较ArrayList更大。
下面的示例中的Pets类来自于typeinfo.pets,有两点需要注意:1)有一个Pet类,以及Pet的各种子类型;2)静态的Pets.arrayList()方法将返回一个填充了随机选取的Pet对象的ArrayList:
第一行输出展示了最初的由Pet构成的List。与数组不同,List允许在它被创之后添加、移除元素,或者自我调整尺寸。这正是它的重要价值所在:一种可修改的序列。
contains()方法可以确定某个对象是否在列表中,若移除一个对象,则可以将这个对象传递给remove()方法。同样,如果有一个对象的引用,则可以用indexOf()来发现该对象在List中所处位置的索引编号。
当确定一个元素是否属于某个List,发现某个元素的索引,以及从某个List中移除一个元素时,都会用到equals()方法。每个Pet都别定义为唯一的对象,即使在列表中已经有两个Cymric,若再创建一个Cymric,并把它传递给indexOf()方法,其结果仍会是-1,而且尝试使用remove()方法来删除这个对象,也会返回false。
对于其他类,如String,只有在内容完全一样的情况下才会是等价的,此时equals()将返回True。因此,List的行为根据equals()的行为而有所变化。
在List中间插入元素是可行的,但是有一个问题:对于LinkedList,在列表中间插入和删除都是廉价操作,但是对于ArrayList,这是代价高昂的操作。
subList()方法允许你从较大的列表中创建出一个片段,将其结果传递给这个较大的列表的containsAll()方法是,会得到True。在这里,顺序并不重要,在sub上调用名字很直观的Collections.sort()和Collections.shuffle()方法,不会影响containsAll()的结果。subList()所产生的列表的幕后就是初始列表,因此,对返回的列表的修改都会反映到初始类表中,反之亦然。
retainAll()方法是一种有效的“交集”操作,在本例中,它保留了所有同时在copy与sub中的元素。所产生的行为依赖于equals()方法。
用元素的索引值来移除元素的结果,与通过对象引用来移除相比,它显得更加直观,因为在使用索引值时,不必担心equals()的行为。
removeAll()方法的行为也是基于equals()方法的,它将从List中移除在参数List中的所有元素。
set()方法根据指定的索引(第一个参数),用第二个参数替换整个位置的元素。
对于List,有一个重载的addAll()方法可以在初始List的中间插入新的列表,而不仅仅只能用Collection中的addAll()方法将其追加到表尾。、
isEmpty()和clear()方法,前一个返回boolean值,判断列表是否为空;后一个将整个列表清空。
可以通过方法toArray()方法,将任意的Collection转换为一个数组,这是一个重载方法,其无参数版本返回的是Object数组;带参版本将产生指定类型的数据。若参数数组太小,存放不下List中的所有元素,toArray()方法将创建一个具有合适尺寸的数组。
Pet对象具有一个id()方法,可以在所产生的数组中的对象上调用这个方法。
6.迭代器
任何容器类,都必须有某种方式可以插入元素并将它们再次取回。毕竟,持有事物是容器最基本的工作。对于List,add()是插入元素的方法之一;而get()是取出元素的方法之一。
从更高层的角度来看,会发现一个缺点:要使用容器,必须对容器的确切类型编程。考虑下面的情况:如果原本是对着List编码的,但是后来发现如果能够把相同的代码应用于Set,将会显得非常方便,此时应该怎么办?如何在不重写代码的情况下就可以应用于不同类型的容器?
迭代器(也是一种设计模式)可以达成此目的。迭代器是一个对象,它的工作是遍历并选择序列中的对象,而客户端程序员不必知道或关心该序列底层的结构。此外迭代器通常被称为轻量级对象:创建它的代价小。因此,经常可以见到对迭代器有些奇怪的限制;例如,Java的Iterator只能单向移动,这个Iterator只能用来:
1).使用方法iterator()要求容器返回一个Iterator。Iterator将准备好返回序列的第一个元素。
2).使用next()获得序列中的下一个元素。
3).使用hasNext()检查序列中是否还要元素。
4).使用remove()将迭代器新近返回的元素删除。
观察下面例子:
有了Iterator就不必为容器中元素的数量操心了,那是由hasNext()和next()关心的事情。
如果只是向前遍历List,并不打算修改List对象本身,foreach语法会显得更加简洁。
Iterator还可以移除由next()产生的最后一个元素,这意味着在调用remove()之前必须先调用next()。
接受对象容器并传递它,从而在每个对象上都执行操作。
现在考虑一个display()方法,它不必知晓容器的确切类型:
display()方法不包含任何有关它所遍历的序列的类型信息,而这也展示了Iterator的真正威力 。
能够将遍历序列的操作与序列底层的结构分离,正因如此,可以说:迭代器统一了对容器的访问方式。
ListIterator:
ListIterator是一个更加强大的Iterator的子类型,它只能用于各种List类的访问。尽管Iterator只能向前移动,但是ListIterator可以双向移动。还可以产生相对于迭代器在列表中指向的当前位置的前一个和后一个元素的索引,并且可以使用set()方法替换它访问过得最后一个元素。可以通过调用ListIterator()方法产生一个指向List开始处的ListIterator,并且还可以通过调用listIterator(n)方法创建一个一开始就指向列表索引为n的元素处的ListIterator。
看下面例子:
Pet.randomPet()方法用来替换在列表中从位置3开始向前的所有Pet对象。
7.LinkedList
LinkedList也像ArrayList一样实现了基本的List接口,但是它执行某些操作(在List的中间插入和移除)时比ArrayList更高效,但是随机访问操作方面却要逊色一些。
LinkedList还添加了可以使其用作栈、队列或双端队列的方法。
这些方法中有些彼此之间只是名称有些差异,例如:
getFirst()和element()完全一样,它们都返回列表的头(第一个元素),而并不移除它,如果List为空,则抛出NoSuchElementException。peek()方法与这两个方式只是稍有差异,它在列表为空时返回null。
removeFirst()与remove()也是完全一样,它们移除并返回列表的头,而在列表为空时抛出NoSuchElementException。poll()稍有差异,它在列表为空时返回null。
addFirst()与add()和addLast()相同,它们都将某个元素插入到列表的尾部。
removeLast()移除并返回列表的最后一个元素。
例子如下:重复地执行了ListFeatures.java中的行为:
Pets.arrayList()的结果交给了LinkedList的构造器,以便使用它来组装LinkedList。Queue中,在LinkedList的基础上添加了element()、offer()、peek()、poll()和remove()方法,以使其可以成为一个Queue实现。
8.Stack
“栈”通常是指“后进先出(LIFO)”的容器,有时也称为叠加栈。
LinkedList具有能够直接实现栈的所有功能的方法,因此可以直接将LinkedList作为栈使用。不过,有时一个真正的“栈”更能把事情讲清楚。
通过使用泛型,引入了在栈的类定义中最简单的可行示例。类名之后的<T>告诉编译器这将是一个参数化类型,而其中的类型参数,即在类被使用时将会被实际类型替换的参数,就是T。
Stack使用LinkedList实现的,而LinkedList也被告知它将持有T类型对象;注意,push()接受的是T类型的对象,而peek()和pop()将返回T类型的对象。peek()将提供栈顶元素,但是并不将其从栈顶移除,而pop()将移除并返回栈顶元素。(这里将LinkedList的头当成了栈顶)
如果只需要栈的行为,使用继承就不合适了,因为这样会产生具有LinkedList的其他所有方法的类:
如果想在自己的代码中使用这个Stack类,当你在创建其实例时,就需要完整指定包名,或者更改这个类的名称;否则,就有可能与java.util包中的Stack发生冲突。例如,如果我们在上面的例子中导入java.util.*,那么就必须使用包名以防止冲突:
这两个Stack具有相同的接口,但是在java.util中没有任何公共的Stack接口。
因为Java设计人员的失误,java.util.Stack设计欠佳,所以建议使用LinkedList来产生更好的Stack。
9.Set
Set不保存重复的元素;Set中最常用的是测试归属性,可以很容易地询问某个对象是否在某个Set中。正因如此,查找成了Set中最重要的操作,因此通常会选择一个HashSet的实现,它专门对快速查找进行了优化。(Hash查找最快。)
Set具有与Collection完全一样的接口,因此没有任何额外的功能,实际上Set就是Collection,只是行为不同。Set是基于对象的值来确定归属性的。
下面是使用存放Integer对象的HashSet的示例:
在0到29之间的 10000个生育户数被添加到了Set中,因此,每个数都会重复多次,但是看结果,每一个数只有一个实例出现在结果中。
输出顺序没有任何规律可循,因为处于速度原因的考虑,HashSet使用了散列。HashSet所维护的顺序与TreeSet或LinkedHashSet都不同,因为它们的实现具有不同的元素存储方式。
TreeSet将元素存储在红-黑数据结构中,而HashSet使用的是散列函数。LinkedHashSet因为查询速度的原因也使用了散列,但是看起来它使用了链表来维护元素的插入顺序。
如果想对结果排序,一种方式是使用TreeSet来替代HashSet。
你将会执行的最常见的操作之一,就是使用contains()测试Set的归属性,但是还有很多操作会想起文氏图:
能够产生每个元素都唯一的列表是相当有用的功能,例如:想要列出在上面的SetOperations.java文件中所有的单词的时候。通过使用本书将要介绍的net.mindview.TextFile工具,可以打开一个文件,并将其读入一个Set中:
TextFile继承自List<String>,其构造器将打开文件,并根据正则表达式“\\W+”将其断开为单词,这个正则表达式表示“一个或多个字母”,所产生的结果传递给了TreeSet的构造器,它将把List中的内容添加到自身中。
由于它是TreeSet,因此其结果是排序的。这里排序是按照字典序进行的,因此大写和小写被划分到了不同的组中。String.CASE_INSENTIVE_ORDER比较器表示按照字母序排序。
10.Map
将对象映射到其他对象的能力是一种解决编程问题的杀手锏;考虑一个程序,它将用来检查Java的Random类的随机性。理想情况下,Random可以产生理想的数字分布,但要想测试它,则需要生成大量的随机数,并对落入各种不同范围的数字进行计数。Map可以很容易地解决该问题。在这个问题中,键是由Random产生的数字,值是该数字出现的次数:
在main()中,自动包装机制将随机生成的int转换为HashMap可以使用的Integer引用(不能使用基本类型的容器)。
如果键不在容器中,get()方法将返回null(表示该数字第一次随机生成)。否则,get()方法将产生与该键相关联的Integer值,然后这个值被递增。
下面的示例允许你使用一个String描述来查找Pet,它还展示了你可以使用怎样的方法通过使用containsKey()和containsValue()来测试Map,以便查看它是否包含某个键或某个值:
Map与数组和其他的Collection一样,可以很容易地扩展到多维,而我们只需将其值设置为Map(这些Map的值可以是其他容器,甚至是其他Map)。
因此,能够很容易地将容器组合起来从而快速地生成强大的数据结构。
假如:假设你正在跟踪拥有多个宠物的人,你所需只是一个Map<Person, List<Pet>>:
Map可以返回他的键的Set,它的值的Collection,或者它的键值对的Set。
keySet()方法产生了由petPeople中的所有键组成的Set,它在foreach语句中被用来迭代遍历该Map。
11.Queue
队列是一个典型的先进先出(FIFO)的容器。即从容器的一端放入事物,从另一端取出,并且将事物放入容器的顺序与取出的顺序是相同的。队列常被当作一种可靠的将对象从程序的某个区域传输到另一个区域的途径。队列在并发编程中特别重要:
LinkedList提供了方法以支持队列的行为,并且它实现了Queue接口,因此LinkedList可以用作Queue的一种实现。通过将LinkedList向上转型为Queue,下面的示例使用了在Queue接口中与Queue相关的方法:
offer()方法是与Queue相关的方法之一,它在允许的情况下,将一个元素插入到队尾,或者返回false。
peek()和element()都将在不移除的情况下返回队头,但是peek()方法在队列为空时返回null,而element()会抛出NoSuchElementException异常;
poll()和remove()方法将移除并返回队头,但是poll()在队列为空时返回null,而remove()会抛出NoSuchElementException异常。
自动包装机制会自动地将nextInt()方法的int结果转换为queue所需的Integer对象,将char c转换为qc所需的Character对象;
Queue接口窄化了对LinkedList的方法的访问权限,以使得只有恰当的方法才可以使用,因此,你能够访问的LinkedList的方法会变少。(这里可以将Queue转型为LinkedList,但是不建议这么做)
注意:与Queue相关的方法提供了完整而独立的功能。即,对于Queue所继承的Collection,在不需要使用它的任何方法的情况下,就可以拥有一个可用的Queue。
PriorityQueue:
先进先出描述了最典型的队列规则。队列规则是指在给定一组队列中的元素的情况下,确定下一个弹出队列的元素的规则。先进先出声明的是下一个元素应该是等待时间最长的元素。
优先级队列声明下一个弹出元素是最需要的元素(具有最高的优先级)。如果构建了一个消息系统,某些消息比其他消息更重要,因而应该更快地得到处理,那么它们何时得到处理就与它们何时到达无关。
在PriorityQueue上调用offer()方法来插入一个对象时,这个对象会在队列中被排序。默认的排序将使用对象在队列中的自然顺序。但是,你可以通过提供自己的Comparator来修改这个顺序。
PriorityQueue可以确保你调用peek()、pool()和remove()方法时,获取的元素将是队列中优先级最高的元素。
下面例子中,第一个值集与前一个示例中的随机值相同,因此可以看到它们从PriorityQueue中弹出的顺序与前一个示例不同:
在这里,重复是允许的,最小的值拥有最高的优先级(如果是String,空格也算作值,并且优先级比字母的高)。
例子展示了如何通过自己的Comparator对象来改变排序,这里调用Collection.reverseorder()来对原来的顺序进行倒序排列。
最后一部分添加了一个HashSet来消除重复的Character;
Integer、String和Character可以与PriorityQueue一起工作,因为这些类已经内建了自然排序。如果你想在PriorityQueue中使用自己的类,就必须包括额外的功能以产生自然排序,或者必须提供自己的Comparator。
12.Collection和Iterator
Collection是描述所有序列容器的共性的根接口,它可能会被认为是一个“附属接口”,即因为要表示其他若干接口的共性而出现的接口。另外,java.util.AbstractCollection类提供了Collection的默认实现,使得你可以创建AbstractCollection的子类型,而其中没有不必要的代码重复。
使用接口描述的一个理由是它可以使我们能够创建更通用的代码。通过针对接口而非具体实现来编写代码,我们的代码可以应用于更多的对象类型。因此,如果我编写的方法将接受一个Collection,那么该方法就可以应用于任何实现了Collection的类--这也使得一个新类可以选择去实现Collection接口,以便我的方法可以使用它。
将Collection和迭代器绑定到了一起,实现Collection就意味着需要提供iterator()方法:
两个版本的display()方法都可以使用Map或Collection的子类型来工作,而且Collection接口和Iterator都可以将display()方法与底层容器的特定实现解耦。
这两种方式都可以凑效,但是使用Collection要更方便一些,因为它是Iterator类型,因此,在display(Collection)实现中,可以使用foreach结构,从而使代码更加清晰。
当你要实现一个不是Collection的外部类时,由于让它去实现Collection接口可能非常困难或麻烦,因此使用Iterator就会变得非常吸引人。
例如:如果我们继承一个持有Pet对象的类来创建一个Collection的实现,那么我们必须实现所有的Collection方法,即使我们在display()方法中不必使用它们,也必须如此。
尽管这可以通过继承AbstractCollection而很容易地实现,但是你无论如何还是要被强制去执行iterator()和size(),以便提供AbstractCollection没有实现,但是AbstractCollection中的其他方法会使用到的方法:
remove()方法是一个“可选操作”,这里不必实现它,如果你调用它,它会抛出异常。
从本例中可以看到,如果你实现Collection,就必须实现iterator(),并且只拿实现iterator()与继承AbstractCollection相比,花费的代价只有略微减少。但是,如果你的类已经继承了其他的类,那么你就不能再继承AbstractCollection了。在这种情况下,要实现Collection,就必须实现该接口中的所有方法。此时,继承并提供创建迭代器的能力就会显得容易很多了。
NonCollectionSequence继承自PetSequence
这里用了Iterator的匿名内部类;
生成Iterator是将队列与消费队列的方法连接在一起耦合度最小的方式,并且与实现Collection相比,它在序列上所施加的约束也少得多。
13.Foreach与迭代器
到目前为止,foreach语法主要用于数组,但是它也可以应用于任何Collection对象。你实际上已经看到过很多使用ArrayList时用到它的示例,下面是一个更通用的证明:、
由于cs是一个Collection,所以这段代码展示了能够与foreach一起工作是所有Collection对象的特性。
之所以能够工作,是因为Java SE5引入了新的被称为Iterable的接口,该接口包含一个能够产生Iterator的iterator()方法,并且Iterator接口被foreach用来在序列中移动。因此,如果你创建了任何实现Iterable的类,都可以将它用于foreach语句中:
iterator()方法返回的是实现了Iterator<String>的匿名内部类的实例,该匿名内部类可以遍历数组中的所有单词。
在main()中,可以看到IterableClass确实可以用于foreach语句中。
在Java SE5中,大量的类都是Iterable类型,主要包括所有的Collection类。例如:下面的代码可以显示所有的操作系统环境变量:
System.getenv()返回一个Map,entrySet()产生一个由Map.Entry()的元素构成的Set,并且这个Set是一个Iterable,因此它可以用于foreach循环。
foreach语句可以用于数组或其他任何Iterable,但是这并不意味着数组肯定也是一个Iterable,而任何自动包装也不会自动发生:
尝试把数组当作一个Iterable参数传递会导致失败,这说明不存在任何从数组到Iterable的自动转换,必须手工执行这种转换。
适配器方法惯用法:
如果现在有一个Iterable类,你想要添加一种或多种在foreach语句中使用这个类的方法,应该怎么做呢?
例如:假设你希望可以选择以向前或是向后的方法迭代一个单词列表。如果直接继承这个类,并覆盖iterator()方法,你只能替换现有的方法,而不能实现选择。
一种解决方案是所谓适配器方法的惯用法。“适配器”部分来自于设计模式,因为你必须提供特定接口以满足foreach语句。当你有一个接口并需要另一个接口时,编写适配器就可以解决问题。这里,我希望在默认的前向迭代器的基础上,添加产生反向迭代器的能力,因此我不能使用覆盖,而是添加一个能够产生Iterable对象的方法,该对象可以用于foreach语句。这使得我们可提供多种使用foreach的方式:
如果直接将ral对象置于foreach语句中,将得到(默认的)前向迭代器。但是如果在该对象上调用reversed()方法,就会产生不同的行为。
通过使用这种方式,可以在IterableClass.java示例中添加两种适配器方法:
注意,第二个方法random()没有创建它自己的Iterable,而是直接返回被打乱的List中的Iterator。
从输出中可以看到,Collection.shuffle()方法没有影响到原来的数组,而只是打乱了shuffled中的引用。之所以这样,只是因为randomized()方法用一个ArrayList(shuffled)将Arrays.asList()方法的结果包装了起来。如果这个由Arrays.asList()方法产生的List被直接打乱,那么它就会修改底层的数组,如下:
在第一种情况中,Arrays.asList()的输出被传递给了ArrayList()的构造器,因此打乱这些引用不会修改该数组。
但是,如果直接使用Arrays.asList(ia)的结果,这种打乱就会修改ia的顺序。意识到Arrays.asList()产生的List对象会使用底层数组作为其物理实现是很重要的。
只要你执行的操作会修改这个List,并且你不想原来的数组被修改,那么你就应该在另一个容器中创建一个副本。
14.总结
Java提供了大量持有对象的方式:
1).数组将数字与对象联系起来。它保存类型明确的对象,查询对象时,不需要对结果做类型转换。它可以是多维的,可以保存基本类型的数据。但是,数组一旦生成,其容量就不能改变。
2).Collection保存单一的元素,而Map保存相关联的键值对。有了Java的泛型,并且在从容器中获取元素时,不必进行类型转换。各种Collection和各种Map都可以在你向其中添加更多的元素时,自动调整其尺寸。容器不能持有基本类型,但是自动包装机制会仔细地执行基本类型到容器中所持有的包装器类型之间的双向转换。
3).像数组一样,List也建立数字索引与对象的关联,因此,数组和List都是排好序的容器。List能够自动扩充容量。
4).如果要进行大量的随机访问,就使用ArrayList;如果要经常从表中间插入或删除元素,则应该使用LinkedList。
5).各种Queue以及栈的行为,由LinkedList提供支持。
6).Map是一种将对象(而非数字)与对象相关联的设计。HashMap设计用来快速访问;而TreeMap保持“键”始终处于排序状态,所以没有HashMap快。LinkedHashMap保持元素插入的顺序,但是也通过散列提供了快速访问能力。
7).Set不接受重复元素。HashSet提供最快的查询速度,而TreeSet保持元素处于排序状态。LinkedHashSet以插入顺序保持元素。
8).新程序中不应该使用过时的Vector、Hashtable和Stack。
下图是Java容器的简图:
总共有四种容器:Map,List、Set和Queue。
点线框表示接口,实线框表示普通的(具体的)类。带有空心箭头的点线表示一个特定的类实现了一个接口;实心箭头表示某个类可以生成箭头所指向类的对象。
例如:任意的Collection可以生成Iterator,而List可以生成ListIterator(也能生成普通的Iterator,因为List继承自Collection)。