我有抄书的习惯,因为这样才能强迫自己仔细去读每一句话。
如果一个程序只包含固定数量的且生命周期都是已知的对象,那么这是一个非常简单的程序。
Java使用类库提供了一套想当完整的容器类来解决这个问题,其中的基本类型是List、Set、Queue和Map。这些对象也称为集合类,但是由于Java中存在Collection这个名字来指代类库中的一个特殊子类,所以我使用了范围更广的术语“容器”来称呼他们。
11.1 泛型和类型安全的容器
使用Java SE5之前的容器有一个主要问题就是编译器允许你向容器中插入不正确的类型。
例如,考虑一个apple对象的容器,我们使用最基本最可靠的容器ArrayList。现在你可以把ArrayList当作“可以自动扩充的数组”来看待。使用ArrayList非常简单,创建一个实例,用add()插入对象,然后用get()访问对象,此时需要索引,就像数组一样,但是不需要方括号。ArrayList还有一个size()方法,使你可以知道已经有多少元素添加了进来,从而不会不小心因索引越界而发生错误(抛出运行时异常)。
在本例中,可以同时放入apple对象和orange对象,当取出的时候,获得的是object对象,因此在使用apple对象的getId()之前,你必须进行强制类转,但当你把orange转换成apple的时候,会发生转换错误。
在第十五章中,你将会了解到,使用Java泛型来创建类会非常复杂。但是,应用预定义的泛型通常会很简单。
例如,ArrayList<apple>,其中尖括号里面是类型参数(可以有多个),它指定了这个容器内可以保存的类型。通过使用泛型,就可以在编译期防止将错误类型的对象放置到容器中。
现在,把orange对象放置到这个ArrayList<apple>中会得到一个编译期错误而不是一个运行时错误,同时取出ArrayList中的对象时,也不再需要类型转换了,因为List知道自己保存的是什么类型。
11.2 基本概念
Java容器类库的用途是“保存对象”,并将其划分为两个不同的概念:
1)Collection。一个独立元素的序列。这些元素都服从一条或多条规则,List必须按照插入的顺序保存元素,而Set不能有重复元素。Queue按照排队规则来确定对象产生的顺序(通常与它们被插入的顺序相同)。
2)Map。一组成对的“键值对“对象,允许你使用键来查找值。ArrayList允许你使用数字来查找值,因此在某种意义上讲,它将数字与对象关联在一起(MYSQL_NUM)。映射表允许我们使用另一个对象来查找对象,它也被成为关联数组(MYSQL_ASSOC,哈哈),因为它将某些对象与另一些对象关联在一起;或者被称为字典,因为你可以使用键对象来查找值对象,就像在字典中使用单词来定义一样。Map是强大的编程工具。
所有的Collection都可以用foreach语法遍历,在本章的后续部分,你将会学习到被称之为”迭代器“的更灵活的概念。
11.3 添加一组元素
在java.util包中的Arrays和Collections类中有很多很实用的方法,可以在一个Collection中添加一组元素。
Arrays.asList()方法接受一个数组或者一个逗号分割的元素列表(使用可变参数),并将其转换成一个List对象。
Collections.addAll()方法接受一个Collection对象,以及一个数组或者是用逗号分割的列表,将元素添加到Collection中。
package com.prettydz.exercises; import java.util.*; public class AddingGroups { public static void main(String[] args) { // Arrays.asList 是个很有意思的函数 Collection<Integer> collection = new ArrayList<Integer>(Arrays.asList(1, 2, 3, 4, 5)); Integer[] moreInts = { 6, 7, 8, 9, 10 }; // collection.addAll()方法运行起来快很多,但是只能接受另一个Collection对象作为参数。 collection.addAll(Arrays.asList(moreInts)); // Collections.addAll()方法更加灵活。 Collections.addAll(collection, 11, 12, 13, 14, 15); Collections.addAll(collection, moreInts); // 不会很逆天的,输出结果是[16, 100, 18, 19, 20] List<Integer> list = Arrays.asList(16, 17, 18, 19, 20); list.set(1, 100); /** //list 的取出用get()方法 for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); } */ System.out.println(list.toString()); // 这个操作会报java.lang.UnsupportedOperationException异常 // Arrays.asList()可以直接用于输出,将其当作List,但是在这种情况下,其底层表示的是数组,因此不能调整大小。 list.add(21); } }
Map更加复杂,并且除了用另一个Map之外,Java标准类库并没有提供其他任何自动初始化它们的方法。 11.4 容器的打印 直接上代码~
package com.prettydz.exercises; import java.util.*; public class PrintingContainers { static Collection fill(Collection<String> c) { c.add(“rat”); c.add(“cat”); c.add(“dog”); // 重复添加 c.add(“dog”); return c; } static Map fill(Map<String, String> m) { m.put(“rat”, ”Fuzzy”); m.put(“cat”, ”Rags”); m.put(“dog”, ”Bosco”); // 重复赋值 m.put(“dog”, ”Spot”); return m; } private static void print(Collection<String> c) { System.out.println(c.toString()); } private static void print(Map<String, String> m) { //System.out.println(m.toString()); //练习下Map的遍历:) System.out.print(“{“); Set<String> key = m.keySet(); for (Iterator<String> it = key.iterator(); it.hasNext();) { String tmp_key = it.next(); System.out.print(tmp_key+”=”+m.get(tmp_key)); if (it.hasNext()) System.out.print(“, ”); else System.out.println(“}”); } } 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>())); } }
ArrayList和LinkedList都是List类型,从输出可以看出,它们都按照被插入的顺序保存元素。两者的不同之处不仅在于执行某些类型的操作时的性能,而且LinkedList包含的操作也多于ArrayList。这些将在本章后续部分更加详细的讨论。
HashSet、TreeSet和LinkedHashSet都是Set类型,输出显示在Set中,每个相同的项只保留一次(不允许重复出现),但是输出也显示了不同的Set存储元素方式的不同。HashSet使用的是相当复杂的方式来存储元素,这将在17章介绍(这种技术是最快的获取元素的方式),因此,存储的顺序看起来并无实际意义。如果存储顺序很重要,那么可以使用TreeSet,它按照比较结果的升序保存对象;或者使用LinkedSet,它按照被添加的顺序保存对象。
本例使用了三种基本风格的Map:HashMap、TreeMap和LinkedMap。与HashSet一样,HashMap也提供了最快的查找技术,也没有按照任何明显的顺序来保存其元素。TreeMap按照比较结果的升序保存键,而LinkedHashMap则按照插入的顺序保存键值,同时还保留了HashMap的查询速度。
11.5 List
List承诺可以将元素维护在特定的序列中。List接口在Collection的基础上添加了大量的方法,使得可以在List中间插入和移除元素。
ArrayList,它长于随机访问元素,但是在List的中间插入和移除元素时比较慢。
LinkedList,它通过比较低的在List中间进行的插入和删除操作,提供了优化的顺序访问。LinkedList在随机访问方面比较慢,但是它的特性集较ArrayList更大。
11.6 迭代器
迭代器是一个对象,它的工作是遍历并选择序列中的对象,而程序猿不必知道或关心该序列底层的结构。此外,迭代器通常被称为轻量级对象:创建它的代价小。因此通常可以见到对迭代器有奇怪的限制,例如:Java的Iterator只能单向移动,这个迭代器只能用来:
1)使用方法iterator()要求容器返回一个Iterator。Iterator将准备好返回序列的第一个元素。
2)使用next()获得一个序列的下一个元素。
3)使用hasNext()检查序列中是否还有元素。
4)使用remove()将迭代器新近返回的元素删除。
11.6.1 ListIterator
ListIterator是一个更加强大的Iterator的子类型,它只能用于各种List类的访问。尽管Iterator只能向前移动,但是ListIterator可以双向移动。它还可以 产生相对于迭代器的列表中指向当前位置的前一个和后一个元素的索引,并且可以使用set()方法替换它访问过的最后一个元素。你可以通过调用listIterator()方法产生一个指向List开始处的ListIterator,也可以通过listIterator(n)直接创建一个指向列表索引为n的ListIterator。(是索引不是第N个)
11.7 LinkedList
LinkedList较ArrayList添加了可以使其用作栈、队列和双端队列的方法。
如果你浏览下Queue接口就会发现,它在LinkedList的基础上添加了element()、offer()、peek()、poll()和remove()方法,以使其可以成为一个Queue的实现。
11.8 Stack
“栈”通常是指“先进后出”(LIFO,last in first out)的容器。
LinkedList具有能够直接实现栈的左右功能的方法,因此可以直接用LinkedList作为栈使用。
11.9 Set
Set不保存重复的元素(至于如何判断元素相同则较为复杂,稍后便会看到)。
Set具有与Collection完全一样的接口,因此没有任何额外的功能,不像前面有两个不同的List。实际上Set就是Collection,只是行为不同。(这是继承与多态思想的典型应用:表现不同的行为。)
你将会执行的最常见的操作之一,就是用contains()测试Set的归属性。
11.10 Map
将对象映射到其他对象的能力是一种解决编程问题的杀手锏。
你可以使用containsKey()和containsValue()来测试一个Map。
11.11 Queue
队列是一个典型的FIFO的容器。
LinkedList提供了方法以支持队列的行为,并且它实现了Queue接口,因此LinkedList可以用作Queue的一种实现。
通过将LinkedList向上转型为Queue,下列:
/****************************************************************************/
package com.prettydz.exercises; import java.util.*; public class QueueDemo { public static void printQ(Queue q) { while (q.peek() != null) System.out.print(q.remove() + " "); System.out.println(); } public static void main(String[] args) { Queue<Integer> queue = new LinkedList<Integer>(); Random random = new Random(47); for (int i = 0; i < 10; i++) queue.offer(random.nextInt(20)); printQ(queue); Queue<Character> qc = new LinkedList<Character>(); for (char c : "xbzbing".toCharArray()) qc.offer(c); printQ(qc); } }
//~输出结果
18 15 13 1 1 9 8 0 2 7
x b z b i n g
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方法会变少。
11.11.1 PriorityQueue
优先级队列声明下一个弹出元素是最需要的元素(具有最高的优先级)。例如在飞机场,当飞机临近起飞时,这架飞机的乘客可以在办理登机手续的时候排在队头,如果构建一个信息系统,某些信息比其他信息更重要,因而应该更快的得到处理,那么它们何时得到处理就与它们何时到达无关。PriorityQueue添加到Java SE5中,是为了提供这种行为的自动实现。
当你在PriorityQueue上调用offer()方法插入一个对象时,这个对象会在队列中被排序。(这依赖于具体实现。优先级队列算法通常会在插入时排序,但是它们也有可能在移除时选择最重要的元素。如果对象的优先级在它在队列中等待时就可以进行修改,那么算法的选择就显得非常重要了。)默认的排序将使用对象在队列中的自然排序,但是你可以通过提供自己的Comparator(在PriorityQueue的构造函数中提供)来修改这个顺序。PriorityQueue可以确保当你调用peek()、poll()和remove()方法时,获取的元素将是队列中优先级最高的元素。
11.12 Collection和Iterator
本节作者用实现了自己版本的Collection。
11.13 Foreach与迭代器
到目前为止,foreach语法主要用于数组,但是它也可以应用于任何Collection对象。
之所以能够这样工作,是因为Java SE5引入了新的被成为Iterable的接口,该接口包含一个能够产生Iterator的iterator()方法。
11.13.1 适配器方法惯用法
11.14 总结
4)如果要进行大量的随机访问,就用ArrayList,如果要经常从表中间插入或删除元素,则应该使用LinkedList。
8)新程序中不应该使用过时的Vector、Hashtable和Stack。
留言交流