17.2 填充容器
虽然容器打印的问题解决了,容器的填充仍然像java.util.Arrays一样面临同样的不足。就像Arrays一样,相应的Collections类也有一些实用的static方法,其中包括fill()。与Arrays版本一样,此fill()方法也是只复制同一个对象引用来填充整个容器的,并且只对List对象有用,但是所产生的列表可以传递给构造器或addAll()方法:

这个示例展示了两种用对单个对象的引用来填充Collection的方式,第一种是使用Collections.nCopies()创建传递给构造器的List,这里填充的是ArrayList。
StringAddress的toString()方法调用Object.toString()并产生该类的名字,后面紧跟该对象的散列码的无符号十六进制表示(通过hashCode()生成的)。从输出中你可以看到所有引用都被设置为指向相同的对象,在第二种方法的Collection.fill()被调用之后也是如此。fill()方法的用处更有限,因为它只能替换已经在List中存在的元素,而不能添加新的元素。
17.2.1 一种Generator解决方案
事实上,所有的Collection子类型都有一个接收另一个Collection对象的构造器,用所接收的Collection对象中的元素来填充新的容器。为了更加容易地创建测试数据,我们需要做的是构建接收Generator(在第15章中定义并在第16章中深入探讨过)和quantity数值并将它们作为构造器参数的类:

这个类使用Generator在容器中放置所需数量的对象,然后所产生的容器可以传递给任何Collection的构造器,这个构造器会把其中的数据复制到自身中。addAll()方法是所有Collection子类型的一部分,它也可以用来组装现有的Collection。
泛型便利方法可以减少在使用类时所必需的类型检查数量。
CollectionData是适配器设计模式的一个实例,它将Generator适配到Collection的构造器上。
下面是初始化LinkedHashSet的一个示例:

这些元素的顺序与它们的插入顺序相同,因为LinkedHashSet维护的是保持了插入顺序的链接列表。
在第16章中定义的所有操作符现在通过CollectionData适配器都是可用的。下面是使用了其中两个操作符的示例:

RandomGenerator.String所产生的String长度是通过构造器参数控制的。
17.2.2 Map生成器
我们可以对Map使用相同的方式,但是这需要有一个Pair类,因为为了组装Map,每次调用Generator的next()方法都必须产生一个对象对(一个键和一个值):

key和value域都是public和final的,这是为了使Pair成为只读的数据传输对象(或信使)。
Map适配器现在可以使用各种不同的Generator、Iterator和常量值的组合来填充Map初始化对象:


这给了你一个机会,去选择使用单一的Generator<Pair<K,V>>、两个分离的Generator、一个Generator和一个常量值、一个Iterable(包括任何Collection)和一个Generator,还是一个Iterable和一个单一值。泛型便利方法可以减少在创建MapData类时所必需的类型检查数量。
下面是一个使用MapData的示例。LettersGenerator通过产生一个Iterator还实现了Iterable,通过这种方式,它可以被用来测试MapData.map()方法,而这些方法都需要用到Iterable:


这个示例也使用了第16章中的生成器。
可以使用工具来创建任何用于Map或Collection的生成数据集,然后通过构造器或Map.putAll()和Collection.addAll()方法来初始化Map和Collection。
17.2.3 使用Abstract类
对于产生用于容器的测试数据问题,另一种解决方式是创建定制的Collection和Map实现。每个java.util容器都有其自己的Abstract类,它们提供了该容器的部分实现,因此你必须做的只是去实现那些产生想要的容器所必需的方法。如果所产生的容器是只读的,就像它通常用的测试数据那样,那么你需要提供的方法数量将减少到最少。
尽管在本例中不是特别需要,但是下面的解决方案还是提供了一个机会来演示另一种设计模式:享元。你可以在普通的解决方案需要过多的对象,或者产生普通对象太占用空间时使用享元。享元模式使得对象的一部分可以被具体化,因此,与对象中的所有事物都包含在对象内部不同,我们可以在更加高效的外部表中查找对象的一部分或整体(或者通过某些其他节省空间的计算来产生对象的一部分或整体)。
这个示例的关键之处在于演示通过继承java.util.Abstract来创建定制的Map和Collection到底有多简单。为了创建只读的Map,可以继承AbstractMap并实现entrySet()。为了创建只读的Set,可以继承AbstractSet并实现iterator()和size()。
本例中使用的数据集是由世界上的国家以及它们的首都构成的Map。capitals()方法产生国家与首都的Map,name()方法产生国名的List。在两种情况中,你都可以通过提供表所需尺寸的int参数来获取部分列表:








二维数组String DATA是public的,因此它可以在其他地方使用。FlyweightMap必须实现entrySet()方法,它需要定制的Set实现和定制的Map.Entry类。这里正是享元部分:每个Map.Entry对象都只存储了它的索引,而不是实际的键和值。当你调用getKey()和getValue()时,它们会使用该索引来返回恰当的DATA元素。EntrySet可以确保它的size不会大于DATA。
你可以在EntrySet.Iterator中看到享元其他部分的实现。与为DATA中的每个数据对都创建Map.Entry对象不同,每个迭代器只有一个Map.Entry。Entry对象被用作数据的视窗,它只包含在静态字符串数组中的索引。你每次调用迭代器的next()方法时,Entry中的index都会递增,使其指向下一个元素对,然后从next()返回该Iterator所持有的单一的Entry对象。
select()方法将产生一个包含指定尺寸的EntrySet的FlyweightMap,它会被用于重载过的capitals()和names()方法,正如在main()中所演示的那样。
对于某些测试,Countries的尺寸受限会成为问题。我们可以采用与产生定制容器相同的方式来解决,其中定制容器是经过初始化的,并且具有任意尺寸的数据集。下面的类是一个List,它可以具有任意尺寸,并且用Integer数据(有效地)进行了预初始化:

为了从AbstractList创建只读的List,你必须实现get()和size()。这里再次使用了享元解决方案:当你寻找值时,get()将产生它,因此这个List实际上并不必组装。
下面是包含经过预初始化,并且都是唯一的Integer和String对的Map,它可以具有任意尺寸:


这里使用的是LinkedHashSet,而不是定制的Set类,因此享元并未完全实现。
练习1:(1) 创建一个List(用ArrayList和LinkedList都尝试一下),然后用Countries来填充。对该列表排序并打印,然后将Collections.shuffle()方法重复地应用于该列表,并且每次都打印它,这样你就可以看到shuffle()方法是如何每次都将列表随机打乱的了。
练习2:(2) 生成一个Map和Set,使其包含所有以字母A开头的国家。
练习3:(1) 使用Countries,用同样的数据多次填充Set,然后验证此Set中没有重复的元素。使用HashSet、LinkedHashSet和TreeSet做此测试。
练习4:(2) 创建一个Collection初始化器,它将打开一个文件,并用TextFile将其断开为单词,然后将这些单词作为所产生的Collection的数据源使用。请演示它是可以工作的。
练习5:(3) 修改CountingMapData.java,通过添加像Countries.java中那样的定制EntrySet类,来完全实现享元。






