Merge pull request #148 from xiangflight/master

[revision 12](完结 集合)
This commit is contained in:
LingCoder
2019-07-20 13:17:54 +08:00
committed by GitHub

View File

@@ -312,15 +312,15 @@ public class PrintingCollections {
这显示了Java集合库中的两个主要类型。它们的区别在于集合中的每个“槽”slot保存的元素个数。 **Collection** 类型在每个槽中只能保存一个元素。此类集合包括: **List** 它以特定的顺序保存一组元素; **Set** ,其中元素不允许重复; **Queue** ,只能在集合一端插入对象,并从另一端移除对象(就本例而言,这只是查看序列的另一种方式,因此并没有显示它)。 **Map** 在每个槽中存放了两个元素,即*键*和与之关联的*值*。
默认的打印行为,使用集合几桶的toString方法即可生成可读性很好的结果。 **Collection** 打印出的内容用方括号括住,每个元素由逗号分隔。 **Map** 则由大括号括住,每个键和值用等号连接(键在左侧,值在右侧)。
默认的打印行为,使用集合提供的 `toString()` 方法即可生成可读性很好的结果。 **Collection** 打印出的内容用方括号括住,每个元素由逗号分隔。 **Map** 则由大括号括住,每个键和值用等号连接(键在左侧,值在右侧)。
第一个 **fill()** 方法适用于所有类型的 **Collection** ,这些类型都实现了 **add()** 方法以添加新元素。
**ArrayList****LinkedList** 都是 **List** 的类型,从输出中可以看出,它们都按插入顺序保存元素。两者之间的区别不仅在于执行某些类型的操作时的性能,而且 **LinkedList** 包含的操作多于 **ArrayList** 。本章后面将对这些内容进行更全面的探讨。
**HashSet** **TreeSet****LinkedHashSet****Set** 的类型。从输出中可以看到, **Set** 仅保存每个相同项中的一个,并且不同的 **Set** 实现存储元素的方式也不同。 **HashSet** 使用相当复杂的方法存储元素,这在[附录:集合主题]()中进行了探讨。现在只需要知道,这种技术是检索元素的最快方法,因此,存储顺序看似荒谬(通常只关心某事物是否是 **Set** 的成员,而存储顺序并不重要)。如果存储顺序很重要,则可以使用**TreeSet** ,它将按比较结果的升序保存对象)或 **LinkedHashSet** ,它按照被添加的先后顺序保存对象。
**HashSet** **TreeSet****LinkedHashSet****Set** 的类型。从输出中可以看到, **Set** 仅保存每个相同项中的一个,并且不同的 **Set** 实现存储元素的方式也不同。 **HashSet** 使用相当复杂的方法存储元素,这在[附录:集合主题]()中进行了探讨。现在只需要知道,这种技术是检索元素的最快方法,因此,存储顺序看上去没有什么意义(通常只关心某事物是否是 **Set** 的成员,而存储顺序并不重要)。如果存储顺序很重要,则可以使用 **TreeSet** ,它将按比较结果的升序保存对象)或 **LinkedHashSet** ,它按照被添加的先后顺序保存对象。
**Map** (也称为*关联数组*)使用*键*来查找对象,就像一个简单的数据库。所关联的对象称为*值*。 假设有一个 **Map** 将美国州名与它们的首府联系在一起如果想要俄亥俄州Ohio的首府可以用“Ohio”作为键来查找几乎就像使用数组下标一样。 这是由于这种行为,对于每个键, **Map**接受一次。
**Map** (也称为*关联数组*)使用*键*来查找对象,就像一个简单的数据库。所关联的对象称为*值*。 假设有一个 **Map** 将美国州名与它们的首府联系在一起如果想要俄亥俄州Ohio的首府可以用“Ohio”作为键来查找几乎就像使用数组下标一样。 这是由于这种行为,对于每个键, **Map**存储一次。
**Map.put(key, value)** 添加一个所想要添加的值并将它与一个键(用来查找值)相关联。 **Map.get(key)** 生成与该键相关联的值。上面的示例仅添加键值对,并没有执行查找。这将在稍后展示。
@@ -332,9 +332,10 @@ public class PrintingCollections {
键和值保存在 **HashMap** 中的顺序不是插入顺序,因为 **HashMap** 实现使用了非常快速的算法来控制顺序。 **TreeMap** 通过比较结果的升序来保存键, **LinkedHashMap** 在保持 **HashMap** 查找速度的同时按键的插入顺序保存键。
<!-- List -->
## 列表List
**List**s承诺以特定顺序保存元素。 **List** 接口在 **Collection** 的基础上添加了许多方法,允许在 **List** 的中间插入和删除元素。
**List**s承诺将元素保存在特定的序列中。 **List** 接口在 **Collection** 的基础上添加了许多方法,允许在 **List** 的中间插入和删除元素。
有两种类型的 **List**
@@ -439,33 +440,34 @@ sub: [Mouse, Pug]
*/
```
打印行都编了号,因此输出可以与源代码相关。 第1行输出展示了原始的由 **Pet** 组成的 **List** 。 与数组不同, **List** 可以在创建后添加或删除元素,并自行调整大小。这正是它的重要价值:一种可修改的序列。在第2行输出中可以看到添加一个 **Hamster** 的结果,该对象将被追加到列表的末尾。
打印行都编了号,因此可从输出追溯到源代码。 第 1 行输出展示了原始的由 **Pet** 组成的 **List** 。 与数组不同, **List** 可以在创建后添加或删除元素,并自行调整大小。这正是它的重要价值:一种可修改的序列。在第 2 行输出中可以看到添加一个 **Hamster** 的结果,该对象将被追加到列表的末尾。
可以使用 **contains()** 方法确定对象是否在列表中。如果要删除一个对象,可以将该对象的引用传递给 **remove()** 方法。同样,如果有一个对象的引用,可以使用 **indexOf()****List** 中找到该对象所在位置的下标号,如第4行输出所示中所示。
可以使用 **contains()** 方法确定对象是否在列表中。如果要删除一个对象,可以将该对象的引用传递给 **remove()** 方法。同样,如果有一个对象的引用,可以使用 **indexOf()****List** 中找到该对象所在位置的下标号,如第 4 行输出所示中所示。
当确定元素是否是属于某个 **List** ,寻找某个元素的索引,以及通过引用从 **List** 中删除元素时,都会用到 **equals()** 方法(根类 **Object** 的一个方法)。每个 **Pet** 被定义为一个唯一的对象,所以即使列表中已经有两个 **Cymrics** ,如果再创建一个新的 **Cymric** 对象并将其传递给 **indexOf()** 方法,结果仍为 **-1** (表示未找到),并且尝试调用 **remove()** 方法来删除这个对象将返回 **false** 。对于其他类, **equals()** 的定义可能有所不同。例如,如果两个 **String** 的内容相同,则这两个 **String** 相等。因此,为了防止出现意外,请务必注意 **List** 行为会根据 **equals()** 行为而发生变化。
第7、8行输出展示了删除与 **List** 中的对象完全匹配的对象是成功的。
7、8 行输出展示了删除与 **List** 中的对象完全匹配的对象是成功的。
可以在 **List** 的中间插入一个元素,就像在第9行输出和它之前的代码那样。但这会带来一个问题:对于 **LinkedList** ,在列表中间插入和删除都是廉价操作(在本例中,除了对列表中间进行的真正的随机访问),但对于 **ArrayList** ,这可是代价高昂的操作。这是否意味着永远不应该在 **ArrayList** 的中间插入元素,并最好是转换为 **LinkedList** ?不,它只是意味着你应该意识到这个问题,如果你开始在某个 **ArrayList** 中间进执行很多插入操作,并且程序开始变慢,那么你应该看看你的 **List** 实现有可能就是罪魁祸首(发现此类瓶颈的最佳方式是使用仿真器)。优化是一个很棘手的问题,最好的策略就是置之不顾,直到发现必须要去担心它了(尽管去理解这些问题总是一个很好的主意)。
可以在 **List** 的中间插入一个元素,就像在第 9 行输出和它之前的代码那样。但这会带来一个问题:对于 **LinkedList** ,在列表中间插入和删除都是廉价操作(在本例中,除了对列表中间进行的真正的随机访问),但对于 **ArrayList** ,这可是代价高昂的操作。这是否意味着永远不应该在 **ArrayList** 的中间插入元素,并最好是转换为 **LinkedList** ?不,它只是意味着你应该意识到这个问题,如果你开始在某个 **ArrayList** 中间进执行很多插入操作,并且程序开始变慢,那么你应该看看你的 **List** 实现有可能就是罪魁祸首(发现此类瓶颈的最佳方式是使用分析器 profiler)。优化是一个很棘手的问题,最好的策略就是置之不顾,直到发现必须要去担心它了(尽管去理解这些问题总是一个很好的主意)。
**subList()** 方法可以轻松地从更大的列表中创建切片,当将切片结果传递给原来这个较大的列表的 **containsAll()** 方法时,很自然地会得到 **true**。请注意顺序并不重要在第11、12行输出中可以看到**sub** 上调用直观命名的 **Collections.sort()****Collections.shuffle()** 方法,不会影响 **containsAll()** 的结果。 **subList()** 所产生的列表的幕后支持就是原始列表。因此,对所返回列表的更改都将会反映在原始列表中,反之亦然。
**subList()** 方法可以轻松地从更大的列表中创建切片,当将切片结果传递给原来这个较大的列表的 **containsAll()** 方法时,很自然地会得到 **true**。请注意,顺序并不重要,在第 11、12 行输出中可以看到,在 **sub** 上调用直观命名的 **Collections.sort()****Collections.shuffle()** 方法,不会影响 **containsAll()** 的结果。 **subList()** 所产生的列表的幕后支持就是原始列表。因此,对所返回列表的更改都将会反映在原始列表中,反之亦然。
**retainAll()** 方法实际上是一个“集合交集”操作,在本例中,它保留了同时在 **copy****sub** 中的所有元素。请再次注意,所产生的结果行为依赖于 **equals()** 方法。
第14行输出展示了使用索引号来删除元素的结果与通过对象引用来删除元素相比它显得更加直观因为在使用索引时不必担心 **equals()** 的行为。
14 行输出展示了使用索引号来删除元素的结果,与通过对象引用来删除元素相比,它显得更加直观,因为在使用索引时,不必担心 **equals()** 的行为。
**removeAll()** 方法也是基于 **equals()** 方法运行的。 顾名思义,它会从 **List** 中删除在参数 **List** 中的所有元素。
**set()** 方法的命名显得很不合时宜,因为它与 **Set** 类存在潜在的冲突。在这里使用“replace”可能更适合因为它的功能是用第二个参数替换索引处的元素第一个参数
第17行输出表明对于 **List** ,有一个重载的 **addAll()** 方法可以将新列表插入到原始列表的中间,而不仅仅只能用 **Collection****addAll()** 方法将追加到列表末尾。
17 行输出表明,对于 **List** ,有一个重载的 **addAll()** 方法可以将新列表插入到原始列表的中间,而不仅仅只能用 **Collection****addAll()** 方法将追加到列表末尾。
第18-20行输出展示了 **isEmpty()****clear()** 方法的效果。
18 - 20 行输出展示了 **isEmpty()****clear()** 方法的效果。
第22、23行输出展示了如何使用 **toArray()** 方法将任意的 **Collection** 转换为数组。这是一个重载方法,其无参版本返回一个 **Object** 数组,但是如果将目标类型的数组传递给这个重载版本,那么它会生成一个指定类型的数组(假设它通过了类型检查)。如果参数数组太小而无法容纳 **List** 中的所有元素(就像本例一样),则 **toArray()** 会创建一个具有合适尺寸的新数组。 **Pet** 对象有一个 **id()** 方法,可以在所产生的数组中的对象上调用这个方法。
22、23 行输出展示了如何使用 **toArray()** 方法将任意的 **Collection** 转换为数组。这是一个重载方法,其无参版本返回一个 **Object** 数组,但是如果将目标类型的数组传递给这个重载版本,那么它会生成一个指定类型的数组(假设它通过了类型检查)。如果参数数组太小而无法容纳 **List** 中的所有元素(就像本例一样),则 **toArray()** 会创建一个具有合适尺寸的新数组。 **Pet** 对象有一个 **id()** 方法,可以在所产生的数组中的对象上调用这个方法。
<!-- Iterators -->
## 迭代器Iterators
在任何集合中,都必须有某种方式可以插入元素并再次获取它们。毕竟,保存事物是集合最基本的工作。对于 **List** **add()** 是插入元素的一种方式, **get()** 是获取元素的一种方式。
@@ -473,6 +475,7 @@ sub: [Mouse, Pug]
如果从更高层次的角度考虑,会发现这里有个缺点:要使用集合,必须对集合的确切类型编程。这一开始可能看起来不是很糟糕,但是考虑下面的情况:如果原本是对 **List** 编码的,但是后来发现如果能够将相同的代码应用于 **Set** 会更方便,此时应该怎么做?或者假设想从一开始就编写一段通用代码,它不知道或不关心它正在使用什么类型的集合,因此它可以用于不同类型的集合,那么如何才能不重写代码就可以应用于不同类型的集合?
*迭代器*(也是一种设计模式)的概念实现了这种抽象。迭代器是一个对象,它在一个序列中移动并选择该序列中的每个对象,而客户端程序员不知道或不关心该序列的底层结构。另外,迭代器通常被称为*轻量级对象*lightweight object创建它的代价小。因此经常可以看到一些对迭代器有些奇怪的约束。例如Java的 **Iterator** 只能单向移动。这个 **Iterator** 只能用来:
1. 使用 **iterator()** 方法要求集合返回一个 **Iterator****Iterator** 将准备好返回序列中的第一个元素。
2. 使用 **next()** 方法获得序列中的下一个元素。
3. 使用 **hasNext()** 方法检查序列中是否还有元素。
@@ -608,7 +611,7 @@ public class CrossCollectionIteration2 {
<!-- ListIterator -->
### ListIterator
**ListIterator** 是一个更强大的 **Iterator**子类型,它只能由各种 **List** 类生成。虽然 **Iterator** 只能向前移动,但 **ListIterator** 可以双向移动。它还可以生成相对于迭代器在列表中指向的当前位置的后一个和前一个元素的索引,并且可以使用 **set()** 方法替换它访问过的最后一个元素。可以通过调用 **listIterator()** 方法来生成指向 **List** 开头处的 **ListIterator** ,还可以通过调用** listIterator(n)** 创建一个一开始就指向列表索引号为 **n** 的元素处的 **ListIterator** 。 下面的示例演示了所有这些能力:
**ListIterator** 是一个更强大的 **Iterator** 子类型,它只能由各种 **List** 类生成。虽然 **Iterator** 只能向前移动,但 **ListIterator** 可以双向移动。它还可以生成相对于迭代器在列表中指向的当前位置的后一个和前一个元素的索引,并且可以使用 **set()** 方法替换它访问过的最后一个元素。可以通过调用 **listIterator()** 方法来生成指向 **List** 开头处的 **ListIterator** ,还可以通过调用 **listIterator(n)** 创建一个一开始就指向列表索引号为 **n** 的元素处的 **ListIterator** 。 下面的示例演示了所有这些能力:
```java
// collections/ListIteration.java
@@ -647,14 +650,15 @@ EgyptianMau]
*/
```
**Pets.get()** 方法用来从位置3开始替换 **List** 中的所有Pet对象。
**Pets.get()** 方法用来从位置 3 开始替换 **List** 中的所有 Pet 对象。
<!-- LinkedList -->
## 链表LinkedList
**LinkedList** 也像 **ArrayList** 一样实现了基本的 **List** 接口,但它在 **List** 中间执行插入和删除操作时**ArrayList** 更高效。在随机访问操作效率方面却要逊色一些。
**LinkedList** 也像 **ArrayList** 一样实现了基本的 **List** 接口,但它在 **List** 中间执行插入和删除操作时比 **ArrayList** 更高效。然而,它在随机访问操作效率方面却要逊色一些。
**LinkedList还添加了一些方法使其可以被用作栈、队列或双端队列deque** 。在这些方法中,有些彼此之间可能只是名称有些差异,或者只存在些许差异,以使得这些名字在特定用法的上下文环境中更加适用(特别是在 **Queue** 中)。例如:
**LinkedList 还添加了一些方法使其可以被用作栈、队列或双端队列deque** 。在这些方法中,有些彼此之间可能只是名称有些差异,或者只存在些许差异,以使得这些名字在特定用法的上下文环境中更加适用(特别是在 **Queue** 中)。例如:
- **getFirst()** 和 **element()** 是相同的,它们都返回列表的头部(第一个元素)而并不删除它,如果 **List** 为空,则抛出 **NoSuchElementException** 异常。 **peek()** 方法与这两个方法只是稍有差异,它在列表为空时返回 **null**
- **removeFirst()** 和 **remove()** 也是相同的,它们删除并返回列表的头部元素,并在列表为空时抛出 **NoSuchElementException** 异常。 **poll()** 稍有差异,它在列表为空时返回 **null**
@@ -721,11 +725,12 @@ pets.removeLast(): Hamster
**Pets.list()** 的结果被传递给 **LinkedList** 的构造器,以便使用它来填充 **LinkedList** 。如果查看 **Queue** 接口就会发现,它在 **LinkedList** 的基础上添加了 **element()** **offer()** **peek()** **poll()****remove()** 方法,以使其可以成为一个 **Queue** 的实现。 **Queue** 的完整示例将在本章稍后给出。
<!-- Stack -->
## 堆栈Stack
堆栈是“后进先出”LIFO集合。它有时被称为*叠加栈*pushdown stack因为最后“压入”push栈的元素第一个被“弹出”pop栈。经常用来类比栈的事物是带有弹簧支架的自助餐厅托盘。最后装入的托盘总是最先拿出来使用的。
Java 1.0中附带了一个 **Stack**结果设计得很糟糕为了向后兼容我们永远坚持Java中的旧设计错误。Java 6添加了 **ArrayDeque** ,其中包含直接实现堆栈功能的方法:
Java 1.0 中附带了一个 **Stack** 类,结果设计得很糟糕(为了向后兼容,我们永远坚持 Java 中的旧设计错误。Java 6 添加了 **ArrayDeque** ,其中包含直接实现堆栈功能的方法:
```java
// collections/StackTest.java
@@ -767,7 +772,7 @@ public class Stack<T> {
}
```
这里引入了使用泛型的类定义的最简单的可能示例。类名称后面的 **<T>** 告诉编译器这是一个参数化类型,而其中的类型参数,即在类被使用时将会被实际类型替换的参数,就是 **T** 。基本上,这个类是在声明“我们在定义一个可以持有 **T** 类型对象的 **Stack** 。” **Stack** 是使用 **ArrayDeque** 实现的,而 **ArrayDeque** 也被告知它将持有 **T** 类型对象。注意, **push()** 接受类型为 **T** 的对象,而 **peek()****pop()** 返回类型为 **T** 的对象。 **peek()** 方法将返回栈顶元素,但并不将其从栈顶删除,而 **pop()** 删除并返回顶部元素。
这里引入了使用泛型的类定义的最简单的可能示例。类名称后面的 **<T>** 告诉编译器这是一个参数化类型,而其中的类型参数 **T** 会在使用类时被实际类型替换。基本上,这个类是在声明“我们在定义一个可以持有 **T** 类型对象的 **Stack** 。” **Stack** 是使用 **ArrayDeque** 实现的,而 **ArrayDeque** 也被告知它将持有 **T** 类型对象。注意, **push()** 接受类型为 **T** 的对象,而 **peek()****pop()** 返回类型为 **T** 的对象。 **peek()** 方法将返回栈顶元素,但并不将其从栈顶删除,而 **pop()** 删除并返回顶部元素。
如果只需要栈的行为,那么使用继承是不合适的,因为这将产生一个具有 **ArrayDeque** 的其它所有方法的类(在[附录:集合主题]()中将会看到, **Java 1.0** 设计者在创建 **java.util.Stack** 时,就犯了这个错误)。使用组合,可以选择要公开的方法以及如何命名它们。
@@ -830,7 +835,7 @@ import onjava.Stack;
<!-- Set -->
## 集合Set
**Set** 不保存重复的元素。 如果试图将相同对象的多个实例添加到 **Set** 中,那么它会阻止这种行为。 **Set** 最常见的用途是测试归属性,可以很轻松地询问某个对象是否在一个 **Set** 中。因此,查找通常是 **Set** 最重要的操作,因此通常会选择 **HashSet** 实现,该实现针对快速查找进行了优化。
**Set** 不保存重复的元素。 如果试图将相同对象的多个实例添加到 **Set** 中,那么它会阻止这种重复行为。 **Set** 最常见的用途是测试归属性,可以很轻松地询问某个对象是否在一个 **Set** 中。因此,查找通常是 **Set** 最重要的操作,因此通常会选择 **HashSet** 实现,该实现针对快速查找进行了优化。
**Set** 具有与 **Collection** 相同的接口,因此没有任何额外的功能,不像前面两种不同类型的 **List** 那样。实际上, **Set** 就是一个 **Collection** ,只是行为不同。(这是继承和多态思想的典型应用:表现不同的行为。)**Set** 根据对象的“值”确定归属性,更复杂的问题将在[附录:集合主题]()中介绍。
@@ -855,9 +860,9 @@ public class SetOfInteger {
*/
```
0到29之间的10000个随机整数被添加到 **Set** 中,因此可以想象每个值都重复了很多次。但是从结果中可以看到,每一个数只有一个实例出现在结果中。
0 到 29 之间的 10000 个随机整数被添加到 **Set** 中,因此可以想象每个值都重复了很多次。但是从结果中可以看到,每一个数只有一个实例出现在结果中。
早期Java版本中的 **HashSet** 产生的输出没有可辨别的顺序。这是因为出于对速度的追求, **HashSet** 使用了散列,请参阅[附录:集合主题]()一章。由 **HashSet** 维护的顺序与 **TreeSet****LinkedHashSet** 不同,因为它们的实现具有不同的元素存储方式。 **TreeSet** 将元素存储在红-黑树数据结构中,而 **HashSet** 使用散列函数。 **LinkedHashSet** 因为查询速度的原因也使用了散列,但是看起来使用了链表来维护元素的插入顺序。显然,哈希算法已经更改,现在 **Integer** 按顺序排序。 但是,您不应该依赖此行为:
早期 Java 版本中的 **HashSet** 产生的输出没有可辨别的顺序。这是因为出于对速度的追求, **HashSet** 使用了散列,请参阅[附录:集合主题]()一章。由 **HashSet** 维护的顺序与 **TreeSet****LinkedHashSet** 不同,因为它们的实现具有不同的元素存储方式。 **TreeSet** 将元素存储在红-黑树数据结构中,而 **HashSet** 使用散列函数。 **LinkedHashSet** 因为查询速度的原因也使用了散列,但是看起来使用了链表来维护元素的插入顺序。显然,哈希算法已经更改,现在 **Integer** 按顺序排序。 但是,您不应该依赖此行为:
```java
// collections/SetOfString.java
@@ -911,7 +916,7 @@ public class SortedSetOfString {
*/
```
最常见的操作之一是使用 **contains()** 测试成员归属性,但也有一些其它操作,可能会让你想起在小学学过的文氏图(译者注:用圆表示集与集之间关系的图
最常见的操作之一是使用 **contains()** 测试成员归属性,但也有一些其它操作,可能会让你想起在小学学过的维恩图(译者注:利用图形的交合表示多个集合之间的逻辑关系
```java
// collections/SetOperations.java
@@ -1023,7 +1028,7 @@ String, System, to, true, util, void, X, Y, Z]
<!-- Map -->
## 映射Map
将对象映射到其他对象的能力是解决编程问题的有效方法。例如考虑一个程序它被用来检查Java的 **Random** 类的随机性。理想情况下, **Random** 会产生完美的数字分布,但为了测试这一点,则需要生成大量的随机数,并计算落在各种范围内的数字个数。 **Map** 可以很容易地解决这个问题。在本例中,键是 **Random** 生成的数字,而值是该数字出现的次数:
将对象映射到其他对象的能力是解决编程问题的有效方法。例如,考虑一个程序,它被用来检查 Java **Random** 类的随机性。理想情况下, **Random** 会产生完美的数字分布,但为了测试这一点,则需要生成大量的随机数,并计算落在各种范围内的数字个数。 **Map** 可以很容易地解决这个问题。在本例中,键是 **Random** 生成的数字,而值是该数字出现的次数:
```java
// collections/Statistics.java
@@ -1055,7 +1060,7 @@ public class Statistics {
- **[1]** 自动包装机制将随机生成的 **int** 转换为可以与 **HashMap** 一起使用的 **Integer** 引用(不能使用基本类型的集合)。如果键不在集合中,则 **get()** 返回 **null** (这意味着这是第一次找到该数字)。否则, **get()** 会为键生成与之关联的 **Integer** 值,然后该值被递增(自动包装机制再次简化了表达式,但实际上确实发生了对 **Integer** 的装箱和拆箱)。
接下来的示例将使用一个 **String** 描述来查找 **Pet** 对象。它还展示了通过使用 **containsKey()**和 **containsValue()** 方法去测试一个 **Map** ,以查看它是否包含某个键或某个值:
接下来的示例将使用一个 **String** 描述来查找 **Pet** 对象。它还展示了通过使用 **containsKey()** **containsValue()** 方法去测试一个 **Map** ,以查看它是否包含某个键或某个值:
```java
// collections/PetMap.java
@@ -1152,9 +1157,10 @@ Person Luke has:
*/
```
**Map** 可以返回由其键组成的 **Set** ,由其值组成的 **Collection** ,或者其键值对的 **Set****keySet()** 方法生成由在 **petPeople** 中的所有键组成的 **Set** ,它在*for-in*语句中被用来遍历该 **Map**
**Map** 可以返回由其键组成的 **Set** ,由其值组成的 **Collection** ,或者其键值对的 **Set****keySet()** 方法生成由在 **petPeople** 中的所有键组成的 **Set** ,它在 *for-in* 语句中被用来遍历该 **Map**
<!-- Queue -->
## 队列Queue
队列是一个典型的“先进先出”FIFO集合。 即从集合的一端放入事物,再从另一端去获取它们,事物放入集合的顺序和被取出的顺序是相同的。队列通常被当做一种可靠的将对象从程序的某个区域传输到另一个区域的途径。队列在[并发编程]()中尤为重要,因为它们可以安全地将对象从一个任务传输到另一个任务。
@@ -1201,7 +1207,7 @@ B r o n t o s a u r u s
先进先出FIFO描述了最典型的*队列规则*queuing discipline。队列规则是指在给定队列中的一组元素的情况下确定下一个弹出队列的元素的规则。先进先出声明的是下一个弹出的元素应该是等待时间最长的元素。
优先级队列声明下一个弹出的元素是最需要的元素具有最高的优先级。例如在机场当飞机临近起飞时这架飞机的乘客可以在办理登机手续时排到队头。如果构建了一个消息传递系统某些消息比其他消息更重要应该尽快处理而不管它们何时到达。在Java 5中添加了 **PriorityQueue** ,以便自动实现这种行为。
优先级队列声明下一个弹出的元素是最需要的元素具有最高的优先级。例如在机场当飞机临近起飞时这架飞机的乘客可以在办理登机手续时排到队头。如果构建了一个消息传递系统某些消息比其他消息更重要应该尽快处理而不管它们何时到达。在Java 5 中添加了 **PriorityQueue** ,以便自动实现这种行为。
当在 **PriorityQueue** 上调用 **offer()** 方法来插入一个对象时,该对象会在队列中被排序。[^5]默认的排序使用队列中对象的*自然顺序*natural order但是可以通过提供自己的 **Comparator** 来修改这个顺序。 **PriorityQueue** 确保在调用**peek()** **poll()****remove()** 方法时,获得的元素将是队列中优先级最高的元素。
@@ -1260,7 +1266,7 @@ C B A A
*/
```
**PriorityQueue** 是允许重复的,最小的值具有最高的优先级(如果是 **String** ,空格也可以算作值,并且比字母的优先级高)。为了展示如何通过提供自己的 **Comparator** 对象来改变顺序,第三个对 **PriorityQueue\<Integer\>** 构造器的调用,和第二个对 **PriorityQueue\<String\>** 的调用使用了由 **Collections.reverseOrder()** Java SE5中新添加的产生的反序的 **Comparator**
**PriorityQueue** 是允许重复的,最小的值具有最高的优先级(如果是 **String** ,空格也可以算作值,并且比字母的优先级高)。为了展示如何通过提供自己的 **Comparator** 对象来改变顺序,第三个对 **PriorityQueue\<Integer\>** 构造器的调用,和第二个对 **PriorityQueue\<String\>** 的调用使用了由 **Collections.reverseOrder()** Java 5 中新添加的)产生的反序的 **Comparator**
最后一部分添加了一个 **HashSet** 来消除重复的 **Character**
@@ -1271,7 +1277,7 @@ C B A A
**Collection** 是所有序列集合共有的根接口。它可能会被认为是一种“附属接口”incidental interface即因为要表示其他若干个接口的共性而出现的接口。此外**java.util.AbstractCollection** 类提供了 **Collection** 的默认实现,使得你可以创建 **AbstractCollection** 的子类型,而其中没有不必要的代码重复。
使用接口描述的一个理由是它可以使我们创建更通用的代码。通过针对接口而非具体实现来编写代码,我们的代码可以应用于更多类型的对象。[^6]因此,如果所编写的方法接受一个 **Collection** ,那么该方法可以应用于任何实现了 **Collection** 的类——这也就使得一个新类可以选择去实现 **Collection** 接口以便该方法可以使用它。标准C++类库中的的集合并没有共同的基类——集合之间的所有共性都是通过迭代器实现的。在Java中遵循C++的方式看起来似乎很明智,即用迭代器而不是 **Collection** 来表示集合之间的共性。但是,这两种方法绑定在了一起,因为实现 **Collection** 就意味着需要提供 **iterator()** 方法:
使用接口描述的一个理由是它可以使我们创建更通用的代码。通过针对接口而非具体实现来编写代码,我们的代码可以应用于更多类型的对象。[^6]因此,如果所编写的方法接受一个 **Collection** ,那么该方法可以应用于任何实现了 **Collection** 的类——这也就使得一个新类可以选择去实现 **Collection** 接口,以便该方法可以使用它。标准 C++ 类库中的的集合并没有共同的基类——集合之间的所有共性都是通过迭代器实现的。在 Java 中,遵循 C++ 的方式看起来似乎很明智,即用迭代器而不是 **Collection** 来表示集合之间的共性。但是,这两种方法绑定在了一起,因为实现 **Collection** 就意味着需要提供 **iterator()** 方法:
```java
// collections/InterfaceVsIterator.java
@@ -1330,7 +1336,7 @@ Britney=Pug, Sam=Cymric, Spot=Pug, Fluffy=Manx}
两个版本的 **display()** 方法都可以使用 **Map****Collection** 的子类型来工作。 而且**Collection** 接口和 **Iterator** 都将 **display()** 方法与低层集合的特定实现解耦。
在本例中,这两种方式都可以奏效。事实上, **Collection** 要更方便一点,因为它是 **Iterable** 类型,因此在 **display(Collection)** 的实现中可以使用*for-in*构造,这使得代码更加清晰。
在本例中,这两种方式都可以奏效。事实上, **Collection** 要更方便一点,因为它是 **Iterable** 类型,因此在 **display(Collection)** 的实现中可以使用 *for-in* 构造,这使得代码更加清晰。
当需要实现一个不是 **Collection** 的外部类时,由于让它去实现 **Collection** 接口可能非常困难或麻烦,因此使用 **Iterator** 就会变得非常吸引人。例如,如果我们通过继承一个持有 **Pet** 对象的类来创建一个 **Collection** 的实现,那么我们必须实现 **Collection** 所有的方法,即使我们不在 **display()** 方法中使用它们,也必须这样做。虽然这可以通过继承 **AbstractCollection** 而很容易地实现,但是无论如何还是要被强制去实现 **iterator()****size()** 方法,这些方法 **AbstractCollection** 没有实现,但是 **AbstractCollection** 中的其它方法会用到:
@@ -1422,7 +1428,7 @@ public class NonCollectionSequence extends PetSequence {
<!-- for-in and Iterators -->
## for-in和迭代器
到目前为止,*for-in*语法主要用于数组,但它也适用于任何 **Collection** 对象。实际上在使用 **ArrayList** 时,已经看到了一些使用它的示例,下面是一个更通用的证明:
到目前为止,*for-in* 语法主要用于数组,但它也适用于任何 **Collection** 对象。实际上在使用 **ArrayList** 时,已经看到了一些使用它的示例,下面是一个更通用的证明:
```java
// collections/ForInCollections.java
@@ -1443,9 +1449,9 @@ public class ForInCollections {
*/
```
由于 **cs** 是一个 **Collection** ,因此该代码展示了使用*for-in*是所有 **Collection** 对象的特征。
由于 **cs** 是一个 **Collection** ,因此该代码展示了使用 *for-in* 是所有 **Collection** 对象的特征。
这样做的原因是Java SE5引入了一个名为 **Iterable** 的接口,该接口包含一个能够生成 **Iterator****iterator()** 方法。*for-in*使用此 **Iterable** 接口来遍历序列。因此,如果创建了任何实现了 **Iterable** 的类,都可以将它用于*for-in*语句中:
这样做的原因是 Java 5 引入了一个名为 **Iterable** 的接口,该接口包含一个能够生成 **Iterator****iterator()** 方法。*for-in* 使用此 **Iterable** 接口来遍历序列。因此,如果创建了任何实现了 **Iterable** 的类,都可以将它用于 *for-in* 语句中:
```java
// collections/IterableClass.java
@@ -1482,9 +1488,9 @@ And that is how we know the Earth to be banana-shaped.
*/
```
**iterator()** 返回的是实现了 **Iterator\<String\>** 的匿名内部类的实例,该匿名内部类可以遍历数组中的每个单词。在 **main()** 中,可以看到 **IterableClass** 确实可以用于*for-in*语句。
**iterator()** 返回的是实现了 **Iterator\<String\>** 的匿名内部类的实例,该匿名内部类可以遍历数组中的每个单词。在 **main()** 中,可以看到 **IterableClass** 确实可以用于 *for-in* 语句。
在Java SE5中许多类都是 **Iterable** ,主要包括所有的 **Collection** 类(但不包括各种 **Maps** )。 例如,下面的代码可以显示所有的操作系统环境变量:
Java 5 中,许多类都是 **Iterable** ,主要包括所有的 **Collection** 类(但不包括各种 **Maps** )。 例如,下面的代码可以显示所有的操作系统环境变量:
```java
// collections/EnvironmentVariables.java
@@ -1501,9 +1507,9 @@ public class EnvironmentVariables {
}
```
**System.getenv()** [^7]返回一个 **Map** **entrySet()** 产生一个由 **Map.Entry** 的元素构成的 **Set** ,并且这个 **Set** 是一个 **Iterable** ,因此它可以用于*for-in*循环。
**System.getenv()** [^7]返回一个 **Map** **entrySet()** 产生一个由 **Map.Entry** 的元素构成的 **Set** ,并且这个 **Set** 是一个 **Iterable** ,因此它可以用于 *for-in* 循环。
*for-in*语句适用于数组或其它任何 **Iterable** ,但这并不意味着数组肯定也是个 **Iterable** ,也不会发生任何自动装箱:
*for-in* 语句适用于数组或其它任何 **Iterable** ,但这并不意味着数组肯定也是个 **Iterable** ,也不会发生任何自动装箱:
```java
// collections/ArrayIsNotIterable.java
@@ -1533,10 +1539,10 @@ public class ArrayIsNotIterable {
<!-- The Adapter Method Idiom -->
### 适配器方法惯用法
如果现在有一个 **Iterable** 类,你想要添加一种或多种在*for-in*语句中使用这个类的方法,应该怎么做呢? 例如,假设你希望可以选择是在正向还是反向遍历一个单词列表。如果直接继承这个类,并覆盖 **iterator()** 方法,则只能替换现有的方法,而不能实现选择。
如果现在有一个 **Iterable** 类,你想要添加一种或多种在 *for-in* 语句中使用这个类的方法,应该怎么做呢? 例如,假设你希望可以选择是在正向还是反向遍历一个单词列表。如果直接继承这个类,并覆盖 **iterator()** 方法,则只能替换现有的方法,而不能实现选择。
一种解决方案是所谓*适配器方法*Adapter Method的惯用法。“适配器”部分来自于设计模式因为必须要提供特定的接口来满足*for-in*语句。如果已经有一个接口并且需要另一个接口时,则编写适配器就可以解决这个问题。
在这里,若希望在默认的正向迭代器的基础上,添加产生反向迭代器的能力,因此不能使用覆盖,相反,而是添加了一个能够生成 **Iterable** 对象的方法,该对象可以用于*for-in*语句。这使得我们可以提供多种使用*for-in*语句的方式:
一种解决方案是所谓*适配器方法*Adapter Method的惯用法。“适配器”部分来自于设计模式因为必须要提供特定的接口来满足 *for-in* 语句。如果已经有一个接口并且需要另一个接口时,则编写适配器就可以解决这个问题。
在这里,若希望在默认的正向迭代器的基础上,添加产生反向迭代器的能力,因此不能使用覆盖,相反,而是添加了一个能够生成 **Iterable** 对象的方法,该对象可以用于 *for-in* 语句。这使得我们可以提供多种使用 *for-in* 语句的方式:
```java
// collections/AdapterMethodIdiom.java
@@ -1586,7 +1592,7 @@ be to not or be To
*/
```
**main()** 中,如果直接将 **ral** 对象放在*for-in*语句中,则会得到(默认的)正向迭代器。但是如果在该对象上调用 **reversed()** 方法,它会产生不同的行为。
**main()** 中,如果直接将 **ral** 对象放在 *for-in* 语句中,则会得到(默认的)正向迭代器。但是如果在该对象上调用 **reversed()** 方法,它会产生不同的行为。
通过使用这种方式,可以在 **IterableClass.java** 示例中添加两种适配器方法:
@@ -1684,11 +1690,11 @@ array: [9, 1, 6, 3, 7, 2, 5, 10, 4, 8]
<!-- Summary -->
## 本章小结
Java提供了许多保存对象的方法
Java 提供了许多保存对象的方法:
1. 数组将数字索引与对象相关联。它保存类型明确的对象,因此在查找对象时不必对结果做类型转换。它可以是多维的,可以保存基本类型的数据。虽然可以在运行时创建数组,但是一旦创建数组,就无法更改数组的大小。
2. **Collection** 保存单一的元素,而 **Map** 包含相关联的键值对。使用Java泛型可以指定集合中保存的对象的类型因此不能将错误类型的对象放入集合中并且在从集合中获取元素时不必进行类型转换。各种 **Collection** 和各种 **Map** 都可以在你向其中添加更多的元素时,自动调整其尺寸大小。集合不能保存基本类型,但自动装箱机制会负责执行基本类型和集合中保存的包装类型之间的双向转换。
2. **Collection** 保存单一的元素,而 **Map** 包含相关联的键值对。使用 Java 泛型,可以指定集合中保存的对象的类型,因此不能将错误类型的对象放入集合中,并且在从集合中获取元素时,不必进行类型转换。各种 **Collection** 和各种 **Map** 都可以在你向其中添加更多的元素时,自动调整其尺寸大小。集合不能保存基本类型,但自动装箱机制会负责执行基本类型和集合中保存的包装类型之间的双向转换。
3. 像数组一样, **List** 也将数字索引与对象相关联,因此,数组和 **List** 都是有序集合。
@@ -1698,14 +1704,15 @@ Java提供了许多保存对象的方法
6. **Map** 是一种将对象(而非数字)与对象相关联的设计。 **HashMap** 专为快速访问而设计,而 **TreeMap** 保持键始终处于排序状态,所以没有 **HashMap** 快。 **LinkedHashMap** 按插入顺序保存其元素,但使用散列提供快速访问的能力。
7. **Set** 不接受重复元素。 **HashSet** 提供最快的查询速度,而 **TreeSet** 保持元素处于排序状态。 **LinkedHashSets** 按插入顺序保存其元素,但使用散列提供快速访问的能力。
7. **Set** 不接受重复元素。 **HashSet** 提供最快的查询速度,而 **TreeSet** 保持元素处于排序状态。 **LinkedHashSet** 按插入顺序保存其元素,但使用散列提供快速访问的能力。
8. 不要在新代码中使用遗留类 **Vector** **Hashtable****Stack**
8. 不要在新代码中使用遗留类 **Vector** **Hashtable** 和 **Stack**
浏览一下Java集合的简图不包含抽象类或遗留组件会很有帮助。这里仅包括在一般情况下会碰到的接口和类。
![simple collection taxonomy](../images/simple-collection-taxonomy.png)
### 简单集合分类
可以看到,实际上只有四个基本的集合组件: **Map** **List** **Set****Queue** ,它们各有两到三个实现版本(**Queue** 的 **java.util.concurrent** 实现未包含在此图中)。最常使用的集合用黑色粗线线框表示。
@@ -1784,13 +1791,13 @@ Serializable]
*/
```
**TreeSet** 之外的所有 **Set** 都具有与 **Collection** 完全相同的接口。**List** 和 **Collection** 存在着明显的不同,尽管 **List** 所要求的方法都在 **Collection** 中。另一方面,在 **Queue** 接口中的方法是独立的,在创建具有 **Queue** 功能的实现时,不需要使用 **Collection** 方法。最后, **Map****Collection** 之间唯一的交集是Map可以使用 **entrySet()****values()** 方法来产生 **Collection**
**TreeSet** 之外的所有 **Set** 都具有与 **Collection** 完全相同的接口。**List** 和 **Collection** 存在着明显的不同,尽管 **List** 所要求的方法都在 **Collection** 中。另一方面,在 **Queue** 接口中的方法是独立的,在创建具有 **Queue** 功能的实现时,不需要使用 **Collection** 方法。最后, **Map****Collection** 之间唯一的交集是 **Map** 可以使用 **entrySet()****values()** 方法来产生 **Collection**
请注意,标记接口 **java.util.RandomAccess** 附加到了 **ArrayList** 上,但不附加到 **LinkedList** 上。这为根据特定 **List** 动态改变其行为的算法提供了信息。
从面向对象的继承层次结构来看,这种组织结构确实有些奇怪。但是,当了解了 **java.util** 中更多的有关集合的内容后(特别是在[附录:集合主题]()中的内容),就会发现出了继承结构有点奇怪外,还有更多的问题。集合类库一直以来都是设计难题——解决这些问题涉及到要去满足经常彼此之间互为牵制的各方面需求。所以要做好准备,在各处做出妥协。
尽管存在这些问题但Java集合仍是在日常工作中使用的基本工具它可以使程序更简洁、更强大、更有效。你可能需要一段时间才能熟悉集合类库的某些方面但我想你很快就会找到自己的路子来获得和使用这个类库中的类。
尽管存在这些问题,但 Java 集合仍是在日常工作中使用的基本工具,它可以使程序更简洁、更强大、更有效。你可能需要一段时间才能熟悉集合类库的某些方面,但我想你很快就会找到自己的路子,来获得和使用这个类库中的类。
[^1]: 许多语言例如PerlPython和Ruby都有集合的本地支持。
@@ -1804,7 +1811,7 @@ Serializable]
[^6]: 有些人提倡这样一种自动创建机制,即对一个类中所有可能的方法组合都自动创建一个接口,有时候对于单个的类都是如此。 我相信接口的意义不应该仅限于方法组合的机械地复制,因此我在创建接口之前,总是要先看到增加接口带来的价值。
[^7]: 这在Java SE5之前是不可用的因为该方法被认为与操作系统的耦合度过紧因此违反“一次编写处处运行”的原则。现在提供它这一事实表明Java的设计者们更加务实了。
[^7]: 这在Java 5之前是不可用的因为该方法被认为与操作系统的耦合度过紧因此违反“一次编写处处运行”的原则。现在提供它这一事实表明Java的设计者们更加务实了。
<!-- 分页 -->