11.3 添加一组元素
在java.util包中的Arrays和Collection类中都有很多实用方讼,可以在一个Collection中添加
一组元素。Arrays.asList()方法接受一个数组或是一个用逗号分割的元素列表(使用可变参数) ,
并将其转换为一个List对象。ColIections.addAlI()方法接受一个Collection对象,以及一个数组或
是一个用逗号分割的列表,将元素添加到Collection中。下面的示例展示了这两个方怯,以及更
加传统addAlI()方法,所有ColIection类型都包含该方法:
package com.cy.container; import java.util.*; public class AddingGroups { public static void main(String[] args) { Collection<Integer> collection = new ArrayList<Integer>(Arrays.asList(1,2,3,4,5)); Integer[] moreInts = { 6, 7, 8, 9, 10 }; collection.addAll(Arrays.asList(moreInts)); Collections.addAll(collection, 11,12,13,14,15); Collections.addAll(collection, moreInts); System.out.println(collection); // Produces a list "backed by" an array: List<Integer> list = Arrays.asList(16, 17, 18, 19, 20); list.set(1, 99); // OK -- modify an element list.add(21); // Runtime error because the underlying array cannot be resized. } }
打印:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 6, 7, 8, 9, 10] Exception in thread "main" java.lang.UnsupportedOperationException at java.util.AbstractList.add(AbstractList.java:148) at java.util.AbstractList.add(AbstractList.java:108) at com.cy.container.AddingGroups.main(AddingGroups.java:19)
Collection的构造器可以接受另一个Collection ,用它来将自身初始化,因此你可以使用
Arrays.List()来为这个构造器产生输入。但是, Collection.addAlIO方捧运行起来要快得多,而且
构建一个不包含元素的Collection ,然后调用Collections.addAII()这种方式很方便,因此它是首
选方式。
Collection.addAII()成员方法只能接受另一个Collection对象作为参数,因此它不如
Arrays.asList()或Collections.addAII()灵活,这两个方法使用的都是可变参数列表。
你也可以直接使用Arrays.asList()的输出,将其当作List ,但是在这种情况下,其底层表示
的是数组,因此不能调整尺寸。如果你试图用add()或delete()方法在这种列表中添加或删除元素,
就有可能会引发去改变数组尺寸的尝试,因此你将在运行时获得"Unsupported Operation (不支
持的操作)错误。
11.4一些基本类型的容器
package com.cy.container; //: holding/PrintingContainers.java // Containers print themselves automatically. import java.util.*; import static com.java.util.Print.*; public class PrintingContainers { static Collection fill(Collection<String> collection) { collection.add("rat"); collection.add("cat"); collection.add("dog"); collection.add("dog"); return collection; } static Map fill(Map<String,String> map) { map.put("rat", "Fuzzy"); map.put("cat", "Rags"); map.put("dog", "Bosco"); map.put("dog", "Spot"); return map; } public static void main(String[] args) { print(fill(new ArrayList<String>())); print(fill(new LinkedList<String>())); print(fill(new HashSet<String>())); print(fill(new TreeSet<String>())); print(fill(new LinkedHashSet<String>())); print(fill(new HashMap<String,String>())); print(fill(new TreeMap<String,String>())); print(fill(new LinkedHashMap<String,String>())); } } /* Output: [rat, cat, dog, dog] [rat, cat, dog, dog] [dog, cat, rat] [cat, dog, rat] [rat, cat, dog] {dog=Spot, cat=Rags, rat=Fuzzy} {cat=Rags, dog=Spot, rat=Fuzzy} {rat=Fuzzy, cat=Rags, dog=Spot} *///:~
ArrayList和LinkedList都是List类型,从输出可以看出,它们都按照被插入的顺序保存元素。
两者的不同之处不仅在于执行某些类型的操作时的性能,而且LinkedList包含的操作也多于
ArrayList。这些将在本章后续部分更详细地讨论。
HashSet 、TreeSet和LinkedHashSet郁是Set类型,输出显示在Set中,每个相同的项只有保
存一次,但是输出也显示了不同的Set实现存储元素的方式也不同。
Hashset使用的是相当复杂的方式来存储元素的,这种方式将在第17章中介绍,此刻你只需要知道这种技术是最快的获取
元素方式,因此,存储的顺序看起来并无实际意义(通常你只会关心某事物是否是某个Set的成
员,而不会关心它在Set出现的顺序〉。如果存储顺序很重要,那么可以使用TreeSet ,它按照比
较结果的升序保存对象;或者使用LinkedHashSet ,它按照被添加的顺序保存对象。
你不必指定(或考虑) Map的尺寸,因为它自己会自动地调整尺寸。Map还知道如
何打印自己,它会显示相关联的键和值。键和值在Map中的保存顺序并不是它们的插入顺序,因为HashMap实现使用的是一种非常快的算法来控制顺序。
本例使用了三种基本风格的Map: HashMap 、TreeMap和LinkedHashMap。 与HashSet一
样, HashMap也提供了最快的查找技术,也没有按照任何明显的顺序来保存其元素。TreeMap
按照比较结果的升序保存键,而LinkedHashMap则按照插入顺序保存键,同时还保留了
HashMap的查询速度。
11.5 List
有两种类型的List:
• 基本的ArrayList ,它长于随机访问元素,但是在List的中间插入和移除元素时较慢。
• LinkedList ,它通过代价较低的在List中间进行的插入和删除操作,提供了优化的顺序访问。LinkedList在随机访问方面相对比较比较慢,但是它的特性集较ArrayList更大。
在List中提插入元素是可行的,也是这带来了一个问题:
对于LinkedList ,在列表中插入和删除都是廉价操作,但是对于ArrayList ,这可是代价高昂的操作。这是否意味着你应该永远都不要在ArrayList的中间插入元素,并最好是切换到LinkedList? 不,这仅仅意味着,你应该意识到这个问题,如果你开始在某个ArrayList的中间执行很多插入操作,并且你的程序开始变慢,那么你应该看看你的List实现有可能就是罪耻祸首(发现此类瓶颈的最佳方式是使用仿真器)优化是一个很棘手的问题,最好的策略就是置之不顾,直到你发现需要担心它了(尽管理解这些
问题总是一种好的思路)。
11.6迭代器
我们有时会说:选代器统一了对容器的访问方式。
11.6.1 Listlterator
ListIterator是一个更加强大的Iterator的子类型,它只能用于各种List类的访问。尽管
Iterator只能向前移动,但是ListIterator可以双向移动。它还可以产生相对于选代器在列袋中指
向的当前位置的前一个和后一个元素的索引,并且可以使用set()方法替换它访问过的最后一个
元素。你可以通过调用listIterator()方法产生一个指向List开始处的ListIterator,并且还可以通
过调用ListIterator(n)方法创建一个一开始就指向列表索引为n的元素处的ListIterator.
package com.cy.container; //: holding/ListIteration.java import typeinfo.pets.*; import java.util.*; public class ListIteration { public static void main(String[] args) { List<Pet> pets = Pets.arrayList(8); ListIterator<Pet> it = pets.listIterator(); while(it.hasNext()) System.out.print(it.next() + ", " + it.nextIndex() + ", " + it.previousIndex() + "; "); System.out.println(); // Backwards: while(it.hasPrevious()) System.out.print(it.previous().id() + " "); System.out.println(); System.out.println(pets); it = pets.listIterator(3); while(it.hasNext()) { it.next(); it.set(Pets.randomPet()); } System.out.println(pets); } } /* Output: Rat, 1, 0; Manx, 2, 1; Cymric, 3, 2; Mutt, 4, 3; Pug, 5, 4; Cymric, 6, 5; Pug, 7, 6; Manx, 8, 7; 7 6 5 4 3 2 1 0 [Rat, Manx, Cymric, Mutt, Pug, Cymric, Pug, Manx] [Rat, Manx, Cymric, Cymric, Rat, EgyptianMau, Hamster, EgyptianMau] *///:~
Pet.randomPet()方法用来替换在列表中从位置3开始向前的所有Pet对象。
11.7 LinkedList
LinkedList也像ArrayList一样实现了基本的List接口,但是它执行某些操作(在List的中间
插入和移除)时比ArrayList更高效,但在随机访问操作方面却要逊色一些。
LinkedList还添加了可以使其用作栈、队列、或双端队列的方法。
这些方法中有些彼此之间只是名称有些差异,或者只存在些许差异,以使得这些名字在特
定用法的上下文环境中更加适用(特别是在Queue中)。例如, getFirst()和element()完全一样,
它们都返回列表的头(第一个元素) ,而并不移除它,如果List为空,则抛出NoSuchElementException
peek()方法与这两个方式只是稍有差异,它在列表为空时返回null.
removeFirst()与remove()也是完全一样的,它们移除并返回列表的头,而在列表为空时抛出
NoSuchElementException。poll()稍有差异,它在列表为空时返回null。
addFirst()与add()和addLast()相同,它们都将某个元素插入到列表的尾(端)部。
removeLast()移除并返回列表的最后一个元素。
package com.cy.container; //: holding/LinkedListFeatures.java import typeinfo.pets.*; import java.util.*; import static com.java.util.Print.*; public class LinkedListFeatures { public static void main(String[] args) { LinkedList<Pet> pets = new LinkedList<Pet>(Pets.arrayList(5)); print(pets); // Identical: print("pets.getFirst(): " + pets.getFirst()); print("pets.element(): " + pets.element()); // Only differs in empty-list behavior: print("pets.peek(): " + pets.peek()); // Identical; remove and return the first element: print("pets.remove(): " + pets.remove()); print("pets.removeFirst(): " + pets.removeFirst()); // Only differs in empty-list behavior: print("pets.poll(): " + pets.poll()); print(pets); pets.addFirst(new Rat()); print("After addFirst(): " + pets); pets.offer(Pets.randomPet()); print("After offer(): " + pets); pets.add(Pets.randomPet()); print("After add(): " + pets); pets.addLast(new Hamster()); print("After addLast(): " + pets); print("pets.removeLast(): " + pets.removeLast()); } } /* Output: [Rat, Manx, Cymric, Mutt, Pug] pets.getFirst(): Rat pets.element(): Rat pets.peek(): Rat pets.remove(): Rat pets.removeFirst(): Manx pets.poll(): Cymric [Mutt, Pug] After addFirst(): [Rat, Mutt, Pug] After offer(): [Rat, Mutt, Pug, Cymric] After add(): [Rat, Mutt, Pug, Cymric, Pug] After addLast(): [Rat, Mutt, Pug, Cymric, Pug, Hamster] pets.removeLast(): Hamster *///:~
Pets.arrayList()的结果交给了LinkedList的构造器,以便使用它来组装LinkedList。如果
你浏览一下Queue接口就会发现,它在LinkedList的基础上添加了element() 、offer() 、peek() 、
poll()和remove()方法,以使其可以成为一个Queue的实现。Queue的完整示例将在本章稍后
给出。
11.8 Stack
"栈"通常是指"后进先出" 的容器。有时栈也被称为叠加栈,因为最后"压入"
栈的元素,第一个"弹出"栈。经常用来类比栈的事物是装有弹簧的储放器中的自助餐托盘,
最后装入的托盘总是最先拿出使用的。
LinkedList具有能够直接实现栈的所有功能的方法,因此可以直接将LinkedList作为栈使用。
不过,有时一个真正的"栈"更能把事情讲清楚:
package com.java.util; // Making a stack from a LinkedList. import java.util.LinkedList; public class Stack<T> { private LinkedList<T> storage = new LinkedList<T>(); public void push(T v) { storage.addFirst(v); } public T peek() { return storage.getFirst(); } public T pop() { return storage.removeFirst(); } public boolean empty() { return storage.isEmpty(); } public String toString() { return storage.toString(); } }
这里通过使用泛型,引入了在栈的类定义中最简单的可行示例。类名之后的<T>告诉编译
器这将是一个参数化类型,而其中的类型参数,即在类被使用时将会被实际类型替换的参数,
就是T。 大体上,这个类是在声明"我们在定义一个可以持有T类型对象的Stack 。" Stack是用
LinkedList实现的,而LinkedList也被告知它将持有τ类型对象。注意, push()接受的是T类型的
对象,而peek()和pop()将返回T类型的对象。peek()方法将提供栈顶元素,但是并不将其从栈顶
移除,而pop()将移除并返回栈顶元素。
如果你只需要栈的行为,这里使用继承就不合适了,因为这样会产生具有LinkedList的其他
所有方法的类(就象你将在第17章中所看到的, Java1.0的设计者在创建java.utiI.Stack时,就犯
了这个错误)。
新的Stack类:
package com.cy.container; //: holding/StackTest.java import com.java.util.*; public class StackTest { public static void main(String[] args) { Stack<String> stack = new Stack<String>(); for(String s : "My dog has fleas".split(" ")) stack.push(s); while(!stack.empty()) System.out.print(stack.pop() + " "); } } /* Output: fleas has dog My *///:~
如果你想在自己的代码中使用这个Stack类,当你在创建其实例时,就需要完整指定包名,
或者更改这个类的名称;否则,就有可能与java.util包中的Stack发生冲突。例如,如果我们在
上面的例子中导人java.util.*,那么就必须使用包名以防止冲突:
package com.cy.container; //: holding/StackCollision.java public class StackCollision { public static void main(String[] args) { com.java.util.Stack<String> stack = new com.java.util.Stack<String>(); for(String s : "My dog has fleas".split(" ")) stack.push(s); while(!stack.empty()) System.out.print(stack.pop() + " "); System.out.println(); java.util.Stack<String> stack2 = new java.util.Stack<String>(); for(String s : "My dog has fleas".split(" ")) stack2.push(s); while(!stack2.empty()) System.out.print(stack2.pop() + " "); } } /* Output: fleas has dog My fleas has dog My *///:~
这两个Stack具有相同的接口,但是在java.util中没有任何公共的Stack接口,这可能是因为
在Javal.0中的设计欠佳的最初的java.util.Stack类占用了这个名字。尽管已经有了java.util.Stack,
但是LinkedList可以产生更好的Stack ,因此com.java.util.Stack所采用的方式更是可取的。
11.9 Set
package com.cy.container; import java.util.*; public class SetOfInteger { public static void main(String[] args) { Random rand = new Random(47); Set<Integer> intset = new HashSet<Integer>(); for(int i = 0; i < 10000; i++) intset.add(rand.nextInt(30)); System.out.println(intset); } } //[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 16, 19, 18, 21, 20, 23, 22, 25, 24, 27, 26, 29, 28]
在0到29之间的10000个随机数被添加到了Set中,因此你可以想象,每一个数都重复了许多
次。但是你可以看到,每一个数只有一个实例出现在结果中。
你还可以往意到,输出的顺序没有任何规律可蟹,这是因为出于速度原因的考虑, HashSet
使用了散列…散列将校第17章中介绍。HashSet所维护的顺序与TreeSet或LinkedHashSet都不
同,因为它们的实现具有不同的元素存错方式。TreeSet将无素存储在红…黑树数据结构中,而
HashSet使用的是散列函数。LinkedHashList因为查询速度的原因也使用了散列,但是看起来它
使用了链表来维护无素的插入顺序。
如果你相对结果排序,一种方式是使用TreeSet来代替HashSet:
package com.cy.container; import java.util.*; public class SortedSetOfInteger { public static void main(String[] args) { Random rand = new Random(47); SortedSet<Integer> intset = new TreeSet<Integer>(); for(int i = 0; i < 10000; i++) intset.add(rand.nextInt(30)); System.out.println(intset); } } /* Output: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29] *///:~
你将会执行的最常见的操作之…,就是使用contains()测试Set的归属性,但是还有很多操作
会让你想起在上小学时所教授的文氏图(译者注:用圆表示集与集之间关系的图) :
11.10 Map
将对象映射到其他对象的能力是一种解决编程问题的杀手锏。例如,考虑一个程序,它将
用来检查Java的Random类的随机性。理想状态下, Random可以将产生理想的数字分布,但要
想测试它,翻需要生成大量的随机数,并对落入各种不同范围的数字进行计数。Map可以很容
易地解决该问题。在本例中,键是由Random产生的数字,而值是该数字出现的次数:
package com.cy.container; import java.util.*; public class Statistics { public static void main(String[] args) { Random rand = new Random(47); Map<Integer,Integer> m = new HashMap<Integer,Integer>(); for(int i = 0; i < 10000; i++) { // Produce a number between 0 and 10: int r = rand.nextInt(10); Integer freq = m.get(r); m.put(r, freq == null ? 1 : freq + 1); } System.out.println(m); } } /* Output: {0=994, 1=1033, 2=1010, 3=1014, 4=958, 5=1000, 6=1052, 7=980, 8=946, 9=1013} *///:~
11.11 Queue
队列是一个典型的先进先出(FIFO) 的容器。即从容器的一端放人事物,从另一端取出,
并且事物放入容器的顺序与取出的顺序是相同的。队列常被当作一种可靠的将对象从程序的某
个区域传输到另一个区域的途径。队列在并发编程中特别重要,就像你将在第2 1章中所看到的,
因为它们可以安全地将对象从一个任务传输给另一个任务。
LinkedList提供了方法以支持队列的行为, 并且它实现了Queue接口,因此LinkedList可以
用作Queue的一种实现。通过将LinkedList向上转型为Queue ,下面的示例使用了在Queue接口
中与Queue相关的方法:
package com.cy.container; import java.util.*; public class QueueDemo { public static void printQ(Queue queue) { while(queue.peek() != null) System.out.print(queue.remove() + " "); System.out.println(); } public static void main(String[] args) { Queue<Integer> queue = new LinkedList<Integer>(); Random rand = new Random(47); for(int i = 0; i < 10; i++) queue.offer(rand.nextInt(i + 10)); printQ(queue); Queue<Character> qc = new LinkedList<Character>(); for(char c : "Brontosaurus".toCharArray()) qc.offer(c); printQ(qc); } } /* Output: 8 1 1 1 5 14 3 1 0 1 B r o n t o s a u r u s *///:~
offer()方法是与Queue相关的方法之一,它在允许的情况下,将一个元素插入到队尾,或者
返回false. peek()和element()都将在不移除的情况下返回队头,但是peek()方法在队列为空时返
回null, 而element()会抛出NoSuchElcmentException异常。poll()和remove()方法将移除并返回
队头,但是poll()在队列为空时返回null ,而remove()会抛出NoSuchElcmentException异常。
自动包装机制会自动地将nextInt()方法的int结果转换为queue所需的Integer对象,将char c
转换为qc所需的Character对象。Queue接口窄化了对LinkedList的方法的访问权限,以使得只
有恰当的方法才可以使用,因此,你能够访问的LinkedList的方法会变少(这里你实际上可以将
queuc转型回LinkedList ,但是至少我们不鼓励这么做)。
11 .11.1 PrìorìtyQueue
先进先出描述了最典型的队列规则。队列规则是指在给定一组队列中的元素的情况下,确
定下一个弹出队列的元素的规则。先进先出声明的是下一个元素应该是等待时间最长的元素。
优先级队列声明下一个弹出元素是最需要的元素(具有最高的优先级)。例如,在飞机场,
当飞机临近起飞时,这架飞机的乘客可以在办理整机手续时排到队头。如果构建了一个消息系
统,某些消息比其他消息更重要, 因而应该更快地得到处理,那么它们何时得到处理就与它们
何时到达无关。PriorityQueue添加到JavaSE5中,是为了提供这种行为的一种自动实现。
当你在PriorityQueue上调用offer()方法来插入一个对象时,这个对象会在队列中被排序。
默认的排序将使用对象在队列中的自然顺序,但是你可以通过提供自己的Comparator来修改这
个顺序。PriorityQueue可以确保当你调用peek()、poll()和remove()方法肘,获取的元素将是队
列中优先级最高的元素。
让PriorityQueue与Integer 、String和Character这样的内置类型一起工作易如反掌。在下面
的示例中,第一个值集与前一个示例中的随机值相同,因此你可以看到它们从PriorityQueue中
弹出的顺序与前一个示例不同:
package com.cy.container; import java.util.*; public class PriorityQueueDemo { public static void main(String[] args) { PriorityQueue<Integer> priorityQueue = new PriorityQueue<Integer>(); Random rand = new Random(47); for(int i = 0; i < 10; i++) priorityQueue.offer(rand.nextInt(i + 10)); QueueDemo.printQ(priorityQueue); List<Integer> ints = Arrays.asList(25, 22, 20, 18, 14, 9, 3, 1, 1, 2, 3, 9, 14, 18, 21, 23, 25); priorityQueue = new PriorityQueue<Integer>(ints); QueueDemo.printQ(priorityQueue); priorityQueue = new PriorityQueue<Integer>(ints.size(), Collections.reverseOrder()); priorityQueue.addAll(ints); QueueDemo.printQ(priorityQueue); String fact = "EDUCATION SHOULD ESCHEW OBFUSCATION"; List<String> strings = Arrays.asList(fact.split("")); PriorityQueue<String> stringPQ = new PriorityQueue<String>(strings); QueueDemo.printQ(stringPQ); stringPQ = new PriorityQueue<String>(strings.size(), Collections.reverseOrder()); stringPQ.addAll(strings); QueueDemo.printQ(stringPQ); Set<Character> charSet = new HashSet<Character>(); for(char c : fact.toCharArray()) charSet.add(c); // Autoboxing PriorityQueue<Character> characterPQ = new PriorityQueue<Character>(charSet); QueueDemo.printQ(characterPQ); } } /* Output: 0 1 1 1 1 1 3 5 8 14 1 1 2 3 3 9 9 14 14 18 18 20 21 22 23 25 25 25 25 23 22 21 20 18 18 14 14 9 9 3 3 2 1 1 A A B C C C D D E E E F H H I I L N N O O O O S S S T T U U U W W U U U T T S S S O O O O N N L I I H H F E E E D D C C C B A A A B C D E F H I L N O S T U W *///:~
你可以看到,重复是允许的,最小的值拥有最高的优先级(如果是String ,空格也可以算作
值,并且比字母的优先级高)。为了展示你可以使用怎样的方法通过提供自己的Comparator对
象来改变排序, 第三个对PriorityQueue<Integer>的构造器调用, 和第二个对PriorityQueue
<String>的调用使用了由Collection.reverseOrder() (新添加到IJava SE5中的)产生的反序的
Comparator 。
最后一部分添加了一个HashSet来消除重复的Character ,这么做只是为了增添点乐趣。
Integer 、String和Character可以与PriorityQueue一起工作,因为这些类已经内建了自然
排序。如果你想在PriorityQueue中使用自己的类,就必须包括额外的功能以产生自然排序,或
者必须提供自己的Comparator。在第17章中有一个更加复杂的示例将演示这种情况。
11 .13 Foreach与迭代器
foreach可以应用于任何Colledion对象.
之所以能够工作,是因为Java SE5引入了新的被称为Iterable的接口, 该接口包含一个能够
产生Iterator的iterator()方法,并且Iterable接口被foreach用来在序列中移动.因此如果你创建了
任何实现Iterable的类,都可以将它用于foreach语句中:
package com.cy.container; import java.util.*; public class IterableClass implements Iterable<String> { protected String[] words = ("And that is how we know the Earth to be banana-shaped.").split(" "); @Override public Iterator<String> iterator() { return new Iterator<String>() { private int index = 0; public String next() { return words[index++]; } public boolean hasNext() { return index < words.length; } public void remove() { // Not implemented throw new UnsupportedOperationException(); } }; } public static void main(String[] args) { for(String s : new IterableClass()) System.out.print(s + " "); } } /* Output: And that is how we know the Earth to be banana-shaped. *///:~
iterator()方法返回的是实现了Iterator<String>的匿名内部类的实例,该匿名内部类可以遍
历数组中的所有单词. 在main()中,你可以看到IterableClass确实可以用于foreach语句中.
在Java SE5中,大量的类都是Iterable类型,主要包括所有的Collection类(但是不包括各种
Map) 。例如, 下面的代码可以显示所有的操作系统环境变量:
package com.cy.container; import java.util.*; public class EnvironmentVariables { public static void main(String[] args) { for(Map.Entry entry: System.getenv().entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue()); } } } /* USERPROFILE: C:UsersCY ProgramData: C:ProgramData PATHEXT: .COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC JAVA_HOME: D:jdk1.7.0 ProgramFiles(x86): C:Program Files (x86) TEMP: C:UsersCYAppDataLocalTemp */
11.13.1 适配器方法惯用法
如果现有一个Iterable类,你想要添加一种或多种在foreach语句中使用这个类的方法,应该
怎么做呢?例如,假设你希望可以选择以向前的方向或是向后的方向迭代一个单词列表。如果
直接继承这个类,并覆盖iterator()方法,你只能替换现有的方法,而不能实现选择。
一种解决方案是所谓适配器方法的惯用法。"适配器"部分来自于设计模式,因为你必须提
供特定接口以满足foreach语句。当你有一个接口并需要另一个接口时,编写适配器就可以解决
问题。这里,我希望在默认的前向选代器的基础上,添加产生反向迭代器的能力,因此我不能
使用覆盖,而是添加了一个能够产生Iterable对象的方法,该对象可以用于foreach语句。正如你
所见,这使得我们可以提供多种使用foreach的方式:
package com.cy.container; import java.util.*; @SuppressWarnings("serial") class ReversibleArrayList<T> extends ArrayList<T> { public ReversibleArrayList(Collection<T> c) { super(c); } public Iterable<T> reversed() { return new Iterable<T>() { public Iterator<T> iterator() { return new Iterator<T>() { int current = size() - 1; @Override public boolean hasNext() { return current > -1; } @Override public T next() { return get(current--); } @Override public void remove() { // Not implemented throw new UnsupportedOperationException(); } }; } }; } } public class AdapterMethodIdiom { public static void main(String[] args) { ReversibleArrayList<String> ral = new ReversibleArrayList<String>(Arrays.asList("To be or not to be".split(" "))); // Grabs the ordinary iterator via iterator(): for(String s : ral) System.out.print(s + " "); System.out.println(); // Hand it the Iterable of your choice for(String s : ral.reversed()) System.out.print(s + " "); } } /* Output: To be or not to be be to not or be To *///:~
如果直接将ral对象置于foreach语句中,将得到(默认的)前向迭代器。但是如果在该对象
上调用reversed()方法,就会产生不同的行为。
通过使用这种方式,我可以在IterableClass.java示例中添加两种适配器方法:
package com.cy.container; import java.util.*; public class MultiIterableClass extends IterableClass { public Iterable<String> reversed() { return new Iterable<String>() { public Iterator<String> iterator() { return new Iterator<String>() { int current = words.length - 1; public boolean hasNext() { return current > -1; } public String next() { return words[current--]; } public void remove() { // Not implemented throw new UnsupportedOperationException(); } }; } }; } public Iterable<String> randomized() { return new Iterable<String>() { public Iterator<String> iterator() { List<String> shuffled = new ArrayList<String>(Arrays.asList(words)); Collections.shuffle(shuffled, new Random(47)); return shuffled.iterator(); } }; } public static void main(String[] args) { MultiIterableClass mic = new MultiIterableClass(); for(String s : mic.reversed()) System.out.print(s + " "); System.out.println(); for(String s : mic.randomized()) System.out.print(s + " "); System.out.println(); for(String s : mic) System.out.print(s + " "); } } /* Output: banana-shaped. be to Earth the know we how is that And is banana-shaped. Earth that how the be And we know to And that is how we know the Earth to be banana-shaped. *///:~
注意,第二个方法random()没有创建它自己的Iterator ,而是直接返回被打乱的List中的lterator.
从输出中可以看到, Collection.shuffle()方法没有影响到原来的数组,而只是打乱了shuffled
中的引用。之所以这样,只是因为randomized()方法用一个ArrayList将Arrays.asList()方法的结
果包装了起来。如果这个由Arrays.asList()方法产生的List被直接打乱, 那么它就会修改底层的
数组,就像下面这样:
package com.cy.container; import java.util.*; public class ModifyingArraysAsList { public static void main(String[] args) { Random rand = new Random(47); Integer[] ia = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; List<Integer> list1 = new ArrayList<Integer>(Arrays.asList(ia)); System.out.println("Before shuffling: " + list1); Collections.shuffle(list1, rand); System.out.println("After shuffling: " + list1); System.out.println("array: " + Arrays.toString(ia)); List<Integer> list2 = Arrays.asList(ia); System.out.println("Before shuffling: " + list2); Collections.shuffle(list2, rand); System.out.println("After shuffling: " + list2); System.out.println("array: " + Arrays.toString(ia)); } } /* Output: Before shuffling: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] After shuffling: [4, 6, 3, 1, 8, 7, 2, 5, 10, 9] array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] Before shuffling: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] After shuffling: [9, 1, 6, 3, 7, 2, 5, 10, 4, 8] array: [9, 1, 6, 3, 7, 2, 5, 10, 4, 8] *///:~
在第一种情况中, Arrays.asList()的输出被传递给了ArrayList()的构造器,这将创建一个引
用ia的元素的ArrayLi s t ,因此打乱这些引用不会修改该数组。但是,如果直接使用
Arrays.asList(ia)的结果,这种打乱就会修改ia的顺序。意识到Arrays.asList()产生的List对象会
使用底层数组作为其物理实现是很重要的。只要你执行的操作会修改这个List ,并且你不想原来
的数组被修改,那么你就应该在另一个容器中创建一个副本。
11.14 总结
4) 如果要进行大量的随机访问,就使用ArrayList ,如果要经常从表中间插入或删除元素,则应该使用LinkedList。
5) 各种Queue以及栈的行为,由LinkedList提供支持。
6) Map是一种将对象(而非数字)与对象相关联的设计。HashMap设计用来快速访问,而
TreeMap保持"键"始终处于排序状态,所以没有HashMap快。LinkedHashMap保持元素插入
的顺序,但是也通过散列提供了快速访问能力。
7) Set不接受重复元素。HashSet提供最快的查询速度,而TreeSet保持元素处于排序状态。
LinkedHashSet以插入顺序保存元素。
简单的容器分类:
你可以看到,其实只有四种容器: Map 、List 、Set和Queue ,它们各有两到三个实现版本
( Queue的java.util.concurrent实现没有包括在上面这张图中)。常用的容器用黑色粗线框表示。
点线框表示接口,实线框表示普通的(具体的)类。 带有空心箭头的点线表示一个特定的
类实现了一个接口,实心箭头表示某个类可以生成箭头所指向类的对象。例如,任意的
Collection可以生成Iterator , 而List可以生成ListIterator (也能生成普通的Iterator, 因为List继
承自Collection ) 。
-------------------------------