diff --git a/README.md b/README.md index 72146164..28bdf59d 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ * [3.4 比较](https://github.com/zerotoneorg/Java-Generics-and-Collections/tree/e41f1e9a8eb376f2de33abd6022ce3b2989ba771/ch03/04_Comparator.md#比较) * [3.5 枚举类型](https://github.com/zerotoneorg/Java-Generics-and-Collections/tree/e41f1e9a8eb376f2de33abd6022ce3b2989ba771/ch03/05_Enumerated_Types.md#枚举类型) * [3.6 多重界限](https://github.com/zerotoneorg/Java-Generics-and-Collections/tree/e41f1e9a8eb376f2de33abd6022ce3b2989ba771/ch03/06_Multiple_Bounds.md#多重界限) - * [3.7 桥梁](https://github.com/zerotoneorg/Java-Generics-and-Collections/tree/e41f1e9a8eb376f2de33abd6022ce3b2989ba771/ch03/07_Bridges.md#桥梁) + * [3.7 桥接器](https://github.com/zerotoneorg/Java-Generics-and-Collections/tree/e41f1e9a8eb376f2de33abd6022ce3b2989ba771/ch03/07_Bridges.md#桥接器) * [3.8 协变覆盖](https://github.com/zerotoneorg/Java-Generics-and-Collections/tree/e41f1e9a8eb376f2de33abd6022ce3b2989ba771/ch03/08_Covariant_Overriding.md#协变覆盖) * [第四章\(声明\)](https://github.com/zerotoneorg/Java-Generics-and-Collections/tree/e41f1e9a8eb376f2de33abd6022ce3b2989ba771/ch04/00_Declarations.md#声明) * [4.1 构造函数](https://github.com/zerotoneorg/Java-Generics-and-Collections/tree/e41f1e9a8eb376f2de33abd6022ce3b2989ba771/ch04/01_Constructors.md#构造函数) @@ -71,7 +71,7 @@ * [8.3 专注于创建可维持类型](https://github.com/zerotoneorg/Java-Generics-and-Collections/tree/e41f1e9a8eb376f2de33abd6022ce3b2989ba771/ch08/03_Specialize_to_Create_Reifiable_Types.md#专注于创建可维持类型) * [8.4 保持二进制兼容性](https://github.com/zerotoneorg/Java-Generics-and-Collections/tree/e41f1e9a8eb376f2de33abd6022ce3b2989ba771/ch08/04_Maintain_Binary_Compatibility.md#保持二进制兼容性) * [第九章\(设计模式\)](https://github.com/zerotoneorg/Java-Generics-and-Collections/tree/e41f1e9a8eb376f2de33abd6022ce3b2989ba771/ch09/00_Design_Patterns.md#设计模式) - * [9.1 游客](https://github.com/zerotoneorg/Java-Generics-and-Collections/tree/e41f1e9a8eb376f2de33abd6022ce3b2989ba771/ch09/01_Visitor.md#游客) + * [9.1 访问者](https://github.com/zerotoneorg/Java-Generics-and-Collections/tree/e41f1e9a8eb376f2de33abd6022ce3b2989ba771/ch09/01_Visitor.md#访问者) * [9.2 翻译员](https://github.com/zerotoneorg/Java-Generics-and-Collections/tree/e41f1e9a8eb376f2de33abd6022ce3b2989ba771/ch09/02_Interpreter.md#翻译员) * [9.3 功能](https://github.com/zerotoneorg/Java-Generics-and-Collections/tree/e41f1e9a8eb376f2de33abd6022ce3b2989ba771/ch09/03_Function.md#功能) * [9.4 策略](https://github.com/zerotoneorg/Java-Generics-and-Collections/tree/e41f1e9a8eb376f2de33abd6022ce3b2989ba771/ch09/04_Strategy.md#策略) diff --git a/ch02/02_Wildcards_with_extends.md b/ch02/02_Wildcards_with_extends.md index 709816cf..8aeaab47 100644 --- a/ch02/02_Wildcards_with_extends.md +++ b/ch02/02_Wildcards_with_extends.md @@ -5,27 +5,27 @@ `Collection` 接口中的另一个方法是 `addAll`,它将一个集合的所有成员添加到另一个集合中: - ```java - interface Collection { - ... - public boolean addAll(Collection c); - ... - } - ``` +```java + interface Collection { + ... + public boolean addAll(Collection c); + ... + } +``` 显然,给定 `E` 类型的元素的集合,可以将另一个集合的所有成员添加到 `E` 类型的元素中。这个古怪的短语 `? extends E` 意味着也可以添加元素任何类型都是 `E` 的子类型的所有成员 。问号称为通配符,因为它代表某种类型,它是 `E` 的子类型。 这是一个例子。 我们创建一个空的数字列表,并首先添加一个整数列表,然后是一个双精度列表: - ```java - List nums = new ArrayList(); - List ints = Arrays.asList(1, 2); - List dbls = Arrays.asList(2.78, 3.14); - nums.addAll(ints); - nums.addAll(dbls); - assert nums.toString().equals("[1, 2, 2.78, 3.14]"); - ``` +```java + List nums = new ArrayList(); + List ints = Arrays.asList(1, 2); + List dbls = Arrays.asList(2.78, 3.14); + nums.addAll(ints); + nums.addAll(dbls); + assert nums.toString().equals("[1, 2, 2.78, 3.14]"); +``` 第一个调用是允许的,因为 `nums` 的类型是 `List`,它是 `Collection` 的子类型,而 `ints` 的类型是 `List` ,它是 `Collection` 的子类型 `Collection`。 第二个调用同样被允许。 在这两个调用中,`E` 被认为是 `Number`。 如果 `addAll` 的方法签名 @@ -33,14 +33,14 @@ 声明变量时我们也可以使用通配符。 下面是上一节末尾的示例变体,通过在第二行添加通配符进行更改: - ```java - List ints = new ArrayList(); - ints.add(1); - ints.add(2); - List nums = ints; - nums.add(3.14); // 编译报错 - assert ints.toString().equals("[1, 2, 3.14]"); // uh oh! - ``` +```java + List ints = new ArrayList(); + ints.add(1); + ints.add(2); + List nums = ints; + nums.add(3.14); // 编译报错 + assert ints.toString().equals("[1, 2, 3.14]"); // uh oh! +``` 之前,第四行导致了一个编译时错误(因为 `List` 不是 `List` 的子类型),但是第五行没有问题(因为 `double` 是一个数字,所以你可以添 加一个 `double` 到一个 `List`)。 现在,第四行是正确的(因为 `List` 是 `List` 的子类型),但是第五行会导致编 diff --git a/ch02/03_Wildcards_with_super.md b/ch02/03_Wildcards_with_super.md index fd93131e..d93224d4 100644 --- a/ch02/03_Wildcards_with_super.md +++ b/ch02/03_Wildcards_with_super.md @@ -5,33 +5,33 @@ 下面是一个方法,它将方便类集合中的源列表中的所有元素复制到目标列表中: - ```java - public static void copy(List dst, List src) { - for (int i = 0; i < src.size(); i++) { - dst.set(i, src.get(i)); - } +```java + public static void copy(List dst, List src) { + for (int i = 0; i < src.size(); i++) { + dst.set(i, src.get(i)); } - ``` + } +``` 这个奇怪的短语 `? super T` 意味着目的地列表可能有元素任何类型都是 `T` 的超类型,就像源列表可能有任何类型的元素是 `T` 的子类型: 这是一个调用示例: - ```java - List objs = Arrays.asList(2, 3.14, "four"); - List ints = Arrays.asList(5, 6); - Collections.copy(objs, ints); - assert objs.toString().equals("[5, 6, four]"); - ``` +```java + List objs = Arrays.asList(2, 3.14, "four"); + List ints = Arrays.asList(5, 6); + Collections.copy(objs, ints); + assert objs.toString().equals("[5, 6, four]"); +``` 与任何泛型方法一样,可以推断或者可以明确给出类型参数。 在这种情况下,有四种可能的选择,所有这些类型检查和所有这些都具有相同的效果: - ```java - Collections.copy(objs, ints); - Collections.copy(objs, ints); - Collections.copy(objs, ints); - Collections.copy(objs, ints); - ``` +```java + Collections.copy(objs, ints); + Collections.copy(objs, ints); + Collections.copy(objs, ints); + Collections.copy(objs, ints); +``` 第一次调用离开类型参数隐式; 它被认为是 `Integer`,因为这是最有效的选择。 在第三行中,类型参数 `T` 取为 `Number`。 该调用被允许,因为 `objs` 的类型 是 `List`,它是 `List` 的子类型(因为对象是一个数字的超类型,通配符的要求)和整数有 `List`,这是 @@ -39,12 +39,12 @@ 我们也可以用几个可能的签名来声明这个方法。 - ```java - public static void copy(List dst, List src) - public static void copy(List dst, List src) - public static void copy(List dst, List src) - public static void copy(List dst, List src) - ``` +```java + public static void copy(List dst, List src) + public static void copy(List dst, List src) + public static void copy(List dst, List src) + public static void copy(List dst, List src) +``` 第一种限制性太强,因为只有当目的地和来源具有完全相同的类型时才允许调用。 其余三个对于使用隐式类型参数的调用是等效的,但是对于显式类型参数不同。 对于 上面的示例调用,第二个签名仅在类型参数为 `Object` 时起作用,第三个签名仅在类型参数为 `Integer` 时起作用,最后一个签名对所有三个类型参数起作用(如我 diff --git a/ch02/04_The_Get_and_Put_Principle.md b/ch02/04_The_Get_and_Put_Principle.md index dafbd174..3eb89d79 100644 --- a/ch02/04_The_Get_and_Put_Principle.md +++ b/ch02/04_The_Get_and_Put_Principle.md @@ -11,86 +11,86 @@ 我们已经在复制方法的签名中看到了这个原理: - ```java - public static void copy(List dest, List src) - ``` +```java + public static void copy(List dest, List src) +``` 该方法从源 `src` 中获取值,因此使用扩展通配符声明值,并将值放入目标 `dst` 中,因此使用超级通配符声明值。 无论何时使用迭代器,都会从结构中获取值,因此请使用扩展通配符。 这是一个需要一个数字集合的方法,每个转换为一个双精度求和: - ```java - public static double sum(Collection nums) { - double s = 0.0; - for (Number num : nums){ - s += num.doubleValue(); - } - return s; +```java + public static double sum(Collection nums) { + double s = 0.0; + for (Number num : nums){ + s += num.doubleValue(); } - ``` + return s; + } +``` 由于这个使用 `extends`,所有以下的调用是合法的: - ```java - List ints = Arrays.asList(1,2,3); - assert sum(ints) == 6.0; - List doubles = Arrays.asList(2.78,3.14); - assert sum(doubles) == 5.92; - List nums = Arrays.asList(1,2,2.78,3.14); - assert sum(nums) == 8.92; - ``` +```java + List ints = Arrays.asList(1,2,3); + assert sum(ints) == 6.0; + List doubles = Arrays.asList(2.78,3.14); + assert sum(doubles) == 5.92; + List nums = Arrays.asList(1,2,2.78,3.14); + assert sum(nums) == 8.92; +``` 如果不使用 `extends`,前两个调用将不合法 每当你使用add方法时,你把值放到一个结构中,所以使用 `super` 通配符。 这是一个采用数字和整数n的集合的方法将从零开始的前n个整数放入集合中: - ```java - public static void count(Collection ints, int n) { - for (int i = 0; i < n; i++) ints.add(i); - } - ``` +```java + public static void count(Collection ints, int n) { + for (int i = 0; i < n; i++) ints.add(i); + } +``` 由于这使用 `super`,以下所有调用都是合法的: - ```java - List ints = new ArrayList(); - count(ints, 5); - assert ints.toString().equals("[0, 1, 2, 3, 4]"); - List nums = new ArrayList(); - count(nums, 5); nums.add(5.0); - assert nums.toString().equals("[0, 1, 2, 3, 4, 5.0]"); - List objs = new ArrayList(); - count(objs, 5); objs.add("five"); - assert objs.toString().equals("[0, 1, 2, 3, 4, five]"); - ``` +```java + List ints = new ArrayList(); + count(ints, 5); + assert ints.toString().equals("[0, 1, 2, 3, 4]"); + List nums = new ArrayList(); + count(nums, 5); nums.add(5.0); + assert nums.toString().equals("[0, 1, 2, 3, 4, 5.0]"); + List objs = new ArrayList(); + count(objs, 5); objs.add("five"); + assert objs.toString().equals("[0, 1, 2, 3, 4, five]"); +``` 如果 `super` 不被使用,最后两个调用将是不合法的。无论何时您将值放入并从同一结构中获取值,都不应使用通配符。 - ```java - public static double sumCount(Collection nums, int n) { - count(nums, n); - return sum(nums); - } - ``` +```java + public static double sumCount(Collection nums, int n) { + count(nums, n); + return sum(nums); + } +``` 集合被传递给 `sum` 和 `count`,所以它的元素类型都必须继承 `Number`(按总数要求),`Integer` 的超类(按计数要求)。 唯一满足这两个约束的两个类是 `Number` 和 `Integer`,我们选择了第一个。 以下是一个调用示例: - ```java - List nums = new ArrayList(); - double sum = sumCount(nums,5); - assert sum == 10; - ``` +```java + List nums = new ArrayList(); + double sum = sumCount(nums,5); + assert sum == 10; +``` 由于没有通配符,参数必须是 `Number` 的集合。 如果您不喜欢在 `Number` 和 `Integer` 之间进行选择,那么您可能会想到,如果 `Java` 允许您使用 `extends` 和 `super` 编写通配符,则不需要选择。 例 如,我们可以写下以下内容: - ```java - double sumCount(Collection coll, int n) - // 这在java里面是非法的 - ``` +```java + double sumCount(Collection coll, int n) + // 这在java里面是非法的 +``` 然后我们可以在一个数字集合或一个整数集合上调用 `sumCount`。 但 `Java` 不允许这样做。 打乱它的唯一原因是简单,可以想象 `Java` 在将来可能会支持这种表 示法。 但是,现在,如果你想同时获取和放置不要使用通配符。 @@ -100,26 +100,26 @@ 例如,考虑下面的代码片段,它使用一个用扩展通配符声明的列表: - ```java - List ints = new ArrayList(); - ints.add(1); - ints.add(2); - List nums = ints; - double dbl = sum(nums); // ok - nums.add(3.14); // compile-time error - ``` +```java + List ints = new ArrayList(); + ints.add(1); + ints.add(2); + List nums = ints; + double dbl = sum(nums); // ok + nums.add(3.14); // compile-time error +``` 调用它求和是好的,因为它从列表中获取值,但调用 `add` 的不是,因为它将一个值放入列表中。 这也是一样,因为否则我们可以添加双整数列表!相反,考虑下面的 代码片段,它使用一个超级通配符声明的列表: - ```java - List objs = new ArrayList(); - objs.add(1); - objs.add("two"); - List ints = objs; - ints.add(3); // ok - double dbl = sum(ints); // 编译报错 - ``` +```java + List objs = new ArrayList(); + objs.add(1); + objs.add("two"); + List ints = objs; + ints.add(3); // ok + double dbl = sum(ints); // 编译报错 +``` 现在调用 `add` 是正常的,因为它将一个值放入列表中,但是对 `sum` 的调用不是,因为它从列表中获取值。 这也是一样,因为包含一个字符串的列表的总和是没有 意义的! @@ -137,13 +137,13 @@ 同样,你也不能从使用超级通配符声明的类型中获取任何东西 - 除了 `Object` 类型的值,它是每个引用类型的超类型: - ```java - List objs = Arrays.asList(1,"two"); - List ints = objs; - String str = ""; - for (Object obj : ints) str += obj.toString(); - assert str.equals("1two"); - ``` +```java + List objs = Arrays.asList(1,"two"); + List ints = objs; + String str = ""; + for (Object obj : ints) str += obj.toString(); + assert str.equals("1two"); +``` 你可能会觉得有帮助的想法? 将 `T` 扩展为包含每个类型的一个区间,该区间由下面的 `null` 类型和上面的 `T`(其中 `null` 的类型是每个引用类型的子类型) 限定。 同样,你可能会想到? `super T` 包含每个类型在由 `T` 和由上面的 `Object` 限定的区间中。 diff --git a/ch02/05_Arrays.md b/ch02/05_Arrays.md index 619f0416..e75c0ab6 100644 --- a/ch02/05_Arrays.md +++ b/ch02/05_Arrays.md @@ -8,12 +8,12 @@ 在 `Java` 中,数组的子类型是协变的,这意味着当 `S` 是 `T` 的子类型时,类型 `S []` 被认为是 `T []` 的一个子类型。考虑下面的代码片段,它分配一个整数 数组,分配一个数组 的数字,然后尝试在数组中分配一个 `double`: - ```java - Integer[] ints = new Integer[] {1,2,3}; - Number[] nums = ints; - nums[2] = 3.14; // array store exception - assert Arrays.toString(ints).equals("[1, 2, 3.14]"); // uh oh! - ``` +```java + Integer[] ints = new Integer[] {1,2,3}; + Number[] nums = ints; + nums[2] = 3.14; // array store exception + assert Arrays.toString(ints).equals("[1, 2, 3.14]"); // uh oh! +``` 这个程序有什么问题,因为它把一个整数数组放入一个 `double` !哪里有问题? 由于 `Integer []` 被认为是 `Number []` 的子类型,所以根据替换原则,第二行 的赋值必须是合法的。 相反,问题出现在第三行,并在运行时被捕获。 当一个数组被分配时(如在第一行),它被标记为它的被指定的类型(它的组件类型的运行时表 @@ -23,23 +23,23 @@ 相比之下,泛型的子类型关系是不变的,意味着类型 `List` 不被认为是 `List` 的子类型,除了 `S` 和 `T` 相同的普通情况。 这是一个类似于前一个的代 码片段,用列表替换数组: - ```java - List ints = Arrays.asList(1,2,3); - List nums = ints; // 编译时报错 - nums.set(2, 3.14); - assert ints.toString().equals("[1, 2, 3.14]"); // uh oh! - ``` +```java + List ints = Arrays.asList(1,2,3); + List nums = ints; // 编译时报错 + nums.set(2, 3.14); + assert ints.toString().equals("[1, 2, 3.14]"); // uh oh! +``` 由于 `List` 不被认为是 `List` 的子类型,因此在第二行而不是第三行检测到问题,并且在编译时检测到,而不是在运行时检测到。 -通配符重新引入泛型的协变子类型,在当S是T的子类型时,这种类型中 `List` 被认为是 `List` 的子类型? 这是片段的第三个变体: +通配符重新引入泛型的协变子类型,在当 `S` 是 `T` 的子类型时,这种类型中 `List` 被认为是 `List` 的子类型? 这是片段的第三个变体: - ```java - List ints = Arrays.asList(1,2,3); - List nums = ints; - nums.set(2, 3.14); // 编译时报错 - assert ints.toString().equals("[1, 2, 3.14]"); // uh oh! - ``` +```java + List ints = Arrays.asList(1,2,3); + List nums = ints; + nums.set(2, 3.14); // 编译时报错 + assert ints.toString().equals("[1, 2, 3.14]"); // uh oh! +``` 和数组一样,第三行是错误的,但是与数组相比,这个问题在编译时被检测到,而不是运行时。 该分配违反了“获取和放置原则”,因为您不能将值放入使用 `extends` 通配符声明的类型中。 @@ -64,18 +64,18 @@ 总而言之,最好在编译时检测错误,而不是在运行时检测错误,但是 `Java` 数组在运行时被强制检测到某些错误,因为决定做出数组子类型协变。 这是一个很好的决 定? 在泛型出现之前,这是绝对必要的。 例如,看下面的方法,这些方法用于对任何数组进行排序或使用给定值填充数组: - ```java - public static void sort(Object[] a); - public static void fill(Object[] a, Object val); - ``` +```java + public static void sort(Object[] a); + public static void fill(Object[] a, Object val); +``` 由于协变,这些方法可以用来排序或填充任何引用类型的数组。 没有协变性,没有泛型,就没有办法声明适用于所有类型的方法。 但是,现在我们已经有了泛型,协变 阵列就不再需要了。 现在我们可以给这些方法以下签名,直接说明它们适用于所有类型: - ```java - public static void sort(T[] a); - public static void fill(T[] a, T val); - ``` +```java + public static void sort(T[] a); + public static void fill(T[] a, T val); +``` 从某种意义上讲,协变数组是早期 `Java` 版本中缺乏泛型的人为因素。 一旦你有泛型,协变数组可能是错误的设计选择,保留它们的唯一原因是向后兼容。 diff --git a/ch02/06_Wildcards_Versus_Type_Parameters.md b/ch02/06_Wildcards_Versus_Type_Parameters.md index 5e6eaf2e..71342d47 100644 --- a/ch02/06_Wildcards_Versus_Type_Parameters.md +++ b/ch02/06_Wildcards_Versus_Type_Parameters.md @@ -8,81 +8,81 @@ 通配符: 以下是 `Java` 中泛型方法的类型: - ```java - interface Collection { - ... - public boolean contains(Object o); - public boolean containsAll(Collection c); - ... - } - ``` +```java +interface Collection { + ... + public boolean contains(Object o); + public boolean containsAll(Collection c); + ... +} +``` 第一种方法根本不使用泛型! 第二种方法是我们第一次看到一个重要的缩写。 类型 `Collection ` 代表: - ```java - Collection - ``` +```java +Collection +``` 扩展对象是通配符最常用的用法之一,所以提供一个简短的表单来编写它是有意义的. 这些方法让我们测试成员和遏制: - ```java - Object obj = "one"; - List objs = Arrays.asList("one", 2, 3.14, 4); - List ints = Arrays.asList(2, 4); - assert objs.contains(obj); - assert objs.containsAll(ints); - assert !ints.contains(obj); - assert !ints.containsAll(objs); - ``` +```java +Object obj = "one"; +List objs = Arrays.asList("one", 2, 3.14, 4); +List ints = Arrays.asList(2, 4); +assert objs.contains(obj); +assert objs.containsAll(ints); +assert !ints.contains(obj); +assert !ints.containsAll(objs); +``` -给定的对象列表包含字符串“one”和给定的整数列表,但给定的整数列表不包含字符串“one”,也不包含给定的对象列表。 +给定的对象列表包含字符串 “one” 和给定的整数列表,但给定的整数列表不包含字符串 “one”,也不包含给定的对象列表。 -测试 `ints.contains(obj)` 和 `ints.containsAll(objs)` 可能看起来很愚蠢。当然,整数列表将不包含任意对象,如字符串“one”。 但这是允许的,因为有时这 -样的测试可能会成功: +测试 `ints.contains(obj)` 和 `ints.containsAll(objs)` 可能看起来很愚蠢。当然,整数列表将不包含任意对象,如字符串 “one”。 但这是允许的,因为有时 +这样的测试可能会成功: - ```java - Object obj = 1; - List objs = Arrays.asList(1, 3); - List ints = Arrays.asList(1, 2, 3, 4); - assert ints.contains(obj); - assert ints.containsAll(objs); - ``` +```java +Object obj = 1; +List objs = Arrays.asList(1, 3); +List ints = Arrays.asList(1, 2, 3, 4); +assert ints.contains(obj); +assert ints.containsAll(objs); +``` 在这种情况下,对象可能被包含在整数列表中,因为它碰巧是一个整数,并且对象列表可能包含在整数列表中,因为列表中的每个对象碰巧是一个整数。 - 类型参数你 可以合理地选择一个替代设计的集合- 只能测试元素类型的子类型的容器的设计: - ```java - interface MyCollection { // alternative design - ... - public boolean contains(E o); - public boolean containsAll(Collection c); - ... - } - ``` +```java +interface MyCollection { // alternative design + ... + public boolean contains(E o); + public boolean containsAll(Collection c); + ... +} +``` 假设我们有一个实现 `MyCollection` 的 `MyList` 类。 现在这些测试是合法的,只有一个方法: - ```java - Object obj = "one"; - MyList objs = MyList.asList("one", 2, 3.14, 4); - MyList ints = MyList.asList(2, 4); - assert objs.contains(obj); - assert objs.containsAll(ints) - assert !ints.contains(obj); // 编译报错 - assert !ints.containsAll(objs); // 编译报错 - ``` +```java +Object obj = "one"; +MyList objs = MyList.asList("one", 2, 3.14, 4); +MyList ints = MyList.asList(2, 4); +assert objs.contains(obj); +assert objs.containsAll(ints) +assert !ints.contains(obj); // 编译报错 +assert !ints.containsAll(objs); // 编译报错 +``` 最后两个测试是非法的,因为类型声明要求我们只能测试一个列表是否包含该列表的一个子类型的元素。所以我们可以检查一个对象列表是否包含整数列表,而不是相 反。 -两种风格中哪一种更好是味道的问题。第一个允许更多的测试,第二个在编译时捕获更多的错误(同时排除一些明显的测试)。 `Java` 库的设计者选择了第一种更自由 -的替代方案,因为在泛型之前使用集合框架的人可能已经编写了诸如 `ints.containsAll(objs)` 之类的测试,并且该人希望该测试在泛型之后保持有效被添加到 +两种风格中哪一种更好是的实现的问题。第一个允许更多的测试,第二个在编译时捕获更多的错误(同时排除一些明显的测试)。 `Java` 库的设计者选择了第一种更自 +由的替代方案,因为在泛型之前使用集合框架的人可能已经编写了诸如 `ints.containsAll(objs)` 之类的测试,并且该人希望该测试在泛型之后保持有效被添加到 `Java`。但是,在设计新的通用库(如 `MyCollection` )时,如果向后兼容性不太重要,那么在编译时捕获更多错误的设计可能更有意义。 可以说,核心包设计师做出了错误的选择。很少需要像 `ints.containsAll(objs)` 这样的测试,而且这样的测试仍然可以通过声明 `int` 具有 `List` 类 -型而不是 `List` 类型来允许。在普通情况下捕捉更多的错误可能会更好,而不是允许在一个不常见的情况下进行更精确的打字。 +型而不是允许 `List` 类型。在普通情况下捕捉更多的错误可能会更好,而不是允许在一个不常见的情况下进行更精确的实现。 同样的设计选择适用于包含 `Object` 或 `Collection` 的其他方法。在他们的签名中,如 `remove`,`removeAll` 和 `retainAll`。 diff --git a/ch02/07_Wildcard_Capture.md b/ch02/07_Wildcard_Capture.md index ec025050..911c395e 100644 --- a/ch02/07_Wildcard_Capture.md +++ b/ch02/07_Wildcard_Capture.md @@ -3,63 +3,63 @@ ### 通配符捕获 -当调用泛型方法时,可以选择类型参数以匹配由通配符表示的未知类型。 这被称为通配符捕获。 +当调用泛型方法时,可以选择类型参数以匹配由通配符表示的未知类型。这被称为通配符捕获。 -考虑工具类 `java.util.Collections` 中的反向方法,它接受任何类型的列表并将其反转。 它可以是以下两个签名中的任何一个,它们是相同的: +考虑工具类 `java.util.Collections` 中的反向方法,它接受任何类型的列表并将其反向。它可以是以下两个签名中的任何一个,它们是相同的: - ```java - public static void reverse(List list); - public static void reverse(List list); - ``` +```java +public static void reverse(List list); +public static void reverse(List list); +``` 通配符签名稍短且更清晰,是类库中使用的签名。如果使用第二个签名,则很容易实现该方法: - ```java - public static void reverse(List list) { - List tmp = new ArrayList(list); - for (int i = 0; i < list.size(); i++) { - list.set(i, tmp.get(list.size()-i-1)); - } - } - ``` +```java +public static void reverse(List list) { + List tmp = new ArrayList(list); + for (int i = 0; i < list.size(); i++) { + list.set(i, tmp.get(list.size() - i - 1)); + } +} +``` 这会将参数复制到临时列表中,然后以相反的顺序将副本写回到原始文件中。如果您尝试使用类似方法体的第一个签名,它将不起作用: - - ```java - public static void reverse(List list) { - List tmp = new ArrayList(list); - for (int i = 0; i < list.size(); i++) { - list.set(i, tmp.get(list.size()-i-1)); // 编译报错 - } - } - ``` + +```java +public static void reverse(List list) { + List tmp = new ArrayList(list); + for (int i = 0; i < list.size(); i++) { + list.set(i, tmp.get(list.size() - i - 1)); // 编译报错 + } +} +``` 现在从拷贝写入原始文件是不合法的,因为我们试图从对象列表写入未知类型的列表。 用 `List` 替换 `List` 不会解决问题,因为现在我们有两个带有 (可能不同)未知元素类型的列表。 相反,您可以通过使用第二个签名实现私有方法并从第一个签名调用第二个签名来实现具有第一个签名的方法: - ```java - public static void reverse(List list) { rev(list); } - private static void rev(List list) { - List tmp = new ArrayList(list); - for (int i = 0; i < list.size(); i++) { - list.set(i, tmp.get(list.size()-i-1)); - } - } - ``` +```java +public static void reverse(List list) { rev(list); } +private static void rev(List list) { + List tmp = new ArrayList(list); + for (int i = 0; i < list.size(); i++) { + list.set(i, tmp.get(list.size() - i - 1)); + } +} +``` -在这里我们说类型变量T已经捕获了通配符。 这是处理通配符时通常有用的技巧,值得了解。 +在这里我们说类型变量 `T` 已经捕获了通配符。 这是处理通配符时通常有用的技巧,值得了解。   -了解通配符捕获的另一个原因是,即使您不使用上述技术,它也可以显示在错误消息中。 通常,每次出现通配符都表示某种未知类型。 如果编译器打印包含此类型的错 -误消息,则称为捕获?。 例如,使用 `Sun` 当前的编译器,反向版本不正确会生成以下错误消息: +了解通配符捕获的另一个原因是,即使您不使用上述技术,它也可以显示在错误消息中。通常,每次出现通配符都表示某种未知类型。 如果编译器打印包含此类型的错误 +消息,则称为捕获 `?`。 例如,使用 `Sun` 当前的编译器,反向版本不正确会生成以下错误消息: - ```java - Capture.java:6: set(int,capture of ?) in java.util.List - cannot be applied to (int,java.lang.Object) - list.set(i, tmp.get(list.size()-i-1)); - ^ - ``` +```java +Capture.java:6: set(int,capture of ?) in java.util.List +cannot be applied to (int,java.lang.Object) +list.set(i, tmp.get(list.size() - i - 1)); + ^ +``` 因此,如果你看到这个奇怪的短语 ` capture of ?` 在错误消息中,它将来自通配符类型。 即使有两个不同的通配符,编译器也会打印与每个相关的类型作为 `capture of ?`。 有界的通配符会生成更加冗长的名称,如 `capture of ? extends Number`。 diff --git a/ch02/08_Restrictions_on_Wildcards.md b/ch02/08_Restrictions_on_Wildcards.md index 671a6904..2209b4f2 100644 --- a/ch02/08_Restrictions_on_Wildcards.md +++ b/ch02/08_Restrictions_on_Wildcards.md @@ -7,43 +7,44 @@ **实例创建**在类实例创建表达式中,如果类型是参数化类型,则没有任何类型参数可能是通配符。 例如,以下是非法的: - ```java - List list = new ArrayList(); // 编译报错 - Map map = new HashMap(); // 编译报错 - ``` +```java +List list = new ArrayList(); // 编译报错 +Map map = new HashMap(); // 编译报错 +``` -这通常不是困难。 `Get` 和 `Put` 原则告诉我们,如果一个结构体包含通配符,那么我们只应该从中得到值(如果它是一个扩展通配符)或者只将值放入它中(如果它是一个超级通配符)。 为了使结构有用,我们必须同时做到这两点。 因此,我们通常以精确的类型创建结构,即使我们使用通配符类型将值放入或从结构中获取值,如 -下例所示: - - ```java - List nums = new ArrayList(); - List sink = nums; - List source = nums; - for (int i=0; i<10; i++) sink.add(i); - double sum=0; for (Number num : source) sum+=num.doubleValue(); - ``` +这通常不困难。 获取和放置告诉我们,如果一个结构体包含通配符,那么我们只应该从中得到值(如果它是一个扩展通配符)或者只将值放入它中(如果它是一个超级 +通配符)。 为了使结构有用,我们必须同时做到这两点。 因此,我们通常以精确的类型创建结构,即使我们使用通配符类型将值放入或从结构中获取值,如下例所示: + +```java +List nums = new ArrayList(); +List sink = nums; +List source = nums; +for (int i=0; i<10; i++) sink.add(i); +double sum=0; for (Number num : source) sum+=num.doubleValue(); +``` 这里通配符出现在第二行和第三行,但不在创建列表的第一行。 禁止包含通配符的实例创建中只有顶级参数。 允许嵌套通配符。 因此,以下是合法的: - ```java - List> lists = new ArrayList>(); - lists.add(Arrays.asList(1,2,3)); - lists.add(Arrays.asList("four","five")); - assert lists.toString().equals("[[1, 2, 3], [four, five]]"); - ``` +```java +List> lists = new ArrayList>(); +lists.add(Arrays.asList(1,2,3)); +lists.add(Arrays.asList("four","five")); +assert lists.toString().equals("[[1, 2, 3], [four, five]]"); +``` 即使列表的列表是以通配符类型创建的,其中的每个单独列表都有一个特定的类型:第一个列表是整数列表,第二个列表是字符串列表。 通配符类型禁止我们将内部列表 中的元素作为 `Object` 以外的任何类型提取,但由于这是 `toString` 使用的类型,因此此代码的类型很好。 -记住限制的一种方式是通配符和普通类型之间的关系类似于接口和类通配符之间的关系,接口更普遍,普通类型和类更具体,实例创建需要更具体的信息。 考虑以下三条陈述: +记住限制的一种方式是通配符和普通类型之间的关系类似于接口和类通配符之间的关系,接口更普遍,普通类型和类更具体,实例创建需要更具体的信息。 考虑以下三条 +陈述: - ```java - List list = new ArrayList(); // ok - List list = new List() // 编译报错 - List list = new ArrayList() // 编译报错 - ``` +```java +List list = new ArrayList(); // ok +List list = new List() // 编译报错 +List list = new ArrayList() // 编译报错 +``` 第一个是合法的; 第二个是非法的,因为实例创建表达式需要一个类,而不是一个接口; 第三个是非法的,因为实例创建表达式需要普通类型而不是通配符。 @@ -52,51 +53,51 @@ **泛型方法调用**如果泛型方法调用包含显式类型参数,那么这些类型参数不能是通配符。 例如,假设我们有以下通用方法: - ```java - class Lists { - public static List factory() { return new ArrayList(); } - } - ``` +```java +class Lists { + public static List factory() { return new ArrayList(); } +} +``` 您可以选择推断的类型参数,也可以传递一个明确的类型参数。 以下两项都是合法的: - ```java - List list = Lists.factory(); - List list = Lists.factory(); - ``` +```java +List list = Lists.factory(); +List list = Lists.factory(); +``` 如果传递一个显式的类型参数,它不能是通配符: - ```java - List list = Lists.factory(); // 编译报错 - ``` +```java +List list = Lists.factory(); // 编译报错 +``` 和以前一样,可以使用嵌套通配符: - ```java - List> = Lists.>factory(); // ok - ``` +```java +List> = Lists.>factory(); // ok +``` 这种限制的动机与之前的相似。 再次,目前还不清楚是否有必要,但这不太可能成为问题。 **超类**在创建类实例时,它会为其超类型调用初始值设定项。 因此,适用于实例创建的任何限制也必须适用于超类型。在类声明中,如果超类型或任何超级接口具有类 型参数,则这些类型不能是通配符。例如,这个声明是非法的: - ```java - class AnyList extends ArrayList {...} // 编译报错 - ``` +```java +class AnyList extends ArrayList {...} // 编译报错 +``` 这也是: - ```java - class AnotherList implements List {...} // 编译报错 - ``` +```java +class AnotherList implements List {...} // 编译报错 +``` 但是,像以前一样,嵌套通配符是允许的: - ```java - class NestedList extends ArrayList> {...} // ok - ``` +```java +class NestedList extends ArrayList> {...} // ok +``` 这种限制的动机与前两种类似。 与以前一样,目前还不清楚是否有必要,但不太可能成为问题。 diff --git a/ch03/01_Comparable.md b/ch03/01_Comparable.md index 872bfea0..a1075106 100644 --- a/ch03/01_Comparable.md +++ b/ch03/01_Comparable.md @@ -6,9 +6,9 @@ 接口 `Comparable ` 包含一个可用于比较一个对象与另一个对象的方法: ```java - interface Comparable { - public int compareTo(T o); - } +interface Comparable { + public int compareTo(T o); +} ``` 根据参数是小于,等于还是大于给定对象,`compareTo` 方法返回一个负值,零值或正值。 当一个类实现 `Comparable` 时,由该接口指定的顺序被称为该类的自然顺 @@ -17,17 +17,17 @@ 通常,属于类的对象只能与属于同一类的对象进行比较。 例如,`Integer` 实现了 `Comparable `: ```java - Integer int0 = 0; - Integer int1 = 1; - assert int0.compareTo(int1) < 0; +Integer int0 = 0; +Integer int1 = 1; +assert int0.compareTo(int1) < 0; ``` 比较返回一个负数,因为 `0` 在数字顺序下先于 `1`。 同样,`String` 实现了 `Comparable `: ```java - String str0 = "zero"; - String str1 = "one"; - assert str0.compareTo(str1) > 0; +String str0 = "zero"; +String str1 = "one"; +assert str0.compareTo(str1) > 0; ``` 这个比较返回一个正数,因为在字母顺序下“零”跟在“一”之后。 @@ -35,9 +35,9 @@ 接口的类型参数允许在编译时捕获无意义的比较: ```java - Integer i = 0; - String s = "one"; - assert i.compareTo(s) < 0; // 编译错误 +Integer i = 0; +String s = "one"; +assert i.compareTo(s) < 0; // 编译错误 ``` 您可以将一个整数与一个整数或一个字符串与一个字符串进行比较,但试图将一个整数与一个字符串进行比较会发出编译时错误。 @@ -45,9 +45,9 @@ 任意数字类型之间不支持比较: ```java - Number m = new Integer(2); - Number n = new Double(3.14); - assert m.compareTo(n) < 0; // 编译错误 +Number m = new Integer(2); +Number n = new Double(3.14); +assert m.compareTo(n) < 0; // 编译错误 ``` 这里的比较是非法的,因为 `Number` 类没有实现 `Comparable` 接口。 @@ -55,7 +55,7 @@ 通常使用 `equals`,我们要求两个对象相等,当且仅当它们相同时: ```java - x.equals(y) if and only if x.compareTo(y) == 0 +x.equals(y) if and only if x.compareTo(y) == 0 ``` 在这种情况下,我们说自然顺序与平等一致。 @@ -79,7 +79,7 @@ 首先,比较是反对称的。 颠倒参数的顺序颠倒了结果: ```java - sgn(x.compareTo(y)) == -sgn(y.compareTo(x)) +sgn(x.compareTo(y)) == -sgn(y.compareTo(x)) ``` 这概括了数字的属性:`x x`。 当且仅当 `y.compareTo(x)` 引发异常时,还需要 `x.compareTo(y)` 引发异常。 @@ -87,7 +87,7 @@ 其次,比较是传递性的。 如果一个值小于第二个,而第二个小于第三个,那么第一个小于第三个: ```java - if x.compareTo(y) < 0 and y.compareTo(z) < 0 then x.compareTo(z) < 0 +if x.compareTo(y) < 0 and y.compareTo(z) < 0 then x.compareTo(z) < 0 ``` 这概括了数字的属性:如果 `x { - ... - public int compareTo(Integer that) { - return this.value < that.value ? -1 : - this.value == that.value ? 0 : 1 ; - } - ... - } +class Integer implements Comparable { + ... + public int compareTo(Integer that) { + return this.value < that.value ? -1 : + this.value == that.value ? 0 : 1 ; + } +... +} ``` 条件表达式根据接收方是小于,等于还是大于参数返回 `-1`,`0` 或 `1`。 你可能会认为下面的代码也会起作用,因为如果接收者小于参数,该方法被允许返回任何负 数。 ```java - class Integer implements Comparable { - ... - public int compareTo(Integer that) { - // bad implementation -- don't do it this way! - return this.value - that.value; - } - ... - } +class Integer implements Comparable { +... + public int compareTo(Integer that) { + // bad implementation -- don't do it this way! + return this.value - that.value; + } +... +} ``` 但是当溢出时,这段代码可能会给出错误的答案。 例如,当比较大的负值和大的正值时,差异可能会大于可存储在整数 `Integer.MAX_VALUE` 中的最大值。 diff --git a/ch03/02_Maximum_of_a_Collection.md b/ch03/02_Maximum_of_a_Collection.md index 58c08a9e..c447b23b 100644 --- a/ch03/02_Maximum_of_a_Collection.md +++ b/ch03/02_Maximum_of_a_Collection.md @@ -9,13 +9,13 @@ 以下代码可以从类 `Collections` 中找到非空集合中的最大元素: ```java - public static > T max(Collection coll) { - T candidate = coll.iterator().next(); - for (T elt : coll) { - if (candidate.compareTo(elt) < 0) candidate = elt; - } - return candidate; +public static > T max(Collection coll) { + T candidate = coll.iterator().next(); + for (T elt : coll) { + if (candidate.compareTo(elt) < 0) candidate = elt; } + return candidate; +} ``` 我们首先看到了在 `1.4` 节签名中声明新类型变量的泛型方法。 例如,`asList` 方法接受一个 `E []` 类型的数组并返回 `List ` 类型的结果,并且对于任何 @@ -28,7 +28,7 @@ 在这种情况下,边界是递归的,因为 `T` 本身的边界取决于 `T`。 甚至可以有相互递归的界限,比如 ```java - , U extends D> +, U extends D> ``` `9.5` 节中会出现一个相互递归界限的例子。 @@ -40,17 +40,17 @@ `Comparable`): ```java - List ints = Arrays.asList(0,1,2); - assert Collections.max(ints) == 2; - List strs = Arrays.asList("zero","one","two"); - assert Collections.max(strs).equals("zero"); +List ints = Arrays.asList(0,1,2); +assert Collections.max(ints) == 2; +List strs = Arrays.asList("zero","one","two"); +assert Collections.max(strs).equals("zero"); ``` 但是我们可能不会选择 `T` 作为 `Number`(因为 `Number` 不能实现 `Comparable`): ```java - List nums = Arrays.asList(0,1,2,3.14); - assert Collections.max(nums) == 3.14; // 编译报错 +List nums = Arrays.asList(0,1,2,3.14); +assert Collections.max(nums) == 3.14; // 编译报错 ``` 正如预期的那样,这里调用 `max` 是非法的。 @@ -58,15 +58,15 @@ 这是一个效率提示。 前面的实现使用 `foreach` 循环来提高简洁性和清晰度。如果效率是一个紧迫的问题,您可能需要重写该方法以使用显式迭代器,如下所示: ```java - public static > T max(Collection coll) { - Iterator it = coll.iterator(); - T candidate = it.next(); - while (it.hasNext()) { - T elt = it.next(); - if (candidate.compareTo(elt) < 0) candidate = elt; - } - return candidate; - } +public static > T max(Collection coll) { + Iterator it = coll.iterator(); + T candidate = it.next(); + while (it.hasNext()) { + T elt = it.next(); + if (candidate.compareTo(elt) < 0) candidate = elt; + } + return candidate; +} ``` 这分配一次迭代器而不是两次,并执行少一次比较。 @@ -74,23 +74,22 @@ 方法的签名应该尽可能通用以最大化效用。 如果你可以用通配符替换一个类型参数,那么你应该这样做。 我们可以通过替换以下来改进 `max` 的签名: ```java - > T max(Collection coll) +> T max(Collection coll) ``` 同 ```java - > T max(Collection coll) +> T max(Collection coll) ``` -遵循 `Get` 和 `Put` 原则,我们使用 `extends` 来继承 `Collection`,因为我们从集合中获得类型 `T` 的值,并且我们使用 `Comparable` 与 `super`,因为 -我们将类型 `T` 的值放入 `compareTo` 方法中。 在下一节中,我们将看到一个不会键入的示例 - 如果上面的 `super` 子句被忽略。 +遵循获取和放置原则,我们使用 `extends` 来继承 `Collection`,因为我们从集合中获得类型 `T` 的值,并且我们使用 `Comparable` 与 `super`,因为我们将 +类型 `T` 的值放入 `compareTo` 方法中。 在下一节中,我们将看到一个不会键入的示例 - 如果上面的 `super` 子句被忽略。 -如果您查看Java库中此方法的签名,您将看到一些内容看起来比上面的代码更糟糕: +如果您查看 `Java` 库中此方法的签名,您将看到一些内容看起来比上面的代码更糟糕: ```java - > - T max(Collection coll) +> T max(Collection coll) ``` 这是为了向后兼容,正如我们将在 `3.6` 节结束时所解释的那样。 diff --git a/ch03/03_A_Fruity_Example.md b/ch03/03_A_Fruity_Example.md index 9822560a..b513ef8e 100644 --- a/ch03/03_A_Fruity_Example.md +++ b/ch03/03_A_Fruity_Example.md @@ -9,9 +9,9 @@ 例 `3-2` 禁止苹果与橘子的比较。 以下是它声明的三个类: ```java - class Fruit {...} - class Apple extends Fruit implements Comparable {...} - class Orange extends Fruit implements Comparable {...} +class Fruit {...} +class Apple extends Fruit implements Comparable {...} +class Orange extends Fruit implements Comparable {...} ``` 每个水果都有一个名称和一个大小,如果两个水果具有相同的名称和大小,则两个水果相等。 遵循良好的做法,我们还定义了一个 `hashCode` 方法,以确保相同的对 @@ -22,9 +22,9 @@ 例 `3-1` 允许比较苹果和橙子。 将这三个类声明与以前给出的类声明进行比较(突出显示示例示例 `3-2` 和示例 `3-1` 之间的所有区别): ```java - class Fruit implements Comparable {...} - class Apple extends Fruit {...} - class Orange extends Fruit {...} +class Fruit implements Comparable {...} +class Apple extends Fruit {...} +class Orange extends Fruit {...} ``` 和以前一样,每个水果都有一个名字和一个大小,如果两个水果有相同的名字和相同的大小,它们是相等的。 现在通过忽略他们的名字和比较他们的大小来比较任何两种 @@ -34,19 +34,19 @@ 回想一下,在上一节的结尾,我们继承了 `compareTo` 的类型签名以使用 `super`: ```java - > T max(Collection coll) +> T max(Collection coll) ``` 第二个例子说明了为什么需要这个通配符。 如果我们想比较两个桔子,我们在前面的代码中将T代入橙色: ```java - Orange extends Comparable +Orange extends Comparable ``` 这是事实,因为以下两点都成立: ```java - Orange extends Comparable and Fruit super Orange +Orange extends Comparable and Fruit super Orange ``` 如果没有 `super` 通配符,查找 `List ` 的最大值将是非法的,即使找到 `List ` 的最大值是允许的。 diff --git a/ch03/04_Comparator.md b/ch03/04_Comparator.md index 778c3047..d298083b 100644 --- a/ch03/04_Comparator.md +++ b/ch03/04_Comparator.md @@ -9,10 +9,10 @@ 我们使用 `Comparator` 接口指定附加排序,它包含两种方法: ```java - interface Comparator { - public int compare(T o1, T o2); - public boolean equals(Object obj); - } +interface Comparator { + public int compare(T o1, T o2); + public boolean equals(Object obj); +} ``` 根据第一个对象是否小于,等于或大于第二个对象,`compare` 方法返回一个负值,零或正的值 - 就像 `compareTo` 一样。 (`equals` 方法是类 `Object` 所熟 @@ -21,114 +21,122 @@ 例 `3-1`。 允许苹果与橘子进行比较 ```java - abstract class Fruit implements Comparable { - protected String name; - protected int size; - protected Fruit(String name, int size) { - this.name = name; this.size = size; - } - public boolean equals(Object o) { - if (o instanceof Fruit) { - Fruit that = (Fruit)o; - return this.name.equals(that.name) && this.size == that.size; - } else - return false; - } - public int hashCode() { - return name.hashCode()*29 + size; - } - public int compareTo(Fruit that) { - return this.size < that.size ? - 1 : - this.size == that.size ? 0 : 1 ; - } +abstract class Fruit implements Comparable { + protected String name; + protected int size; + protected Fruit(String name, int size) { + this.name = name; this.size = size; } - class Apple extends Fruit { - public Apple(int size) { super("Apple", size); } + public boolean equals(Object o) { + if (o instanceof Fruit) { + Fruit that = (Fruit)o; + return this.name.equals(that.name) && this.size == that.size; + } else + return false; } - class Orange extends Fruit { - public Orange(int size) { super("Orange", size); } + public int hashCode() { + return name.hashCode()*29 + size; } - class Test { - public static void main(String[] args) { - Apple a1 = new Apple(1); - Apple a2 = new Apple(2); - Orange o3 = new Orange(3); - Orange o4 = new Orange(4); - List apples = Arrays.asList(a1,a2); - assert Collections.max(apples).equals(a2); - List oranges = Arrays.asList(o3,o4); - assert Collections.max(oranges).equals(o4); - List mixed = Arrays.asList(a1,o3); - assert Collections.max(mixed).equals(o3); // ok - } + public int compareTo(Fruit that) { + return this.size < that.size ? - 1 : this.size == that.size ? 0 : 1 ; + } +} +class Apple extends Fruit { + public Apple(int size) { super("Apple", size); } +} +class Orange extends Fruit { + public Orange(int size) { super("Orange", size); } +} +class Test { + public static void main(String[] args) { + Apple a1 = new Apple(1); + Apple a2 = new Apple(2); + Orange o3 = new Orange(3); + Orange o4 = new Orange(4); + List apples = Arrays.asList(a1,a2); + assert Collections.max(apples).equals(a2); + List oranges = Arrays.asList(o3,o4); + assert Collections.max(oranges).equals(o4); + List mixed = Arrays.asList(a1,o3); + assert Collections.max(mixed).equals(o3); // ok } +} ``` 例 `3-2`。 禁止苹果与橘子进行比较 ```java - abstract class Fruit { - protected String name; - protected int size; - protected Fruit(String name, int size) { - this.name = name; this.size = size; - } - public boolean equals(Object o) { - if (o instanceof Fruit) { - Fruit that = (Fruit)o; - return this.name.equals(that.name) && this.size == that.size; - } else - return false; - } - public int hashCode() { - return name.hashCode()*29 + size; - } - protected int compareTo(Fruit that) { - return this.size < that.size ? -1 : - this.size == that.size ? 0 : 1 ; - } - } - class Apple extends Fruit implements Comparable { - public Apple(int size) { super("Apple", size); } - public int compareTo(Apple a) { return super.compareTo(a); } - } - class Orange extends Fruit implements Comparable { - public Orange(int size) { super("Orange", size); } - public int compareTo(Orange o) { return super.compareTo(o); } - } - class Test { - public static void main(String[] args) { - Apple a1 = new Apple(1); Apple a2 = new Apple(2); - Orange o3 = new Orange(3); Orange o4 = new Orange(4); - List apples = Arrays.asList(a1,a2); - assert Collections.max(apples).equals(a2); - List oranges = Arrays.asList(o3,o4); - assert Collections.max(oranges).equals(o4); - List mixed = Arrays.asList(a1,o3); - assert Collections.max(mixed).equals(o3); // 编译报错 - } - } +abstract class Fruit { + protected String name; + protected int size; + protected Fruit(String name, int size) { + this.name = name; this.size = size; + } + public boolean equals(Object o) { + if (o instanceof Fruit) { + Fruit that = (Fruit)o; + return this.name.equals(that.name) && this.size == that.size; + } else + return false; + } + public int hashCode() { + return name.hashCode()*29 + size; + } + protected int compareTo(Fruit that) { + return this.size < that.size ? -1 : + this.size == that.size ? 0 : 1 ; + } +} +class Apple extends Fruit implements Comparable { + public Apple(int size) { + super("Apple", size); + } + public int compareTo(Apple a) { + return super.compareTo(a); + } +} +class Orange extends Fruit implements Comparable { + public Orange(int size) { + super("Orange", size); + } + public int compareTo(Orange o) { + return super.compareTo(o); + } +} +class Test { + public static void main(String[] args) { + Apple a1 = new Apple(1); + Apple a2 = new Apple(2); + Orange o3 = new Orange(3); + Orange o4 = new Orange(4); + List apples = Arrays.asList(a1,a2); + assert Collections.max(apples).equals(a2); + List oranges = Arrays.asList(o3,o4); + assert Collections.max(oranges).equals(o4); + List mixed = Arrays.asList(a1,o3); + assert Collections.max(mixed).equals(o3); // 编译报错 + } +} ``` 这是一个比较器,它认为两个字符串中较短的字符串较小。 只有两个字符串具有相同的长度时,才会使用自然(字母)排序进行比较。 ```java - Comparator sizeOrder = - new Comparator() { - public int compare(String s1, String s2) { - return - s1.length() < s2.length() ? -1 : - s1.length() > s2.length() ? 1 : - s1.compareTo(s2) ; - } - }; +Comparator sizeOrder = new Comparator() { + public int compare(String s1, String s2) { + return + s1.length() < s2.length() ? -1 : + s1.length() > s2.length() ? 1 : + s1.compareTo(s2) ; + } +}; ``` 这里是一个例子: ```java - assert "two".compareTo("three") > 0; - assert sizeOrder.compare("two","three") < 0; +assert "two".compareTo("three") > 0; +assert sizeOrder.compare("two","three") < 0; ``` 在自然字母顺序中,“two”大于“three”,而在大小排序中它更小。 @@ -137,24 +145,24 @@ 的参数的泛型方法。 例如,对应于: ```java - public static > - T max(Collection coll) +public static > +T max(Collection coll) ``` 我们还有: ```java - public static T max(Collection coll, Comparator cmp) +public static T max(Collection coll, Comparator cmp) ``` 有类似的方法来找到最小值。 例如,以下是如何使用自然排序和使用大小排序来查找列表的最大值和最小值: ```java - Collection strings = Arrays.asList("from","aaa","to","zzz"); - assert max(strings).equals("zzz"); - assert min(strings).equals("aaa"); - assert max(strings,sizeOrder).equals("from"); - assert min(strings,sizeOrder).equals("to"); +Collection strings = Arrays.asList("from","aaa","to","zzz"); +assert max(strings).equals("zzz"); +assert min(strings).equals("aaa"); +assert max(strings,sizeOrder).equals("from"); +assert min(strings,sizeOrder).equals("to"); ``` 字符串“from”是使用大小排序的最大值,因为它是最长的,“to”是最小值,因为它是最短的。 @@ -162,13 +170,13 @@ 以下是使用比较器的max版本的代码: ```java - public static T max(Collection coll, Comparator cmp){ - T candidate = coll.iterator().next(); - for (T elt : coll) { - if (cmp.compare(candidate, elt) < 0) { candidate = elt; } - } - return candidate; - } +public static T max(Collection coll, Comparator cmp){ + T candidate = coll.iterator().next(); + for (T elt : coll) { + if (cmp.compare(candidate, elt) < 0) { candidate = elt; } + } + return candidate; +} ``` 与之前的版本相比,唯一的变化就是在我们写了 `candidate.compareTo(elt)` 之前,现在我们写了 `cmp.compare(candidate,elt)`。 (为了便于参考,这个代码 @@ -177,19 +185,19 @@ 定义一个提供自然排序的比较器很容易: ```java - public static > Comparator naturalOrder(){ - return new Comparator { - public int compare(T o1, T o2) { return o1.compareTo(o2); } - } - } +public static > Comparator naturalOrder(){ + return new Comparator { + public int compare(T o1, T o2) { return o1.compareTo(o2); } + } +} ``` 使用这种方法,可以很容易地根据使用给定比较器的版本定义使用自然排序的 `max` 版本: ```java - public static > T max(Collection coll){ - return max(coll, Comparators.naturalOrder()); - } +public static > T max(Collection coll){ + return max(coll, Comparators.naturalOrder()); +} ``` 为了调用通用方法 `naturalOrder`,必须明确提供类型参数,因为推断类型的算法将无法正确计算出正确的类型。 @@ -197,21 +205,21 @@ 定义一个采用比较器并返回给定顺序反向的新比较器的方法也很容易: ```java - public static Comparator reverseOrder(final Comparator cmp){ - return new Comparator() { - public int compare(T o1, T o2) { return cmp.compare(o2,o1); } - }; - } +public static Comparator reverseOrder(final Comparator cmp){ + return new Comparator() { + public int compare(T o1, T o2) { return cmp.compare(o2,o1); } + }; +} ``` 这简单地反转了比较器的参数顺序。 (根据比较者的合同,这将等同于以原始顺序保留参数,但否定结果。)这里是返回自然排序的反向的方法: ```java - public static > Comparator reverseOrder(){ - return new Comparator() { - public int compare(T o1, T o2) { return o2.compareTo(o1); } - }; - } +public static > Comparator reverseOrder(){ + return new Comparator() { + public int compare(T o1, T o2) { return o2.compareTo(o1); } + }; +} ``` `java.util.Collections` 提供了类似的方法,参见17.4节。 @@ -219,12 +227,12 @@ 最后,通过使用 `reverseOrder` 的两个版本,我们可以根据 `max` 的两个版本来定义 `min` 的两个版本: ```java - public static T min(Collection coll, Comparator cmp){ - return max(coll, reverseOrder(cmp)); - } - public static > T min(Collection coll){ - return max(coll, Comparators.reverseOrder()); - } +public static T min(Collection coll, Comparator cmp){ + return max(coll, reverseOrder(cmp)); +} +public static > T min(Collection coll){ + return max(coll, Comparators.reverseOrder()); +} ``` (这样结束了例 `3-3` 中总结的代码)。 @@ -238,19 +246,19 @@ 作为比较器的最后一个例子,下面是一个方法,它在元素上使用比较器并在元素列表上返回一个比较器: ```java - public static Comparator> listComparator(final Comparator comp) { - return new Comparator>() { - public int compare(List list1, List list2) { - int n1 = list1.size(); - int n2 = list2.size(); - for (int i = 0; i < Math.min(n1,n2); i++) { - int k = comp.compare(list1.get(i), list2.get(i)); - if (k != 0) return k; - } - return (n1 < n2) ? -1 : (n1 == n2) ? 0 : 1; - } - }; - } +public static Comparator> listComparator(final Comparator comp) { + return new Comparator>() { + public int compare(List list1, List list2) { + int n1 = list1.size(); + int n2 = list2.size(); + for (int i = 0; i < Math.min(n1,n2); i++) { + int k = comp.compare(list1.get(i), list2.get(i)); + if (k != 0) return k; + } + return (n1 < n2) ? -1 : (n1 == n2) ? 0 : 1; + } + }; +} ``` 循环比较两个列表中的相应元素,并在发现不相等的相应元素时(在这种情况下,认为较小元素的列表较小)或到达任一列表的末尾(在这种情况下, 较短的列表被认为 diff --git a/ch03/05_Enumerated_Types.md b/ch03/05_Enumerated_Types.md index b1fc3445..3c0ce862 100644 --- a/ch03/05_Enumerated_Types.md +++ b/ch03/05_Enumerated_Types.md @@ -6,7 +6,7 @@ `Java 5` 包含对枚举类型的支持。 这是一个简单的例子: ```java - enum Season { WINTER, SPRING, SUMMER, FALL } +enum Season { WINTER, SPRING, SUMMER, FALL } ``` 每个枚举类型声明都可以以程式化的方式扩展为相应的类。相应的类被设计为每个枚举常量都有一个实例,绑定到一个合适的静态最终变量。 例如,上面的枚举声明扩展 @@ -15,7 +15,7 @@ 与枚举类型对应的每个类都是 `java.lang.Enum` 的子类。 它在 `Java` 文档中的定义是这样开始的: ```java - class Enum> +class Enum> ``` 你可能会发现这种可怕的一见钟情 - 我们当然都这么做! 但不要恐慌。实际上,我们已经看到了类似的东西。 令人担忧的短语 `E extends Enum ` 与我们在 @@ -27,7 +27,7 @@ 这是 `Enum` 类声明的第一行: ```java - public abstract class Enum> implements Comparable +public abstract class Enum> implements Comparable ``` 下面是季节课的声明的第一行: @@ -39,101 +39,117 @@ class Season extends Enum 例 `3-3`。比较 ```java - class Comparators { - public static T max(Collection coll, Comparator cmp){ - T candidate = coll.iterator().next(); - for (T elt : coll) { - if (cmp.compare(candidate, elt) < 0) { candidate = elt; } - } - return candidate; - } - public static > T max(Collection coll){ - return max(coll, Comparators.naturalOrder()); - } - public static T min(Collection coll, Comparator cmp){ - return max(coll, reverseOrder(cmp)); - } - public static > T min(Collection coll){ - return max(coll, Comparators.reverseOrder()); - } - public static > Comparator naturalOrder(){ - return new Comparator() { - public int compare(T o1, T o2) { return o1.compareTo(o2); } - }; - } - public static Comparator reverseOrder(final Comparator cmp){ - return new Comparator() { - public int compare(T o1, T o2) { return cmp.compare(o2,o1); } - }; - } - public static > Comparator reverseOrder(){ - return new Comparator() { - public int compare(T o1, T o2) { return o2.compareTo(o1); } - }; - } - } +class Comparators { + public static T max(Collection coll, Comparator cmp){ + T candidate = coll.iterator().next(); + for (T elt : coll) { + if (cmp.compare(candidate, elt) < 0) { + candidate = elt; + } + } + return candidate; + } + public static > T max(Collection coll){ + return max(coll, Comparators.naturalOrder()); + } + public static T min(Collection coll, Comparator cmp){ + return max(coll, reverseOrder(cmp)); + } + public static > T min(Collection coll){ + return max(coll, Comparators.reverseOrder()); + } + public static > Comparator naturalOrder(){ + return new Comparator() { + public int compare(T o1, T o2) { return o1.compareTo(o2); } + }; + } + public static Comparator reverseOrder(final Comparator cmp){ + return new Comparator() { + public int compare(T o1, T o2) { + return cmp.compare(o2,o1); + } + }; + } + public static > Comparator reverseOrder(){ + return new Comparator() { + public int compare(T o1, T o2) { return o2.compareTo(o1); } + }; + } +} ``` 例 `3-4`。 枚举类型的基类 ```java - public abstract class Enum> implements Comparable { - private final String name; - private final int ordinal; - protected Enum(String name, int ordinal) { - this.name = name; this.ordinal = ordinal; - } - public final String name() { return name; } - public final int ordinal() { return ordinal; } - public String toString() { return name; } - public final int compareTo(E o) { - return ordinal - o.ordinal; - } - } +public abstract class Enum> implements Comparable { + private final String name; + private final int ordinal; + protected Enum(String name, int ordinal) { + this.name = name; this.ordinal = ordinal; + } + public final String name() { + return name; + } + public final int ordinal() { + return ordinal; + } + public String toString() { + return name; + } + public final int compareTo(E o) { + return ordinal - o.ordinal; + } +} ``` 例 `3-5`。 对应于枚举类型的类 ```java - // corresponds to - // enum Season { WINTER, SPRING, SUMMER, FALL } - final class Season extends Enum { - private Season(String name, int ordinal) { super(name,ordinal); } - public static final Season WINTER = new Season("WINTER",0); - public static final Season SPRING = new Season("SPRING",1); - public static final Season SUMMER = new Season("SUMMER",2); - public static final Season FALL = new Season("FALL",3); - private static final Season[] VALUES = { WINTER, SPRING, SUMMER, FALL }; - public static Season[] values() { return VALUES.clone(); } - public static Season valueOf(String name) { - for (Season e : VALUES) if (e.name().equals(name)) return e; - throw new IllegalArgumentException(); - } - } +// corresponds to +// enum Season { WINTER, SPRING, SUMMER, FALL } +final class Season extends Enum { + private Season(String name, int ordinal) { + super(name,ordinal); + } + public static final Season WINTER = new Season("WINTER",0); + public static final Season SPRING = new Season("SPRING",1); + public static final Season SUMMER = new Season("SUMMER",2); + public static final Season FALL = new Season("FALL",3); + private static final Season[] VALUES = { WINTER, SPRING, SUMMER, FALL }; + public static Season[] values() { + return VALUES.clone(); + } + public static Season valueOf(String name) { + for (Season e : VALUES) + if (e.name().equals(name)) + return e; + throw new IllegalArgumentException(); + } +} ``` 匹配的东西,我们可以开始看到这是如何工作的。 类型变量 `E` 表示 `Enum` 的子类,它实现了一个特定的枚举类型,比如 `Season`。每个 `E` 必须满足: ```java - E extends Enum +E extends Enum ``` 所以我们可以把 `E` 作为 `Season`,因为: ```java - Season extends Enum +Season extends Enum ``` 此外,`Enum` 的声明告诉我们: ```java - Enum implements Comparable +Enum implements Comparable ``` 所以它是这样的: ```java - Enum implements Comparable +Enum implements Comparable ``` 因此,我们可以将 `Season` 类型的两个值相互比较,但我们无法将季节类型的值与任何其他类型的值进行比较。 @@ -141,13 +157,13 @@ class Season extends Enum 没有类型变量,`Enum` 类的声明就会像这样开始: ```java - class Enum implements Comparable +class Enum implements Comparable ``` 而 `Season` 类的声明将如下开始: ```java - class Season extends Enum +class Season extends Enum ``` 这更简单,但它太简单了。 有了这个定义,`Season` 将实现 `Comparable ` 而不是 `Comparable`,这意味着我们可以将 `Season` 类型的值与 diff --git a/ch03/06_Multiple_Bounds.md b/ch03/06_Multiple_Bounds.md index d8768387..db3de658 100644 --- a/ch03/06_Multiple_Bounds.md +++ b/ch03/06_Multiple_Bounds.md @@ -12,22 +12,21 @@ 目标: ```java - public static void copy(S src, T trg, int size) - throws IOException { - try { - CharBuffer buf = CharBuffer.allocate(size); - int i = src.read(buf); - while (i >= 0) { - buf.flip(); // prepare buffer for writing - trg.append(buf); - buf.clear(); // prepare buffer for reading - i = src.read(buf); - } - } finally { - src.close(); - trg.close(); - } - } +public static void copy(S src, T trg, int size) throws IOException { + try { + CharBuffer buf = CharBuffer.allocate(size); + int i = src.read(buf); + while (i >= 0) { + buf.flip(); // prepare buffer for writing + trg.append(buf); + buf.clear(); // prepare buffer for reading + i = src.read(buf); + } + } finally { + src.close(); + trg.close(); + } +} ``` 此方法从源重复读入缓冲区并从缓冲区追加到目标中。 当源为空时,它关闭源和目标。 (这个例子偏离了最佳做法,因为这些文件是在不同于打开文件的块中关闭 @@ -37,13 +36,13 @@ 例如,可以使用两个文件作为源和目标或使用包含在缓冲区中的相同两个文件作为源和目标来调用此方法: ```java - int size = 32; - FileReader r = new FileReader("file.in"); - FileWriter w = new FileWriter("file.out"); - copy(r,w,size); - BufferedReader br = new BufferedReader(new FileReader("file.in")); - BufferedWriter bw = new BufferedWriter(new FileWriter("file.out")); - copy(br,bw,size); +int size = 32; +FileReader r = new FileReader("file.in"); +FileWriter w = new FileWriter("file.out"); +copy(r,w,size); +BufferedReader br = new BufferedReader(new FileReader("file.in")); +BufferedWriter bw = new BufferedWriter(new FileWriter("file.out")); +copy(br,bw,size); ``` 其他可能的来源包括 `FilterReader`,`PipedReader` 和 `StringReader`,其他可能的目标包括 `FilterWriter`,`PipedWriter` 和 `PrintStream`。但是你 @@ -53,16 +52,16 @@ `Writer` 的子类。 所以你可能想知道为什么我们不像这样简化方法签名: ```java - public static void copy(Reader src, Writer trg, int size) +public static void copy(Reader src, Writer trg, int size) ``` 这将确实承认大部分相同的课程,但不是全部。 例如,`PrintStream` 实现了 `Appendable` 和 `Closeable`,但不是 `Writer` 的子类。 此外,你不能排除一些 -使用你的代码的程序员可能有他或她自己的自定义类,比如实现 `Readable` 和 `Closeable`,但不是 `Reader`的子类。 +使用你的代码的程序员可能有他或她自己的自定义类,比如实现 `Readable` 和 `Closeable`,但不是 `Reader` 的子类。 当出现多个界限时,第一个界限用于擦除。我们在第3.2节中提到了这一点: ```java - public static > T max(Collection coll) +public static > T max(Collection coll) ``` 如果没有突出显示的文本,`max` 的已擦除类型签名将具有 `Comparable` 作为返回类型,而在旧库中,返回类型为 `Object`。 第 `5` 章和第 `8.4` 节将进一步 diff --git a/ch03/07_Bridges.md b/ch03/07_Bridges.md index 7f64bd6e..23e551e7 100644 --- a/ch03/07_Bridges.md +++ b/ch03/07_Bridges.md @@ -1,7 +1,7 @@ 《《《 [返回首页](../README.md)
《《《 [上一节](06_Multiple_Bounds.md) -### 桥梁 +### 桥接器 正如我们前面提到的,泛型是通过擦除来实现的:当你用泛型编写代码时,它的编译方式几乎与没有泛型编写的代码完全相同。在参数化接口(如 `Comparable`)的 情况下,这可能会导致编译器插入其他方法;这些附加的方法被称为网桥。 @@ -9,7 +9,7 @@ 示例 `3-6` 显示了 `Comparable` 接口以及泛型之前的 `Java` 中的 `Integer` 类的简化版本。在非通用接口中,`compareTo` 方法接受一个 `Object` 类型的 参数。在非泛型类中,有两个 `compareTo` 方法。第一个是您可能期望的简单方法,用于将整数与另一个整数进行比较。第二个将整数与任意对象进行比较:它将该对 象转换为整数并调用第一个方法。第二种方法对于重写 `Comparable` 接口中的 `compareTo` 方法是必需的,因为只有当方法签名相同时才会覆盖。这第二种方法被称 -为桥梁。 +为桥接器。 例 `3-7` 显示了当 `Comparable` 接口和 `Integer` 类被基因化时发生了什么。在通用接口中,`compareTo` 方法接受 `T` 类型的参数。在泛型类中,单个 `compareTo` 方法接受 `Integer` 类型的参数。 @@ -19,49 +19,53 @@ 例 `3-6`。 传统的可比较整数的代码 ```java - interface Comparable { - public int compareTo(Object o); - } - class Integer implements Comparable { - private final int value; - public Integer(int value) { this.value = value; } - public int compareTo(Integer i) { - return (value < i.value) ? -1 : (value == i.value) ? 0 : 1; - } - public int compareTo(Object o) { - return compareTo((Integer)o); - } - } +interface Comparable { + public int compareTo(Object o); +} +class Integer implements Comparable { + private final int value; + public Integer(int value) { + this.value = value; + } + public int compareTo(Integer i) { + return (value < i.value) ? -1 : (value == i.value) ? 0 : 1; + } + public int compareTo(Object o) { + return compareTo((Integer)o); + } +} ``` 例 `3-7`。 可比较整数的通用代码 ```java - interface Comparable { - public int compareTo(T o); - } - class Integer implements Comparable { - private final int value; - public Integer(int value) { this.value = value; } - public int compareTo(Integer i) { - return (value < i.value) ? -1 : (value == i.value) ? 0 : 1; - } - } +interface Comparable { + public int compareTo(T o); +} +class Integer implements Comparable { + private final int value; + public Integer(int value) { + this.value = value; + } + public int compareTo(Integer i) { + return (value < i.value) ? -1 : (value == i.value) ? 0 : 1; + } +} ``` 如果您应用反射,您可以看到桥。 这里是代码,它使用 `toGenericString` 来打印方法的通用签名(参见 `7.5` 节),在 `Integer` 类中查找名称为 `compareTo` 的所有方法。 ```java - for (Method m : Integer.class.getMethods()) - if (m.getName().equals("compareTo")) - System.out.println(m.toGenericString()); +for (Method m : Integer.class.getMethods()) + if (m.getName().equals("compareTo")) + System.out.println(m.toGenericString()); ``` 在通用版本的 `Integer` 类上运行此代码会产生以下输出: ```java - public int Integer.compareTo(Integer) - public bridge int Integer.compareTo(java.lang.Object) +public int Integer.compareTo(Integer) +public bridge int Integer.compareTo(java.lang.Object) ``` 这确实包含两种方法,一种是采用 `Integer` 类型参数的声明方法,另一种是采用 `Object` 类型参数的桥接方法。 (截至撰写本文时,`Sun JVM` 打印的是 diff --git a/ch03/08_Covariant_Overriding.md b/ch03/08_Covariant_Overriding.md index 86c04d6c..3b2c3312 100644 --- a/ch03/08_Covariant_Overriding.md +++ b/ch03/08_Covariant_Overriding.md @@ -11,61 +11,61 @@ `Object` 类的克隆方法说明了协变覆盖的优点: ```java - class Object { - ... - public Object clone() { ... } - } +class Object { + ... + public Object clone() { ... } +} ``` 在 `Java 1.4` 中,任何覆盖 `clone` 的类都必须给它完全相同的返回类型,即 `Object`: ```java - class Point { - public int x; - public int y; - public Point(int x, int y) { this.x=x; this.y=y; } - public Object clone() { return new Point(x,y); } - } +class Point { + public int x; + public int y; + public Point(int x, int y) { this.x=x; this.y=y; } + public Object clone() { return new Point(x,y); } +} ``` 在这里,尽管克隆总是返回一个 `Point`,但规则要求它具有返回类型 `Object`。 这很烦人,因为每次克隆的调用都必须输出结果。 ```java - Point p = new Point(1,2); - Point q = (Point)p.clone(); + Point p = new Point(1,2); + Point q = (Point)p.clone(); ``` 在 `Java 5` 中,可以给克隆方法一个更重要的返回类型: ```java - class Point { - public int x; - public int y; - public Point(int x, int y) { this.x=x; this.y=y; } - public Point clone() { return new Point(x,y); } - } +class Point { + public int x; + public int y; + public Point(int x, int y) { this.x=x; this.y=y; } + public Point clone() { return new Point(x,y); } +} ``` -现在我们可以克隆没有转换: +现在我们可以克隆无须转换: ```java - Point p = new Point(1,2); - Point q = p.clone(); +Point p = new Point(1,2); +Point q = p.clone(); ``` 协变覆盖使用前一节中描述的桥接技术来实现。 和以前一样,如果您应用反射,您可以看到桥。 这里是在类 `Point` 中找到名称为 `clone` 的所有方法的代码: ```java - for (Method m : Point.class.getMethods()) - if (m.getName().equals("clone")) - System.out.println(m.toGenericString()); +for (Method m : Point.class.getMethods()) + if (m.getName().equals("clone")) + System.out.println(m.toGenericString()); ``` 在Point类的协变版本上运行此代码会产生以下输出: ```java - public Point Point.clone() - public bridge java.lang.Object Point.clone() +public Point Point.clone() +public bridge java.lang.Object Point.clone() ``` 这里桥接技术利用了这样一个事实,即在类文件中,同一类的两个方法可能具有相同的参数签名,尽管这在 `Java` 源代码中是不允许的。 桥接方法只是简单地调用第一 diff --git a/ch04/01_Constructors.md b/ch04/01_Constructors.md index 009008b1..361b815f 100644 --- a/ch04/01_Constructors.md +++ b/ch04/01_Constructors.md @@ -6,29 +6,35 @@ 在泛型类中,类型参数出现在声明类的头中,但不在构造函数中: ```java - class Pair { - private final T first; - private final U second; - public Pair(T first, U second) { this.first=first; this.second=second; } - public T getFirst() { return first; } - public U getSecond() { return second; } - } +class Pair { + private final T first; + private final U second; + public Pair(T first, U second) { + this.first=first; this.second=second; + } + public T getFirst() { + return first; + } + public U getSecond() { + return second; + } +} ``` 类型参数 `T` 和 `U` 在类的开头声明,而不在构造函数中声明。 但是,实际的类型参数在调用时会传递给构造函数: ```java - Pair pair = new Pair("one",2); - assert pair.getFirst().equals("one") && pair.getSecond() == 2; + Pair pair = new Pair("one",2); + assert pair.getFirst().equals("one") && pair.getSecond() == 2; ``` __注意这一点!__ 一个常见的错误是在调用构造函数时忘记类型参数: ```java - Pair pair = new Pair("one",2); + Pair pair = new Pair("one",2); ``` -这个错误会产生警告,但不会产生错误。 它被认为是合法的,因为 `Pair` 被视为原始类型,但是从原始类型转换为相应的参数化类型会生成未经检查的警告; 见 `5.3` +这个会产生警告,但不会产生错误。 它被认为是合法的,因为 `Pair` 被视为原始类型,但是从原始类型转换为相应的参数化类型会生成未经检查的警告; 见 `5.3` 节,它解释了 `-Xlint:unchecked` 标志如何帮助你发现这种错误。 《《《 [下一节](02_Static_Members.md)
diff --git a/ch04/02_Static_Members.md b/ch04/02_Static_Members.md index 08727b84..fe8007ab 100644 --- a/ch04/02_Static_Members.md +++ b/ch04/02_Static_Members.md @@ -7,9 +7,9 @@ 反射: ```java - List ints = Arrays.asList(1,2,3); - List strings = Arrays.asList("one","two"); - assert ints.getClass() == strings.getClass(); + List ints = Arrays.asList(1,2,3); + List strings = Arrays.asList("one","two"); + assert ints.getClass() == strings.getClass(); ``` 这里在运行时与整数列表关联的类与与字符串列表关联的类相同。 @@ -20,16 +20,26 @@ 例如,下面是一个类 `Cell`,其中每个单元格都有一个整型标识符和一个类型为 `T` 的值: ```java - class Cell { - private final int id; - private final T value; - private static int count = 0; - private static synchronized int nextId() { return count++; } - public Cell(T value) { this.value=value; id=nextId(); } - public T getValue() { return value; } - public int getId() { return id; } - public static synchronized int getCount() { return count; } - } +class Cell { + private final int id; + private final T value; + private static int count = 0; + private static synchronized int nextId() { + return count++; + } + public Cell(T value) { + this.value=value; id=nextId(); + } + public T getValue() { + return value; + } + public int getId() { + return id; + } + public static synchronized int getCount() { + return count; + } +} ``` 静态字段 `count` 用于为每个单元分配不同的标识符。 静态 `nextId` 方法被同步,以确保即使在多个线程的情况下也能生成唯一的标识符。 静态 `getCount` 方 @@ -38,9 +48,9 @@ 这里是分配包含字符串的单元格和包含整数的单元格的代码,它们分别分配了标识符 `0` 和 `1`: ```java - Cell a = new Cell("one"); - Cell b = new Cell(2); - assert a.getId() == 0 && b.getId() == 1 && Cell.getCount() == 2; + Cell a = new Cell("one"); + Cell b = new Cell(2); + assert a.getId() == 0 && b.getId() == 1 && Cell.getCount() == 2; ``` 静态成员在类的所有实例中共享,因此在分配字符串或整数单元格时,相同的计数会递增。 @@ -48,9 +58,9 @@ 由于静态成员独立于任何类型参数,因此在访问静态成员时,我们不允许使用类型参数跟随类名称: ```java - Cell.getCount(); // ok - Cell.getCount(); // 编译报错 - Cell.getCount(); // 编译报错 + Cell.getCount(); // ok + Cell.getCount(); // 编译报错 + Cell.getCount(); // 编译报错 ``` 计数是静态的,所以它是整个类的一个属性,而不是任何特定的实例。 @@ -58,34 +68,47 @@ 出于同样的原因,您不能在静态成员中的任何地方引用类型参数。 以下是 `Cell` 的第二个版本,它试图使用一个静态变量来保存存储在任何单元中的所有值的列表: ```java - class Cell2 { - private final T value; - private static List values = new ArrayList(); // illegal - public Cell(T value) { this.value=value; values.add(value); } - public T getValue() { return value; } - public static List getValues() { return values; } // illegal - } +class Cell2 { + private final T value; + private static List values = new ArrayList(); // illegal + public Cell(T value) { + this.value=value; + values.add(value); + } + public T getValue() { + return value; + } + public static List getValues() { + return values; + } // illegal +} ``` 由于类可能在不同的地方使用不同的类型参数,因此在声明静态字段值或静态方法 `getValues()` 时引用 `T` 是没有意义的,并且这些行在编译时会报告为错误。如果 我们想要一个单元格中保存的所有值的列表,那么我们需要使用一个对象列表,如下面的变体所示: ```java - class Cell2 { - private final T value; - private static List values = new ArrayList(); // ok - public Cell(T value) { this.value=value; values.add(value); } - public T getValue() { return value; } - public static List getValues() { return values; } // ok - } +class Cell2 { + private final T value; + private static List values = new ArrayList(); // ok + public Cell(T value) { + this.value=value; values.add(value); + } + public T getValue() { + return value; + } + public static List getValues() { + return values; + } // ok +} ``` 这段代码编译和运行没有任何困难: ```java - Cell2 a = new Cell2("one"); - Cell2 b = new Cell2(2); - assert Cell2.getValues().toString().equals("[one, 2]"); +Cell2 a = new Cell2("one"); +Cell2 b = new Cell2(2); +assert Cell2.getValues().toString().equals("[one, 2]"); ``` 《《《 [下一节](03_Nested_Classes.md)
diff --git a/ch04/03_Nested_Classes.md b/ch04/03_Nested_Classes.md index 697ca811..c3609f69 100644 --- a/ch04/03_Nested_Classes.md +++ b/ch04/03_Nested_Classes.md @@ -11,41 +11,49 @@ 例 `4-1`。 类型参数在嵌套非静态类的范围内 ```java - public class LinkedCollection extends AbstractCollection { - private class Node { - private E element; - private Node next = null; - private Node(E elt) { element = elt; } - } - private Node first = new Node(null); - private Node last = first; - private int size = 0; - public LinkedCollection() {} - public LinkedCollection(Collection c) { addAll(c); } - public int size() { return size; } - public boolean add(E elt) { - last.next = new Node(elt); last = last.next; size++; - return true; - } - public Iterator iterator() { - return new Iterator() { - private Node current = first; - public boolean hasNext() { - return current.next != null; - } - public E next() { - if (current.next != null) { - current = current.next; - return current.element; - } else - throw new NoSuchElementException(); - } - public void remove() { - throw new UnsupportedOperationException(); - } - }; - } - } +public class LinkedCollection extends AbstractCollection { + private class Node { + private E element; + private Node next = null; + private Node(E elt) { + element = elt; + } + } + private Node first = new Node(null); + private Node last = first; + private int size = 0; + public LinkedCollection() {} + public LinkedCollection(Collection c) { + addAll(c); + } + public int size() { + return size; + } + public boolean add(E elt) { + last.next = new Node(elt); + last = last.next; + size++; + return true; + } + public Iterator iterator() { + return new Iterator() { + private Node current = first; + public boolean hasNext() { + return current.next != null; + } + public E next() { + if (current.next != null) { + current = current.next; + return current.element; + } else + throw new NoSuchElementException(); + } + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } +} ``` 相比之下,例 `4-2` 显示了一个类似的实现,但是这次嵌套的 `Node` 类是静态的,所以类型参数 `E` 不在这个类的范围内。 相反,嵌套类用它自己的类型参数 `T` @@ -56,43 +64,53 @@ 例 `4-2`。 类型参数不在嵌套静态类的范围内 ```java - class LinkedCollection extends AbstractCollection { - private static class Node { - private T element; - private Node next = null; - private Node(T elt) { element = elt; } - } - private Node first = new Node(null); - private Node last = first; - private int size = 0; - public LinkedCollection() {} - public LinkedCollection(Collection c) { addAll(c); } - public int size() { return size; } - public boolean add(E elt) { - last.next = new Node(elt); last = last.next; size++; - return true; - } - private static class LinkedIterator implements Iterator { - private Node current; - public LinkedIterator(Node first) { current = first; } - public boolean hasNext() { - return current.next != null; - } - public T next() { - if (current.next != null) { +class LinkedCollection extends AbstractCollection { + private static class Node { + private T element; + private Node next = null; + private Node(T elt) { + element = elt; + } + } + private Node first = new Node(null); + private Node last = first; + private int size = 0; + public LinkedCollection() {} + public LinkedCollection(Collection c) { + addAll(c); + } + public int size() { + return size; + } + public boolean add(E elt) { + last.next = new Node(elt); + last = last.next; + size++; + return true; + } + private static class LinkedIterator implements Iterator { + private Node current; + public LinkedIterator(Node first) { + current = first; + } + public boolean hasNext() { + return current.next != null; + } + public T next() { + if (current.next != null) { current = current.next; return current.element; - } else - throw new NoSuchElementException(); - } - public void remove() { - throw new UnsupportedOperationException(); - } - } - public Iterator iterator() { - return new LinkedIterator(first); - } - } + } else + throw new NoSuchElementException(); + } + public void remove() { + throw new UnsupportedOperationException(); + } + } + public Iterator iterator() { + return new LinkedIterator(first); + } +} ``` 在这里描述的两种替代方案中,第二种是优选的。 非静态的嵌套类通过包含对封装实例的引用来实现,因为它们通常可以访问该实例的组件。 静态嵌套类通常既简单又 diff --git a/ch04/04_How_Erasure_Works.md b/ch04/04_How_Erasure_Works.md index 52bc0313..836a31a6 100644 --- a/ch04/04_How_Erasure_Works.md +++ b/ch04/04_How_Erasure_Works.md @@ -23,32 +23,34 @@ 例如,这里有一个有两种便利方法的类。 一个将整数列表中的每个整数加在一起,另一个将字符串列表中的每个字符串连接在一起: ```java - class Overloaded { - public static int sum(List ints) { - int sum = 0; - for (int i : ints) sum += i; - return sum; - } - public static String sum(List strings) { - StringBuffer sum = new StringBuffer(); - for (String s : strings) sum.append(s); - return sum.toString(); - } +class Overloaded { + public static int sum(List ints) { + int sum = 0; + for (int i : ints) + sum += i; + return sum; } + public static String sum(List strings) { + StringBuffer sum = new StringBuffer(); + for (String s : strings) + sum.append(s); + return sum.toString(); + } +} ``` 这按预期工作: ```java - assert sum(Arrays.asList(1,2,3)) == 6; - assert sum(Arrays.asList("a","b")).equals("ab"); +assert sum(Arrays.asList(1,2,3)) == 6; +assert sum(Arrays.asList("a","b")).equals("ab"); ``` 以下是两种方法签名的删除: ```java - int sum(List) - String sum(List) +int sum(List) +String sum(List) ``` 这两种方法有不同的返回类型,这足以让 `Java` 区分它们。 @@ -56,30 +58,31 @@ 但是,假设我们改变了方法,以便每个方法都将其结果附加到参数列表的末尾,而不是返回一个值: ```java - class Overloaded2 { - // 编译时错误,不能重载两个擦除相同的方法 - public static boolean allZero(List ints) { - for (int i : ints) if (i != 0) return false; - return true; - } - public static boolean allZero(List strings) { - for (String s : strings) if (s.length() != 0) return false; - return true; - } - } +class Overloaded2 { + // 编译时错误,不能重载两个擦除相同的方法 + public static boolean allZero(List ints) { + for (int i : ints) if (i != 0) return false; + return true; + } + public static boolean allZero(List strings) { + for (String s : strings) if (s.length() != 0) + return false; + return true; + } +} ``` 我们打算让这个代码工作如下: ```java - assert allZero(Arrays.asList(0,0,0)); - assert allZero(Arrays.asList("","","")); +assert allZero(Arrays.asList(0,0,0)); +assert allZero(Arrays.asList("","","")); ``` 但是,在这种情况下,两种方法的签名的删除是相同的: ```java - boolean allZero(List) +boolean allZero(List) ``` 因此,编译时会报告名称冲突。 不可能给两个方法使用相同的名称,并尝试通过重载来区分它们,因为在擦除之后不可能区分一个方法调用和另一个方法调用。 @@ -87,17 +90,19 @@ 再举一个例子,这里是整数类的一个不好的版本,它试图使一个整数与一个整数或一个长整数进行比较: ```java - class Integer implements Comparable, Comparable { - // 编译时错误,不能实现两个擦除相同的接口 - private final int value; - public Integer(int value) { this.value = value; } - public int compareTo(Integer i) { - return (value < i.value) ? -1 : (value == i.value) ? 0 : 1; - } - public int compareTo(Long l) { - return (value < l.value) ? -1 : (value == l.value) ? 0 : 1; - } - } +class Integer implements Comparable, Comparable { + // 编译时错误,不能实现两个擦除相同的接口 + private final int value; + public Integer(int value) { + this.value = value; + } + public int compareTo(Integer i) { + return (value < i.value) ? -1 : (value == i.value) ? 0 : 1; + } + public int compareTo(Long l) { + return (value < l.value) ? -1 : (value == l.value) ? 0 : 1; + } +} ``` 如果这得到支持,通常需要对桥接方法进行复杂而混乱的定义(参见第 `3.7` 节)。 到目前为止,最简单和最容易理解的选择是禁止这种情况。 diff --git a/ch05/02_Generic_Library_with_Generic_Client.md b/ch05/02_Generic_Library_with_Generic_Client.md index 982d55fe..ac4d1484 100644 --- a/ch05/02_Generic_Library_with_Generic_Client.md +++ b/ch05/02_Generic_Library_with_Generic_Client.md @@ -15,49 +15,49 @@ 例 `5-1`。 传统客户端的旧版库 ```java - l/Stack.java: - interface Stack { - public boolean empty(); - public void push(Object elt); - public Object pop(); - } - l/ArrayStack.java: - import java.util.*; - class ArrayStack implements Stack { - private List list; - public ArrayStack() { list = new ArrayList(); } - public boolean empty() { return list.size() == 0; } - public void push(Object elt) { list.add(elt); } - public Object pop() { - Object elt = list.remove(list.size()-1); - return elt; - } - public String toString() { return "stack"+list.toString(); } - } - l/Stacks.java: - class Stacks { - public static Stack reverse(Stack in) { - Stack out = new ArrayStack(); - while (!in.empty()) { - Object elt = in.pop(); - out.push(elt); - } - return out; - } - } - l/Client.java: - class Client { - public static void main(String[] args) { - Stack stack = new ArrayStack(); - for (int i = 0; i<4; i++) stack.push(new Integer(i)); - assert stack.toString().equals("stack[0, 1, 2, 3]"); - int top = ((Integer)stack.pop()).intValue(); - assert top == 3 && stack.toString().equals("stack[0, 1, 2]"); - Stack reverse = Stacks.reverse(stack); - assert stack.empty(); - assert reverse.toString().equals("stack[2, 1, 0]"); - } - } +l/Stack.java: + interface Stack { + public boolean empty(); + public void push(Object elt); + public Object pop(); + } +l/ArrayStack.java: + import java.util.*; + class ArrayStack implements Stack { + private List list; + public ArrayStack() { list = new ArrayList(); } + public boolean empty() { return list.size() == 0; } + public void push(Object elt) { list.add(elt); } + public Object pop() { + Object elt = list.remove(list.size()-1); + return elt; + } + public String toString() { return "stack"+list.toString(); } + } +l/Stacks.java: + class Stacks { + public static Stack reverse(Stack in) { + Stack out = new ArrayStack(); + while (!in.empty()) { + Object elt = in.pop(); + out.push(elt); + } + return out; + } + } +l/Client.java: + class Client { + public static void main(String[] args) { + Stack stack = new ArrayStack(); + for (int i = 0; i<4; i++) stack.push(new Integer(i)); + assert stack.toString().equals("stack[0, 1, 2, 3]"); + int top = ((Integer)stack.pop()).intValue(); + assert top == 3 && stack.toString().equals("stack[0, 1, 2]"); + Stack reverse = Stacks.reverse(stack); + assert stack.empty(); + assert reverse.toString().equals("stack[2, 1, 0]"); + } + } ``` 《《《 [下一节](03_Generic_Library_with_Legacy_Client.md)
diff --git a/ch05/03_Generic_Library_with_Legacy_Client.md b/ch05/03_Generic_Library_with_Legacy_Client.md index 301419cf..4fe196ae 100644 --- a/ch05/03_Generic_Library_with_Legacy_Client.md +++ b/ch05/03_Generic_Library_with_Legacy_Client.md @@ -18,85 +18,94 @@ 如,在目录 `l`)。 `Sun` 的 `Java 5` 编译器会产生以下消息: ```java - % javac g/Stack.java g/ArrayStack.java g/Stacks.java l/Client.java - Note: Client.java uses unchecked or unsafe operations. - Note: Recompile with -Xlint:unchecked for details. +% javac g/Stack.java g/ArrayStack.java g/Stacks.java l/Client.java +Note: Client.java uses unchecked or unsafe operations. +Note: Recompile with -Xlint:unchecked for details. ``` 未经检查的警告表明,编译器无法提供与泛型在统一使用时相同的安全保证。 但是,当通过更新遗留代码生成通用代码时,我们知道从两者都生成了等效的类文件,因此 -(即使未经检查的警告)使用通用库运行旧版客户端将产生与运行旧版客户端相同的结果 与遗留图书馆。 在这里,我们假设更新图书馆的唯一改变是引入泛型,并且不 +(即使未经检查的警告)使用通用库运行旧版客户端将产生与运行旧版客户端相同的结果 与遗留类库。 在这里,我们假设更新类库的唯一改变是引入泛型,并且不 管是故意还是错误地引入行为改变。 例 `5-2`。 具有通用客户端的通用库 ```java - g/Stack.java: - interface Stack { - public boolean empty(); - public void push(E elt); - public E pop(); - } - - g/ArrayStack.java: - import java.util.*; - class ArrayStack implements Stack { - private List list; - public ArrayStack() { list = new ArrayList(); } - public boolean empty() { return list.size() == 0; } - public void push(E elt) { list.add(elt); } - public E pop() { - E elt = list.remove(list.size()-1); - return elt; - } - public String toString() { return "stack"+list.toString(); } - } - - g/Stacks.java: - class Stacks { - public static Stack reverse(Stack in) { - Stack out = new ArrayStack(); - while (!in.empty()) { - T elt = in.pop(); - out.push(elt); - } - return out; - } - } - - g/Client.java: - class Client { - public static void main(String[] args) { - Stack stack = new ArrayStack(); - for (int i = 0; i<4; i++) stack.push(i); - assert stack.toString().equals("stack[0, 1, 2, 3]"); - int top = stack.pop(); - assert top == 3 && stack.toString().equals("stack[0, 1, 2]"); - Stack reverse = Stacks.reverse(stack); - assert stack.empty(); - assert reverse.toString().equals("stack[2, 1, 0]"); +g/Stack.java: +interface Stack { + public boolean empty(); + public void push(E elt); + public E pop(); +} + +g/ArrayStack.java: +import java.util.*; +class ArrayStack implements Stack { + private List list; + public ArrayStack() { + list = new ArrayList(); + } + public boolean empty() { + return list.size() == 0; } - } + public void push(E elt) { + list.add(elt); + } + public E pop() { + E elt = list.remove(list.size()-1); + return elt; + } + public String toString() { + return "stack"+list.toString(); + } +} + +g/Stacks.java: +class Stacks { + public static Stack reverse(Stack in) { + Stack out = new ArrayStack(); + while (!in.empty()) { + T elt = in.pop(); + out.push(elt); + } + return out; + } +} + +g/Client.java: +class Client { + public static void main(String[] args) { + Stack stack = new ArrayStack(); + for (int i = 0; i<4; i++) + stack.push(i); + assert stack.toString().equals("stack[0, 1, 2, 3]"); + int top = stack.pop(); + assert top == 3 && stack.toString().equals("stack[0, 1, 2]"); + Stack reverse = Stacks.reverse(stack); + assert stack.empty(); + assert reverse.toString().equals("stack[2, 1, 0]"); + } +} ``` 如果我们遵循上面的建议,并在启用适当的开关的情况下重新运行编译器,我们会得到更多的细节: ```java - % javac -Xlint:unchecked g/Stack.java g/ArrayStack.java \ - % g/Stacks.java l/Client.java - l/Client.java:4: warning: [unchecked] unchecked call - to push(E) as a member of the raw type Stack - for (int i = 0; i<4; i++) stack.push(new Integer(i)); - ^ - l/Client.java:8: warning: [unchecked] unchecked conversion - found : Stack - required: Stack - Stack reverse = Stacks.reverse(stack); - ^ - l/Client.java:8: warning: [unchecked] unchecked method invocation: - reverse(Stack) in Stacks is applied to (Stack) - Stack reverse = Stacks.reverse(stack); - ^ - 3 warnings +% javac -Xlint:unchecked g/Stack.java g/ArrayStack.java \ +% g/Stacks.java l/Client.java +l/Client.java:4: warning: [unchecked] unchecked call +to push(E) as a member of the raw type Stack +for (int i = 0; i<4; i++) stack.push(new Integer(i)); + ^ +l/Client.java:8: warning: [unchecked] unchecked conversion +found : Stack +required: Stack +Stack reverse = Stacks.reverse(stack); + ^ +l/Client.java:8: warning: [unchecked] unchecked method invocation: +reverse(Stack) in Stacks is applied to (Stack) +Stack reverse = Stacks.reverse(stack); + ^ +3 warnings ``` 并非每种原始类型都会引发警告。因为每个参数化类型都是相应原始类型的子类型,但是相反,传递一个参数化类型(其中原始类型是预期的)是安全的(因此,没有警 @@ -111,13 +120,13 @@ 当我们用 `Java 5` 编译器和库编译所有文件的旧版本时发生的情况: ```java - % javac -Xlint:unchecked l/Stack.java l/ArrayStack.java \ - % l/Stacks.java l/Client.java - l/ArrayStack.java:6: warning: [unchecked] unchecked call to add(E) - as a member of the raw type java.util.List - public void push(Object elt) list.add(elt); - ^ - 1 warning +% javac -Xlint:unchecked l/Stack.java l/ArrayStack.java \ +% l/Stacks.java l/Client.java +l/ArrayStack.java:6: warning: [unchecked] unchecked call to add(E) +as a member of the raw type java.util.List +public void push(Object elt) list.add(elt); + ^ +1 warning ``` 在这里,传统方法 `push` 中使用泛型方法 `add` 的警告是由于类似于从旧客户端发出使用泛型方法 `push` 的先前警告的原因而发布的。 @@ -126,8 +135,8 @@ 在纯代码的情况下,可以使用 `-source 1.4` 开关关闭此类警告: ```java - % javac -source 1.4 l/Stack.java l/ArrayStack.java \ - % l/Stacks.java l/Client.java +% javac -source 1.4 l/Stack.java l/ArrayStack.java \ +% l/Stacks.java l/Client.java ``` 这编译了遗留代码并且没有发出警告或错误。 这种关闭警告的方法只适用于真正的遗留代码,没有 `Java 5` 中引入的通用功能或其他功能。 也可以使用注释关闭未经 diff --git a/ch05/04_Legacy_Library_with_Generic_Client.md b/ch05/04_Legacy_Library_with_Generic_Client.md index 4c5018e0..42ded8cf 100644 --- a/ch05/04_Legacy_Library_with_Generic_Client.md +++ b/ch05/04_Legacy_Library_with_Generic_Client.md @@ -9,24 +9,23 @@ 在这种情况下,更新库以在其方法签名中使用参数化类型是有意义的,但不能更改方法体。 有三种方法可以实现这一点:对源代码进行最小限度的更改,创建存根文件或 使用包装器。我们建议在仅有权访问类时有权访问源代码和使用存根时使用最少的更改 文件,我们建议不要使用包装。 - -### 用最小的变化来演变一个类库 +#### 用最小的变化来演变一个类库 示例 `5-3` 显示了最小更改技术。 这里库的来源已经被编辑过,但只是为了改变方法签名,而不是方法体。 所需的确切更改以粗体显示。 当您有权访问源时,推荐使 用这种技术来使库变得通用。 确切地说,所需的改变是: - - 根据需要为接口或类声明添加类型参数(对于接口 `Stack` 和类 `ArrayStack`) - - - 将类型参数添加到扩展或实现子句中的任何新参数化接口或类(对 `ArrayStack` 的 `implements` 子句中的 `Stack`), - - - 根据需要为每个方法签名添加类型参数(用于在 `Stack` 和 `ArrayStack` 中进行推入和弹出操作,并在堆栈中进行反向操作) - - - 在返回类型包含一个类型参数(对于在 `ArrayStack` 中弹出,其中返回类型为 `E`)的每个返回中添加一个未经检查的强制转换 - 没有此强制转换的情况 - 下,您将得到一个错误而不是未经检查的警告 - - - 可选择添加批注以抑制未经检查的警告(对于 `ArrayStack` 和 `Stacks`) +- 根据需要为接口或类声明添加类型参数(对于接口 `Stack` 和类 `ArrayStack`) + +- 将类型参数添加到扩展或实现子句中的任何新参数化接口或类(对 `ArrayStack` 的 `implements` 子句中的 `Stack`), + +- 根据需要为每个方法签名添加类型参数(用于在 `Stack` 和 `ArrayStack` 中进行推入和弹出操作,并在堆栈中进行反向操作) + +- 在返回类型包含一个类型参数(对于在 `ArrayStack` 中弹出,其中返回类型为 `E`)的每个返回中添加一个未经检查的强制转换 - 没有此强制转换的情况 +下,您将得到一个错误而不是未经检查的警告 + +- 可选择添加批注以抑制未经检查的警告(对于 `ArrayStack` 和 `Stacks`) 值得注意的是我们不需要做出一些改变。 在方法体中,我们可以保留 `Object` 的出现(参见 `ArrayStack` 中的第一行 `pop`),并且我们不需要为任何出现的raw 类型添加类型参数(请参阅 `Stacks` 中的第一行)。 此外,只有当返回类型是类型参数(如 `pop` 中)时,我们才需要将转换添加到 `return` 子句中,但是当返 @@ -69,39 +68,47 @@ 例 `5-3`。 使用最小的变化来发展一个类库 ``` - m/Stack.java: - interface Stack { - public boolean empty(); - public void push(E elt); - public E pop(); - } - - m/ArrayStack.java: - @SuppressWarnings("unchecked") - class ArrayStack implements Stack { - private List list; - public ArrayStack() { list = new ArrayList(); } - public boolean empty() { return list.size() == 0; } - public void push(E elt) { list.add(elt); } // unchecked call - public E pop() { - Object elt = list.remove(list.size()-1); - return (E)elt; // unchecked cast - } - public String toString() { return "stack"+list.toString(); } - } - - m/Stacks.java: - @SuppressWarnings("unchecked") - class Stacks { - public static Stack reverse(Stack in) { - Stack out = new ArrayStack(); - while (!in.empty()) { - Object elt = in.pop(); - out.push(elt); // unchecked call - } - return out; // unchecked conversion - } +m/Stack.java: +interface Stack { + public boolean empty(); + public void push(E elt); + public E pop(); +} + +m/ArrayStack.java: + @SuppressWarnings("unchecked") + class ArrayStack implements Stack { + private List list; + public ArrayStack() { + list = new ArrayList(); + } + public boolean empty() { + return list.size() == 0; + } + public void push(E elt) { + list.add(elt); + } // unchecked call + public E pop() { + Object elt = list.remove(list.size()-1); + return (E)elt; // unchecked cast } + public String toString() { + return "stack"+list.toString(); + } +} + +m/Stacks.java: +@SuppressWarnings("unchecked") +class Stacks { + public static Stack reverse(Stack in) { + Stack out = new ArrayStack(); + while (!in.empty()) { + Object elt = in.pop(); + out.push(elt); // unchecked call + } + return out; // unchecked conversion + } +} ``` 消除(而不是抑制)编译库生成的未经检查的警告的唯一方法是更新整个库源以使用泛型。 这是完全合理的,因为除非更新整个源代码,否则编译器无法检查声明的通用 @@ -111,33 +118,41 @@ 例 `5-4`。 使用存根发展类库 ```java - s/Stack.java: - interface Stack { - public boolean empty(); - public void push(E elt); - public E pop(); - } - - s/StubException.java: - class StubException extends UnsupportedOperationException {} - - s/ArrayStack.java: - class ArrayStack implements Stack { - public boolean empty() { throw new StubException(); } - public void push(E elt) { throw new StubException(); } - public E pop() { throw new StubException(); } - public String toString() { throw new StubException(); } - } - - s/Stacks.java: - class Stacks { - public static Stack reverse(Stack in) { - throw new StubException(); - } - } +s/Stack.java: +interface Stack { + public boolean empty(); + public void push(E elt); + public E pop(); +} + +s/StubException.java: +class StubException extends UnsupportedOperationException {} + +s/ArrayStack.java: +class ArrayStack implements Stack { + public boolean empty() { + throw new StubException(); + } + public void push(E elt) { + throw new StubException(); + } + public E pop() { + throw new StubException(); + } + public String toString() { + throw new StubException(); + } +} + +s/Stacks.java: +class Stacks { + public static Stack reverse(Stack in) { + throw new StubException(); + } +} ``` -### 使用存根演化库 +#### 使用存根演化库 示例 `5-4` 中显示了存根技术。 在这里,我们编写了具有通用签名但不包含 `body` 的存根。我们针对通用签名编译通用客户端,但针对遗留类文件运行代码。 这种 技术适用于源未发布或其他人负责维护源的情况。 @@ -149,14 +164,14 @@ 件(比如在目录 `l` 中)进行这种操作。 ```java - % javac -classpath s g/Client.java - % java -ea -classpath l g/Client +% javac -classpath s g/Client.java +% java -ea -classpath l g/Client ``` 再说一遍,这是有效的,因为为传统文件和通用文件生成的类文件基本相同,除了关于类型的辅助信息。 特别是,客户端编译的通用签名与传统签名(除了关于类型参数 的辅助信息)相匹配,因此代码可以成功运行并提供与以前相同的答案。 -### 使用包装进化库 +#### 使用包装进化库 例 `5-5` 给出了这个包装技术。 在这里,我们保留原有的源文件和类文件不变,并提供通过代理访问遗留类的通用包装类。我们主要介绍这种技术,主要是为了警告您 不要使用它 - 通常最好使用最少的更改或存根。 @@ -188,48 +203,61 @@ 例 `5-5`。 使用包装器发展一个库 ```java - //不要这样做---不推荐使用包装类 - l/Stack.java, l/Stacks.java, l/ArrayStack.java: - // As in Example 5.1 - w/GenericStack.java: - interface GenericStack { - public Stack unwrap(); - public boolean empty(); - public void push(E elt); - public E pop(); - } - w/GenericStackWrapper.java: - @SuppressWarnings("unchecked") - class GenericStackWrapper implements GenericStack { - private Stack stack; - public GenericStackWrapper(Stack stack) { this.stack = stack; } - public Stack unwrap() { return stack; } - public boolean empty() { return stack.empty(); } - public void push(E elt) { stack.push(elt); } - public E pop() { return (E)stack.pop(); } // unchecked cast - public String toString() { return stack.toString(); } - } - w/GenericStacks.java: - class GenericStacks { - public static GenericStack reverse(GenericStack in) { - Stack rawIn = in.unwrap(); - Stack rawOut = Stacks.reverse(rawIn); - return new GenericStackWrapper(rawOut); - } - } - w/Client.java: - class Client { - public static void main(String[] args) { - GenericStack stack = new GenericStackWrapper(new ArrayStack()); - for (int i = 0; i<4; i++) stack.push(i); - assert stack.toString().equals("stack[0, 1, 2, 3]"); - int top = stack.pop(); - assert top == 3 && stack.toString().equals("stack[0, 1, 2]"); - GenericStack reverse = GenericStacks.reverse(stack); - assert stack.empty(); - assert reverse.toString().equals("stack[2, 1, 0]"); - } - } +//不要这样做---不推荐使用包装类 +l/Stack.java, l/Stacks.java, l/ArrayStack.java: +// As in Example 5.1 +w/GenericStack.java: +interface GenericStack { + public Stack unwrap(); + public boolean empty(); + public void push(E elt); + public E pop(); +} +w/GenericStackWrapper.java: +@SuppressWarnings("unchecked") +class GenericStackWrapper implements GenericStack { + private Stack stack; + public GenericStackWrapper(Stack stack) { + this.stack = stack; + } + public Stack unwrap() { + return stack; + } + public boolean empty() { + return stack.empty(); + } + public void push(E elt) { + stack.push(elt); + } + public E pop() { + return (E)stack.pop(); + } // unchecked cast + public String toString() { + return stack.toString(); + } +} +w/GenericStacks.java: +class GenericStacks { + public static GenericStack reverse(GenericStack in) { + Stack rawIn = in.unwrap(); + Stack rawOut = Stacks.reverse(rawIn); + return new GenericStackWrapper(rawOut); + } +} +w/Client.java: +class Client { + public static void main(String[] args) { + GenericStack stack = new GenericStackWrapper(new ArrayStack()); + for (int i = 0; i<4; i++) + stack.push(i); + assert stack.toString().equals("stack[0, 1, 2, 3]"); + int top = stack.pop(); + assert top == 3 && stack.toString().equals("stack[0, 1, 2]"); + GenericStack reverse = GenericStacks.reverse(stack); + assert stack.empty(); + assert reverse.toString().equals("stack[2, 1, 0]"); + } +} ``` 包装也呈现更深和更微妙的问题。 如果代码使用对象标识,则可能会出现问题,因为遗留对象和包装对象是不同的。 此外,复杂的结构将需要多层包装纸。 想象一下, diff --git a/ch06/02_Instance_Tests_and_Casts.md b/ch06/02_Instance_Tests_and_Casts.md index 5122d713..02514162 100644 --- a/ch06/02_Instance_Tests_and_Casts.md +++ b/ch06/02_Instance_Tests_and_Casts.md @@ -8,17 +8,22 @@ 作为一个例子,考虑使用实例测试并以书面形式强制转换。 下面是 `java.lang` 中类 `Integer` 定义的一个片段(从实际源代码稍微简化了一下): ```java - public class Integer extends Number { - private final int value; - public Integer(int value) { this.value=value; } - public int intValue() { return value; } - public boolean equals(Object o) { - if (o instanceof Integer) { - return value == ((Integer)o).intValue(); - } else return false; - } - ... - } +public class Integer extends Number { + private final int value; + public Integer(int value) { + this.value=value; + } + public int intValue() { + return value; + } + public boolean equals(Object o) { + if (o instanceof Integer) { + return value == ((Integer)o).intValue(); + } else + return false; + } + ... + } ``` `equals` 方法接受 `Object` 类型的参数,检查对象是否为 `Integer` 类的实例,如果是,则将其转换为 `Integer` 并比较两个整数的值。 此代码可用,因为 @@ -27,23 +32,24 @@ 现在考虑一下如何在列表上定义相等性,就像 `java.util` 中的 `AbstractList` 类一样。 定义这个的自然但不正确的方式如下: ```java - import java.util.*; - public abstract class AbstractList extends AbstractCollection implements List { - public boolean equals(Object o) { - if (o instanceof List) { // compile-time error - Iterator it1 = iterator(); - Iterator it2 = ((List)o).iterator(); // unchecked cast - while (it1.hasNext() && it2.hasNext()) { - E e1 = it1.next(); - E e2 = it2.next(); - if (!(e1 == null ? e2 == null : e1.equals(e2))) - return false; - } - return !it1.hasNext() && !it2.hasNext(); - } else return false; - } - ... - } +import java.util.*; +public abstract class AbstractList extends AbstractCollection implements List { + public boolean equals(Object o) { + if (o instanceof List) { // compile-time error + Iterator it1 = iterator(); + Iterator it2 = ((List)o).iterator(); // unchecked cast + while (it1.hasNext() && it2.hasNext()) { + E e1 = it1.next(); + E e2 = it2.next(); + if (!(e1 == null ? e2 == null : e1.equals(e2))) + return false; + } + return !it1.hasNext() && !it2.hasNext(); + } else + return false; + } + ... + } ``` 同样,`equals` 方法接受 `Object` 类型的参数,检查对象是否为 `List` 类型的实例,如果是,则将其转换为 `List` 并比较两个列表中的相应元素。此代 @@ -56,17 +62,17 @@ 编译上面的代码报告了两个问题,一个是实例测试的错误,另一个是未经检查的演员警告: ```java - % javac -Xlint:unchecked AbstractList.java - AbstractList.java:6: illegal generic type for instanceof - if (!(o instanceof List)) return false; // compile-time error - ^ - AbstractList.java:8: warning: [unchecked] unchecked cast - found : java.lang.Object - required: List - Iterator it2 = ((List)o).iterator(); // unchecked cast - ^ - 1 error - 1 warning +% javac -Xlint:unchecked AbstractList.java +AbstractList.java:6: illegal generic type for instanceof +if (!(o instanceof List)) return false; // compile-time error + ^ +AbstractList.java:8: warning: [unchecked] unchecked cast +found : java.lang.Object +required: List + Iterator it2 = ((List)o).iterator(); // unchecked cast + ^ +1 error +1 warning ``` 实例检查报告错误,因为没有可能的方法来测试给定对象是否属于类型 `List`。 演员报告未经检查的警告; 它将执行转换,但它不能检查列表元素实际上是否为 @@ -75,23 +81,23 @@ 为了解决这个问题,我们用可调整类型 `List` 替换了不可保留类型 `List`。 这是一个更正的定义(再次,从实际来源稍微简化): ```java - import java.util.*; - public abstract class AbstractList extends AbstractCollection implements List { - public boolean equals(Object o) { - if (o instanceof List) { - Iterator it1 = iterator(); - Iterator it2 = ((List)o).iterator(); - while (it1.hasNext() && it2.hasNext()) { - E e1 = it1.next(); - Object e2 = it2.next(); - if (!(e1 == null ? e2 == null : e1.equals(e2))) - return false; - } - return !it1.hasNext() && !it2.hasNext(); - } else - return false; - } - ... +import java.util.*; +public abstract class AbstractList extends AbstractCollection implements List { + public boolean equals(Object o) { + if (o instanceof List) { + Iterator it1 = iterator(); + Iterator it2 = ((List)o).iterator(); + while (it1.hasNext() && it2.hasNext()) { + E e1 = it1.next(); + Object e2 = it2.next(); + if (!(e1 == null ? e2 == null : e1.equals(e2))) + return false; + } + return !it1.hasNext() && !it2.hasNext(); + } else + return false; + } + ... } ``` @@ -110,12 +116,12 @@ 例如,以下方法将集合转换为列表: ```java - public static List asList(Collection c) throws InvalidArgumentException { - if (c instanceof List) { - return (List)c; - } else - throw new InvalidArgumentException("Argument not a list"); - } +public static List asList(Collection c) throws InvalidArgumentException { + if (c instanceof List) { + return (List)c; + } else + throw new InvalidArgumentException("Argument not a list"); +} ``` 编译此代码将成功,不会出现错误或警告。 实例测试没有错误,因为 `List` 是可重用的类型。 由于转换源的类型为 `Collection`,转换不会报告警告,并且 @@ -130,25 +136,27 @@ 例如,下面是将对象列表提升为字符串列表的代码,如果对象列表仅包含字符串,则会抛出类转换异常: ```java - class Promote { - public static List promote(List objs) { - for (Object o : objs) - if (!(o instanceof String)) - throw new ClassCastException(); - return (List)(List)objs; // unchecked cast - } - public static void main(String[] args) { - List objs1 = Arrays.asList("one","two"); - List objs2 = Arrays.asList(1,"two"); - List strs1 = promote(objs1); - assert (List)strs1 == (List)objs1; - boolean caught = false; - try { - List strs2 = promote(objs2); - } catch (ClassCastException e) { caught = true; } - assert caught; - } - } +class Promote { + public static List promote(List objs) { + for (Object o : objs) + if (!(o instanceof String)) + throw new ClassCastException(); + return (List)(List)objs; // unchecked cast + } + public static void main(String[] args) { + List objs1 = Arrays.asList("one","two"); + List objs2 = Arrays.asList(1,"two"); + List strs1 = promote(objs1); + assert (List)strs1 == (List)objs1; + boolean caught = false; + try { + List strs2 = promote(objs2); + } catch (ClassCastException e) { + caught = true; + } + assert caught; + } +} ``` 如果任何对象不是字符串,该方法会在对象列表上抛出循环并抛出类抛出异常。 因此,当方法的最后一行到达时,将对象列表转换为字符串列表是安全的。 @@ -157,13 +165,13 @@ 类型列表; 这个演员是安全的。 其次,将通配符类型列表转换为字符串列表; 此演员阵容是允许的,但会产生未经检查的警告: ```java - % javac -Xlint:unchecked Promote.java - Promote.java:7: warning: [unchecked] unchecked cast - found : java.util.List - required: java.util.List - return (List)(List)objs; // unchecked cast - ^ - 1 warning + % javac -Xlint:unchecked Promote.java + Promote.java:7: warning: [unchecked] unchecked cast + found : java.util.List + required: java.util.List + return (List)(List)objs; // unchecked cast + ^ + 1 warning ``` 测试代码将该方法应用于两个列表,一个仅包含字符串(因此成功),另一个包含整数(因此引发异常)。在第一个断言中,为了比较对象列表和字符串列表,我们必须 diff --git a/ch06/03_Exception_Handling.md b/ch06/03_Exception_Handling.md index bbc9ecf8..05768f9a 100644 --- a/ch06/03_Exception_Handling.md +++ b/ch06/03_Exception_Handling.md @@ -10,25 +10,29 @@ 例如,下面是一个新的异常的允许定义,它包含一个整数值: ```java - class IntegerException extends Exception { - private final int value; - public IntegerException(int value) { this.value = value; } - public int getValue() { return value; } - } +class IntegerException extends Exception { + private final int value; + public IntegerException(int value) { + this.value = value; + } + public int getValue() { + return value; + } +} ``` 这里是一个简单的例子,说明如何使用异常: ```java - class IntegerExceptionTest { - public static void main(String[] args) { - try { - throw new IntegerException(42); - } catch (IntegerException e) { - assert e.getValue() == 42; - } - } +class IntegerExceptionTest { + public static void main(String[] args) { + try { + throw new IntegerException(42); + } catch (IntegerException e) { + assert e.getValue() == 42; } + } +} ``` `try` 语句的主体用 `catch` 语句捕获的给定值抛出异常。 @@ -36,52 +40,56 @@ 相反,以下定义的新异常是禁止的,因为它创建了一个参数化类型: ```java - class ParametricException extends Exception { // 编译报错 - private final T value; - public ParametricException(T value) { this.value = value; } - public T getValue() { return value; } - } +class ParametricException extends Exception { // 编译报错 + private final T value; + public ParametricException(T value) { + this.value = value; + } + public T getValue() { + return value; + } +} ``` 试图编译上述报告错误: ```java - % javac ParametricException.java - ParametricException.java:1: a generic class may not extend - java.lang.Throwable - class ParametricException extends Exception { // 编译报错 - ^ - 1 error +% javac ParametricException.java +ParametricException.java:1: a generic class may not extend +java.lang.Throwable +class ParametricException extends Exception { // 编译报错 + ^ +1 error ``` 这种限制是明智的,因为几乎任何捕捉这种异常的尝试都必须失败,因为该类型不可确定。 人们可能会期望典型的例外使用如下所示: ```java - class ParametricExceptionTest { - public static void main(String[] args) { - try { - throw new ParametricException(42); - } catch (ParametricException e) { // compile-time error - assert e.getValue()==42; - } - } - } +class ParametricExceptionTest { + public static void main(String[] args) { + try { + throw new ParametricException(42); + } catch (ParametricException e) { // compile-time error + assert e.getValue()==42; + } + } +} ``` 这是不允许的,因为 `catch` 子句中的类型是不可确定的。 在撰写本文时,`Sun` 编译器在这种情况下报告了一系列语法错误: ```java - % javac ParametricExceptionTest.java - ParametricExceptionTest.java:5: expected - } catch (ParametricException e) { - ^ - ParametricExceptionTest.java:8: ')' expected - } - ^ - ParametricExceptionTest.java:9: '}' expected - } - ^ - 3 errors +% javac ParametricExceptionTest.java +ParametricExceptionTest.java:5: expected +} catch (ParametricException e) { + ^ +ParametricExceptionTest.java:8: ')' expected +} +^ +ParametricExceptionTest.java:9: '}' expected +} +^ +3 errors ``` 由于异常不能是参数化的,因此语法受到限制,因此必须将该类型编写为标识符,而没有以下参数。 diff --git a/ch06/04_Array_Creation.md b/ch06/04_Array_Creation.md index 73ac6d2a..384042f5 100644 --- a/ch06/04_Array_Creation.md +++ b/ch06/04_Array_Creation.md @@ -9,9 +9,9 @@ 回想一下第 `2.5` 节中的这个例子。 ```java - Integer[] ints = new Integer[] {1,2,3}; - Number[] nums = ints; - nums[2] = 3.14; // 数组存储异常 +Integer[] ints = new Integer[] {1,2,3}; +Number[] nums = ints; +nums[2] = 3.14; // 数组存储异常 ``` 第一行分配一个新数组,其中指定它是一个整数数组。 第二行将此数组赋给一个包含数字数组的变量; 这是允许的,因为与泛型类型不同,数组是协变的。 第三行的赋 @@ -23,24 +23,24 @@ 考虑以下(不正确)代码将集合转换为数组: ```java - import java.util.*; - class Annoying { - public static T[] toArray(Collection c) { -        T[] a = new T[c.size()]; // 编译错误 - int i=0; for (T x : c) a[i++] = x; - return a; - } +import java.util.*; + class Annoying { + public static T[] toArray(Collection c) { + T[] a = new T[c.size()]; // 编译错误 + int i=0; for (T x : c) a[i++] = x; + return a; } + } ``` 这是一个错误,因为类型变量不是可确定类型。 尝试编译此代码会报告一个通用数组创建错误: ```java - % javac Annoying.java - Annoying.java:4: generic array creation - T[] a = new T[c.size()]; // 编译错误 - ^ - 1 error +% javac Annoying.java +Annoying.java:4: generic array creation +T[] a = new T[c.size()]; // 编译错误 + ^ +1 error ``` 我们很快就讨论这个问题的解决方法。 @@ -48,24 +48,24 @@ 作为第二个示例,请考虑返回包含两个列表的数组的以下(不正确)代码: ```java - import java.util.*; - class AlsoAnnoying { - public static List[] twoLists() { - List a = Arrays.asList(1,2,3); - List b = Arrays.asList(4,5,6); - return new List[] {a, b}; // 编译错误 - } - } +import java.util.*; +class AlsoAnnoying { + public static List[] twoLists() { + List a = Arrays.asList(1,2,3); + List b = Arrays.asList(4,5,6); + return new List[] {a, b}; // 编译错误 + } +} ``` 这是一个错误,因为参数化类型不是可确定类型。 尝试编译此代码也会报告一个通用数组创建错误: ```java - % javac AlsoAnnoying.java - AlsoAnnoying.java:6: generic array creation - return new List[] {a, b}; // 编译错误 - ^ - 1 error +% javac AlsoAnnoying.java +AlsoAnnoying.java:6: generic array creation +return new List[] {a, b}; // 编译错误 + ^ +1 error ``` 我们也很快讨论这个问题的解决方法。 diff --git a/ch06/05_The_Principle_of_Truth_in_Advertising.md b/ch06/05_The_Principle_of_Truth_in_Advertising.md index 19a4be08..1f2cbd01 100644 --- a/ch06/05_The_Principle_of_Truth_in_Advertising.md +++ b/ch06/05_The_Principle_of_Truth_in_Advertising.md @@ -10,39 +10,39 @@ 这里是第二次尝试将集合转换为数组,这次使用未经检查的转换,并添加了测试代码: ```java - import java.util.*; - class Wrong { - public static T[] toArray(Collection c) { - T[] a = (T[])new Object[c.size()]; // 未经检查的转换 - int i=0; for (T x : c) a[i++] = x; - return a; - } - public static void main(String[] args) { - List strings = Arrays.asList("one","two"); - String[] a = toArray(strings); // 类抛出错误 - } - } +import java.util.*; +class Wrong { + public static T[] toArray(Collection c) { + T[] a = (T[])new Object[c.size()]; // 未经检查的转换 + int i=0; for (T x : c) a[i++] = x; + return a; + } + public static void main(String[] args) { + List strings = Arrays.asList("one","two"); + String[] a = toArray(strings); // 类抛出错误 + } +} ``` 上一节中的代码使用短语 `new T[c.size()]` 创建数组,导致编译器报告通用数组创建错误。 新代码改为分配一个对象数组并将其转换为 `T []` 类型,这会导致编 译器发出未经检查的强制转换警告: ```java - % javac -Xlint Wrong.java - Wrong.java:4: warning: [unchecked] 未经检查的转换 - found : java.lang.Object[] - required: T[] - T[] a = (T[])new Object[c.size()]; // 未经检查的转换 - ^ - 1 warning +% javac -Xlint Wrong.java +Wrong.java:4: warning: [unchecked] 未经检查的转换 +found : java.lang.Object[] +required: T[] + T[] a = (T[])new Object[c.size()]; // 未经检查的转换 + ^ +1 warning ``` 正如你从这个程序选择的名字中猜出的那样,这个警告不应该被忽略。 事实上,运行这个程序给出了以下结果: ```java - % java Wrong - Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; - at Wrong.main(Wrong.java:11) +% java Wrong +Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; + at Wrong.main(Wrong.java:11) ``` 难懂的短语 `[Ljava.lang.Object` 是数组的指定类型,其中 `[L` 表示它是引用类型的数组,而 `java.lang.Object` 是数组的组件类型。 类转换错误消息引用包 @@ -52,18 +52,18 @@ 在调用 `toArray` 时插入相应的强制转换,从而生成以下等效代码: ```java - import java.util.*; - class Wrong { - public static Object[] toArray(Collection c) { - Object[] a = (Object[])new Object[c.size()]; // unchecked cast - int i=0; for (Object x : c) a[i++] = x; - return a; - } - public static void main(String[] args) { - List strings = Arrays.asList(args); - String[] a = (String[])toArray(strings); // class cast error - } - } +import java.util.*; +class Wrong { + public static Object[] toArray(Collection c) { + Object[] a = (Object[])new Object[c.size()]; // unchecked cast + int i=0; for (Object x : c) a[i++] = x; + return a; + } + public static void main(String[] args) { + List strings = Arrays.asList(args); + String[] a = (String[])toArray(strings); // class cast error + } +} ``` 类型擦除将未选中的转换转换为 `T []` 转换为 `Object []` 的转换,并在调用 `toArray` 时将转换插入 `String []`。 运行时,这些转换中的第一个成功。 但 @@ -87,25 +87,26 @@ 以下是实施替代方案的代码: ```java - import java.util.*; - class Right { - public static T[] Array(toCollection c, T[] a) { - if (a.length< c.size()) - a = (T[])java.lang.reflect.Array. // unchecked cast - newInstance(a.get Class().getComponentType(), c.size()); - int i=0; for (T x : c) a[i++] = x; - if (i< a.length) a[i] = null; - return a; - } - public static void main(String[] args) { - List strings = Arrays.asList("one", "two"); - String[] a = toArray(strings, new String[0]); - assert Arrays.toString(a).equals("[one, two]"); - String[] b = new String[] { "x","x","x","x" }; - toArray(strings, b); - assert Arrays.toString(b).equals("[one, two, null, x]"); - } - } +import java.util.*; +class Right { + public static T[] Array(toCollection c, T[] a) { + if (a.length< c.size()) + a = (T[])java.lang.reflect.Array. // unchecked cast + newInstance(a.get Class().getComponentType(), c.size()); + int i=0; for (T x : c) a[i++] = x; + if (i< a.length) + a[i] = null; + return a; + } + public static void main(String[] args) { + List strings = Arrays.asList("one", "two"); + String[] a = toArray(strings, new String[0]); + assert Arrays.toString(a).equals("[one, two]"); + String[] b = new String[] { "x","x","x","x" }; + toArray(strings, b); + assert Arrays.toString(b).equals("[one, two, null, x]"); + } +} ``` 这使用反射库中的三个方法来分配一个与旧数组具有相同组件类型的新数组:方法 `getClass`(在 `java.lang.Object` 中)返回表示数组类型的 `Class` 对象 @@ -127,11 +128,11 @@ 集合框架包含两个将集合转换为数组的方法,类似于我们刚刚讨论的那个: ```java - interface Collection { - ... - public Object[] toArray(); - public T[] toArray(T[] a) - } +interface Collection { + ... + public Object[] toArray(); + public T[] toArray(T[] a) +} ``` 第一个方法返回一个带有指定组件类型 `Object` 的数组,而第二个方法从参数数组中复制指定组件类型,就像上面的静态方法一样。 就像那种方法一样,如果有空间 @@ -153,20 +154,20 @@ 需要未经检查的转换。 ```java - import java.util.*; - class RightWithClass { - public static T[] toArray(Collection c, Class k) { - T[] a = (T[])java.lang.reflect.Array. // unchecked cast - newInstance(k, c.size()); - int i=0; for (T x : c) a[i++] = x; - return a; - } - public static void main(String[] args) { - List strings = Arrays.asList("one", "two"); - String[] a = toArray(strings, String.class); - assert Arrays.toString(a).equals("[one, two]"); - } - } +import java.util.*; +class RightWithClass { + public static T[] toArray(Collection c, Class k) { + T[] a = (T[])java.lang.reflect.Array. // unchecked cast + newInstance(k, c.size()); + int i=0; for (T x : c) a[i++] = x; + return a; + } + public static void main(String[] args) { + List strings = Arrays.asList("one", "two"); + String[] a = toArray(strings, String.class); + assert Arrays.toString(a).equals("[one, two]"); + } +} ``` 转换方法现在传递类标记 `String.class` 而不是字符串数组。 diff --git a/ch06/06_The_Principle_of_Indecent_Exposure.md b/ch06/06_The_Principle_of_Indecent_Exposure.md index eae789d8..1dba2d0c 100644 --- a/ch06/06_The_Principle_of_Indecent_Exposure.md +++ b/ch06/06_The_Principle_of_Indecent_Exposure.md @@ -9,10 +9,10 @@ 回想一下,`2.5` 节介绍了为什么需要物化的一个例子: ```java - Integer[] ints = new Integer[] {1}; - Number[] nums = ints; - nums[0] = 1.01; // 数组存储异常 - int n = ints[0]; +Integer[] ints = new Integer[] {1}; +Number[] nums = ints; +nums[0] = 1.01; // 数组存储异常 +int n = ints[0]; ``` 这将整数数组赋给一个数组数组,然后尝试将一个 `double` 存储到数组数组中。 该尝试引发数组存储异常,因为该检查与实体类型有关。 这也是一样,因为否则最后 @@ -21,41 +21,39 @@ 下面是一个类似的例子,数组数组被数组列表所取代: ```java - List[] intLists - = (List[])new List[] {Arrays.asList(1)}; // 未经检查的转换 - List[] numLists = intLists; - numLists[0] = Arrays.asList(1.01); - int n = intLists[0].get(0); // 类抛出异常! +List[] intLists = (List[])new List[] {Arrays.asList(1)}; // 未经检查的转换 +List[] numLists = intLists; +numLists[0] = Arrays.asList(1.01); +int n = intLists[0].get(0); // 类抛出异常! ``` 这将整数列表分配给数组列表,然后尝试将双列表存储到数组列表中。 这次尝试的存储不会失败,即使它应该,因为针对被指定类型的检查是不充分的:被指定的信息只 -包含删除类型,表示它是一个 `List` 数组,而不是一个 `List`。因此,商店成功,程序意外地在别处失败。 +包含删除类型,表示它是一个 `List` 数组,而不是一个 `List`。因此,编译成功,程序意外地在别处失败。 例 `6-1`。 避免不可接受类型的数组 ```java - DeceptiveLibrary.java: - import java.util.*; - public class DeceptiveLibrary { - public static List[] intLists(int size) { - List[] intLists = - (List[]) new List[size]; // 未经检查的转换 - for (int i = 0; i < size; i++) - intLists[i] = Arrays.asList(i+1); - return ints; - } - } - - InnocentClient.java: - import java.util.*; - public class InnocentClient { - public static void main(String[] args) { - List[] intLists = DeceptiveLibrary.intLists(1); - List[] numLists = intLists; - numLists[0] = Arrays.asList(1.01); - int i = intLists[0].get(0); // 类抛出异常! - } - } +DeceptiveLibrary.java: +import java.util.*; +public class DeceptiveLibrary { + public static List[] intLists(int size) { + List[] intLists = (List[]) new List[size]; // 未经检查的转换 + for (int i = 0; i < size; i++) + intLists[i] = Arrays.asList(i+1); + return ints; + } +} + +InnocentClient.java: +import java.util.*; +public class InnocentClient { + public static void main(String[] args) { + List[] intLists = DeceptiveLibrary.intLists(1); + List[] numLists = intLists; + numLists[0] = Arrays.asList(1.01); + int i = intLists[0].get(0); // 类抛出异常! + } +} ``` 例 `6-1` 给出了一个类似的例子,分为两类,以说明设计不佳的图书馆如何为无辜的客户创造问题。 名为 `DeceptiveLibrary` 的第一个类定义了一个静态方法,该 @@ -63,13 +61,13 @@ `List`。 演员阵容会产生一个未经检查的警告: ```java - %javac -Xlint:unchecked DeceptiveLibrary.java - DeceptiveLibrary.java:5: warning: [unchecked] unchecked cast - found : java.util.List[] - required: java.util.List[] - (List[]) new List[size]; // unchecked cast - ^ - 1 warning +%javac -Xlint:unchecked DeceptiveLibrary.java +DeceptiveLibrary.java:5: warning: [unchecked] unchecked cast +found : java.util.List[] +required: java.util.List[] + (List[]) new List[size]; // unchecked cast + ^ +1 warning ``` 由于该数组确实是一个整数列表数组,因此该数组似乎是合理的,并且您可能认为可以安全地忽略此警告。 正如我们将要看到的,你无视这个警告! @@ -78,9 +76,9 @@ 运行代码会用双精度列表覆盖整数列表。 尝试从整数列表中提取整数会导致通过擦除隐式插入的强制转换失败: ```java - %java InnocentClient - Exception in thread "main" java.lang.ClassCastException: java.lang.Double - at InnocentClient.main(InnocentClient.java:7) +%java InnocentClient +Exception in thread "main" java.lang.ClassCastException: java.lang.Double +at InnocentClient.main(InnocentClient.java:7) ``` 如前一节所述,此错误消息可能会令人困惑,因为该行看起来不包含演员表! @@ -90,15 +88,15 @@ > 从不公开暴露一个阵列,其中组件不具有可调整类型。 再次,这是一种情况,在程序的一部分中未经检查的转换可能导致在完全不同的部分发生类转换错误,其中转换不会出现在源代码中,而是通过擦除引入。由于此类错误 -可能会非常混乱,因此必须谨慎使用未经检查的演员表。 +可能会非常混乱,因此必须谨慎使用未经检查的列表。 广告真相原则与不雅暴露原则密切相关。第一个要求数组的运行时类型被适当地赋值,第二个要求数组的编译时类型必须是可赋值的。 即使在Java泛型的设计者中,也要花费一些时间来理解不雅暴露原则的重要性。例如,反射库中的以下两种方法违反了该原则: ```java - TypeVariable>[] java.lang.Class.getTypeParameters() - TypeVariable[] java.lang.Reflect.Method.getTypeParameters() +TypeVariable>[] java.lang.Class.getTypeParameters() +TypeVariable[] java.lang.Reflect.Method.getTypeParameters() ``` 遵循前面的模型,创建自己的Innocent Client版本并不难,它会在没有投射的地方抛出类抛出错误,在这种情况下,正式Java库会播放 `DeceptiveLibrary` 的角 diff --git a/ch06/07_How_to_Define_ArrayList.md b/ch06/07_How_to_Define_ArrayList.md index bd9749bd..fcd3966a 100644 --- a/ch06/07_How_to_Define_ArrayList.md +++ b/ch06/07_How_to_Define_ArrayList.md @@ -16,13 +16,13 @@ 有两个地方分配数组的新实例,一个在类的初始化器中,另一个在增加数组容量的方法中(这又从 `add` 方法中调用)。在这两个地方,数组都被分配为一个 `Object []`,并且未勾选的类型转换为 `E []`。 -包含数组的字段是私人的是非常重要的;否则将违反广告真理原则和不雅暴露原则。这违反了广告中的真理原则,因为 `E` 可能被绑定到 `Object` 以外的类型(如 +包含数组的字段是私有的是非常重要的;否则将违反广告真理原则和不雅暴露原则。这违反了广告中的真理原则,因为 `E` 可能被绑定到 `Object` 以外的类型(如 `String`)。这会违反不雅暴露原则,因为 `E` 可能会绑定到不是可保留类型的类型(例如 `List`)。但是,这些原则都没有违反,因为该数组并非公开的: 它存储在私人领域,没有指向数组的指针从类中逃脱。我们可以称之为封闭门背后的任何原则。 我们在这里定义 `ArrayList` 的方式接近 `Sun` 发布的源代码中的实际定义。最近,该库的共同作者 `Neal Gafter` 认为他使用了糟糕的风格 - 如果声明私有数组的 类型为 `Object []`,并且在从数组中检索元素时使用强制类型(`E`)会更好。对于这一点,有些话要说,尽管对于我们在这里使用的风格也有一些要说的,这可以最大限 -度地减少对未经检查的演员的需求。 +度地减少对未经检查的实例的需求。 `toArray` 的方法确实会公开返回一个数组,但它使用了第 `6.5` 节中所述的技巧,依照广告中的真理原则。和之前一样,有一个参数数组,如果它不足以容纳集合,则使 用反射来分配具有相同指定类型的新数组。这个实现类似于我们前面看到的实现,除了可以使用更高效的 `arraycopy` 例程将私有数组复制到公用数组中以返回。 diff --git a/ch06/08_Array_Creation_and_Varargs.md b/ch06/08_Array_Creation_and_Varargs.md index ec9a1bf4..2ebc85ea 100644 --- a/ch06/08_Array_Creation_and_Varargs.md +++ b/ch06/08_Array_Creation_and_Varargs.md @@ -9,83 +9,105 @@ 例 `6-2`。 如何定义 `ArrayList` ```java - import java.util.*; - class ArrayList extends AbstractList implements RandomAccess { - private E[] arr; - private int size = 0; - public ArrayList(int cap) { - if (cap < 0) - throw new IllegalArgumentException("Illegal Capacity: "+cap); - arr = (E[])new Object[cap]; // unchecked cast - } - public ArrayList() { this(10); } - public ArrayList(Collection c) { this(c.size()); addAll(c); } - public void ensureCapacity(int mincap) { - int oldcap = arr.length; - if (mincap > oldcap) { - int newcap = Math.max(mincap, (oldcap*3)/2+1); - E[] oldarr = arr; - arr = (E[])new Object[newcap]; // unchecked cast - System.arraycopy(oldarr,0,arr,0,size); - } - } - public int size() { return size; } - private void checkBounds(int i, int size) { - if (i < 0 || i >= size) - throw new IndexOutOfBoundsException("Index: "+i+", Size: "+size); - } - public E get(int i) { checkBounds(i,size); return arr[i]; } - public E set(int i, E elt) { - checkBounds(i,size); E old = arr[i]; arr[i] = elt; return old; - } - public void add(int i, E elt) { - checkBounds(i,size+1); ensureCapacity(size+1); - System.arraycopy(arr,i,arr,i+1,size-i); arr[i] = elt; size++; - } - public E remove(int i) { - checkBounds(i,size); E old = arr[i]; arr[i] = null; size--; - System.arraycopy(arr,i+1,arr,i,size-i); return old; - } - public T[] toArray(T[] a) { - if (a.length < size) - a = (T[])java.lang.reflect.Array. // unchecked cast - newInstance(a.getClass().getComponentType(), size); - System.arraycopy(arr,0,a,0,size); - if (size < a.length) a[size] = null; - return a; - } - public Object[] toArray() { return toArray(new Object[0]); } +import java.util.*; +class ArrayList extends AbstractList implements RandomAccess { + private E[] arr; + private int size = 0; + public ArrayList(int cap) { + if (cap < 0) + throw new IllegalArgumentException("Illegal Capacity: "+cap); + arr = (E[])new Object[cap]; // unchecked cast + } + public ArrayList() { + this(10); + } + public ArrayList(Collection c) { + this(c.size()); + addAll(c); + } + public void ensureCapacity(int mincap) { + int oldcap = arr.length; + if (mincap > oldcap) { + int newcap = Math.max(mincap, (oldcap*3)/2+1); + E[] oldarr = arr; + arr = (E[])new Object[newcap]; // unchecked cast + System.arraycopy(oldarr,0,arr,0,size); } + } + public int size() { + return size; + } + private void checkBounds(int i, int size) { + if (i < 0 || i >= size) + throw new IndexOutOfBoundsException("Index: "+i+", Size: "+size); + } + public E get(int i) { + checkBounds(i,size); + return arr[i]; + } + public E set(int i, E elt) { + checkBounds(i,size); + E old = arr[i]; + arr[i] = elt; + return old; + } + public void add(int i, E elt) { + checkBounds(i,size+1); + ensureCapacity(size+1); + System.arraycopy(arr,i,arr,i+1,size-i); + arr[i] = elt; + size++; + } + public E remove(int i) { + checkBounds(i,size); + E old = arr[i]; + arr[i] = null; + size--; + System.arraycopy(arr,i+1,arr,i,size-i); return old; + } + public T[] toArray(T[] a) { + if (a.length < size) + a = (T[])java.lang.reflect.Array. // unchecked cast + newInstance(a.getClass().getComponentType(), size); + System.arraycopy(arr,0,a,0,size); + if (size < a.length) + a[size] = null; + return a; + } + public Object[] toArray() { + return toArray(new Object[0]); + } +} ``` 在 `1.4` 节我们讨论了声明为的方法 `java.util.Arrays.asList` 如下: ```java - public static List asList(E... arr) +public static List asList(E... arr) ``` 例如,这里有三个对这个方法的调用: ```java - List a = Arrays.asList(1, 2, 3); - List b = Arrays.asList(4, 5, 6); - List> x = Arrays.asList(a, b); // 通用数组创建 +List a = Arrays.asList(1, 2, 3); +List b = Arrays.asList(4, 5, 6); +List> x = Arrays.asList(a, b); // 通用数组创建 ``` 回想一下,可变长度的参数列表是通过将参数打包到数组中并传递它来实现的。 因此这三个呼叫相当于以下内容: ```java - List a = Arrays.asList(new Integer[] { 1, 2, 3 }); - List b = Arrays.asList(new Integer[] { 4, 5, 6 }); - List> x = Arrays.asList(new List[] { a, b }); // 通用数组创建 +List a = Arrays.asList(new Integer[] { 1, 2, 3 }); +List b = Arrays.asList(new Integer[] { 4, 5, 6 }); +List> x = Arrays.asList(new List[] { a, b }); // 通用数组创建 ``` 前两个调用很好,但由于 `List` 不是可重用的类型,所以第三次在编译时警告未经检查的泛型数组的创建。 ```java - VarargError.java:6: warning: [unchecked] unchecked generic array creation - of type java.util.List[] for varargs parameter - List> x = Arrays.asList(a, b); +VarargError.java:6: warning: [unchecked] unchecked generic array creation +of type java.util.List[] for varargs parameter +List> x = Arrays.asList(a, b); ``` 此警告可能会造成混淆,特别是因为该源代码行不包含数组创建的显式实例! @@ -93,9 +115,9 @@ 如果您尝试创建泛型类型的列表,则会出现类似的问题。 这是一个使用 `Arrays.asList` 创建包含给定元素的长度列表的方法: ```java - public static List singleton(E elt) { - return Arrays.asList(elt); // 通用数组创建 - } +public static List singleton(E elt) { + return Arrays.asList(elt); // 通用数组创建 +} ``` 这也会产生警告,出于同样的原因可能会造成混淆。 diff --git a/ch06/09_Arrays_as_a_Deprecated_Type.md b/ch06/09_Arrays_as_a_Deprecated_Type.md index 8af7bf1e..61f82934 100644 --- a/ch06/09_Arrays_as_a_Deprecated_Type.md +++ b/ch06/09_Arrays_as_a_Deprecated_Type.md @@ -5,49 +5,49 @@ 我们已经看到,在许多方面集合优于数组: - - 集合比数组提供更精确的输入。 通过列表,可以编写 `List`,`List ` 或 `List `; 而对于数组,只能写 `T []`,这对应于 -列表的三个选项中的第二个。 更精确的打字可以在编译时检测到更多的错误,而不是运行时。 这使编码,调试,测试和维护更容易,并且还提高了效率。 (见 `2.5` +- 集合比数组提供更精确的输入。 通过列表,可以编写 `List`,`List ` 或 `List `; 而对于数组,只能写 `T []`,这对应于列 +表的三个选项中的第二个。 更精确的打字可以在编译时检测到更多的错误,而不是运行时。 这使编码,调试,测试和维护更容易,并且还提高了效率。 (见 `2.5` 节) - - 集合比数组更灵活。 集合提供了各种表示形式,包括数组,链表,树和散列表,而数组具有固定的表示形式,这些库为集合提供了比数组更多的方法和便利算法。 +- 集合比数组更灵活。 集合提供了各种表示形式,包括数组,链表,树和散列表,而数组具有固定的表示形式,这些库为集合提供了比数组更多的方法和便利算法。 (见 `2.5` 节) - - 集合可能具有任何类型的元素,而数组只能具有可定义类型的组件。 在创建数组时,必须遵守广告中的真理原则 - 具体化类型必须符合静态类型 - 以及不雅暴露 -原则 - 从不公开暴露组件不具有可确定类型的数组。 (见第 `6.5` 节和第 `6.6` 节)。 +- 集合可能具有任何类型的元素,而数组只能具有可定义类型的组件。 在创建数组时,必须遵守广告中的真理原则 - 具体化类型必须符合静态类型 - 以及不雅暴露原则 +- 从不公开暴露组件不具有可确定类型的数组。 (见第 `6.5` 节和第 `6.6` 节)。 回想起来,在Java 5中有几个地方避免使用数组可能会改进设计: - - 变长参数(可变参数)由一个数组表示,因此受到相同的限制。 如果可变参数绑定到不可确定类型的实参,则会发出一个通用数组创建警告(这引发了与未检查警 -告相同的担忧)。 例如,函数 `Arrays.asList` 需要一个可变参数。 使用此函数返回 `List` 类型的结果没有任何困难,但创建类型为 +- 变长参数(可变参数)由一个数组表示,因此受到相同的限制。 如果可变参数绑定到不可确定类型的实参,则会发出一个通用数组创建警告(这引发了与未检查警告相 +同的担忧)。 例如,函数 `Arrays.asList` 需要一个可变参数。 使用此函数返回 `List` 类型的结果没有任何困难,但创建类型为 `List>` 或类型为 `List ` 的结果存在问题。 如果列表优先于数组,则不会出现此问题。 (见 `6.8` 节) - - Java库中的某些方法的签名违反了不雅曝光原则: +- Java库中的某些方法的签名违反了不雅曝光原则: - ```java - TypeVariable>[] java.lang.Class.getTypeParameters() - TypeVariable[] java.lang.Reflect.Method.getTypeParameters() - ``` +```java +TypeVariable>[] java.lang.Class.getTypeParameters() +TypeVariable[] java.lang.Reflect.Method.getTypeParameters() +``` 调用这些方法的代码有可能违反伴随泛型的铸铁保证:即使编译器没有发出未经检查的警告,它也可能在代码中没有显式强制转换的情况下引发类转换异常。 (编译库时 发出了警告 - 错误地忽略了)。同样,如果列表优先于数组,则不会出现此问题。 (见 `6.6` 节) `Java 5` 设计中一些复杂性的一个原因是为使用数组提供了很好的支持。 回想起来,选择更简单的设计可能会更好,但是使得数组的使用并不方便: - - 数组必须使用可定义类型的组件创建,因此为尽量减少这种限制,尝试尽可能地使可保存类型的概念成为一般。 如果设计人员愿意限制实现类型的概念,他们可以 -通过包含原始类型(如 `List`)来简化它,但不包括带有无限通配符的类型(如 `List`)。 如果他们这样做了,可重用类型就会成为未参数化类型的代名词(即原 -始类型,原始类型和没有类型参数的类型)。这种变化将简化实例测试中允许的类型。 考虑以下三个测试: +- 数组必须使用可定义类型的组件创建,因此为尽量减少这种限制,尝试尽可能地使可保存类型的概念成为一般。 如果设计人员愿意限制实现类型的概念,他们可以通过 +包含原始类型(如 `List`)来简化它,但不包括带有无限通配符的类型(如 `List`)。 如果他们这样做了,可重用类型就会成为未参数化类型的代名词(即原始类 +型,原始类型和没有类型参数的类型)。这种变化将简化实例测试中允许的类型。 考虑以下三个测试: - ```java - obj instanceof List - obj instanceof List - obj instanceof List - ``` +```java + obj instanceof List + obj instanceof List + obj instanceof List +``` 目前,前两个是允许的,但第三个不是。 通过限制建议,只允许第一个限制。 可以说,这可能更容易理解。 这也符合对类标记的处理,因为目前允许 `List.class`, 但 `List .class` 是非法的。 - - 目前,数组创建仅限于可调整类型的数组。 但是允许声明一个不可确认类型的数组,或者将其转换为不可确定的数组类型,代价是在代码中的某处未经检查的警 -告。 正如我们所看到的,这样的警告违反了与泛型相伴随的铸铁保证,并且即使源代码不包含转换,也可能导致类转换错误。 +- 目前,数组创建仅限于可调整类型的数组。 但是允许声明一个不可确认类型的数组,或者将其转换为不可确定的数组类型,代价是在代码中的某处未经检查的警告。 +正如我们所看到的,这样的警告违反了与泛型相伴随的铸铁保证,并且即使源代码不包含转换,也可能导致类转换错误。 一个更简单和更安全的设计将取缔任何不可接受类型的数组(使用刚才描述的更简单形式的可重写类型)。 这种设计意味着我们永远不能声明一个 `E []` 类型的数组, 其中 `E` 是一个类型变量。 @@ -58,19 +58,19 @@ 此更改也意味着您无法为集合(或类似方法)的 `toArray` 方法分配泛型类型。 代替: ```java - public T[] toArray(T[] arr) + public T[] toArray(T[] arr) ``` 我们会有: ```java - public Object[] toArray(Object[] arr) + public Object[] toArray(Object[] arr) ``` 并且这种方法的许多用途都需要显式地输出结果。 这确实会让用户的生活变得更加尴尬,但可以说,简单性和安全性的提高是值得的。 - - 前面的更改意味着通常会使用优先于数组的列表。 通过允许 `Java` 程序员将 `l [i]` 编写为 `l.get(i)` 的缩写,并将 `l [i] = v` 作为 `l.put(i,v)` -的缩写,可以使列表的使用变得更容易。 (有些人喜欢这种“语法糖”,而有些人则认为它是“句法老鼠毒”。) +- 前面的更改意味着通常会使用优先于数组的列表。 通过允许 `Java` 程序员将 `l [i]` 编写为 `l.get(i)` 的缩写,并将 `l [i] = v` 作为 `l.put(i,v)` 的 +缩写,可以使列表的使用变得更容易。 (有些人喜欢这种“语法糖”,而有些人则认为它是“句法老鼠毒”。) 其中一些变化仍然可以以后向兼容的方式进行调整。我们在 `6.8` 节中提到,可能需要添加基于列表而不是数组的第二种可变参数形式。 允许使用缩写来使列表索引看 起来像数组索引可以很容易地被合并到未来的 `Java` 版本中。 @@ -83,4 +83,3 @@ 《《《 [下一节](10_Summing_Up.md)
《《《 [返回首页](../README.md) - diff --git a/ch06/10_Summing_Up.md b/ch06/10_Summing_Up.md index f5e27982..040b9706 100644 --- a/ch06/10_Summing_Up.md +++ b/ch06/10_Summing_Up.md @@ -1,20 +1,20 @@ 《《《 [返回首页](../README.md)
《《《 [上一节](09_Arrays_as_a_Deprecated_Type.md) -### 加起来 +### 总结 我们通过给出需要或推荐可评估类型的地方的清单来得出结论。 - - 实例测试必须针对可确定类型。 - - 演员阵容通常应该是可调整的类型。 (投射到不可辨认的类型通常会发出未经检查的警告。) - - 扩展 `Throwable` 的类不能参数化。 - - 数组实例创建必须处于可修饰类型。 - - 数组的指定类型必须是其静态类型的删除子类型(请参阅广告中的真理原则),并且公开暴露的数组应该是可确定类型的(请参阅不雅曝光原则)。 - - 可变参数应该是可确定的类型。 (可变类型的变量将发出未经检查的警告。)这些限制来自泛型通过擦除来实现的事实,它们应该被视为我们在前一章探讨的易于进化 -的价格。 +- 实例测试必须针对可确定类型。 +- 实例类型通常应该是可调整的类型。 (投射到不可辨认的类型通常会发出未经检查的警告。) +- 扩展 `Throwable` 的类不能参数化。 +- 数组实例创建必须处于可修饰类型。 +- 数组的指定类型必须是其静态类型的删除子类型(请参阅广告中的真理原则),并且公开暴露的数组应该是可确定类型的(请参阅不雅曝光原则)。 +- 可变参数应该是可确定的类型。 (可变类型的变量将发出未经检查的警告。)这些限制来自泛型通过擦除来实现的事实,它们应该被视为我们在前一章探讨的易于进化的 +价格。 为了完整性,我们还列出了与反射相关的限制: - - 类令牌对应于可重用类型,`Class` 中的类型参数应该是可重用类型。 (见 `7.2` 节) +- 类令牌对应于可重用类型,`Class` 中的类型参数应该是可重用类型。 (见 `7.2` 节) 这些在下一章讨论。 diff --git a/ch07/00_Reflection.md b/ch07/00_Reflection.md index 4abbabbd..9470a219 100644 --- a/ch07/00_Reflection.md +++ b/ch07/00_Reflection.md @@ -1,7 +1,7 @@ 《《《 [返回首页](../README.md)
《《《 [上一节](../ch06/10_Summing_Up.md) -## 反射 +### 反射 反射是一组功能的术语,它允许程序检查自己的定义。 `Java` 中的反射在类浏览器,对象检查器,调试器,解释器,服务(如 `JavaBeans™` 和对象序列化)以及任何创 建,检查或操作任意 `Java` 对象的工具中发挥作用。 @@ -16,10 +16,7 @@ 通过对泛型的反思,我们的意思是反射现在返回有关泛型的信息。有一些新的接口可以表示泛型类型,包括类型变量,参数化类型和通配符类型,还有一些新的方法可以获得 泛型类型的字段,构造函数和方法。 -我们依次解释每一个点。我们不承担任何以前的反思知识,但我们专注于与泛型相关的方面。 - - 我们依次解释每一个点。我们不承担任何以前的反思知识,但我们专注于与泛型相关的方面。 《《《 [下一节](01_Generics_for_Reflection.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch07/01_Generics_for_Reflection.md b/ch07/01_Generics_for_Reflection.md index 97ec22bc..a94b6c05 100644 --- a/ch07/01_Generics_for_Reflection.md +++ b/ch07/01_Generics_for_Reflection.md @@ -3,35 +3,38 @@ ### 反射的泛型 -`Java` 支持自 `1.0` 版以来的反射以及 `1.1` 版以后的类文字。 它们的核心是 `Class` 类,它表示运行时对象类型的信息。 您可以编写一个类型,后跟 `.class` 作为文字,表示与该类型相对应的类标记,并且方法 `getClass` 在每个对象上定义并返回一个类标记,该标记表示该对象在运行时携带的特定类型信息。这里是一个例子: +`Java` 支持自 `1.0` 版以来的反射以及 `1.1` 版以后的类文字。 它们的核心是 `Class` 类,它表示运行时对象类型的信息。 您可以编写一个类型,后跟`.class` +作为文字,表示与该类型相对应的类标记,并且方法 `getClass` 在每个对象上定义并返回一个类标记,该标记表示该对象在运行时携带的特定类型信息。这里是一个例 +子: ```java - Class ki = Integer.class; - Number n = new Integer(42); - Class kn = n.getClass(); - assert ki == kn; +Class ki = Integer.class; +Number n = new Integer(42); +Class kn = n.getClass(); +assert ki == kn; ``` -对于给定的类加载器,相同的类型总是由相同的类标记表示。为了强调这一点,在这里我们使用标识符(`==` 运算符)比较类标记。但是,在大多数情况下,使用相等 等于方法)。 +对于给定的类加载器,相同的类型总是由相同的类标记表示。为了强调这一点,在这里我们使用标识符(`==` 运算符)比较类标记。但是,在大多数情况下,使用相等 +等于方法)。 `Java 5` 中的一个变化是 `Class` 类现在接受一个类型参数,所以 `Class ` 是类型 `T` 的类标记的类型。 前面的代码现在写成如下所示: ```java - Class ki = Integer.class; - Number n = new Integer(42); - Class kn = n.getClass(); - assert ki == kn; +Class ki = Integer.class; +Number n = new Integer(42); +Class kn = n.getClass(); +assert ki == kn; ``` -类标记和 `getClass` 方法由编译器专门处理。 通常,如果T是一个没有类型参数的类型,那么 `T.class` 的类型为 `Class`,并且如果 `e` 是 `T` 类型的表 -达式,那么 `e.getClass()` 的类型为 `Class`。 (我们将看到 `T` 在下一节中有类型参数时会发生什么。)通配符是必需的,因为变量引用的对象 -的类型可能是变量类型的子类型,在这种情况下, 其中 `Number` 类型的变量包含 `Integer` 类型的对象。 +类标记和 `getClass` 方法由编译器专门处理。 通常,如果 `T` 是一个没有类型参数的类型,那么 `T.class` 的类型为 `Class`,并且如果 `e` 是 `T` 类型 +的表达式,那么 `e.getClass()` 的类型为 `Class`。 (我们将看到 `T` 在下一节中有类型参数时会发生什么。)通配符是必需的,因为变量引用的 +对象的类型可能是变量类型的子类型,在这种情况下, 其中 `Number` 类型的变量包含 `Integer` 类型的对象。 对于反射的许多用途,您不会知道类标记的确切类型(如果您确实需要,您可能不需要使用反射),并且在这些情况下,您可以使用 `Class` 编写类型,使用 一个 无界的通配符。 但是,在某些情况下,类型参数提供的类型信息是无价的,就像我们在 `6.5` 节中讨论的 `toArray` 的变体一样: ```java - public static T[] toArray(Collection c, Class k) +public static T[] toArray(Collection c, Class k) ``` 在这里,类型参数让编译器检查由类标记表示的类型与集合和数组的类型是否匹配。 @@ -39,45 +42,48 @@ **用于反射的泛型的其他示例** `Class` 类仅包含几个以有趣的方式使用 `type` 参数的方法: ```java - class Class { - public T newInstance(); - public T cast(Object o); - public Class getSuperclass(); - public Class asSubclass(Class k); - public A getAnnotation(Class k); - public boolean isAnnotationPresent(Class k); - ... - } +class Class { + public T newInstance(); + public T cast(Object o); + public Class getSuperclass(); + public Class asSubclass(Class k); + public A getAnnotation(Class k); + public boolean isAnnotationPresent(Class k); +... +} ``` 第一个返回该类的新实例,当然这个实例的类型为 `T`。 第二个将任意对象转换为接收者类,因此它会抛出类抛出异常或返回类型 `T` 的结果。 第三个返回超类,它 必须具有指定的类型。 第四个检查接收者类是参数类的一个子类,并且引发一个类转换异常或者返回接收者适当改变的类型。 -第五和第六种方法是新注释工具的一部分。 这些方法很有趣,因为它们显示了如何使用类的类型参数来取得良好效果。 例如,保留是注释的一个子类,因此您可以按如 +第五和第六种方法是新注解工具的一部分。 这些方法很有趣,因为它们显示了如何使用类的类型参数来取得良好效果。 例如,保留是注解的一个子类,因此您可以按如 下方式在类 `k` 上提取保留注释: ```java - Retention r = k.getAnnotation(Retention.class); +Retention r = k.getAnnotation(Retention.class); ``` - -这种通用类型具有两个优点。 首先,这意味着调用结果不需要强制转换,因为泛型类型系统可以精确地指定正确的类型。其次,这意味着如果您不小心使用类标记调用不是 `Annotation` 子类的类的方法,那么会在编译时而不是在运行时检测到。 +这种通用类型具有两个优点。 首先,这意味着调用结果不需要强制转换,因为泛型类型系统可以精确地指定正确的类型。其次,这意味着如果您不小心使用类标记调用不 +是 `Annotation` 子类的类的方法,那么会在编译时而不是在运行时检测到。 类标记的另一种用法类似于注释类,它出现在 `java.awt` 包的 `Component` 类的 `getListeners` 方法中: ```java - public T[] getListeners(Class listenerType); +public T[] getListeners(Class listenerType); ``` 同样,这意味着 `getListeners` 的代码不需要强制转换,这意味着编译器可以检查该方法是否使用适当类型的类标记调用。 -作为类标记的一个有趣用法的最后一个示例,便捷类 `Collections` 包含一个构建包装的方法,该包装检查添加到给定列表或从给定列表中提取的每个元素是否属于给定类。(其他集合类也有类似的方法,例如集合和地图。)它具有以下签名: +作为类标记的一个有趣用法的最后一个示例,便捷类 `Collections` 包含一个构建包装的方法,该包装检查添加到给定列表或从给定列表中提取的每个元素是否属于给 +定类。(其他集合类也有类似的方法,例如集合和地图。)它具有以下签名: ```java - public static List checkedList(List l, Class k) +public static List checkedList(List l, Class k) ``` -包装在编译时通过动态检查来补充静态检查,这对于提高安全性或与遗留代码的接口(见第 `8.1` 节)可能很有用。该实现调用前面描述的类 `Class` 中的方法,其中接收方是传递到方法中的类标记,并且该转换将应用于使用 `get`,`set` 或 `add` 添加到列表中的任何元素,或者将其写入列表中。然而,`Class` 的类型参数意味着 `checkedList` 的代码不需要额外的转换(除了调用类类中的 `cast` 方法外),并且编译器可以检查该方法是否使用类标记调用 一个合适的类型。 +包装在编译时通过动态检查来补充静态检查,这对于提高安全性或与遗留代码的接口(见第 `8.1` 节)可能很有用。该实现调用前面描述的类 `Class` 中的方法,其中 +接收方是传递到方法中的类标记,并且该转换将应用于使用 `get`,`set` 或 `add` 添加到列表中的任何元素,或者将其写入列表中。然而,`Class` 的类型参数 +意味着 `checkedList` 的代码不需要额外的转换(除了调用类类中的 `cast` 方法外),并且编译器可以检查该方法是否使用类标记调用 一个合适的类型。 《《《 [下一节](02_Reflected_Types_are_Reifiable_Types.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch07/02_Reflected_Types_are_Reifiable_Types.md b/ch07/02_Reflected_Types_are_Reifiable_Types.md index 61cca305..2b19bf37 100644 --- a/ch07/02_Reflected_Types_are_Reifiable_Types.md +++ b/ch07/02_Reflected_Types_are_Reifiable_Types.md @@ -3,13 +3,13 @@ ### 反射类型是可维持类型 -反思使程序可以使用物化类型信息。 因此,必要的是,每个类别标记对应于可确定类型。 如果您尝试反映参数化类型,则会获得相应原始类型的指定信息: +反射使程序可以使用物化类型信息。 因此,必要的是,每个类别标记对应于可确定类型。 如果您尝试反映参数化类型,则会获得相应原始类型的指定信息: ```java - List ints = new ArrayList(); - List strs = new ArrayList(); - assert ints.get Class() == strs.getClass(); - assert ints.getClass() == ArrayList.class; +List ints = new ArrayList(); +List strs = new ArrayList(); +assert ints.get Class() == strs.getClass(); +assert ints.getClass() == ArrayList.class; ``` 这里整型的类型列表和字符串的类型列表都由同一个类标记表示,其中的类文字被写入 `ArrayList.class`。 @@ -21,9 +21,9 @@ 中 `| T |` 是类型 `T` 的删除。 这是一个例子: ```java - List ints = new ArrayList(); - Class k = ints.getClass(); - assert k == ArrayList.class; + List ints = new ArrayList(); + Class k = ints.getClass(); + assert k == ArrayList.class; ``` 这里表达式 `int` 具有 `List` 类型,所以表达式 `int.getClass()` 的类型为 `Class `; 这是因为擦除 `List` 会生 @@ -32,27 +32,27 @@ 类文字也受到限制; 在类文字中为类型提供类型参数甚至在语法上都是无效的。 因此,以下片段是非法的: ```java - class ClassLiteral { - public Class k = List.class; // syntax error - } +class ClassLiteral { + public Class k = List.class; // syntax error +} ``` 事实上,`Java` 的语法使得前面一个短语难以解析,并且可能触发语法错误级联: ```java - % javac ClassLiteral.java - ClassLiteral.java:2: illegal start of expression - public Class k = List.class; // syntax error - ^ - ClassLiteral.java:2: ';' expected - public Class k = List.class; // syntax error - ^ - ClassLiteral.java:2: expected - public Class k = List.class; // syntax error - ^ - ClassLiteral.java:4: '}' expected - ^ - 4 errors +% javac ClassLiteral.java +ClassLiteral.java:2: illegal start of expression +public Class k = List.class; // syntax error + ^ +ClassLiteral.java:2: ';' expected +public Class k = List.class; // syntax error + ^ +ClassLiteral.java:2: expected +public Class k = List.class; // syntax error + ^ +ClassLiteral.java:4: '}' expected +^ +4 errors ``` 解析器在这个短语中遇到了很多麻烦,当它到达文件末尾时仍然感到困惑! @@ -63,4 +63,4 @@ 对类别标记的限制导致一个有用的属性。 无论何处出现类型为 `Class` 的类型,类型 `T` 都应该是可重用的类型。 对于 `T[]` 形式的类型也是如此. 《《《 [下一节](03_Reflection_for_Primitive_Types.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch07/03_Reflection_for_Primitive_Types.md b/ch07/03_Reflection_for_Primitive_Types.md index 3cfa0c00..d5fd17c8 100644 --- a/ch07/03_Reflection_for_Primitive_Types.md +++ b/ch07/03_Reflection_for_Primitive_Types.md @@ -10,7 +10,7 @@ `int.class.newInstance()` 返回 `Integer` 类型的值,但实际上这些调用会引发异常。 同样,你可能会期待这个调用: ```java - java.lang.reflect.Array.newInstance(int.class,size) +java.lang.reflect.Array.newInstance(int.class,size) ``` 返回 `Integer[]` 类型的值,但实际上该调用返回的是一个 `int[]` 类型的值。 这些例子表明,给类标记 `int.class` 的 `Class` 类型可能更有意义. @@ -19,4 +19,4 @@ 《《《 [下一节](04_A_Generic_Reflection_Library.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch07/04_A_Generic_Reflection_Library.md b/ch07/04_A_Generic_Reflection_Library.md index a602e302..b61aacaa 100644 --- a/ch07/04_A_Generic_Reflection_Library.md +++ b/ch07/04_A_Generic_Reflection_Library.md @@ -3,58 +3,73 @@ ### 一个通用的反射库 -正如我们所看到的,粗心使用未经检查的演员阵容可能会导致问题,例如违反广告中的真相原则或不雅暴露原则(参见第 `6.5` 节和第 `6.6` 节)。 最小化使用未经检查的强制转换的一种技术是将它们封装在库中。 该库可以仔细检查以确保其使用未经检查的强制转换是安全的,而调用该库的代码可以没有未经检查的强制转换。 `Sun` 正在考虑添加类似于这里描述的库方法。 +正如我们所看到的,粗心使用未经检查的演员阵容可能会导致问题,例如违反广告中的真相原则或不雅暴露原则(参见第 `6.5` 节和第 `6.6` 节)。 最小化使用未经 +检查的强制转换的一种技术是将它们封装在库中。 该库可以仔细检查以确保其使用未经检查的强制转换是安全的,而调用该库的代码可以没有未经检查的强制转换 `Sun` +正在考虑添加类似于这里描述的库方法。 例 `7-1` 提供了一个以类型安全的方式使用反射的通用函数库。 它定义了一个包含以下方法的便捷类 `GenericReflection`: ```java - public static T newInstance(T object) - public static Class getComponentType(T[] a) - public static T[] new Array(Class k, int size) - public static T[] newArray(T[] a, int size) +public static T newInstance(T object) +public static Class getComponentType(T[] a) +public static T[] new Array(Class k, int size) +public static T[] newArray(T[] a, int size) ``` -第一个接受一个对象,找到该对象的类,并返回该类的新实例;这必须与原始对象具有相同的类型。第二个接收数组并返回一个类标记作为其组件类型,如其运行时类型信息所携带的。相反,第三个分配一个新的数组,其组件类型由给定的类标记和指定的大小指定。第四个接受一个数组和一个大小,并且分配一个与给定数组和给定大小具有相同组件类型的新数组;它只是构成对前两种方法的调用。前三种方法中的每一种的代码都包含对 `Java` 反射库中一个或两个相应方法的调用,以及对相应返回类型的未检查转换。 +第一个接受一个对象,找到该对象的类,并返回该类的新实例;这必须与原始对象具有相同的类型。第二个接收数组并返回一个类标记作为其组件类型,如其运行时类型信 +息所携带的。相反,第三个分配一个新的数组,其组件类型由给定的类标记和指定的大小指定。第四个接受一个数组和一个大小,并且分配一个与给定数组和给定大小具 +有相同组件类型的新数组;它只是构成对前两种方法的调用。前三种方法中的每一种的代码都包含对 `Java` 反射库中一个或两个相应方法的调用,以及对相应返回类型的 +未检查转换。 -由于各种原因,`Java` 反射库中的方法无法返回足够精确的类型,因此需要未经检查的强制转换。方法 `getComponentType` 位于 `Class` 类中,并且 `Java` 无法将方法的签名中的接收方类型限制为 `Class `(尽管如果接收方不是类,该调用会引发异常令牌为数组类型)。 `java.lang.reflect.Array` 中的 `newInstance` 方法必须具有返回类型 `Object` 而不是返回类型 `T[]`,因为它可能会返回基本类型的数组。方法 `getClass` 在类型 `T` 的接收器上调用时返回不是类型为 `Class ` 但类型 `Class `,因为需要擦除以确保类令牌始终具有可调整类型。但是,在每种情况下,未经检查的转换都是安全的,用户可以调用这里定义的四个库例程而不违反铸铁保证。 +由于各种原因,`Java` 反射库中的方法无法返回足够精确的类型,因此需要未经检查的强制转换。方法 `getComponentType` 位于 `Class` 类中,并且 `Java` +无法将方法的签名中的接收方类型限制为 `Class `(尽管如果接收方不是类,该调用会引发异常令牌为数组类型)。 `java.lang.reflect.Array` 中的 +`newInstance` 方法必须具有返回类型 `Object` 而不是返回类型 `T[]`,因为它可能会返回基本类型的数组。方法 `getClass` 在类型 `T` 的接收器上调用时返回 +不是类型为 `Class ` 但类型 `Class `,因为需要擦除以确保类令牌始终具有可调整类型。但是,在每种情况下,未经检查的转换都是安全的,用 +户可以调用这里定义的四个库例程而不违反铸铁保证。 -例7-1。 用于泛型反射的类型安全库 +例 7-1。 用于泛型反射的类型安全库 ```java - class GenericReflection { - public static T newInstance(T obj) throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { - Object newobj = obj.getClass().getConstructor().newInstance(); - return (T)newobj; // unchecked cast - } - public static Class getComponentType(T[] a) { - Class k = a.getClass().getComponentType(); - return (Class)k; // unchecked cast - } - public static T[] newArray(Class k, int size) { - if (k.isPrimitive()) - throw new IllegalArgumentException ("Argument cannot be primitive: "+k); - Object a = java.lang.reflect.Array.newInstance(k, size); - return (T[])a; // unchecked cast - } - public static T[] newArray(T[] a, int size) { - return newArray(getComponentType(a), size); - } - } +class GenericReflection { + public static T newInstance(T obj) throws InstantiationException, IllegalAccessException, InvocationTargetException, + NoSuchMethodException { + Object newobj = obj.getClass().getConstructor().newInstance(); + return (T)newobj; // unchecked cast + } + public static Class getComponentType(T[] a) { + Class k = a.getClass().getComponentType(); + return (Class)k; // unchecked cast + } + public static T[] newArray(Class k, int size) { + if (k.isPrimitive()) + throw new IllegalArgumentException ("Argument cannot be primitive: "+k); + Object a = java.lang.reflect.Array.newInstance(k, size); + return (T[])a; // unchecked cast + } + public static T[] newArray(T[] a, int size) { + return newArray(getComponentType(a), size); + } +} ``` -第一种方法优先于 `Class.newInstance` 使用 `Constructor.newInstance`(在 `java.lang.reflect` 中),以避免后者出现已知问题。引用 `Sun` 的 `Class.newInstance` 文档:“请注意,此方法传播由 `nullary` 构造函数抛出的任何异常,包括检查的异常。使用此方法可以有效绕过编译时异常检查,否则编译器会执行该异常。 `Constructor.newInstance` 方法通过将构造函数抛出的任何异常包装在(`checked`)`InvocationTargetException` 中来避免此问题。“ +第一种方法优先于 `Class.newInstance` 使用 `Constructor.newInstance`(在 `java.lang.reflect` 中),以避免后者出现已知问题。引用 `Sun` 的 +`Class.newInstance` 文档:“请注意,此方法传播由 `nullary` 构造函数抛出的任何异常,包括检查的异常。使用此方法可以有效绕过编译时异常检查,否则编译器 +会执行该异常。 `Constructor.newInstance` 方法通过将构造函数抛出的任何异常包装在(`checked`)`InvocationTargetException` 中来避免此问题。“ -第二种方法保证在任何遵循不雅曝光原则和广告真理原则的节目中都能很好地输入。第一个原则保证编译时的组件类型将是可重用的类型,然后第二个原则保证在运行时返回的通用组件类型必须是在编译时声明的可重用组件类型的子类型。 +第二种方法保证在任何遵循不雅曝光原则和广告真理原则的节目中都能很好地输入。第一个原则保证编译时的组件类型将是可重用的类型,然后第二个原则保证在运行时 +返回的通用组件类型必须是在编译时声明的可重用组件类型的子类型。 -如果第三种方法的类参数是基本类型,则会引发非法参数异常。这会捕获以下棘手的情况:如果第一个参数是 `int.class`,那么它的类型是 `Class`,但新数组的类型为 `int[]`,它不是 `Integer[]` 的子类型。如果 `int.class` 具有 `Class` 类型而不是 `Class` 类型,则不会出现此问题,如前一节所述。 +如果第三种方法的类参数是基本类型,则会引发非法参数异常。这会捕获以下棘手的情况:如果第一个参数是 `int.class`,那么它的类型是 `Class`,但新 +数组的类型为 `int[]`,它不是 `Integer[]` 的子类型。如果 `int.class` 具有 `Class` 类型而不是 `Class` 类型,则不会出现此问题,如前一 +节所述。 作为使用第一种方法的一个例子,下面是一个方法,它将一个集合复制到一个相同类型的新集合中,从而保留参数的类型: ```java - public static > C copy(C coll) { - C copy = GenericReflection.newInstance(coll); - copy.addAll(coll); return copy; - } +public static > C copy(C coll) { + C copy = GenericReflection.newInstance(coll); + copy.addAll(coll); return copy; +} ``` 调用 `ArrayList` 上的副本将返回一个新的 `ArrayList`,同时在 `HashSet` 上调用副本将返回一个新的 `HashSet`. @@ -62,16 +77,18 @@ 作为使用最后一个方法的一个例子,下面是第 `6.5` 节的 `toArray` 方法,重写它以通过对通用反射库的调用来替换未经检查的强制转换: ```java - public static T[] toArray(Collection c, T[] a) { - if (a.length < c.size()) - a = GenericReflection.newArray(a, c.size()); - int i=0; for (T x : c) a[i++] = x; - if (i < a.length) a[i] = null; - return a; - } +public static T[] toArray(Collection c, T[] a) { + if (a.length < c.size()) + a = GenericReflection.newArray(a, c.size()); + int i=0; for (T x : c) a[i++] = x; + if (i < a.length) + a[i] = null; + return a; +} ``` -一般来说,我们建议如果您需要使用未经检查的强制转换,那么您应该将它们封装到少数库方法中,就像我们在这里所做的那样。 不要让未经检查的代码在您的程序中激增! +一般来说,我们建议如果您需要使用未经检查的强制转换,那么您应该将它们封装到少数库方法中,就像我们在这里所做的那样。 不要让未经检查的代码在您的程序中激 +增! 《《《 [下一节](05_Reflection_for_Generics.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch07/05_Reflection_for_Generics.md b/ch07/05_Reflection_for_Generics.md index 650e5ddb..796fe971 100644 --- a/ch07/05_Reflection_for_Generics.md +++ b/ch07/05_Reflection_for_Generics.md @@ -3,79 +3,85 @@ ### 泛型的反思 -泛型以两种方式改变反射库。 我们已经讨论了反射的泛型,其中 `Java` 为类 `Class` 添加了一个类型参数。 我们现在讨论泛型的反射,其中 `Java` 添加了支 -持访问泛型类型的方法和类。 +泛型以两种方式改变反射库。我们已经讨论了反射的泛型,其中 `Java` 为类 `Class` 添加了一个类型参数。我们现在讨论泛型的反射,其中 `Java` 添加了支持 +访问泛型类型的方法和类。 -例 `7-2` 显示了泛型使用反射的简单演示。 它使用反射来查找与给定名称关联的类,并使用反射库类 `Field`,`Constructor` 和 `Method` 打印出与该类关联的字 -段,构造函数和方法。 两种不同的方法可用于将字段,构造函数或方法转换为用于打印的字符串:旧的 `toString` 方法和新的 `toGenericString` 方法。 旧方法 -主要是为了向后兼容性而维护的。 示例 `7-3` 中显示了一个小样本类,示例 `7-4` 中显示了使用此类运行的示例。 +例 `7-2` 显示了泛型使用反射的简单演示。它使用反射来查找与给定名称关联的类,并使用反射库类 `Field`,`Constructor` 和 `Method` 打印出与该类关联的字 +段,构造函数和方法。两种不同的方法可用于将字段,构造函数或方法转换为用于打印的字符串:旧的 `toString` 方法和新的 `toGenericString` 方法。 旧方法主 +要是为了向后兼容性而维护的。 示例 `7-3` 中显示了一个小样本类,示例 `7-4` 中显示了使用此类运行的示例。 例 `7-2`。 对泛型的反思 ```java - import java.lang.reflect.*; - import java.util.*; - class ReflectionForGenerics { - public static void toString(Class k) { - System.out.println(k + " (toString)"); - for (Field f : k.getDeclaredFields()) - System.out.println(f.toString()); - for (Constructor c : k.getDeclaredConstructors()) - System.out.println(c.toString()); - for (Method m : k.getDeclaredMethods()) - System.out.println(m.toString()); - System.out.println(); - } - public static void toGenericString(Class k) { - System.out.println(k + " (toGenericString)"); - for (Field f : k.getDeclaredFields()) - System.out.println(f.toGenericString()); - for (Constructor c : k.getDeclaredConstructors()) - System.out.println(c.toGenericString()); - for (Method m : k.getDeclaredMethods()) - System.out.println(m.toGenericString()); - System.out.println(); - } - public static void main (String[] args) throws ClassNotFoundException { - for (String name : args) { - Class k = Class.forName(name); - toString(k); - toGenericString(k); - } - } - } +import java.lang.reflect.*; +import java.util.*; +class ReflectionForGenerics { + public static void toString(Class k) { + System.out.println(k + " (toString)"); + for (Field f : k.getDeclaredFields()) + System.out.println(f.toString()); + for (Constructor c : k.getDeclaredConstructors()) + System.out.println(c.toString()); + for (Method m : k.getDeclaredMethods()) + System.out.println(m.toString()); + System.out.println(); + } + public static void toGenericString(Class k) { + System.out.println(k + " (toGenericString)"); + for (Field f : k.getDeclaredFields()) + System.out.println(f.toGenericString()); + for (Constructor c : k.getDeclaredConstructors()) + System.out.println(c.toGenericString()); + for (Method m : k.getDeclaredMethods()) + System.out.println(m.toGenericString()); + System.out.println(); + } + public static void main (String[] args) throws ClassNotFoundException { + for (String name : args) { + Class k = Class.forName(name); + toString(k); + toGenericString(k); + } + } +} ``` 例 `7-3`。 示例类 ```java - class Cell { - private E value; - public Cell(E value) { this.value=value; } - public E getValue() { return value; } - public void setValue(E value) { this.value=value; } - public static Cell copy(Cell cell) { - return new Cell(cell.getValue()); - } - } +class Cell { + private E value; + public Cell(E value) { + this.value=value; + } + public E getValue() { + return value; + } + public void setValue(E value) { + this.value=value; + } + public static Cell copy(Cell cell) { + return new Cell(cell.getValue()); + } +} ``` 例 `7-4`。 示例运行 ```java - % java ReflectionForGenerics Cell - class Cell (toString) - private java.lang.Object Cell.value - public Cell(java.lang.Object) - public java.lang.Object Cell.getValue() - public static Cell Cell.copy(Cell) - public void Cell.setValue(java.lang.Object) - class Cell (toGenericString) - private E Cell.value - public Cell(E) - public E Cell.getValue() - public static Cell Cell.copy(Cell) - public void Cell.setValue(E) +% java ReflectionForGenerics Cell +class Cell (toString) +private java.lang.Object Cell.value +public Cell(java.lang.Object) +public java.lang.Object Cell.getValue() +public static Cell Cell.copy(Cell) +public void Cell.setValue(java.lang.Object) +class Cell (toGenericString) +private E Cell.value +public Cell(E) +public E Cell.getValue() +public static Cell Cell.copy(Cell) +public void Cell.setValue(E) ``` 示例运行表明,尽管对象和类标记的实体化类型信息不包含有关泛型的信息,但该类的实际字节码确实可以对有关泛型和擦除类型的信息进行编码。 关于泛型类型的信息 @@ -85,4 +91,4 @@ 部分解释如何访问它。 《《《 [下一节](06_Reflecting_Generic_Types.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch07/06_Reflecting_Generic_Types.md b/ch07/06_Reflecting_Generic_Types.md index 37f3b62d..96e48622 100644 --- a/ch07/06_Reflecting_Generic_Types.md +++ b/ch07/06_Reflecting_Generic_Types.md @@ -5,33 +5,35 @@ 反射库提供了一个 `Type` 接口来描述一个通用类型。 有一个类实现了这个接口和四个其他接口来扩展它,对应于五种不同的类型: - - `Class` 类,表示原始类型或原始类型 - - - 接口 `ParameterizedType`,表示通用类或接口的参数类型的应用程序,您可以从中提取参数类型的数组 - - - `TypeVariable` 接口,代表一个类型变量,从中可以提取类型变量的边界 - - - `GenericArrayType` 接口,表示数组,您可以从中提取数组组件类型 - - - `WildcardType` 接口,表示通配符,您可以从中抽取通配符的下限或上限 - +- `Class` 类,表示原始类型或原始类型 + +- 接口 `ParameterizedType`,表示通用类或接口的参数类型的应用程序,您可以从中提取参数类型的数组 + +- `TypeVariable` 接口,代表一个类型变量,从中可以提取类型变量的边界 + +- `GenericArrayType` 接口,表示数组,您可以从中提取数组组件类型 + +- `WildcardType` 接口,表示通配符,您可以从中抽取通配符的下限或上限 + 通过在每个接口上执行一系列实例测试,您可以确定您拥有哪种类型,并打印或处理类型;我们将很快看到一个例子。 方法可用于将类的超类和超接口作为类型返回,并访问字段的泛型类型,构造函数的参数类型以及方法的参数和结果类型。 -您还可以提取代表类或接口声明或泛型方法或构造函数的形式参数的类型变量。类型变量的类型需要一个参数,并写入 `TypeVariable`,其中 `D` 表示声明类型变量的对象的类型。因此,类的类型变量具有类型 `TypeVariable>`,而泛型方法的类型变量具有类型 `TypeVariable`。可以说,类型参数是令人困惑的,并不是非常有用。由于它对 `6.6` 节中描述的问题负责,因此 `Sun` 可能会在将来删除它。 +您还可以提取代表类或接口声明或泛型方法或构造函数的形式参数的类型变量。类型变量的类型需要一个参数,并写入 `TypeVariable`,其中 `D` 表示声明类型变 +量的对象的类型。因此,类的类型变量具有类型 `TypeVariable>`,而泛型方法的类型变量具有类型 `TypeVariable`。可以说,类型参数是令人 +困惑的,并不是非常有用。由于它对 `6.6` 节中描述的问题负责,因此 `Sun` 可能会在将来删除它。 例 `7-5` 使用这些方法打印出与类关联的所有标题信息。这里有两个使用例子: ```java - % java ReflectionDemo java.util.AbstractList - class java.util.AbstractList - extends java.util.AbstractCollection - implements java.util.List - - % java ReflectionDemo java.lang.Enum - class java.lang.Enum> - implements java.lang.Comparable,java.io.Serializable +% java ReflectionDemo java.util.AbstractList +class java.util.AbstractList +extends java.util.AbstractCollection +implements java.util.List + +% java ReflectionDemo java.lang.Enum +class java.lang.Enum> +implements java.lang.Comparable,java.io.Serializable ``` 例 `7-5` 中的代码冗长而直接。 它包含打印类的每个组件的方法:它的超类,它的接口,它的字段和它的方法。 代码的核心是 `printType` 方法,它使用级联的实 @@ -40,109 +42,116 @@ 例 `7-5`。 如何操作 `Type` 类型 ```java - import java.util.*; - import java.lang.reflect.*; - import java.io.*; - class ReflectionDemo { - private final static PrintStream out = System.out; - public static void printSuperclass(Type sup) { - if (sup != null && !sup.equals(Object.class)) { - out.print("extends "); - printType(sup); - out.println(); - } - } - public static void printInterfaces(Type[] impls) { - if (impls != null && impls.length > 0) { - out.print("implements "); - int i = 0; - for (Type impl : impls) { - if (i++ > 0) out.print(","); - printType(impl); - } - out.println(); - } - } - public static void printTypeParameters(TypeVariable[] vars) { - if (vars != null && vars.length > 0) { - out.print("<"); - int i = 0; - for (TypeVariable var : vars) { - if (i++ > 0) out.print(","); - out.print(var.getName()); - printBounds(var.getBounds()); - } - out.print(">"); - } - } - public static void printBounds(Type[] bounds) { - if (bounds != null && bounds.length > 0 && !(bounds.length == 1 && bounds[0] == Object.class)) { - out.print(" extends "); - int i = 0; - for (Type bound : bounds) { - if (i++ > 0) out.print("&"); - printType(bound); - } - } +import java.util.*; +import java.lang.reflect.*; +import java.io.*; +class ReflectionDemo { + private final static PrintStream out = System.out; + public static void printSuperclass(Type sup) { + if (sup != null && !sup.equals(Object.class)) { + out.print("extends "); + printType(sup); + out.println(); + } + } + public static void printInterfaces(Type[] impls) { + if (impls != null && impls.length > 0) { + out.print("implements "); + int i = 0; + for (Type impl : impls) { + if (i++ > 0) + out.print(","); + printType(impl); } - public static void printParams(Type[] types) { - if (types != null && types.length > 0) { - out.print("<"); - int i = 0; - for (Type type : types) { - if (i++ > 0) out.print(","); - printType(type); - } - out.print(">"); - } + out.println(); + } + } + public static void printTypeParameters(TypeVariable[] vars) { + if (vars != null && vars.length > 0) { + out.print("<"); + int i = 0; + for (TypeVariable var : vars) { + if (i++ > 0) + out.print(","); + out.print(var.getName()); + printBounds(var.getBounds()); } - public static void printType(Type type) { - if (type instanceof Class) { - Class c = (Class)type; - out.print(c.getName()); - } else if (type instanceof ParameterizedType) { - ParameterizedType p = (ParameterizedType)type; - Class c = (Class)p.getRawType(); - Type o = p.getOwnerType(); - if (o != null) { printType(o); out.print("."); } - out.print(c.getName()); - printParams(p.getActualTypeArguments()); - } else if (type instanceof TypeVariable) { - TypeVariable v = (TypeVariable)type; - out.print(v.getName()); - } else if (type instanceof GenericArrayType) { - GenericArrayType a = (GenericArrayType)type; - printType(a.getGenericComponentType()); - out.print("[]"); - } else if (type instanceof WildcardType) { - WildcardType w = (WildcardType)type; - Type[] upper = w.getUpperBounds(); - Type[] lower = w.getLowerBounds(); - if (upper.length == 1 && lower.length == 0) { - out.print("? extends "); - printType(upper[0]); - } else if (upper.length == 0 && lower.length == 1) { - out.print("? super "); - printType(lower[0]); - } else - throw new AssertionError(); - } + out.print(">"); + } + } + public static void printBounds(Type[] bounds) { + if (bounds != null && bounds.length > 0 && !(bounds.length == 1 && bounds[0] == Object.class)) { + out.print(" extends "); + int i = 0; + for (Type bound : bounds) { + if (i++ > 0) + out.print("&"); + printType(bound); } - public static void printClass(Class c) { - out.print("class "); - out.print(c.getName()); - printTypeParameters(c.getTypeParameters()); - out.println(); - printSuperclass(c.getGenericSuperclass()); - printInterfaces(c.getGenericInterfaces()); + } + } + public static void printParams(Type[] types) { + if (types != null && types.length > 0) { + out.print("<"); + int i = 0; + for (Type type : types) { + if (i++ > 0) + out.print(","); + printType(type); } - public static void main(String[] args) throws ClassNotFoundException { - for (String name : args) { - Class c = Class.forName(name); - printClass(c); - } + out.print(">"); + } + } + public static void printType(Type type) { + if (type instanceof Class) { + Class c = (Class)type; + out.print(c.getName()); + } else if (type instanceof ParameterizedType) { + ParameterizedType p = (ParameterizedType)type; + Class c = (Class)p.getRawType(); + Type o = p.getOwnerType(); + if (o != null) { + printType(o); + out.print("."); } + out.print(c.getName()); + printParams(p.getActualTypeArguments()); + } else if (type instanceof TypeVariable) { + TypeVariable v = (TypeVariable)type; + out.print(v.getName()); + } else if (type instanceof GenericArrayType) { + GenericArrayType a = (GenericArrayType)type; + printType(a.getGenericComponentType()); + out.print("[]"); + } else if (type instanceof WildcardType) { + WildcardType w = (WildcardType)type; + Type[] upper = w.getUpperBounds(); + Type[] lower = w.getLowerBounds(); + if (upper.length == 1 && lower.length == 0) { + out.print("? extends "); + printType(upper[0]); + } else if (upper.length == 0 && lower.length == 1) { + out.print("? super "); + printType(lower[0]); + } else + throw new AssertionError(); + } + } + public static void printClass(Class c) { + out.print("class "); + out.print(c.getName()); + printTypeParameters(c.getTypeParameters()); + out.println(); + printSuperclass(c.getGenericSuperclass()); + printInterfaces(c.getGenericInterfaces()); + } + public static void main(String[] args) throws ClassNotFoundException { + for (String name : args) { + Class c = Class.forName(name); + printClass(c); } + } +} ``` 如果 `Type` 接口有一个 `toGenericString` 方法,那么大部分代码都是不必要的。 `Sun` 正在考虑这一改变。 diff --git a/ch08/00_Effective_Generics.md b/ch08/00_Effective_Generics.md index 3d53e5d3..ae372f04 100644 --- a/ch08/00_Effective_Generics.md +++ b/ch08/00_Effective_Generics.md @@ -1,9 +1,10 @@ 《《《 [返回首页](../README.md)
《《《 [上一节](../ch07/06_Reflecting_Generic_Types.md) -## 有效的泛型 +### 有效的泛型 -本章包含如何在实际编码中有效使用泛型的建议。 我们考虑检查集合,安全问题,专用类和二进制兼容性。 本节的标题是对 `Joshua Bloch` 的着作 `Effective Java(Addison-Wesley)` 的致敬。 +本章包含如何在实际编码中有效使用泛型的建议。 我们考虑检查集合,安全问题,专用类和二进制兼容性。 本节的标题是对 `Joshua Bloch` 的着作 +`Effective Java(Addison-Wesley)` 的致敬。 《《《 [下一节](01_Take_Care_when_Callin_Legacy_Code.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch08/01_Take_Care_when_Callin_Legacy_Code.md b/ch08/01_Take_Care_when_Callin_Legacy_Code.md index a478e161..a9d0cc93 100644 --- a/ch08/01_Take_Care_when_Callin_Legacy_Code.md +++ b/ch08/01_Take_Care_when_Callin_Legacy_Code.md @@ -3,91 +3,74 @@ ### 调用遗留代码时要小心 -正如我们所看到的,泛型类型在编译时被检查,而不是运行时。 通常,这正是我们想要的,因为在编译时检查会更早地报告错误,并且不会导致运行时开销。 但是,有时这可能不合适,因为我们无法确定编译时检查是否足够(比如说,因为我们将参数化类型的实例传递给旧客户端或我们不信任的客户端 ),还是因为我们在运行时需要关于类型的信息(比如说,因为我们需要一个可重用类型作为数组组件)。 一个托收集合通常会诀窍,如果不行,我们可以创建一个专门的类。 我们考虑本节中的已检查集合,下一节中的安全问题以及之后的部分中的专门类。 +正如我们所看到的,泛型类型在编译时被检查,而不是运行时。 通常,这正是我们想要的,因为在编译时检查会更早地报告错误,并且不会导致运行时开销。 但是,有 +时这可能不合适,因为我们无法确定编译时检查是否足够(比如说,因为我们将参数化类型的实例传递给旧客户端或我们不信任的客户端 ),还是因为我们在运行时需要 +关于类型的信息(比如说,因为我们需要一个可重用类型作为数组组件)。 一个托收集合通常会诀窍,如果不行,我们可以创建一个专门的类。 我们考虑本节中的已检 +查集合,下一节中的安全问题以及之后的部分中的专门类。 考虑一个遗留库,其中包含将项目添加到给定列表并返回包含给定项目的新列表的方法: ```java - class LegacyLibrary { - public static void addItems(List list) { - list.add(new Integer(1)); list.add("two"); - } - public static List getItems() { - List list = new ArrayList(); - list.add(new Integer(3)); list.add("four"); - return list; - } - } +class LegacyLibrary { + public static void addItems(List list) { + list.add(new Integer(1)); + list.add("two"); + } + public static List getItems() { + List list = new ArrayList(); + list.add(new Integer(3)); + list.add("four"); + return list; + } +} ``` 现在考虑一个使用这个遗留库的客户端,被告知(不正确)这些项目总是整数: ```java - class NaiveClient { - public static void processItems() { - List list = new ArrayList(); - Legacy Library.addItems(list); - List list2 = LegacyLibrary.getItems(); // unchecked - // sometime later ... - int s = 0; - for (int i : list) s += i; // 类抛出异常 - for (int i : list2) s += i; // 类抛出异常 - } - } +class NaiveClient { + public static void processItems() { + List list = new ArrayList(); + Legacy Library.addItems(list); + List list2 = LegacyLibrary.getItems(); // unchecked + // sometime later ... + int s = 0; + for (int i : list) s += i; // 类抛出异常 + for (int i : list2) s += i; // 类抛出异常 + } +} ``` -将整数列表传递给方法 `addItems` 时没有警告,因为参数化类型 `List` 被认为是 `List` 的一个子类型。由 `getItems` 返回的列表从 `List` 到 `List` 的转换确实发出未经检查的警告。在运行时,尝试从这些列表中提取数据时会引发类转换异常,因为将转换类型为 `Integer` 隐式插入通过擦除将会失败。 (这些强制转换的失败并不构成对铸铁保证的违反,因为这种保证不适用于存在遗留代码或未经检查的警告。)因为异常引发远离字符串添加的地方到列表中,该错误可能很难查明。 +将整数列表传递给方法 `addItems` 时没有警告,因为参数化类型 `List` 被认为是 `List` 的一个子类型。由 `getItems` 返回的列表从 `List` 到 +`List` 的转换确实发出未经检查的警告。在运行时,尝试从这些列表中提取数据时会引发类转换异常,因为将转换类型为 `Integer` 隐式插入通过擦除将会 +失败。 (这些强制转换的失败并不构成对铸铁保证的违反,因为这种保证不适用于存在遗留代码或未经检查的警告。)因为异常引发远离字符串添加的地方到列表中,该 +错误可能很难查明。 如果通过应用最小改变或存根技术使遗留文库得到了生化(参见第 `5.4.1` 节和第 `5.4.2` 节),那么只要泛型类型被正确赋值,这些问题就不会出现。 一个不那么客观的客户可能会设计出更早捕获错误并且更易于调试的代码。 ```java - class WaryClient { - public static void processItems() { - List list = new ArrayList(); - List view = Collections.checkedList(list, Integer.class); - LegacyLibrary.addItems(view); // 类抛出异常 - List list2 = LegacyLibrary.getItems(); // unchecked - for (int i : list2) {} // 类抛出异常 - // sometime later ... - int s = 0; - for (int i : list) s += i; - for (int i : list2) s += i; - } - } +class WaryClient { + public static void processItems() { + List list = new ArrayList(); + List view = Collections.checkedList(list, Integer.class); + LegacyLibrary.addItems(view); // 类抛出异常 + List list2 = LegacyLibrary.getItems(); // unchecked + for (int i : list2) {} // 类抛出异常 + // sometime later ... + int s = 0; + for (int i : list) s += i; + for (int i : list2) s += i; + } +} ``` -方便类集合中的 `checkedList` 方法获取列表和类标记并返回列表的已选中视图; 每当尝试向检查的视图添加一个元素时,反射用于检查该元素是否属于指定的类,然后将其添加到基础列表(请参阅第 `17.3.3` 节)。 使用已检查的列表视图将导致在尝试向列表中添加字符串时,在方法 `addItems` 内引发类转换异常。由于方法 `getItems` 创建自己的列表,客户端不能以相同的方式使用包装器。 但是,在返回列表的位置添加一个空循环可以保证错误被捕获到接近有问题的方法调用。 +方便类集合中的 `checkedList` 方法获取列表和类标记并返回列表的已选中视图; 每当尝试向检查的视图添加一个元素时,反射用于检查该元素是否属于指定的类,然 +后将其添加到基础列表(请参阅第 `17.3.3` 节)。 使用已检查的列表视图将导致在尝试向列表中添加字符串时,在方法 `addItems` 内引发类转换异常。由于方法 +`getItems` 创建自己的列表,客户端不能以相同的方式使用包装器。 但是,在返回列表的位置添加一个空循环可以保证错误被捕获到接近有问题的方法调用。 仅当列表元素具有可确定类型时,检查列表才提供有用的保证。 如果你想在列表不是可确定类型时应用这些技术,你可能需要考虑应用 `8.3` 节的专门化技术。 《《《 [下一节](02_Use_Checked_Collections_to_Enforce_Security.md)
《《《 [返回首页](../README.md) - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ch08/02_Use_Checked_Collections_to_Enforce_Security.md b/ch08/02_Use_Checked_Collections_to_Enforce_Security.md index e4761dd2..ae4e0bcf 100644 --- a/ch08/02_Use_Checked_Collections_to_Enforce_Security.md +++ b/ch08/02_Use_Checked_Collections_to_Enforce_Security.md @@ -3,84 +3,73 @@ ### 使用选中的集合来强化安全性 -请注意,通用类型提供的保证仅适用于没有未经检查的警告的情况。 这意味着泛型类型对于确保其他人编写的代码的安全性没有用处,因为您无法知道该代码是否在编译时引发未经检查的警告。 +请注意,通用类型提供的保证仅适用于没有未经检查的警告的情况。 这意味着泛型类型对于确保其他人编写的代码的安全性没有用处,因为您无法知道该代码是否在编译 +时引发未经检查的警告。 假设我们有一个定义订单的类,并且定义了一个经过验证的订单: ```java - class Order { ... } - class AuthenticatedOrder extends Order { ... } +class Order { ... } +class AuthenticatedOrder extends Order { ... } ``` 接口指定订单的供应商和处理商。 在这里,供应商只需提供经过认证的订单,而处理器则处理各种订单: ```java - interface OrderSupplier { - public void addOrders(List orders); - } - interface OrderProcessor { - public void processOrders(List orders); - } +interface OrderSupplier { + public void addOrders(List orders); +} +interface OrderProcessor { + public void processOrders(List orders); +} ``` 从涉及的类型中,您可能会认为以下代理保证只有经过验证的订单可以从供应商传递到处理器: ```java - class NaiveBroker { - public void connect(OrderSupplier supplier, OrderProcessor processor) { - List orders = - new ArrayList(); - supplier.addOrders(orders); - processor.processOrders(orders); - } - } +class NaiveBroker { + public void connect(OrderSupplier supplier, OrderProcessor processor) { + List orders = + new ArrayList(); + supplier.addOrders(orders); + processor.processOrders(orders); + } +} ``` 但是一个狡猾的供应商实际上可能会提供未经认证的订单: ```java - class DeviousSupplier implements OrderSupplier { - public void addOrders(List orders) { - List raw = orders; - Order order = new Order(); // not authenticated - raw.add(order); // unchecked call - } - } +class DeviousSupplier implements OrderSupplier { + public void addOrders(List orders) { + List raw = orders; + Order order = new Order(); // not authenticated + raw.add(order); // unchecked call + } +} ``` 编译这个狡猾的供应商会发出一个未经检查的警告,但是经纪人无法知道这一点。 -无能可能导致与迂回一样多的问题。 编译时发出未经检查的警告的任何代码都可能导致类似的问题,可能仅仅是因为作者犯了一个错误。 特别是,如前一节所述,遗留代码可能会引发这样的问题。 +无能可能导致与迂回一样多的问题。 编译时发出未经检查的警告的任何代码都可能导致类似的问题,可能仅仅是因为作者犯了一个错误。 特别是,如前一节所述,遗留 +代码可能会引发这样的问题。 正确的解决方案是让经纪人将检查清单传递给供应商: ```java - class WaryBroker { - public void connect(OrderSupplier supplier, OrderProcessor processor) { - List orders = new ArrayList(); - supplier.addOrders(Collections.checkedList(orders, AuthenticatedOrder.class)); - processor.processOrders(orders); - } - } +class WaryBroker { + public void connect(OrderSupplier supplier, OrderProcessor processor) { + List orders = new ArrayList(); + supplier.addOrders(Collections.checkedList(orders, AuthenticatedOrder.class)); + processor.processOrders(orders); + } +} ``` 现在,如果供应商尝试将任何内容添加到非认证订单的列表中,则会引发类别强制异常。 -选中的集合不是实施安全性的唯一技术。 如果提供订单的接口返回一个列表而不是接受一个列表,那么代理可以使用前一节中的空循环技术来确保列表只包含经过授权的订单,然后才能通过它们。 也可以使用专业化,如下一节所述,创建一个特殊类型的列表,只能包含授权订单。 +选中的集合不是实施安全性的唯一技术。 如果提供订单的接口返回一个列表而不是接受一个列表,那么代理可以使用前一节中的空循环技术来确保列表只包含经过授权的 +订单,然后才能通过它们。 也可以使用专业化,如下一节所述,创建一个特殊类型的列表,只能包含授权订单。 《《《 [下一节](03_Specialize_to_Create_Reifiable_Types.md)
《《《 [返回首页](../README.md) - - - - - - - - - - - - - - diff --git a/ch08/03_Specialize_to_Create_Reifiable_Types.md b/ch08/03_Specialize_to_Create_Reifiable_Types.md index 7e65ddcb..592d7056 100644 --- a/ch08/03_Specialize_to_Create_Reifiable_Types.md +++ b/ch08/03_Specialize_to_Create_Reifiable_Types.md @@ -8,88 +8,140 @@ 例 `8-1` 显示了如何将列表专门化为字符串; 专门针对其他类型是相似的。 我们首先将 `List` 接口专门化为所需的类型: ```java - interface ListString extends List {} +interface ListString extends List {} ``` 例 `8-1`。 专注于创建可调整类型 ```java - interface ListString extends List {} - class ListStrings { - public static ListString wrap(final List list) { - class Random extends AbstractList implements ListString, RandomAccess { - public int size() { return list.size(); } - public String get(int i) { return list.get(i); } - public String set(int i, String s) { return list.set(i,s); } - public String remove(int i) { return list.remove(i); } - public void add(int i, String s) { list.add(i,s); } - } - class Sequential extends AbstractSequentialList implements ListString { - public int size() { return list.size(); } - public ListIterator listIterator(int index) { - final ListIterator it = list.listIterator(index); - return new ListIterator() { - public void add(String s) { it.add(s); } - public boolean hasNext() { return it.hasNext(); } - public boolean hasPrevious() { return it.hasPrevious(); } - public String next() { return it.next(); } - public int nextIndex() { return it.nextIndex(); } - public String previous() { return it.previous(); } - public int previousIndex() { return it.previousIndex(); } - public void remove() { it.remove(); } - public void set(String s) { it.set(s); } - }; - } - } - return list instanceof RandomAccess ? new Random() : new Sequential(); - } - } - class ArrayListString extends ArrayList implements ListString { - public ArrayListString() { super(); } - public ArrayListString(Collection c) { super(c); } - public ArrayListString(int capacity) { super(capacity); } - } +interface ListString extends List {} +class ListStrings { + public static ListString wrap(final List list) { + class Random extends AbstractList implements ListString, RandomAccess { + public int size() { + return list.size(); + } + public String get(int i) { + return list.get(i); + } + public String set(int i, String s) { + return list.set(i,s); + } + public String remove(int i) { + return list.remove(i); + } + public void add(int i, String s) { + list.add(i,s); + } + } + class Sequential extends AbstractSequentialList implements ListString { + public int size() { + return list.size(); + } + public ListIterator listIterator(int index) { + final ListIterator it = list.listIterator(index); + return new ListIterator() { + public void add(String s) { + it.add(s); + } + public boolean hasNext() { + return it.hasNext(); + } + public boolean hasPrevious() { + return it.hasPrevious(); + } + public String next() { + return it.next(); + } + public int nextIndex() { + return it.nextIndex(); + } + public String previous() { + return it.previous(); + } + public int previousIndex() { + return it.previousIndex(); + } + public void remove() { + it.remove(); + } + public void set(String s) { + it.set(s); + } + }; + } + } + return list instanceof RandomAccess ? new Random() : new Sequential(); + } +} +class ArrayListString extends ArrayList implements ListString { + public ArrayListString() { + super(); + } + public ArrayListString(Collection c) { + super(c); + } + public ArrayListString(int capacity) { + super(capacity); + } +} ``` -这声明了 `ListString`(一个非参数化类型,因此是可修饰的)是 `List` 的一个子类型(一个参数化类型,因此不可赋予)。 因此,第一类型的每个值也属于第二类型,但不是相反。 接口声明没有新的方法; 它只是将现有方法专用于参数类型 `String`。 +这声明了 `ListString`(一个非参数化类型,因此是可修饰的)是 `List` 的一个子类型(一个参数化类型,因此不可赋予)。 因此,第一类型的每个值也 +属于第二类型,但不是相反。 接口声明没有新的方法; 它只是将现有方法专用于参数类型 `String`。 -委托要委托专门化,我们定义一个静态方法包装,它接受一个 `List` 类型的参数并返回一个 `ListString` 类型的结果。`Java` 库在一个名为 `Collections` 的类中放置了作用于接口 `Collection` 的方法,因此我们将方法包放入名为 `ListStrings` 的类中。 +委托要委托专门化,我们定义一个静态方法包装,它接受一个 `List` 类型的参数并返回一个 `ListString` 类型的结果。`Java` 库在一个名为 +`Collections` 的类中放置了作用于接口 `Collection` 的方法,因此我们将方法包放入名为 `ListStrings` 的类中。 这是一个使用的例子: ```java - List> lists = Arrays.asList( - ListStrings.wrap(Arrays.asList("one","two")), - Arrays.asList(3,4), - Arrays.asList("five","six"), - ListStrings.wrap(Arrays.asList("seven","eight")) - ); - ListString[] array = new ListString[2]; - int i = 0; - for (List list : lists) +List> lists = Arrays.asList( + ListStrings.wrap(Arrays.asList("one","two")), + Arrays.asList(3,4), + Arrays.asList("five","six"), + ListStrings.wrap(Arrays.asList("seven","eight")) +); +ListString[] array = new ListString[2]; +int i = 0; +for (List list : lists) if (list instanceof ListString) - array[i++] = (ListString)list; - assert Arrays.toString(array).equals("[[one, two], [seven, eight]]"); + array[i++] = (ListString)list; +assert Arrays.toString(array).equals("[[one, two], [seven, eight]]"); ``` -这将创建一个列表列表,然后扫描它以查找实现 `ListString` 的列表并将它们放入数组中。现在,数组创建,实例测试和强制转换不会产生任何问题,因为它们对可重用类型 `ListString` 而不是不可重派类型 `List` 起作用。注意到未包装的 `List` 不会被识别为 `ListString` 的实例;这就是为什么列表中的第三个列表未被复制到数组中的原因。 +这将创建一个列表列表,然后扫描它以查找实现 `ListString` 的列表并将它们放入数组中。现在,数组创建,实例测试和强制转换不会产生任何问题,因为它们对可重 +用类型 `ListString` 而不是不可重派类型 `List` 起作用。注意到未包装的 `List` 不会被识别为 `ListString` 的实例;这就是为什么列表中 +的第三个列表未被复制到数组中的原因。 -`ListStrings` 类很容易实现,但需要注意保持良好的性能。 `Java` 集合框架规定,无论何时列表支持快速随机访问,它应该实现标记接口 `RandomAccess`,以允许通用算法在应用于随机或顺序访问列表时执行良好。它还提供了两个抽象类 `AbstractList` 和 `AbstractSequentialList`,它们适用于定义随机和顺序访问列表。例如,`ArrayList` 实现 `RandomAccess` 并扩展 `AbstractList`,而 `LinkedList` 扩展 `AbstractSequentialList`。`AbstractList` 类根据提供随机访问的五种抽象方法来定义 `List` 接口的方法,并且必须在子类(`size`,`get`,`set`,`add`,`remove`)中定义。同样,类`AbstractSequentialList` 按照提供顺序访问的两个抽象方法定义了 `List` 接口的所有方法,并且必须在子类(`size`,`listIterator`)中定义。 +`ListStrings` 类很容易实现,但需要注意保持良好的性能。 `Java` 集合框架规定,无论何时列表支持快速随机访问,它应该实现标记接口 `RandomAccess`,以允 +许通用算法在应用于随机或顺序访问列表时执行良好。它还提供了两个抽象类 `AbstractList` 和 `AbstractSequentialList`,它们适用于定义随机和顺序访问列 +表。例如,`ArrayList` 实现 `RandomAccess` 并扩展 `AbstractList`,而 `LinkedList` 扩展 `AbstractSequentialList`。`AbstractList` 类根据提供随 +机访问的五种抽象方法来定义 `List` 接口的方法,并且必须在子类(`size`,`get`,`set`,`add`,`remove`)中定义。同样,类`AbstractSequentialList` +按照提供顺序访问的两个抽象方法定义了 `List` 接口的所有方法,并且必须在子类(`size`,`listIterator`)中定义。 -`wrap` 方法检查给定列表是否实现了接口 `RandomAccess`。如果是这样,它将返回一个 `Random` 类的实例,该类扩展了 `AbstractList` 并实现了 `RandomAccess`,否则它将返回继承 `AbstractSequentialList` 的 `Sequential` 类的实例。 类 `Random` 实现了 `AbstractList` 的子类必须提供的五个方法。同样,`Sequential` 类实现了必须由 `AbstractSequentialList` 的子类提供的两个方法,其中第二个方法返回一个实现 `ListIterato` 接口九个方法的类。如下所述,通过委派实现列表迭代器而不是简单地返回原始列表迭代器可以改进包装器的安全属性。所有这些方法都是通过授权直接实施的。 +`wrap` 方法检查给定列表是否实现了接口 `RandomAccess`。如果是这样,它将返回一个 `Random` 类的实例,该类扩展了 `AbstractList` 并实现了 +`RandomAccess`,否则它将返回继承 `AbstractSequentialList` 的 `Sequential` 类的实例。 类 `Random` 实现了 `AbstractList` 的子类必须提供的五个方 +法。同样,`Sequential` 类实现了必须由 `AbstractSequentialList` 的子类提供的两个方法,其中第二个方法返回一个实现 `ListIterato` 接口九个方法的类。 +如下所述,通过委派实现列表迭代器而不是简单地返回原始列表迭代器可以改进包装器的安全属性。所有这些方法都是通过授权直接实施的。 -`wrap` 方法返回底层列表的视图,如果试图将元素插入到非 `String` 类型的列表中,将引发类转换异常。这些检查与 `checkedList` 包装器提供的检查类似。但是,对于包装来说,相关的强制转换由编译器插入(通过委托来实现 `listIterator` 接口的九个方法的一个原因是为了确保插入这些强制转换),而对于已检查的列表,强制转换是通过反射来执行的。泛型通常会使这些检查变得冗余,但是它们在遗留代码或未检查警告的存在或处理诸如 `8.2` 节中讨论的安全问题时会有所帮助。 +`wrap` 方法返回底层列表的视图,如果试图将元素插入到非 `String` 类型的列表中,将引发类转换异常。这些检查与 `checkedList` 包装器提供的检查类似。但 +是,对于包装来说,相关的强制转换由编译器插入(通过委托来实现 `listIterator` 接口的九个方法的一个原因是为了确保插入这些强制转换),而对于已检查的列 +表,强制转换是通过反射来执行的。泛型通常会使这些检查变得冗余,但是它们在遗留代码或未检查警告的存在或处理诸如 `8.2` 节中讨论的安全问题时会有所帮助。 -此处显示的代码旨在平衡权力与简洁性(它只有三条线),但其他变体也是可能的。不完整的版本可能只实现随机访问,如果可以保证它从未应用于顺序访问列表,反之亦然。更高效的版本可能会跳过 `AbstractList` 和 `AbstractSequentialList` 的使用,而直接将所有 `25` 个 `List` 接口的方法与 `toString` 方法(模型的 `Collections.checkedList` 的源代码)一起委托。您还可能希望在 `ListString` 接口中提供其他方法,例如返回基础 `List` 的 `unwrap` 方法或通过递归将换行应用于返回 `ListString` 而不是 `List` 的 `subList`的版本委托调用。 +此处显示的代码旨在平衡权力与简洁性(它只有三条线),但其他变体也是可能的。不完整的版本可能只实现随机访问,如果可以保证它从未应用于顺序访问列表,反之 +亦然。更高效的版本可能会跳过 `AbstractList` 和 `AbstractSequentialList` 的使用,而直接将所有 `25` 个 `List` 接口的方法与 `toString` 方法(模型 +的 `Collections.checkedList` 的源代码)一起委托。您还可能希望在 `ListString` 接口中提供其他方法,例如返回基础 `List` 的 `unwrap` 方法或 +通过递归将换行应用于返回 `ListString` 而不是 `List` 的 `subList`的版本委托调用。 继承为了通过继承进行特殊化,我们声明了一个实现专用接口并从适当的列表实现继承的专用类。示例 `8-1` 显示了专门化 `ArrayList` 的实现,我们在此重复: ```java - class ArrayListString extends ArrayList implements ListString { - public ArrayListString() { super(); } - public ArrayListString(Collection c) { super(c); } - public ArrayListString(int capacity) { super(capacity); } - } +class ArrayListString extends ArrayList implements ListString { + public ArrayListString() { super(); } + public ArrayListString(Collection c) { super(c); } + public ArrayListString(int capacity) { super(capacity); } +} ``` 代码非常紧凑。 所有的方法都是从超类继承的,所以我们只需要定义专门的构造函数。 如果唯一需要的构造函数是默认的构造函数,那么类的主体可能是完全空的! @@ -97,18 +149,18 @@ 前面的例子仍然有效,如果我们使用继承而不是委托来创建初始列表: ```java - List> lists = Arrays.asList( - new ArrayListString(Arrays.asList("one","two")), - Arrays.asList(3,4), - Arrays.asList("five","six"), - new ArrayListString(Arrays.asList("seven","eight")) - ); - ListString[] array = new ListString[2]; - int i = 0; - for (List list : lists) - if (list instanceof ListString) - array[i++] = (ListString) list; - assert Arrays.toString(array).equals("[[one, two], [seven, eight]]"); +List> lists = Arrays.asList( + new ArrayListString(Arrays.asList("one","two")), + Arrays.asList(3,4), + Arrays.asList("five","six"), + new ArrayListString(Arrays.asList("seven","eight")) +); +ListString[] array = new ListString[2]; +int i = 0; +for (List list : lists) + if (list instanceof ListString) + array[i++] = (ListString) list; +assert Arrays.toString(array).equals("[[one, two], [seven, eight]]"); ``` 像以前一样,数组创建,实例测试和强制转换现在没有问题。 @@ -116,18 +168,18 @@ 但是,委派和继承是不可互换的。 委派专业化创建了一个基础列表的视图,而通过继承进行专业化构建了一个新列表。 此外,委托专业化比继承专业化具有更好的安全属性。 这里是一个例子: ```java - List original = new ArrayList(); - ListString delegated = ListStrings.wrap(original); - ListString inherited = new ArrayListString(original); - delegated.add("one"); - inherited.add("two"); - try { - ((List)delegated).add(3); // 未经检查,类型转换错误 - } catch (ClassCastException e) {} - ((List)inherited).add(4); // 未经检查,没有类型转换错误 - assert original.toString().equals("[one]"); - assert delegated.toString().equals("[one]"); - assert inherited.toString().equals("[two, 4]"); +List original = new ArrayList(); +ListString delegated = ListStrings.wrap(original); +ListString inherited = new ArrayListString(original); +delegated.add("one"); +inherited.add("two"); +try { + ((List)delegated).add(3); // 未经检查,类型转换错误 +} catch (ClassCastException e) {} +((List)inherited).add(4); // 未经检查,没有类型转换错误 +assert original.toString().equals("[one]"); +assert delegated.toString().equals("[one]"); +assert inherited.toString().equals("[two, 4]"); ``` 在这里,原始列表是两个专业列表的基础,一个是由代表团创建的,一个是由继承组成的。添加到委托列表中的元素显示在原始列表中,但添加到继承列表中的元素不会。类型检查通常会阻止任何尝试将不是字符串的元素添加到任何类型为 `List` 的对象,这些对象是专用的或非专用的,但这些尝试可能会在存在旧代码或未经检查的警告时发生。在这里,我们转换为原始类型,并使用未经检查的调用尝试向委托和继承列表添加整数。委派列表上的尝试引发类转换异常,而继承列表上的尝试成功。为了强制第二次尝试失败,我们应该使用 `checkedList` 来包装继承的列表,如第 `8.1` 节所述。 @@ -137,38 +189,48 @@ 通过为列表添加元素或设置元素的任何方法声明专用签名,可以提高通过继承进行专业化的安全属性: ```java - class ArrayListString extends ArrayList implements ListString { - public ArrayListString() { super(); } - public ArrayListString(Collection c) { this.addAll(c); } - public ArrayListString(int capacity) { super(capacity); } - public boolean addAll(Collection c) { - for (String s : c) {} // check that c contains only strings - return super.addAll(c); - } - public boolean add(String element) { return super.add(element); } - public void add(int index, String element) { super.add(index, element); } - public String set(int index, String element) { - return super.set(index, element); - } - } +class ArrayListString extends ArrayList implements ListString { + public ArrayListString() { + super(); + } + public ArrayListString(Collection c) { + this.addAll(c); + } + public ArrayListString(int capacity) { + super(capacity); + } + public boolean addAll(Collection c) { + for (String s : c) {} // check that c contains only strings + return super.addAll(c); + } + public boolean add(String element) { + return super.add(element); + } + public void add(int index, String element) { + super.add(index, element); + } + public String set(int index, String element) { + return super.set(index, element); + } +} ``` -现在,任何尝试添加或设置不是字符串的元素都会引发类转换异常。 然而,这个属性依赖于细微的实现细节,即任何其他添加或设置元素的方法(例如,`listIterator` 中的 `add` 方法)都是按照上面专门的方法实现的。 一般来说,如果需要安全性,授权更加健壮。 +现在,任何尝试添加或设置不是字符串的元素都会引发类转换异常。 然而,这个属性依赖于细微的实现细节,即任何其他添加或设置元素的方法(如,`listIterator` +中的 `add` 方法)都是按照上面专门的方法实现的。 一般来说,如果需要安全性,授权更加健壮。 -其他类型其他类型的专业化工作类似。 例如,在例 `8-1` 中用 `Integer` 替换 `String` 给出了一个接口 `ListInteger` 和类 `ListIntegers` 和 `ArrayListInteger`。 这甚至适用于列表清单。 例如,在例 `8-1` 中用 `ListString` 替换 `String` 给出了一个接口 `ListListString` 和类 `ListListStrings` 和 `ArrayListListString`。 +其他类型其他类型的专业化工作类似。 例如,在例 `8-1` 中用 `Integer` 替换 `String` 给出了一个接口 `ListInteger` 和类 `ListIntegers` 和 +`ArrayListInteger`。 这甚至适用于列表清单。 例如,在例 `8-1` 中用 `ListString` 替换 `String` 给出了一个接口 `ListListString` 和类 +`ListListStrings` 和 `ArrayListListString`。 但是,通配符类型的专业化可能会产生问题。 假设我们想要专门化两种类型的 `List` 和 `List`。 我们可能期望使用以下声明: ```java - // illegal - interface ListNumber extends List, ListExtendsNumber {} - interface ListExtendsNumber extends List {} +// illegal +interface ListNumber extends List, ListExtendsNumber {} +interface ListExtendsNumber extends List {} ``` 这有两个问题:第一个接口使用相同的擦除扩展了两个不同的接口,这是不允许的(见 `4.4` 节),而第二个接口的顶级使用通配符的超类型,这也是不允许的 见 `2.8` 节)。 唯一的解决方法是避免包含通配符的类型的专业化; 幸运的是,这应该很少成为问题。 《《《 [下一节](04_Maintain_Binary_Compatibility.md)
《《《 [返回首页](../README.md) - - - diff --git a/ch08/04_Maintain_Binary_Compatibility.md b/ch08/04_Maintain_Binary_Compatibility.md index 963ac446..2344d393 100644 --- a/ch08/04_Maintain_Binary_Compatibility.md +++ b/ch08/04_Maintain_Binary_Compatibility.md @@ -3,60 +3,63 @@ ### 保持二进制兼容性 -正如我们强调的那样,泛型是通过擦除来实现的,以缓解进化。当将遗留代码转化为泛型代码时,我们希望确保新生代码能够与任何现有代码一起工作,包括我们没有源代码的类文件。当这种情况发生时,我们说传统和通用版本是二进制兼容的。 +正如我们强调的那样,泛型是通过擦除来实现的,以缓解进化。当将遗留代码转化为泛型代码时,我们希望确保新生代码能够与任何现有代码一起工作,包括我们没有源 +代码的类文件。当这种情况发生时,我们说传统和通用版本是二进制兼容的。 -如果擦除通用代码的签名与遗留代码的签名相同,并且两个版本都编译为相同的字节代码,则可以保证二进制兼容性。通常情况下,这是自然发生的自然结果,但在本节中,我们将看到一些可能导致问题的角落案例。 +如果擦除通用代码的签名与遗留代码的签名相同,并且两个版本都编译为相同的字节代码,则可以保证二进制兼容性。通常情况下,这是自然发生的自然结果,但在本节 +中,我们将看到一些可能导致问题的角落案例。 -本节的一些示例摘自Mark Reinhold编写的内部Sun笔记。 +本节的一些示例摘自 `Mark Reinhold` 编写的内部 `Sun` 笔记。 -调整擦除与集合类中的max方法的生成有关的一个边角情况出现了。我们在第3.2节和第3.6节中讨论了这种情况,但值得快速回顾一下。 +调整擦除与集合类中的 `max` 方法的生成有关的一个边角情况出现了。我们在第 `3.2` 节和第 `3.6` 节中讨论了这种情况,但值得快速回顾一下。 这是这种方法的遗留签名: ```java - // 旧版本 - public static Object max(Collection coll) +// 旧版本 +public static Object max(Collection coll) ``` 这里是自然的通用签名,使用通配符来获得最大的灵活性(参见第 `3.2` 节): ```java - // 通用版本 - 打破二进制兼容性 - public static > - T max(Collection coll) +// 通用版本 - 打破二进制兼容性 +public static > T max(Collection coll) ``` -但是这个签名有错误的擦除 - 它的返回类型是 `Comparable` 而不是 `Object`。 为了获得正确的签名,我们需要使用多重边界来摆弄类型参数的边界(参见第 `3.6` 节)。 这是更正后的版本: +但是这个签名有错误的擦除 - 它的返回类型是 `Comparable` 而不是 `Object`。 为了获得正确的签名,我们需要使用多重边界来摆弄类型参数的边界(参见第 +`3.6` 节)。 这是更正后的版本: ```java - // 通用版本 - 保持二进制兼容性 - public static > - T max(Collection coll) +// 通用版本 - 保持二进制兼容性 +public static > T max(Collection coll) ``` 当有多个边界时,最左边界被用于擦除。 所以 `T` 的删除现在是 `Object`,给出了我们需要的结果类型。 -由于原始遗留代码包含的特定类型比它可能具有的特定类型要少,因此会出现一些与遗传有关的问题。 例如,`max` 的遗留版本可能已被赋予返回类型 `Comparable`,它比 `Object` 更具体,然后就不需要使用多重边界来调整类型。 +由于原始遗留代码包含的特定类型比它可能具有的特定类型要少,因此会出现一些与遗传有关的问题。 例如,`max` 的遗留版本可能已被赋予返回类型 `Comparable`, +它比 `Object` 更具体,然后就不需要使用多重边界来调整类型。 -桥梁另一个重要的角落案例与桥梁有关。 同样,`Comparable` 提供了一个很好的例子。实现 `Comparable` 的大多数遗留核心类提供了 `compareTo` 方法的两个重载:一个带有参数类型 `Object`,它重写接口中的 `compareTo` 方法; 和一个更具体的类型。 例如,以下是 `Integer` 旧版本的相关部分: +桥梁另一个重要的角落案例与桥梁有关。 同样,`Comparable` 提供了一个很好的例子。实现 `Comparable` 的大多数遗留核心类提供了 `compareTo` 方法的两个重 +载:一个带有参数类型 `Object`,它重写接口中的 `compareTo` 方法; 和一个更具体的类型。 例如,以下是 `Integer` 旧版本的相关部分: ```java - // 旧版本 - public class Integer implements Comparable { - public int compareTo(Object o) { ... } - public int compareTo(Integer i) { ... } - ... - } +// 旧版本 +public class Integer implements Comparable { + public int compareTo(Object o) { ... } + public int compareTo(Integer i) { ... } + ... +} ``` 这里是相应的通用版本: ```java - // 通用版本 - 保持二进制兼容性 - public final class Integer implements Comparable { - public int compareTo(Integer i) { ... } - ... - } +// 通用版本 - 保持二进制兼容性 +public final class Integer implements Comparable { + public int compareTo(Integer i) { ... } + ... +} ``` 两个版本都具有相同的字节码,因为编译器会为 `compareTo` 与 `Object` 类型的参数生成一个桥接方法(请参阅第 `3.7` 节)。 @@ -64,205 +67,157 @@ 但是,一些遗留代码仅包含 `Object` 方法。 (在泛型之前,一些程序员认为这比定义两种方法更简洁。)下面是 `javax.naming.Name` 的传统版本. ```java - // 旧版本 - public interface Name extends Comparable { - public int compareTo(Object o); - ... - } +// 旧版本 +public interface Name extends Comparable { + public int compareTo(Object o); + ... +} ``` 事实上,名称只与其他名称进行比较,所以我们可能希望以下通用版本: ```java - // 通用版本 - 打破二进制兼容性 - public interface Name extends Comparable { - public int compareTo(Name n); - ... - } +// 通用版本 - 打破二进制兼容性 +public interface Name extends Comparable { + public int compareTo(Name n); + ... +} ``` 但是,选择这种生成功能会破坏二进制兼容性。 由于遗留类包含 `compareTo(Object)` 而不是 `compareTo(Name)`,因此很可能用户可能已声明 `Name` 的实现提供前者而不是后者。 任何这样的类都不适用于上面给出的通用版本的名称。 唯一的解决办法是选择一个不那么雄心勃勃的基因工程: ```java - // 通用版本 - 保持二进制兼容性 - public interface Name extends Comparable { - public int compareTo(Object o) { ... } - ... - } +// 通用版本 - 保持二进制兼容性 +public interface Name extends Comparable { + public int compareTo(Object o) { ... } + ... +} ``` 这与旧版本有相同的擦除,并保证与用户可能已定义的任何子类兼容。 在前面的例子中,如果选择了更加雄心勃勃的基因鉴定,那么在运行时会出现错误,因为实现类没有实现 `compareTo(Name)`。 -但是在某些情况下,这种差异可能是阴险的:与其提出错误,可能会返回不同的值!例如,`Name` 可以通过 `SimpleName` 类来实现,其中一个简单名称由单个字符串,`base` 组成,并且比较两个简单名称比较基本名称。进一步说,`SimpleName` 有一个扩展名的子类,其中扩展名有一个基本字符串和一个扩展名。将扩展名与简单名称进行比较时只比较基本名称,而将扩展名称与另一扩展名称进行比较,比较基数,如果相等,则比较扩展名。假设我们 `Generify Name` 和 `SimpleName`,以便它们定义 `compareTo(Name)`,但我们没有 `ExtendedName` 的源。由于它只定义了 `compareTo(Object)`,所以调用 `compareTo(Name)` 而不是 `compareTo(Object)` 的客户端代码将在 `SimpleName`(定义它的位置)而不是 `ExtendedName`(它未定义的地方)上调用该方法,将被比较,但扩展忽略。这在示例 `8-2` 和示例 `8-3` 中进行了说明。 +但是在某些情况下,这种差异可能是阴险的:与其提出错误,可能会返回不同的值!例如,`Name` 可以通过 `SimpleName` 类来实现,其中一个简单名称由单个字符 +串,`base` 组成,并且比较两个简单名称比较基本名称。进一步说,`SimpleName` 有一个扩展名的子类,其中扩展名有一个基本字符串和一个扩展名。将扩展名与简单 +名称进行比较时只比较基本名称,而将扩展名称与另一扩展名称进行比较,比较基数,如果相等,则比较扩展名。假设我们 `Generify Name` 和 `SimpleName`,以便 +它们定义 `compareTo(Name)`,但我们没有 `ExtendedName` 的源。由于它只定义了 `compareTo(Object)`,所以调用 `compareTo(Name)` 而不是 +`compareTo(Object)` 的客户端代码将在 `SimpleName`(定义它的位置)而不是 `ExtendedName`(它未定义的地方)上调用该方法,将被比较,但扩展忽略。这在 +示例 `8-2` 和示例 `8-3` 中进行了说明。 -我们得到的教训是,除非您有信心可以兼容所有亚类,否则每次生成一个班时都要特别小心。请注意,如果将一个类声明为 `final` ,那么您有更多的余地,因为它不能有子类。 +我们得到的教训是,除非您有信心可以兼容所有亚类,否则每次生成一个类时都要特别小心。请注意,如果将一个类声明为 `final` ,那么您有更多的余地,因为它不能 +有子类。 -还要注意,如果原始的 `Name` 接口不仅声明了一般重载 `compareTo(Object)`,还声明了更具体的重载 `compareTo(Name)`,那么将需要旧版本的 `SimpleName` 和 `ExtendedName` 来实现 `compareTo(Name)` 和这里描述的问题不会出现。 +还要注意,如果原始的 `Name` 接口不仅声明了一般重载 `compareTo(Object)`,还声明了更具体的重载 `compareTo(Name)`,那么将需要旧版本的 `SimpleName` +和 `ExtendedName` 来实现 `compareTo(Name)` 和这里描述的问题不会出现。 -**协变覆盖**另一个角落案例与协变覆盖有关(见 `3.8` 节)。 回想一下,如果参数完全匹配,则一个方法可以重写另一个方法,但重写方法的返回类型是另一个方法的返回类型的子类型。 +**协变覆盖**另一个角落案例与协变覆盖有关(见 `3.8` 节)。 回想一下,如果参数完全匹配,则一个方法可以重写另一个方法,但重写方法的返回类型是另一个方法 +的返回类型的子类型。 这是克隆方法的一个应用: ```java - class Object { - public Object clone() { ... } - ... - } +class Object { + public Object clone() { ... } + ... +} ``` 以下是类 `HashSet` 的旧版本: ```java - // 旧版本 - class HashSet { - public Object clone() { ... } - ... - } +// 旧版本 +class HashSet { + public Object clone() { ... } + ... +} ``` 对于通用版本,您可能希望利用协变覆盖并为克隆选择更具体的返回类型: ```java - // 通用版本 - 打破二进制兼容性 - class HashSet { - public HashSet clone() { ... } - ... - } +// 通用版本 - 打破二进制兼容性 +class HashSet { + public HashSet clone() { ... } + ... +} ``` 例 `8-2`。 用于简单和扩展名称的传统代码 ```java - interface Name extends Comparable { - public int compareTo(Object o); - } - class SimpleName implements Name { - private String base; - public SimpleName(String base) { - this.base = base; - } - public int compareTo(Object o) { - return base.compareTo(((SimpleName)o).base); - } - } - class ExtendedName extends SimpleName { - private String ext; - public ExtendedName(String base, String ext) { - super(base); this.ext = ext; - } - public int compareTo(Object o) { - int c = super.compareTo(o); - if (c == 0 && o instanceof ExtendedName) - return ext.compareTo(((ExtendedName)o).ext); - else - return c; - } - } - class Client { - public static void main(String[] args) { - Name m = new ExtendedName("a","b"); - Name n = new ExtendedName("a","c"); - assert m.compareTo(n) < 0; - } - } +interface Name extends Comparable { + public int compareTo(Object o); +} +class SimpleName implements Name { + private String base; + public SimpleName(String base) { + this.base = base; + } + public int compareTo(Object o) { + return base.compareTo(((SimpleName)o).base); + } +} +class ExtendedName extends SimpleName { + private String ext; + public ExtendedName(String base, String ext) { + super(base); this.ext = ext; + } + public int compareTo(Object o) { + int c = super.compareTo(o); + if (c == 0 && o instanceof ExtendedName) + return ext.compareTo(((ExtendedName)o).ext); + else + return c; + } +} +class Client { + public static void main(String[] args) { + Name m = new ExtendedName("a","b"); + Name n = new ExtendedName("a","c"); + assert m.compareTo(n) < 0; + } +} ``` - + 例 `8-3`。 生成简单的名称和客户端,但不扩展名称 - + ```java - interface Name extends Comparable { - public int compareTo(Name o); - } - class SimpleName implements Name { - private String base; - public SimpleName(String base) { - this.base = base; - } - public int compareTo(Name o) { - return base.compareTo(((SimpleName)o).base); - } - } - - // use legacy class file for ExtendedName - class Test { - public static void main(String[] args) { - Name m = new ExtendedName("a","b"); - Name n = new ExtendedName("a","c"); - assert m.compareTo(n) == 0; // 答案现在不同! - } - } +interface Name extends Comparable { + public int compareTo(Name o); +} +class SimpleName implements Name { + private String base; + public SimpleName(String base) { + this.base = base; + } + public int compareTo(Name o) { + return base.compareTo(((SimpleName)o).base); + } +} + + // use legacy class file for ExtendedName +class Test { + public static void main(String[] args) { + Name m = new ExtendedName("a","b"); + Name n = new ExtendedName("a","c"); + assert m.compareTo(n) == 0; // 答案现在不同! + } +} ``` -但是,选择这种生成功能会破坏二进制兼容性。 用户很可能已经定义了覆盖克隆的 `HashSet` 的子类。 任何这样的子类都不适用于之前给出的 `HashSet` 的通用版本。 唯一的解决办法是选择一个不那么雄心勃勃的基因工程: +但是,选择这种生成功能会破坏二进制兼容性。 用户很可能已经定义了覆盖克隆的 `HashSet` 的子类。 任何这样的子类都不适用于之前给出的 `HashSet` 的通用版 +本。 唯一的解决办法是选择一个不那么雄心勃勃的基因工程: ```java - // 通用版本 - 保持二进制兼容性 - class HashSet { - public Object clone() { ... } - ... - } +// 通用版本 - 保持二进制兼容性 +class HashSet { + public Object clone() { ... } + ... +} ``` 这保证与用户可能定义的任何子类兼容。 同样,如果你还可以生成任何子类,或者如果这个类是最终的,那么你有更多的自由。 《《《 [下一节](../ch09/00_Design_Patterns.md)
《《《 [返回首页](../README.md) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ch09/00_Design_Patterns.md b/ch09/00_Design_Patterns.md index 93d575df..8177deaf 100644 --- a/ch09/00_Design_Patterns.md +++ b/ch09/00_Design_Patterns.md @@ -1,9 +1,10 @@ 《《《 [返回首页](../README.md)
《《《 [上一节](../ch08/04_Maintain_Binary_Compatibility.md) -## 设计模式 +### 设计模式 -本章回顾了五种着名的设计模式 - 访问者,解释器,函数,策略和主题观察者,并展示了他们如何利用泛型。函数模式概括了比较器接口的思想。 其他四种模式在 `Gamma`,`Helm`,`Johnson` 和 `Vlissides`(`Addison-Wesley`)的开创性设计模式中描述。 +本章回顾了五种着名的设计模式 - 访问者,解释器,函数,策略和主题观察者,并展示了他们如何利用泛型。函数模式概括了比较器接口的思想。 其他四种模式在 +`Gamma`,`Helm`,`Johnson` 和 `Vlissides`(`Addison-Wesley`)的开创性设计模式中描述。 《《《 [下一节](01_Visitor.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch09/01_Visitor.md b/ch09/01_Visitor.md index ec2a028f..cb4e02f7 100644 --- a/ch09/01_Visitor.md +++ b/ch09/01_Visitor.md @@ -1,179 +1,202 @@ 《《《 [返回首页](../README.md)
《《《 [上一节](00_Design_Patterns.md) -### 游客 +### 访问者 通常情况下,数据结构由案例分析和递归定义。例如,`Tree` 类型的二叉树是以下之一: - - 一个叶子,包含E类型的单个值 - - 包含一个左子树和一个右子树的分支,都是 `Tree` 类型很容易想到很多其他示例:形状可以是三角形,矩形,两种形状的组合或转换形状; `XML` 节点可以是文本节点,属性节点或元素节点(可能包含其他节点);等等。 +- 一个叶子,包含 `E` 类型的单个值 +- 包含一个左子树和一个右子树的分支,都是 `Tree` 类型很容易想到很多其他示例:形状可以是三角形,矩形,两种形状的组合或转换形状; `XML` 节点可以是文 +本节点,属性节点或元素节点(可能包含其他节点);等等。 -为了以面向对象的语言来表示这样的结构,数据结构由抽象类表示,并且每个事例由子类表示。抽象类为数据结构上的每个可能的操作声明一个抽象方法,并且每个子类都根据相应的情况实现该方法。 +为了以面向对象的语言来表示这样的结构,数据结构由抽象类表示,并且每个事例由子类表示。抽象类为数据结构上的每个可能的操作声明一个抽象方法,并且每个子类 +都根据相应的情况实现该方法。 -例 `9-1` 说明了应用于树的这种技术。有一个抽象类 `Tree`,它有两个抽象方法,`toString` 和 `sum`。 (前者适用于任何树,后者仅适用于数字树 - 为了简单起见,这种限制是在运行时通过强制转换强制执行的,而不是编译时的类型,如后面讨论的那样)。有两个静态工厂方法,一个构造一个叶子,一个构造分支。其中每一个都包含一个嵌套类,它扩展了 `Tree` 并实现了每个方法 `toString` 和 `sum`。 +例 `9-1` 说明了应用于树的这种技术。有一个抽象类 `Tree`,它有两个抽象方法,`toString` 和 `sum`。 (前者适用于任何树,后者仅适用于数字树 - 为了简 +单起见,这种限制是在运行时通过强制转换强制执行的,而不是编译时的类型,如后面讨论的那样)。有两个静态工厂方法,一个构造一个叶子,一个构造分支。其中每 +一个都包含一个嵌套类,它扩展了 `Tree` 并实现了每个方法 `toString` 和 `sum`。 如果您事先知道数据结构上所需的所有操作,或者可以修改需求更改时定义结构的类,则此方法就足够了。 但是,有时情况并非如此,特别是当不同的开发人员时。 例 `9-1`。 一棵简单的树和客户端 ```java - abstract class Tree { - abstract public String toString(); - abstract public Double sum(); - public static Tree leaf(final E e) { - return new Tree() { - public String toString() { - return e.toString(); - } - public Double sum() { - return ((Number)e).doubleValue(); - } - }; - } - public static Tree branch(final Tree l, final Tree r) { - return new Tree() { - public String toString() { - return "("+l.toString()+"^"+r.toString()+")"; - } - public Double sum() { - return l.sum() + r.sum(); - } - }; - } - } - class TreeClient { - public static void main(String[] args) { - Tree t = Tree.branch(Tree.branch(Tree.leaf(1), Tree.leaf(2)), Tree.leaf(3)); - assert t.toString().equals("((1^2)^3)"); - assert t.sum() == 6; - } - } +abstract class Tree { + abstract public String toString(); + abstract public Double sum(); + public static Tree leaf(final E e) { + return new Tree() { + public String toString() { + return e.toString(); + } + public Double sum() { + return ((Number)e).doubleValue(); + } + }; + } + public static Tree branch(final Tree l, final Tree r) { + return new Tree() { + public String toString() { + return "("+l.toString()+"^"+r.toString()+")"; + } + public Double sum() { + return l.sum() + r.sum(); + } + }; + } +} +class TreeClient { + public static void main(String[] args) { + Tree t = Tree.branch(Tree.branch(Tree.leaf(1), Tree.leaf(2)), Tree.leaf(3)); + assert t.toString().equals("((1^2)^3)"); + assert t.sum() == 6; + } +} ``` 负责定义结构的类和作为结构客户的类。 -`Visitor` 模式可以在不修改定义数据结构的类的情况下提供新的操作。 在这种模式中,表示结构的抽象类声明了抽象访问方法,它将访问者作为参数。访问者实现了一个接口,该接口为结构规范中的每个个案指定一种方法。 每个子类通过调用相应案例的访问者方法来实现访问方法。 +`Visitor` 模式可以在不修改定义数据结构的类的情况下提供新的操作。 在这种模式中,表示结构的抽象类声明了抽象访问方法,它将访问者作为参数。访问者实现了 +一个接口,该接口为结构规范中的每个个案指定一种方法。 每个子类通过调用相应案例的访问者方法来实现访问方法。 例 `9-2`。 与访客的一棵树 ```java - abstract class Tree { - public interface Visitor { - public R leaf(E elt); - public R branch(R left, R right); - } - public abstract R visit(Visitor v); - public static Tree leaf(final T e) { - return new Tree() { - public R visit(Visitor v) { - return v.leaf(e); - } - }; - } - public static Tree branch(final Tree l, final Tree r) { - return new Tree() { - public R visit(Visitor v) { - return v.branch(l.visit(v), r.visit(v)); - } - }; - } - } +abstract class Tree { + public interface Visitor { + public R leaf(E elt); + public R branch(R left, R right); + } + public abstract R visit(Visitor v); + public static Tree leaf(final T e) { + return new Tree() { + public R visit(Visitor v) { + return v.leaf(e); + } + }; + } + public static Tree branch(final Tree l, final Tree r) { + return new Tree() { + public R visit(Visitor v) { + return v.branch(l.visit(v), r.visit(v)); + } + }; + } +} ``` -例 `9-2` 说明了应用于树的这种模式。现在抽象类 `Tree` 只有一个抽象方法 `visit`,它接受 `Visitor` 类型的参数。接口 `Visitor` 指定了两个方法,一个接受类型值的叶方法 `E` 并返回一个 `R` 类型的值,以及一个接受两个 `R` 类型值并返回 `R` 类型值的分支方法。叶子对应的子类通过调用叶子元素上的访问者的叶子方法来实现访问,而分支对应的子类通过调用访问者的递归调用的结果调用访问者的分支方法来实现访问左边和右边的子树。 +例 `9-2` 说明了应用于树的这种模式。现在抽象类 `Tree` 只有一个抽象方法 `visit`,它接受 `Visitor` 类型的参数。接口 `Visitor` 指定了 +两个方法,一个接受类型值的叶方法 `E` 并返回一个 `R` 类型的值,以及一个接受两个 `R` 类型值并返回 `R` 类型值的分支方法。叶子对应的子类通过调用叶子元素 +上的访问者的叶子方法来实现访问,而分支对应的子类通过调用访问者的递归调用的结果调用访问者的分支方法来实现访问左边和右边的子树。 -例 `9-3` 说明了如何在客户端树中实现 `toString` 和 `sum` 方法,而不是在定义数据结构的类中。而在这些方法之前,以树为接收方,现在它们是以树为参数的静态方法。 +例 `9-3` 说明了如何在客户端树中实现 `toString` 和 `sum` 方法,而不是在定义数据结构的类中。而在这些方法之前,以树为接收方,现在它们是以树为参数的静 +态方法。 -这两种方法之间存在令人愉快的双重性。对于简单树,每个工厂方法(叶和分支)将每个运算符方法(`toString` 和 `sum`)的定义分组在一起。对于具有访问者的树,每个运算符方法(`toString` 和 `sum`)将每个访问者方法(叶和分支)的定义分组在一起。 +这两种方法之间存在令人愉快的双重性。对于简单树,每个工厂方法(叶和分支)将每个运算符方法(`toString` 和 `sum`)的定义分组在一起。对于具有访问者的 +树,每个运算符方法(`toString` 和 `sum`)将每个访问者方法(叶和分支)的定义分组在一起。 -使用泛型时,每个访问者都有两个类型参数,一个用于树的元素类型,另一个用于访问者的返回类型。没有泛型,每个访问者都必须返回 `Object` 类型的结果,并且需要许多额外的演员。因此,当仿制药不存在时,访问者经常被设计为不返回价值;相反,结果会累积在访问者的本地变量中,从而使数据在整个程序中的流动变得复杂。 +使用泛型时,每个访问者都有两个类型参数,一个用于树的元素类型,另一个用于访问者的返回类型。没有泛型,每个访问者都必须返回 `Object` 类型的结果,并且需 +要许多额外的演员。因此,当仿制药不存在时,访问者经常被设计为不返回价值;相反,结果会累积在访问者的本地变量中,从而使数据在整个程序中的流动变得复杂。 例 `9-3`。有访客的客户 ```java - class TreeClient { - public static String toString(Tree t) { - return t.visit(new Tree.Visitor() { - public String leaf(T e) { - return e.toString(); - } - public String branch(String l, String r) { - return "("+l+"^"+r+")"; - } - }); - } - public static double sum(Tree t) { - return t.visit(new Tree.Visitor() { - public Double leaf(N e) { - return e.doubleValue(); - } - public Double branch(Double l, Double r) { - return l+r; - } - }); - } - public static void main(String[] args) { - Tree t = Tree.branch(Tree.branch(Tree.leaf(1), - Tree.leaf(2)), - Tree.leaf(3)); - assert toString(t).equals("((1^2)^3)"); - assert sum(t) == 6; - } - } +class TreeClient { + public static String toString(Tree t) { + return t.visit(new Tree.Visitor() { + public String leaf(T e) { + return e.toString(); + } + public String branch(String l, String r) { + return "("+l+"^"+r+")"; + } + }); + } + public static double sum(Tree t) { + return t.visit(new Tree.Visitor() { + public Double leaf(N e) { + return e.doubleValue(); + } + public Double branch(Double l, Double r) { + return l+r; + } + }); + } + public static void main(String[] args) { + Tree t = Tree.branch(Tree.branch(Tree.leaf(1), + Tree.leaf(2)), + Tree.leaf(3)); + assert toString(t).equals("((1^2)^3)"); + assert sum(t) == 6; + } +} ``` -有趣的是,总和方法的通用类型对访问者可能更精确。 对于简单的树,`sum` 方法必须有一个类型签名,表明它可以处理任何元素类型; 需要强制转换以将每个叶转换为 `Number` 类型; 如果在不包含数字的树上调用 `sum`,则会在运行时引发类转换错误。 对于访问者来说,`sum` 方法可能有一个类型签名,表示它只对数字元素有效; 不需要演员; 如果在不包含数字的树上调用 `sum`,则会在编译时报告类型错误。 +有趣的是,总和方法的通用类型对访问者可能更精确。 对于简单的树,`sum` 方法必须有一个类型签名,表明它可以处理任何元素类型; 需要强制转换以将每个叶转换为 +`Number` 类型; 如果在不包含数字的树上调用 `sum`,则会在运行时引发类转换错误。 对于访问者来说,`sum` 方法可能有一个类型签名,表示它只对数字元素有效; +不需要实例; 如果在不包含数字的树上调用 `sum`,则会在编译时报告类型错误。 -实际上,您通常会使用简单方法和访问者模式的组合。 例如,您可以选择使用简单的方法定义标准方法,比如 `toString`,而将 `Visitor` 用于其他方法(如 `sum`)。 +实际上,您通常会使用简单方法和访问者模式的组合。 例如,您可以选择使用简单的方法定义标准方法,比如 `toString`,而将 `Visitor` 用于其他方法(如 +`sum`)。 例 `9-4`。 一位具有泛型的口译员 ```java - class Pair { - private final A left; - private final B right; - public Pair(A l, B r) { left=l; right=r; } - public A left() { return left; } - public B right() { return right; } - } - abstract class Exp { - abstract public T eval(); - static Exp lit(final int i) { - return new Exp() { public Integer eval() { return i; } }; - } - static Exp plus(final Exp e1, final Exp e2) { - return new Exp() { - public Integer eval() { - return e1.eval()+e2.eval(); - } - }; - } - static Exp> pair(final Exp e1, final Exp e2) { - return new Exp>() { - public Pair eval() { - return new Pair(e1.eval(), e2.eval()); - } - }; - } - static Exp left(final Exp> e) { - return new Exp() { - public A eval() { - return e.eval().left(); - } - }; - } - static Exp right(final Exp> e) { - return new Exp() { - public B eval() { - return e.eval().right(); - } - }; - } - public static void main(String[] args) { - Exp e = left(pair(plus(lit(3),lit(4)),lit(5))); - assert e.eval() == 7; - } - } +class Pair { + private final A left; + private final B right; + public Pair(A l, B r) { + left=l; right=r; + } + public A left() { + return left; + } + public B right() { + return right; + } +} +abstract class Exp { + abstract public T eval(); + static Exp lit(final int i) { + return new Exp() { + public Integer eval() { + return i; + } + }; + } + static Exp plus(final Exp e1, final Exp e2) { + return new Exp() { + public Integer eval() { + return e1.eval()+e2.eval(); + } + }; + } + static Exp> pair(final Exp e1, final Exp e2) { + return new Exp>() { + public Pair eval() { + return new Pair(e1.eval(), e2.eval()); + } + }; + } + static Exp left(final Exp> e) { + return new Exp() { + public A eval() { + return e.eval().left(); + } + }; + } + static Exp right(final Exp> e) { + return new Exp() { + public B eval() { + return e.eval().right(); + } + }; + } + public static void main(String[] args) { + Exp e = left(pair(plus(lit(3),lit(4)),lit(5))); + assert e.eval() == 7; + } +} ``` 《《《 [下一节](02_Interpreter.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch09/03_Function.md b/ch09/03_Function.md index 61cec220..c892ac82 100644 --- a/ch09/03_Function.md +++ b/ch09/03_Function.md @@ -7,104 +7,117 @@ 函数模式的通用版本演示了如何在方法声明的 `throws` 子句中使用类型变量。当一个类的不同实例包含可能引发不同检查异常的方法时,这可能很有用。 -回想一下 `Throwable` 类有两个主要的子类 `Exception` 和 `Error`,其中第一个子类有另一个主要的子类 `RuntimeException`。检查异常是否是 `RuntimeException` 或 `Error` 的子类。方法的 `throws` 子句可以列出 `Throwable` 的任何子类,但必须列出方法体可能抛出的任何检查异常,包括为正文中调用的方法声明的任何检查异常。 +回想一下 `Throwable` 类有两个主要的子类 `Exception` 和 `Error`,其中第一个子类有另一个主要的子类 `RuntimeException`。检查异常是否是 +`RuntimeException` 或 `Error` 的子类。方法的 `throws` 子句可以列出 `Throwable` 的任何子类,但必须列出方法体可能抛出的任何检查异常,包括为正文中 +调用的方法声明的任何检查异常。 -例 `9-5` 给出了一个在 `throws` 子句中使用类型变量的例子。该示例定义了一个接口 `Function`,它表示一个函数。接口包含一个方法 `apply`,它接受 `A` 类型的参数,返回类型 `B` 的结果,并且可以抛出类型的异常 `X` 。还有一个包含 `applyAll`方法的类。 +例 `9-5` 给出了一个在 `throws` 子句中使用类型变量的例子。该示例定义了一个接口 `Function`,它表示一个函数。接口包含一个方法 `apply`,它接受 +`A` 类型的参数,返回类型 `B` 的结果,并且可以抛出类型的异常 `X` 。还有一个包含 `applyAll`方法的类。 例 `9-5`。在 `throws` 子句中输入参数 ```java - import java.util.*; - import java.lang.reflect.*; - interface Function { - public B apply(A x) throws X; - } - class Functions { - public static List applyAll(Function f, List
list) throws X { - List result = new ArrayList(list.size()); - for (A x : list) - result.add(f.apply(x)); - return result; +import java.util.*; +import java.lang.reflect.*; +interface Function { + public B apply(A x) throws X; +} +class Functions { + public static List applyAll(Function f, List list) throws X { + List result = new ArrayList(list.size()); + for (A x : list) + result.add(f.apply(x)); + return result; + } + public static void main(String[] args) { + Function length = new Function() { + public Integer apply(String s) { + return s.length(); + } + }; + Function, ClassNotFoundException> forName = new Function, ClassNotFoundException>() { + public Class apply(String s) throws ClassNotFoundException { + return Class.forName(s); } - public static void main(String[] args) { - Function length = new Function() { - public Integer apply(String s) { - return s.length(); - } - }; - Function, ClassNotFoundException> forName = new Function, ClassNotFoundException>() { - public Class apply(String s) throws ClassNotFoundException { - return Class.forName(s); - } - }; - Function getRunMethod = new Function() { - public Method apply(String s) throws ClassNotFoundException,NoSuchMethodException { - return Class.forName(s).getMethod("run"); - } - }; - List strings = Arrays.asList(args); - System.out.println(applyAll(length, strings)); - try { - System.out.println(applyAll(forName, strings)); - } catch (ClassNotFoundException e) { - System.out.println(e); - } - try { - System.out.println(applyAll(getRunMethod, strings)); - } catch (ClassNotFoundException e) { - System.out.println(e); - } catch (NoSuchMethodException e) { - System.out.println(e); - } catch (RuntimeException e) { - throw e; - } catch (Exception e) { - throw new AssertionError(); - } + }; + Function getRunMethod = new Function() { + public Method apply(String s) throws ClassNotFoundException,NoSuchMethodException { + return Class.forName(s).getMethod("run"); } + }; + List strings = Arrays.asList(args); + System.out.println(applyAll(length, strings)); + try { + System.out.println(applyAll(forName, strings)); + } catch (ClassNotFoundException e) { + System.out.println(e); + } + try { + System.out.println(applyAll(getRunMethod, strings)); + } catch (ClassNotFoundException e) { + System.out.println(e); + } catch (NoSuchMethodException e) { + System.out.println(e); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new AssertionError(); } + } +} ``` -它接受 `List` 类型的参数,返回 `List` 类型的结果,并且可能再次引发 `X` 类型的异常; 该方法在参数列表的每个元素上调用 `apply` 方法来生成结果列表。 +它接受 `List` 类型的参数,返回 `List` 类型的结果,并且可能再次引发 `X` 类型的异常; 该方法在参数列表的每个元素上调用 `apply` 方法来生成结果列 +表。 -该类的主要方法定义了这种类型的三个对象。 第一个是 `Function` 类型的长度。 它接受一个字符串并返回一个整数,它是给定字符串的长度。 由于它没有引发检查异常,因此第三种类型设置为错误。(将其设置为 `RuntimeException` 也可以。) +该类的主要方法定义了这种类型的三个对象。 第一个是 `Function` 类型的长度。 它接受一个字符串并返回一个整数,它是给定字符串的长 +度。 由于它没有引发检查异常,因此第三种类型设置为错误。(将其设置为 `RuntimeException` 也可以。) -第二个是 `forName` 类型的 `Function,ClassNotFoundException>`。 它接受一个字符串并返回一个类,即由给定字符串命名的类。 `apply` 方法可能会抛出一个 `ClassNotFoundException`,所以这被当作第三个类型参数。 +第二个是 `forName` 类型的 `Function,ClassNotFoundException>`。 它接受一个字符串并返回一个类,即由给定字符串命名的类。 +`apply` 方法可能会抛出一个 `ClassNotFoundException`,所以这被当作第三个类型参数。 -第三个是 `Function` 类型的 `getRunMethod`。它接受一个字符串并返回一个方法,即在给定字符串所指定的类中名为 `run` 的方法。该方法的主体可能会引发 `ClassNotFoundException` 或 `NoSuchMethodException`,因此第三个类型参数将被视为 `Exception`,这是包含这两个异常的最小类。 +第三个是 `Function` 类型的 `getRunMethod`。它接受一个字符串并返回一个方法,即在给定字符串所指定的类中名为 `run` 的方 +法。该方法的主体可能会引发 `ClassNotFoundException` 或 `NoSuchMethodException`,因此第三个类型参数将被视为 `Exception`,这是包含这两个异常的最 +小类。 -最后一个例子显示了将泛型类型用于异常的主要限制。通常没有合适的类或接口包含函数可能引发的所有异常,因此您不得不重新使用异常,这太笼统,无法提供有用的信息。 +最后一个例子显示了将泛型类型用于异常的主要限制。通常没有合适的类或接口包含函数可能引发的所有异常,因此您不得不重新使用异常,这太笼统,无法提供有用的 +信息。 -主要方法使用 `applyAll` 将三个函数中的每一个应用于字符串列表。三个调用中的每一个都被包装在一个 `try` 语句中,该语句适合于它可能抛出的异常。长度函数没有 `try` 语句,因为它不引发检查异常。 `forName` 函数有一个带有 `ClassNotFoundException` 的 `catch` 子句的 `try` 语句,它可能抛出一种异常。`getRunMethod` 函数需要一个带有 `catch` 子句的 `try` 语句,用于 `ClassNotFoundException` 和 `NoSuchMethodException`,它可能抛出两种异常。但是该函数被声明为抛出 `Exception` 类型,所以我们需要两个额外的“catchall”子句,一个重新抛出引发的任何运行时异常,另一个断言如果发生任何未处理的异常由前三条规定。对于这个特定的例子,不需要重新提升运行时异常,但是如果可能有其他代码处理这些异常,这是一个好习惯。 +主要方法使用 `applyAll` 将三个函数中的每一个应用于字符串列表。三个调用中的每一个都被包装在一个 `try` 语句中,该语句适合于它可能抛出的异常。长度函数 +没有 `try` 语句,因为它不引发检查异常。 `forName` 函数有一个带有 `ClassNotFoundException` 的 `catch` 子句的 `try` 语句,它可能抛出一种异常。 +`getRunMethod` 函数需要一个带有 `catch` 子句的 `try` 语句,用于 `ClassNotFoundException` 和 `NoSuchMethodException`,它可能抛出两种异常。但是 +该函数被声明为抛出 `Exception` 类型,所以我们需要两个额外的“catchall”子句,一个重新抛出引发的任何运行时异常,另一个断言如果发生任何未处理的异常由前 +三条规定。对于这个特定的例子,不需要重新提升运行时异常,但是如果可能有其他代码处理这些异常,这是一个好习惯。 例如,下面是典型的代码运行,打印长度列表,类别列表和方法列表(最后一个列表为了便于阅读而重新格式化,因为它不适合一行): ```java - % java Functions java.lang.Thread java.lang.Runnable - [16, 18] - [class java.lang.Thread, interface java.lang.Runnable] - [public void java.lang.Thread.run(), - public abstract void java.lang.Runnable.run()] +% java Functions java.lang.Thread java.lang.Runnable +[16, 18] +[class java.lang.Thread, interface java.lang.Runnable] +[public void java.lang.Thread.run(), +public abstract void java.lang.Runnable.run()] ``` 这是一个引发 `NoSuchMethodException` 的运行,因为 `java.util.List` 没有 `run` 方法: ```java - % java Functions java.lang.Thread java.util.List - [16, 14] - [class java.lang.Thread, interface java.util.List] - java.lang.NoSuchMethodException: java.util.List.run() +% java Functions java.lang.Thread java.util.List +[16, 14] +[class java.lang.Thread, interface java.util.List] +java.lang.NoSuchMethodException: java.util.List.run() ``` 这是一个引发 `ClassNotFoundException` 的运行,因为没有名为 `Fred` 的类: ```java - % java Functions java.lang.Thread Fred - [16, 4] - java.lang.ClassNotFoundException: Fred - java.lang.ClassNotFoundException: Fred +% java Functions java.lang.Thread Fred +[16, 4] +java.lang.ClassNotFoundException: Fred +java.lang.ClassNotFoundException: Fred ``` 该异常会引发两次,一次是在应用 `forName` 时,一次是在应用 `getRunMethod` 时引发的。 《《《 [下一节](04_Strategy.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch09/04_Strategy.md b/ch09/04_Strategy.md index 8195585c..9f536efc 100644 --- a/ch09/04_Strategy.md +++ b/ch09/04_Strategy.md @@ -3,226 +3,274 @@ ### 策略 -策略模式用于将方法与对象分离,使您可以提供许多可能的方法实例。我们对战略模式的讨论说明了许多面向对象程序中的结构化技术,即并行类层次结构。我们将通过考虑纳税人如何应用不同的税收策略来说明战略模式。纳税人将会有一个等级制度,并且有一个相关的税收策略等级制度。例如,有一个适用于任何纳税人的默认策略。纳税人的一个子类是信任,而默认策略的一个子类只适用于信任。 +策略模式用于将方法与对象分离,使您可以提供许多可能的方法实例。我们对战略模式的讨论说明了许多面向对象程序中的结构化技术,即并行类层次结构。我们将通过 +考虑纳税人如何应用不同的税收策略来说明战略模式。纳税人将会有一个等级制度,并且有一个相关的税收策略等级制度。例如,有一个适用于任何纳税人的默认策略。 +纳税人的一个子类是信任,而默认策略的一个子类只适用于信任。 -我们的讨论还将说明一种常用类型的技术 - 使用带递归边界的类型变量。我们已经在Comparable接口和Enum类的定义中看到了这个技巧;这里我们将用它来阐明纳税人与相关税收策略之间的关系。我们还解释了 `getThis` 技巧,它允许我们为具有递归边界的类型变量出现时为此指定更精确的类型。 +我们的讨论还将说明一种常用类型的技术 - 使用带递归边界的类型变量。我们已经在 `Comparable` 接口和 `Enum` 类的定义中看到了这个技巧;这里我们将用它来阐 +明纳税人与相关税收策略之间的关系。我们还解释了 `getThis` 技巧,它允许我们为具有递归边界的类型变量出现时为此指定更精确的类型。 -首先,我们将看看策略模式的基本版本,该模式展示了如何使用泛型来设计并行类层次结构。接下来,我们将看一个高级版本,其中对象包含自己的策略,它使用具有递归边界的类型变量并解释 `getThis` 技巧。 +首先,我们将看看策略模式的基本版本,该模式展示了如何使用泛型来设计并行类层次结构。接下来,我们将看一个高级版本,其中对象包含自己的策略,它使用具有递 +归边界的类型变量并解释 `getThis` 技巧。 本节中的例子是在与 `Heinz M. Kabutz` 的讨论中开发的,也出现在他的在线出版物 `The Java Specialists'Newsletter` 中。 -并行类层次结构策略模式的典型用法是税收计算,如例 `9-6` 所示。我们有一个带有 `Person` 和 `Trust` 子类的 `TaxPayer` 类。每个纳税人都有收入,此外,信托可能是非营利性的。例如,我们 +并行类层次结构策略模式的典型用法是税收计算,如例 `9-6` 所示。我们有一个带有 `Person` 和 `Trust` 子类的 `TaxPayer` 类。每个纳税人都有收入,此外,信 +托可能是非营利性的。例如,我们 例 `9-6`。具有并行类层次结构的基本策略模式 ```java - abstract class TaxPayer { - public long income; // in cents - public TaxPayer(long income) { this.income = income; } - public long getIncome() { return income; } - } - class Person extends TaxPayer { - public Person(long income) { super(income); } - } - class Trust extends TaxPayer { - private boolean nonProfit; - public Trust(long income, boolean nonProfit) { - super(income); this.nonProfit = nonProfit; - } - public boolean isNonProfit() { return nonProfit; } - } - interface TaxStrategy

{ - public long computeTax(P p); - } - class DefaultTaxStrategy

implements TaxStrategy

{ - private static final double RATE = 0.40; - public long computeTax(P payer) { - return Math.round(payer.getIncome() * RATE); - } - } - class DodgingTaxStrategy

implements TaxStrategy

{ - public long computeTax(P payer) { return 0; } - } - class TrustTaxStrategy extends DefaultTaxStrategy { - public long computeTax(Trust trust) { - return trust.isNonProfit() ? 0 : super.computeTax(trust); - } - } +abstract class TaxPayer { + public long income; // in cents + public TaxPayer(long income) { + this.income = income; + } + public long getIncome() { + return income; + } +} +class Person extends TaxPayer { + public Person(long income) { + super(income); + } +} +class Trust extends TaxPayer { + private boolean nonProfit; + public Trust(long income, boolean nonProfit) { + super(income); + this.nonProfit = nonProfit; + } + public boolean isNonProfit() { + return nonProfit; + } +} +interface TaxStrategy

{ + public long computeTax(P p); +} +class DefaultTaxStrategy

implements TaxStrategy

{ + private static final double RATE = 0.40; + public long computeTax(P payer) { + return Math.round(payer.getIncome() * RATE); + } +} +class DodgingTaxStrategy

implements TaxStrategy

{ + public long computeTax(P payer) { + return 0; + } +} +class TrustTaxStrategy extends DefaultTaxStrategy { + public long computeTax(Trust trust) { + return trust.isNonProfit() ? 0 : super.computeTax(trust); + } +} ``` -可以定义一个收入为 `100,000.00` 美元的人和两个收入相同的信托人,一个非营利组织和一个非营利组织。 +可以定义一个收入为 `100,000.00` 美元的人和两个收入相同的信托人,一个非营利组织和一个营利组织。 ```java - Person person = new Person(10000000); - Trust nonProfit = new Trust(10000000, true); - Trust forProfit = new Trust(10000000, false); +Person person = new Person(10000000); +Trust nonProfit = new Trust(10000000, true); +Trust forProfit = new Trust(10000000, false); ``` -按照良好的做法,我们用长整数表示所有的货币价值,例如收入或税收,以毫秒为单位表示价值(参见“有效性的一般编程”一章中的“避免浮动和双重,如果需要确切答案” `Java` 由 `Joshua Bloch`,`Addison-Wesley`编写)。 +按照良好的做法,我们用长整数表示所有的货币价值,例如收入或税收,以毫秒为单位表示价值(参见“有效性的一般编程”一章中的“避免浮动和双重,如果需要确切答 +案” `Java` 由 `Joshua Bloch`,`Addison-Wesley`编写)。 -对于每个纳税人` P`,可能有许多可能的计算税收策略。 每个策略实现接口 `TaxStrategy

`,该接口指定一个方法计算 `Tax`,该方法将 `P` 作为纳税人的参数并返回所支付的税金。 类`DefaultTaxStrategy` 通过将收入乘以 `40%` 的固定税率来计算税额,而 `DodgingTaxStrategy` 则始终计算零税额: +对于每个纳税人` P`,可能有许多可能的计算税收策略。 每个策略实现接口 `TaxStrategy

`,该接口指定一个方法计算 `Tax`,该方法将 `P` 作为纳税人的参数 +并返回所支付的税金。 类`DefaultTaxStrategy` 通过将收入乘以 `40%` 的固定税率来计算税额,而 `DodgingTaxStrategy` 则始终计算零税额: ```java - TaxStrategy defaultStrategy = new DefaultStrategy(); - TaxStrategy dodgingStrategy = new DodgingStrategy(); - assert defaultStrategy.computeTax(person) == 4000000; - assert dodgingStrategy.computeTax(person) == 0; +TaxStrategy defaultStrategy = new DefaultStrategy(); +TaxStrategy dodgingStrategy = new DodgingStrategy(); +assert defaultStrategy.computeTax(person) == 4000000; +assert dodgingStrategy.computeTax(person) == 0; ``` -当然,我们的例子是为了说明而简化的 - 我们不建议您使用这些策略中的任何一种来计算税收! 但是应该清楚这些技术是如何延伸到更复杂的纳税人和税收策略的。最后,如果信托是非营利组织,并且使用默认税策略,则 `TrustTaxStrategy` 类计算零税额: +当然,我们的例子是为了说明而简化的 - 我们不建议您使用这些策略中的任何一种来计算税收! 但是应该清楚这些技术是如何延伸到更复杂的纳税人和税收策略的。最 +后,如果信托是非营利组织,并且使用默认税策略,则 `TrustTaxStrategy` 类计算零税额: ```java - TaxStrategy trustStrategy = new TrustTaxStrategy(); - assert trustStrategy.computeTax(nonProfit) == 0; - assert trustStrategy.computeTax(forProfit) == 4000000; +TaxStrategy trustStrategy = new TrustTaxStrategy(); +assert trustStrategy.computeTax(nonProfit) == 0; +assert trustStrategy.computeTax(forProfit) == 4000000; ``` 泛型允许我们将给定的税收策略专门化为给定类型的纳税人,并允许编制者检测何时将税策略应用于错误类型的纳税人: ```java - trustStrategy.computeTax(person); // 编译报错 +trustStrategy.computeTax(person); // 编译报错 ``` -没有泛型,`TrustTaxStrategy` 的 `computeTax` 方法将不得不接受 `TaxPayer` 类型的参数并将其转换为 `Trust` 类型,并且错误会在运行时抛出异常,而不是在编译时捕获。 +没有泛型,`TrustTaxStrategy` 的 `computeTax` 方法将不得不接受 `TaxPayer` 类型的参数并将其转换为 `Trust` 类型,并且错误会在运行时抛出异常,而不是 +在编译时捕获。 -这个例子说明了许多面向对象程序中的结构化技术 - 即并行类层次结构。在这种情况下,一个类层次结构由 `TaxPayer`,`Person` 和 `Trust` 组成。并行类层次结构由与以下每个策略相对应的策略组成:两种策略:`DefaultTaxStrategy` 和 `DutingTaxStrategy` 适用于任何 `TaxPayer`,没有专门的策略适用于 `Person`,并且 `Trust` 有一个专门的策略。 +这个例子说明了许多面向对象程序中的结构化技术 - 即并行类层次结构。在这种情况下,一个类层次结构由 `TaxPayer`,`Person` 和 `Trust` 组成。并行类层次结 +构由与以下每个策略相对应的策略组成:两种策略:`DefaultTaxStrategy` 和 `DutingTaxStrategy` 适用于任何 `TaxPayer`,没有专门的策略适用于 +`Person`,并且 `Trust` 有一个专门的策略。 -通常,这种并行层次结构之间存在某种联系。在这种情况下,与给定 `TaxPayer` 并行的 `TaxStrategy` 的 `computeTax` 方法需要具有相应类型的参数;例如,`TrustTaxStrategy` 的 `computeTax` 方法需要一个 `Trust` 类型的参数。对于泛型,我们可以在类型本身中整齐地捕捉这个连接。在这种情况下,`TaxStrategy

` 的 `computeTax` 方法需要 `P` 类型的参数,其中 `P` 必须是 `TaxPayer` 的子类。使用我们在此描述的技术,泛型通常可用于捕获其他并行类层次结构中的类似关系。 +通常,这种并行层次结构之间存在某种联系。在这种情况下,与给定 `TaxPayer` 并行的 `TaxStrategy` 的 `computeTax` 方法需要具有相应类型的参数;例如, +`TrustTaxStrategy` 的 `computeTax` 方法需要一个 `Trust` 类型的参数。对于泛型,我们可以在类型本身中整齐地捕捉这个连接。在这种情况下, +`TaxStrategy

` 的 `computeTax` 方法需要 `P` 类型的参数,其中 `P` 必须是 `TaxPayer` 的子类。使用我们在此描述的技术,泛型通常可用于捕获其他并行 +类层次结构中的类似关系。 -具有递归泛型的高级策略模式在策略模式的更高级用途中,对象包含要应用于其的策略。建模这种情况需要递归泛型类型,并利用一种技巧为此指定一个泛型类型。修订后的战略模式如示例 `9-7` 所示。在高级版本中,每个纳税人对象都包含自己的税收策略,每种纳税人的构造函数都包含一个税收策略作为附加参数: +具有递归泛型的高级策略模式在策略模式的更高级用途中,对象包含要应用于其的策略。建模这种情况需要递归泛型类型,并利用一种技巧为此指定一个泛型类型。修订 +后的战略模式如示例 `9-7` 所示。在高级版本中,每个纳税人对象都包含自己的税收策略,每种纳税人的构造函数都包含一个税收策略作为附加参数: ```java - Person normal = new Person(10000000, new DefaultTaxStrategy()); - Person dodger = new Person(10000000, new DodgingTaxStrategy()); - Trust nonProfit = new Trust(10000000, true, new TrustTaxStrategy()); - Trust forProfit = new Trust(10000000, false, new TrustTaxStrategy()); +Person normal = new Person(10000000, new DefaultTaxStrategy()); +Person dodger = new Person(10000000, new DodgingTaxStrategy()); +Trust nonProfit = new Trust(10000000, true, new TrustTaxStrategy()); +Trust forProfit = new Trust(10000000, false, new TrustTaxStrategy()); ``` 现在我们可以直接在纳税人上调用一个没有参数的 `computeTax` 方法,然后调用税收策略的 `computeTax` 方法,并向纳税人传递: ```java - assert normal.computeTax() == 4000000; - assert dodger.computeTax() == 0; - assert nonProfit.computeTax() == 0; - assert forProfit.computeTax() == 4000000; +assert normal.computeTax() == 4000000; +assert dodger.computeTax() == 0; +assert nonProfit.computeTax() == 0; +assert forProfit.computeTax() == 4000000; ``` 这种结构往往是可取的,因为可以将给定的税收策略直接与给定的纳税人联系起来。 -之前,我们使用了 `TaxPayer` 类和接口 `TaxStrategy

`,其中类型变量 `P` 代表策略适用的 `TaxPayer` 的子类。 现在我们必须将类型参数 `P` 添加到两者中,以便类 `TaxPayer

` 可以具有类型 `TaxStrategy

` 的字段。 类型变量 `P` 的新声明必须是递归的,如 `TaxPayer` 类的新标题所示: +之前,我们使用了 `TaxPayer` 类和接口 `TaxStrategy

`,其中类型变量 `P` 代表策略适用的 `TaxPayer` 的子类。 现在我们必须将类型参数 `P` 添加到两者 +中,以便类 `TaxPayer

` 可以具有类型 `TaxStrategy

` 的字段。 类型变量 `P` 的新声明必须是递归的,如 `TaxPayer` 类的新标题所示: ```java - class TaxPayer

> +class TaxPayer

> ``` 我们在之前看到过类似的递归头文件: ```java - interface Comparable> - class Enum> +interface Comparable> +class Enum> ``` -在所有这三种情况下,类或接口都是类型层次结构的基类,类型参数代表基类的特定子类。 因此,`TaxPayer

` 中的 `P` 代表特定类型的纳税人,例如 `Person` 或 `Trust`; 就像 `Comparable` 中的 `T` 代表被比较的特定类,比如 `String`; 或 `Enum` 中的 `E` 表示特定枚举类型,如季节。 +在所有这三种情况下,类或接口都是类型层次结构的基类,类型参数代表基类的特定子类。 因此,`TaxPayer

` 中的 `P` 代表特定类型的纳税人,例如 `Person` +或 `Trust`; 就像 `Comparable` 中的 `T` 代表被比较的特定类,比如 `String`; 或 `Enum` 中的 `E` 表示特定枚举类型,如季节。 -纳税人类包含一个税收策略字段和一个将纳税人转到税收策略的方法,以及一个像 `TaxPayer` 中使用的 `P` 一样的递归声明。 总的来说,我们可能期望它看起来像这样: +纳税人类包含一个税收策略字段和一个将纳税人转到税收策略的方法,以及一个像 `TaxPayer` 中使用的 `P` 一样的递归声明。 总的来说,我们可能期望它看起来像这 +样: ```java - // not well-typed! - class TaxPayer

> { - private TaxStrategy

strategy; - public long computeTax() { return strategy.computeTax(this); } - ... - } - class Person extends TaxPayer { ... } - class Trust extends TaxPayer { ... } +// not well-typed! +class TaxPayer

> { + private TaxStrategy

strategy; + public long computeTax() { + return strategy.computeTax(this); + } + ... +} +class Person extends TaxPayer { ... } +class Trust extends TaxPayer { ... } ``` -但编译器拒绝上面的类型错误。 问题是这有 `TaxPayer

` 类型,而 `computeTax` 的参数必须有 `P` 类型。 事实上,在每个具体的纳税人类别中,例如人员或信任,这种情况确实具有类型 `P`; 例如,`Person` 扩展了 `TaxPayer`,所以 `P` 与此类中的 `Person` 相同。 所以,实际上,这将与 `P` 具有相同的类型,但类型系统不知道! +但编译器拒绝上面的类型错误。 问题是这有 `TaxPayer

` 类型,而 `computeTax` 的参数必须有 `P` 类型。 事实上,在每个具体的纳税人类别中,例如人员或信 +任,这种情况确实具有类型 `P`; 例如,`Person` 扩展了 `TaxPayer`,所以 `P` 与此类中的 `Person` 相同。 所以,实际上,这将与 `P` 具有相同的 +类型,但类型系统不知道! -我们可以用一个技巧来解决这个问题。 在基类 `TaxPayer

` 中,我们定义了一个抽象方法 `getThis`,其目的是返回与此相同的值,但赋予其类型 `P`. 该方法在与特定种类的纳税人相对应的每个类中实例化,例如 `Person` 或 `Trust`,其类型实际上与 `P` 类型相同。 概括地说,修正的代码现在看起来像这样: +我们可以用一个技巧来解决这个问题。 在基类 `TaxPayer

` 中,我们定义了一个抽象方法 `getThis`,其目的是返回与此相同的值,但赋予其类型 `P`. 该方法在 +与特定种类的纳税人相对应的每个类中实例化,例如 `Person` 或 `Trust`,其类型实际上与 `P` 类型相同。 概括地说,修正的代码现在看起来像这样: ```java - // now correctly typed - abstract class TaxPayer

> { - private TaxStrategy

strategy; - protected abstract P getThis(); - public long computeTax() { return strategy.computeTax(getThis()); } - ... - } - final class Person extends TaxPayer { - protected Person getThis() { return this; } - ... - } - final class Trust extends TaxPayer { - protected Trust getThis() { return this; } - ... - } +// now correctly typed +abstract class TaxPayer

> { + private TaxStrategy

strategy; + protected abstract P getThis(); + public long computeTax() { return strategy.computeTax(getThis()); } + ... +} +final class Person extends TaxPayer { + protected Person getThis() { return this; } + ... +} +final class Trust extends TaxPayer { + protected Trust getThis() { return this; } + ... +} ``` -与以前的代码不同之处在于粗体。 这个的出现被调用 `getThis` 所取代; 方法 `getThis` 在基类中声明为抽象,并且在基类的每个最终子类中都适当地实例化。 基类 `TaxPayer

` 必须声明为 `abstract`,因为它声明了 `getThis` 的类型,但未声明正文。 `getThis` 的主体在 `Person` 和 `Trust` 的最后一个子类中声明。 +与以前的代码不同之处在于粗体。 这个的出现被调用 `getThis` 所取代; 方法 `getThis` 在基类中声明为抽象,并且在基类的每个最终子类中都适当地实例化。 基 +类 `TaxPayer

` 必须声明为 `abstract`,因为它声明了 `getThis` 的类型,但未声明正文。 `getThis` 的主体在 `Person` 和 `Trust` 的最后一个子类中声 +明。 -由于信任被声明为最终的,所以它不能有子类。 假设我们想要一个 `Trust` 的子类 `NonProfitTrust`。 那么我们不仅需要删除 `Trust` 类的最终声明,还需要为其添加一个类型参数。 以下是所需代码的草图: +由于信任被声明为最终的,所以它不能有子类。 假设我们想要一个 `Trust` 的子类 `NonProfitTrust`。 那么我们不仅需要删除 `Trust` 类的最终声明,还需要为 +其添加一个类型参数。 以下是所需代码的草图: ```java - abstract class Trust> extends TaxPayer { ... } - final class NonProfitTrust extends Trust { ... } - final class ForProfitTrust extends Trust { ... } +abstract class Trust> extends TaxPayer { ... } +final class NonProfitTrust extends Trust { ... } +final class ForProfitTrust extends Trust { ... } ``` 例 `9-7`。 具有递归边界的高级策略模式 ```java - abstract class TaxPayer

> { - public long income; // in cents - private TaxStrategy

strategy; - public TaxPayer(long income, TaxStrategy

strategy) { - this.income = income; this.strategy = strategy; - } - protected abstract P getThis(); - public long getIncome() { return income; } - public long computeTax() { return strategy.computeTax(getThis()); } - } - class Person extends TaxPayer { - public Person(long income, TaxStrategy strategy) { - super(income, strategy); - } - protected Person getThis() { return this; } - } - class Trust extends TaxPayer { - private boolean nonProfit; - public Trust(long income, boolean nonProfit, TaxStrategy strategy){ - super(income, strategy); this.nonProfit = nonProfit; - } - protected Trust getThis() { return this; } - public boolean isNonProfit() { return nonProfit; } - } - interface TaxStrategy

> { - public long computeTax(P p); - } - class DefaultTaxStrategy

> implements TaxStrategy

{ - private static final double RATE = 0.40; - public long computeTax(P payer) { - return Math.round(payer.getIncome() * RATE); - } - } - class DodgingTaxStrategy

> implements TaxStrategy

{ - public long computeTax(P payer) { return 0; } - } - class TrustTaxStrategy extends DefaultTaxStrategy { - public long computeTax(Trust trust) { - return trust.isNonprofit() ? 0 : super.computeTax(trust); - } - } +abstract class TaxPayer

> { + public long income; // in cents + private TaxStrategy

strategy; + public TaxPayer(long income, TaxStrategy

strategy) { + this.income = income; this.strategy = strategy; + } + protected abstract P getThis(); + public long getIncome() { + return income; + } + public long computeTax() { + return strategy.computeTax(getThis()); + } +} +class Person extends TaxPayer { + public Person(long income, TaxStrategy strategy) { + super(income, strategy); + } + protected Person getThis() { + return this; + } +} +class Trust extends TaxPayer { + private boolean nonProfit; + public Trust(long income, boolean nonProfit, TaxStrategy strategy){ + super(income, strategy); this.nonProfit = nonProfit; + } + protected Trust getThis() { + return this; + } + public boolean isNonProfit() { + return nonProfit; + } +} +interface TaxStrategy

> { + public long computeTax(P p); +} +class DefaultTaxStrategy

> implements TaxStrategy

{ + private static final double RATE = 0.40; + public long computeTax(P payer) { + return Math.round(payer.getIncome() * RATE); + } +} +class DodgingTaxStrategy

> implements TaxStrategy

{ + public long computeTax(P payer) { return 0; } +} +class TrustTaxStrategy extends DefaultTaxStrategy { + public long computeTax(Trust trust) { + return trust.isNonprofit() ? 0 : super.computeTax(trust); + } +} ``` -现在,`NonProfitTrust` 的一个实例采用一种策略,该策略期望 `NonProfitTrust` 作为参数,而 `ForProfitTrust` 的行为相似。 以这种方式设置参数化类型层次结构通常很方便,其中带有子类的类带有类型参数并且是抽象的,而没有子类的类不带类型参数并且是最终的。`getThis` 方法的主体在每个最终的子类中声明。 +现在,`NonProfitTrust` 的一个实例采用一种策略,该策略期望 `NonProfitTrust` 作为参数,而 `ForProfitTrust` 的行为相似。 以这种方式设置参数化类型层 +次结构通常很方便,其中带有子类的类带有类型参数并且是抽象的,而没有子类的类不带类型参数并且是最终的。`getThis` 方法的主体在每个最终的子类中声明。 总结正如我们所看到的,递归类型参数经常出现在 `Java` 中: ```java - class TaxPayer

> - Comparable> - class Enum> +class TaxPayer

> +Comparable> +class Enum> ``` `getThis` 技巧在这种情况下非常有用,只要有人想在基类型中使用这个类型参数提供的更具体的类型。 @@ -230,4 +278,4 @@ 我们将在下一节看到递归类型参数的另一个例子,它应用于两个相互递归的类。 不过,虽然 `getThis` 技巧通常很有用,但我们不会要求它。 《《《 [下一节](05_Subject-Observer.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch09/05_Subject-Observer.md b/ch09/05_Subject-Observer.md index a839d4fd..3e175ff2 100644 --- a/ch09/05_Subject-Observer.md +++ b/ch09/05_Subject-Observer.md @@ -3,82 +3,110 @@ ### 主题观察者 -我们用一个更加扩展的例子来完成,说明通用的 `Subject-Observer` 模式。与策略模式一样,`Subject-Observer` 模式使用并行类层次结构,但这次我们需要两个具有相互递归边界的类型变量,一个代表特定类型的主体,一个代表特定类型的观察者。这是我们的第一个带有相互递归边界的类型变量的例子。 +我们用一个更加扩展的例子来完成,说明通用的 `Subject-Observer` 模式。与策略模式一样,`Subject-Observer` 模式使用并行类层次结构,但这次我们需要两个 +具有相互递归边界的类型变量,一个代表特定类型的主体,一个代表特定类型的观察者。这是我们的第一个带有相互递归边界的类型变量的例子。 -`Java` 库在包 `java.util` 中使用 `Observable` 类和接口 `Observer`(前者对应于主题)实现了非通用版本的 `Subject-Observer` 模式,其签名示例如图 `9-8` 所示。 +`Java` 库在包 `java.util` 中使用 `Observable` 类和接口 `Observer`(前者对应于主题)实现了非通用版本的 `Subject-Observer` 模式,其签名示例如图 +`9-8` 所示。 -`Observable` 类包含注册观察者(`addObserver`)的方法,指示 `observable` 已更改(`setChanged`),并通知所有观察者有任何更改(`notifyObservers`)等等。 `notifyObservers` 方法可以接受将被广播给所有观察者的 `Object` 类型的任意参数。 `Observer` 接口指定由 `notifyObservers` 调用的更新方法。这个方法有两个参数:第一个是 `Observable` 类型,它是已经改变的主体;第二种类型的对象是广播参数。 +`Observable` 类包含注册观察者(`addObserver`)的方法,指示 `observable` 已更改(`setChanged`),并通知所有观察者有任何更改(`notifyObservers`) +等等。 `notifyObservers` 方法可以接受将被广播给所有观察者的 `Object` 类型的任意参数。 `Observer` 接口指定由 `notifyObservers` 调用的更新方法。这 +个方法有两个参数:第一个是 `Observable` 类型,它是已经改变的主体;第二种类型的对象是广播参数。 -对象在方法签名中的出现常常表明有机会进行基因组化。 因此,我们应该期望通过添加与参数类型相对应的类型参数 `A` 来生成类。 此外,我们可以用 `Observable` 和 `Observer` 自己替换类型参数 `S` 和 `O`(用于 `Subject` 和 `Observer`)。 然后在观察者的更新方法中,您可以调用主体 `S` 支持的任何方法,而无需先投射。 +对象在方法签名中的出现常常表明有机会进行基因组化。 因此,我们应该期望通过添加与参数类型相对应的类型参数 `A` 来生成类。 此外,我们可以用 `Observable` +和 `Observer` 自己替换类型参数 `S` 和 `O`(用于 `Subject` 和 `Observer`)。 然后在观察者的更新方法中,您可以调用主体 `S` 支持的任何方法,而无需先 +投射。 例 `9-9` 显示了如何为 `Observable` 类和 `Observer` 接口指定相应的通用签名。 这里是相关的标题: ```java - public class Observable, O extends Observer,A> - public interface Observer, O extends Observer,A> +public class Observable, O extends Observer,A> +public interface Observer, O extends Observer,A> ``` -这两个声明都采用相同的三个类型参数。这些声明是有趣的,因为它们说明了类型参数的范围可以是相互递归的:所有三个类型参数都出现在前两个的范围内。在此之前,我们看到了简单递归的其他例子,例如 `Comparable` 和 `Enum` 的声明,以及前面关于 `Strategy` 模式的部分。但这是我们第一次看到相互递归。 +这两个声明都采用相同的三个类型参数。这些声明是有趣的,因为它们说明了类型参数的范围可以是相互递归的:所有三个类型参数都出现在前两个的范围内。在此之 +前,我们看到了简单递归的其他例子,例如 `Comparable` 和 `Enum` 的声明,以及前面关于 `Strategy` 模式的部分。但这是我们第一次看到相互递归。 -检查声明的主体,可以看到 `O` 但不是 `S` 出现在 `Observable` 类的主体中,并且 `S` 但不是 `O` 出现在 `Observer` 接口的主体中。所以你可能会想:通过从 `Observable` 中删除类型参数 `S` 和 `Observer` 中的类型参数 `O`,可以简化声明吗?但是这是行不通的,因为你需要 `S` 在 `Observable` 的范围内,所以它可以作为参数传递给 `Observer`,并且你需要 `O` 在 `Observer` 的范围内,以便它可以作为参数传递给 `Observable`。 +检查声明的主体,可以看到 `O` 但不是 `S` 出现在 `Observable` 类的主体中,并且 `S` 但不是 `O` 出现在 `Observer` 接口的主体中。所以你可能会想:通过 +从 `Observable` 中删除类型参数 `S` 和 `Observer` 中的类型参数 `O`,可以简化声明吗?但是这是行不通的,因为你需要 `S` 在 `Observable` 的范围内,所 +以它可以作为参数传递给 `Observer`,并且你需要 `O` 在 `Observer` 的范围内,以便它可以作为参数传递给 `Observable`。 -如 `5.4.2` 节所述,通用声明使用存根。我们根据 `Observable` 和 `Observer` 的通用签名来编译客户端,但是针对标准 `Java` 分发中的类文件运行代码。我们使用存根,因为我们不想对库的源进行任何更改,因为它由 `Sun` 维护。 +如 `5.4.2` 节所述,通用声明使用存根。我们根据 `Observable` 和 `Observer` 的通用签名来编译客户端,但是针对标准 `Java` 分发中的类文件运行代码。我们 +使用存根,因为我们不想对库的源进行任何更改,因为它由 `Sun` 维护。 例 `9-8`。泛型之前的可观察和观察者 ```java - package java.util; - public class Observable { - public void addObserver(Observer o) {...} - protected void clearChanged() {...} - public int countObservers() {...} - public void deleteObserver(Observer o) {...} - public boolean hasChanged() {...} - public void notifyObservers() {...} - public void notifyObservers(Object arg) {...} - protected void setChanged() {...} - } - - package java.util; - public interface Observer { - public void update(Observable o, Object arg); - } +package java.util; +public class Observable { + public void addObserver(Observer o) {...} + protected void clearChanged() {...} + public int countObservers() {...} + public void deleteObserver(Observer o) {...} + public boolean hasChanged() {...} + public void notifyObservers() {...} + public void notifyObservers(Object arg) {...} + protected void setChanged() {...} +} + +package java.util; +public interface Observer { + public void update(Observable o, Object arg); +} ``` 例9-9。 泛型的可观察和观察者 ```java - package java.util; - class StubException extends UnsupportedOperationException {} - public class Observable, O extends Observer, A> { - public void addObserver(O o) { throw new StubException(); } - protected void clearChanged() { throw new StubException(); } - public int countObservers() { throw new StubException(); } - public void deleteObserver(O o) { throw new StubException(); } - public boolean hasChanged() { throw new StubException(); } - public void notifyObservers() { throw new StubException(); } - public void notifyObservers(A a) { throw new StubException(); } - protected void setChanged() { throw new StubException(); } - } - package java.util; - public interface Observer, O extends Observer, A> { - public void update(S o, A a); - } +package java.util; +class StubException extends UnsupportedOperationException {} +public class Observable, O extends Observer, A> { + public void addObserver(O o) { + throw new StubException(); + } + protected void clearChanged() { + throw new StubException(); + } + public int countObservers() { + throw new StubException(); + } + public void deleteObserver(O o) { + throw new StubException(); + } + public boolean hasChanged() { + throw new StubException(); + } + public void notifyObservers() { + throw new StubException(); + } + public void notifyObservers(A a) { + throw new StubException(); + } + protected void setChanged() { + throw new StubException(); + } +} +package java.util; +public interface Observer, O extends Observer, A> { + public void update(S o, A a); +} ``` -作为 `Observable` 和 `Observer` 的演示客户端,示例 `9-10` 介绍了货币转换器。 转换器的屏幕截图如图 `9-1` 所示。 转换器允许您为三种货币(美元,欧元和英镑)中的每一种输入转换率,并以任何货币输入一个值。 更改费率条目会导致重新计算相应的值; 更改值的条目会导致重新计算所有值。 +作为 `Observable` 和 `Observer` 的演示客户端,示例 `9-10` 介绍了货币转换器。 转换器的屏幕截图如图 `9-1` 所示。 转换器允许您为三种货币(美元,欧元 +和英镑)中的每一种输入转换率,并以任何货币输入一个值。 更改费率条目会导致重新计算相应的值; 更改值的条目会导致重新计算所有值。 -客户端通过声明 `CModel` 为 `Observable` 的子类并将 `CView` 作为 `Observer` 的子接口来实例化模式。 此外,参数类型被实例化为 `Currency`,一个枚举类型,可用于通知观察者该主题的哪些组件已更改。 这里是相关的标题: +客户端通过声明 `CModel` 为 `Observable` 的子类并将 `CView` 作为 `Observer` 的子接口来实例化模式。 此外,参数类型被实例化为 `Currency`,一个枚举 +类型,可用于通知观察者该主题的哪些组件已更改。 这里是相关的标题: ```java - enum Currency { DOLLAR, EURO, POUND } - class CModel extends Observable - interface CView extends Observer +enum Currency { DOLLAR, EURO, POUND } +class CModel extends Observable +interface CView extends Observer ``` -类RateView和ValueView实现CView,类Converter定义控制显示的顶层框架。 +类 `RateView` 和 `ValueView` 实现 `CView`,类 `Converter` 定义控制显示的顶层框架。 -CModel类有一个方法来设置和获取给定货币的汇率和价值。汇率存储在一个映射中,该映射为每种货币分配一个汇率,并存储该值(作为一个长, +`CModel` 类有一个方法来设置和获取给定货币的汇率和价值。汇率存储在一个映射中,该映射为每种货币分配一个汇率,并存储该值(作为一个长, ![](05_1.png) @@ -86,142 +114,149 @@ CModel类有一个方法来设置和获取给定货币的汇率和价值。汇 以美分,欧分或便士)与其实际货币。 要计算给定货币的价值,该值除以实际货币的利率并乘以给定货币的利率。 -每当费率发生变化时,`CModel` 类将调用 `RateView` 的更新方法,并将相应货币作为参数传递(因为只需更新货币的费率和价值); 当值更改时,它会调用 `ValueView` 的更新方法,并将 `null` 作为参数传递(因为需要更新所有货币的值)。 +每当费率发生变化时,`CModel` 类将调用 `RateView` 的更新方法,并将相应货币作为参数传递(因为只需更新货币的费率和价值); 当值更改时,它会调用 +`ValueView` 的更新方法,并将 `null` 作为参数传递(因为需要更新所有货币的值)。 我们编译并运行代码如下。 首先,我们编译 `Observable` 和 `Observer` 的通用版本: ```java - % javac -d . java/util/Observable.java java/util/Observer.java +% javac -d . java/util/Observable.java java/util/Observer.java ``` -由于这些包位于 `java.util` 包中,它们必须保存在当前目录的子目录 `java/util` 中。 其次,我们在包 `com.eg.converter` 中编译 `Converter` 和相关的类。 默认情况下,`Java` 编译器首先搜索当前目录中的类文件(即使是标准库)。 因此,编译器使用为 `Observable` 和 `Observer` 生成的存根类文件,它们具有正确的通用签名(但不包含可运行代码): +由于这些包位于 `java.util` 包中,它们必须保存在当前目录的子目录 `java/util` 中。 其次,我们在包 `com.eg.converter` 中编译 `Converter` 和相关的 +类。 默认情况下,`Java` 编译器首先搜索当前目录中的类文件(即使是标准库)。 因此,编译器使用为 `Observable` 和 `Observer` 生成的存根类文件,它们具 +有正确的通用签名(但不包含可运行代码): ```java - % javac -d . com/eg/converter/Converter.java +% javac -d . com/eg/converter/Converter.java ``` -第三,我们运行转换器的类文件。 默认情况下,`java` 运行时不会首先在当前目录中搜索包 `java` 和 `javax` 中的类文件 - 由于安全原因,它们始终从标准库中获取。 因此,运行时使用 `Observable` 和 `Observer` 的标准类文件,其中包含我们想要运行的遗留代码(但没有正确的通用签名): +第三,我们运行转换器的类文件。 默认情况下,`java` 运行时不会首先在当前目录中搜索包 `java` 和 `javax` 中的类文件 - 由于安全原因,它们始终从标准库中 +获取。 因此,运行时使用 `Observable` 和 `Observer` 的标准类文件,其中包含我们想要运行的遗留代码(但没有正确的通用签名): ```java - % java com.eg.converter.Converter +% java com.eg.converter.Converter ``` 例9-10。 货币换算 ```java - import java.util.*; - import javax.swing.*; - import javax.swing.event.*; - import java.awt.*; - import java.awt.event.*; - enum Currency { DOLLAR, EURO, POUND } - class CModel extends Observable { - private final EnumMap rates; - private long value = 0; // cents, euro cents, or pence - private Currency currency = Currency.DOLLAR; - public CModel() { - rates = new EnumMap(Currency.class); - } - public void initialize(double... initialRates) { - for (int i=0; i {} - class RateView extends JTextField implements CView { - private final CModel model; - private final Currency currency; - public RateView(final CModel model, final Currency currency) { - this.model = model; - this.currency = currency; - addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) { - try { - double rate = Double.parseDouble(getText()); - model.setRate(currency, rate); - } catch (NumberFormatException x) {} - } - }); - model.addObserver(this); - } - public void update(CModel model, Currency currency) { - if (this.currency == currency) { - double rate = model.getRate(currency); - setText(String.format("%10.6f", rate)); - } +import java.util.*; +import javax.swing.*; +import javax.swing.event.*; +import java.awt.*; +import java.awt.event.*; +enum Currency { DOLLAR, EURO, POUND } +class CModel extends Observable { + private final EnumMap rates; + private long value = 0; // cents, euro cents, or pence + private Currency currency = Currency.DOLLAR; + public CModel() { + rates = new EnumMap(Currency.class); + } + public void initialize(double... initialRates) { + for (int i=0; i {} +class RateView extends JTextField implements CView { + private final CModel model; + private final Currency currency; + public RateView(final CModel model, final Currency currency) { + this.model = model; + this.currency = currency; + addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + try { + double rate = Double.parseDouble(getText()); + model.setRate(currency, rate); + } catch (NumberFormatException x) {} } + }); + model.addObserver(this); + } + public void update(CModel model, Currency currency) { + if (this.currency == currency) { + double rate = model.getRate(currency); + setText(String.format("%10.6f", rate)); } - class ValueView extends JTextField implements CView { - private final CModel model; - private final Currency currency; - public ValueView(final CModel model, final Currency currency) { - this.model = model; - this.currency = currency; - addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) { - try { - long value = Math.round(100.0*Double.parseDouble(getText())); - model.setValue(currency, value); - } catch (NumberFormatException x) {} - } - }); - model.addObserver(this); - } - public void update(CModel model, Currency currency) { - if (currency == null || currency == this.currency) { - long value = model.getValue(this.currency); - setText(String.format("%15d.%02d", value/100, value%100)); - } - } - } - class Converter extends JFrame { - public Converter() { - CModel model = new CModel(); - setTitle("Currency converter"); - setLayout(new GridLayout(Currency.values().length+1, 3)); - add(new JLabel("currency")); - add(new JLabel("rate")); - add(new JLabel("value")); - for (Currency currency : Currency.values()) { - add(new JLabel(currency.name())); - add(new RateView(model, currency)); - add(new ValueView(model, currency)); - } - model.initialize(1.0, 0.83, 0.56); - setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - pack(); - } - public static void main(String[] args) { - new Converter().setVisible(true); + } +} +class ValueView extends JTextField implements CView { + private final CModel model; + private final Currency currency; + public ValueView(final CModel model, final Currency currency) { + this.model = model; + this.currency = currency; + addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + try { + long value = Math.round(100.0*Double.parseDouble(getText())); + model.setValue(currency, value); + } catch (NumberFormatException x) {} } + }); + model.addObserver(this); + } + public void update(CModel model, Currency currency) { + if (currency == null || currency == this.currency) { + long value = model.getValue(this.currency); + setText(String.format("%15d.%02d", value/100, value%100)); + } + } +} +class Converter extends JFrame { + public Converter() { + CModel model = new CModel(); + setTitle("Currency converter"); + setLayout(new GridLayout(Currency.values().length+1, 3)); + add(new JLabel("currency")); + add(new JLabel("rate")); + add(new JLabel("value")); + for (Currency currency : Currency.values()) { + add(new JLabel(currency.name())); + add(new RateView(model, currency)); + add(new ValueView(model, currency)); } + model.initialize(1.0, 0.83, 0.56); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + pack(); + } + public static void main(String[] args) { + new Converter().setVisible(true); + } +} ``` -因此,当我们使用标准库类的存根时,我们不需要修改类路径,就像我们在 `5.4.2` 节中所做的那样,因为默认情况下会获得正确的行为。 (如果您确实想在运行时更改标准库类,则可以使用 `-Xbootclass` 路径标志。) +因此,当我们使用标准库类的存根时,我们不需要修改类路径,就像我们在 `5.4.2` 节中所做的那样,因为默认情况下会获得正确的行为。 (如果您确实想在运行时更 +改标准库类,则可以使用 `-Xbootclass` 路径标志。) -这就结束了我们对泛型的讨论。 现在,您已经有了一个全面的基础,可以使用其他人定义的通用库,定义自己的库,将遗留代码发展为通用代码,了解对泛型的限制并避免陷阱,在需要时使用检查和专业化,以及在设计模式中利用泛型。 +这就结束了我们对泛型的讨论。 现在,您已经有了一个全面的基础,可以使用其他人定义的通用库,定义自己的库,将遗留代码发展为通用代码,了解对泛型的限制并避 +免陷阱,在需要时使用检查和专业化,以及在设计模式中利用泛型。 泛型最重要的用途之一是集合框架,在本书的下一部分,我们将向您展示如何有效地使用此框架并提高您作为 `Java` 程序员的工作效率。 《《《 [下一节](../ch10/00_Collections.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch10/00_Collections.md b/ch10/00_Collections.md index 17bbf03f..a964d96e 100644 --- a/ch10/00_Collections.md +++ b/ch10/00_Collections.md @@ -1,11 +1,16 @@ 《《《 [返回首页](../README.md)
《《《 [上一节](../ch09/05_Subject-Observer.md) -# 集合 +## 集合 -`Java` 集合框架是 `java.util` 和 `java.util.concurrent` 包中的一组接口和类。他们为客户程序提供了如何组织其对象的各种模型以及每种模型的各种实现。这些模型有时被称为抽象数据类型,我们需要它们,因为不同的程序需要不同的方式来组织它们的对象。在一种情况下,您可能希望将程序的对象组织在顺序列表中,因为它们的排序很重要,并且有重复。另一种情况是,一个集合可能是正确的数据类型,因为现在的排序并不重要,你想放弃重复。这两种数据类型(以及其他)由集合框架中的不同接口表示,我们将在本章中查看它们的使用示例。但那不是全部;这些数据类型中没有一个具有单一的“最佳”实现 - 也就是说,对于所有操作,其中一个实现比所有其他实现更好。例如,链接列表可能比用于从中间插入和删除元素的列表的阵列实现更好,但是对于随机访问更糟糕。因此,为您的程序选择正确的实施方式涉及知道如何使用以及可用的方式。 +`Java` 集合框架是 `java.util` 和 `java.util.concurrent` 包中的一组接口和类。他们为客户程序提供了如何组织其对象的各种模型以及每种模型的各种实现。这些 +模型有时被称为抽象数据类型,我们需要它们,因为不同的程序需要不同的方式来组织它们的对象。在一种情况下,您可能希望将程序的对象组织在顺序列表中,因为它们的 +排序很重要,并且有重复。另一种情况是,一个集合可能是正确的数据类型,因为现在的排序并不重要,你想放弃重复。这两种数据类型(以及其他)由集合框架中的不同接 +口表示,我们将在本章中查看它们的使用示例。但那不是全部;这些数据类型中没有一个具有单一的“最佳”实现 - 也就是说,对于所有操作,其中一个实现比所有其他实现更 +好。例如,链接列表可能比用于从中间插入和删除元素的列表的阵列实现更好,但是对于随机访问更糟糕。因此,为您的程序选择正确的实施方式涉及知道如何使用以及可用 +的方式。 本书的这一部分首先概述框架,然后详细介绍每个主界面及其标准实现。最后,我们将看看 `Collections` 类中提供的专用实现和通用算法。 《《《 [下一节](01_The_Main_Interfaces_of_the_Java.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch10/01_The_Main_Interfaces_of_the_Java.md b/ch10/01_The_Main_Interfaces_of_the_Java.md index a0953f56..98f8f53f 100644 --- a/ch10/01_The_Main_Interfaces_of_the_Java.md +++ b/ch10/01_The_Main_Interfaces_of_the_Java.md @@ -7,11 +7,15 @@ - `Iterable` 定义了一个类必须满足的实例才能与 `foreach` 语句一起使用。框架接口具有以下目的: -- 集合包含除地图之外的任何集合所需的核心功能。它没有直接的具体实现;具体收集类也都实现其子接口之一。`Set` 是一个集合,没有重复,顺序不重要。 `SortedSet` 会自动对元素进行排序并按顺序返回它们。 `NavigableSet` 对此进行了扩展,添加了一些方法来查找与目标元素最接近的匹配项。 +- 集合包含除地图之外的任何集合所需的核心功能。它没有直接的具体实现;具体收集类也都实现其子接口之一。`Set` 是一个集合,没有重复,顺序不重要。 +`SortedSet` 会自动对元素进行排序并按顺序返回它们。 `NavigableSet` 对此进行了扩展,添加了一些方法来查找与目标元素最接近的匹配项。 -- 队列是一个集合,旨在接受尾部的元素进行处理,并按其处理顺序在头部生成它们。它的子接口 `Deque` 通过允许在头部和尾部添加或去除元素来扩展它。 `Queue` 和 `Deque` 分别具有子接口 `BlockingQueue` 和 `BlockingDeque`,它们支持并发访问,并允许线程无限期地或最长时间地被阻塞,直到执行所请求的操作。 +- 队列是一个集合,旨在接受尾部的元素进行处理,并按其处理顺序在头部生成它们。它的子接口 `Deque` 通过允许在头部和尾部添加或去除元素来扩展它。 `Queue` 和 +`Deque` 分别具有子接口 `BlockingQueue` 和 `BlockingDeque`,它们支持并发访问,并允许线程无限期地或最长时间地被阻塞,直到执行所请求的操作。 - 列表是一个集合,其顺序很重要,可容纳重复的元素。 -- `Map` 是一个使用键值关联来存储和检索元素的集合。它由 `ConcurrentMap` 进行了扩展,它提供了对并发访问的支持,由 `SortedMap` 进行扩展,它保证以递增键顺序返回其值,通过 `Navigable-Map` 扩展 `SortedMap` 查找与目标元素最接近的匹配项,以及通过扩展 `ConcurrentMap` 和 `NavigableMap` 的 `ConcurrentNavigableMap`。 +- `Map` 是一个使用键值关联来存储和检索元素的集合。它由 `ConcurrentMap` 进行了扩展,它提供了对并发访问的支持,由 `SortedMap` 进行扩展,它保证以递增键 +顺序返回其值,通过 `Navigable-Map` 扩展 `SortedMap` 查找与目标元素最接近的匹配项,以及通过扩展 `ConcurrentMap` 和 `NavigableMap` 的 +`ConcurrentNavigableMap`。 ![](10_1.png) @@ -20,4 +24,4 @@ 第 `12` 章到第 `16` 章将依次关注每个集合框架接口。首先,虽然在第 `11` 章中,我们需要介绍贯穿整个框架设计的一些初步想法。 《《《 [下一节](../ch11/00_Preliminaries.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch11/00_Preliminaries.md b/ch11/00_Preliminaries.md index c190ece5..8c399c8a 100644 --- a/ch11/00_Preliminaries.md +++ b/ch11/00_Preliminaries.md @@ -1,9 +1,9 @@ 《《《 [返回首页](../README.md)
《《《 [上一节](../ch10/01_The_Main_Interfaces_of_the_Java.md) -## 初步措施 +### 初步措施 在本章中,在深入讨论集合本身的细节之前,我们将花时间讨论框架的基本概念。 《《《 [下一节](01_Iterable_and_Iterators.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch11/01_Iterable_and_Iterators.md b/ch11/01_Iterable_and_Iterators.md index d2495a0b..6c9d5a22 100644 --- a/ch11/01_Iterable_and_Iterators.md +++ b/ch11/01_Iterable_and_Iterators.md @@ -13,68 +13,84 @@ public Iterator { } ``` -迭代器的目的是提供一种统一访问集合元素的方法,因此无论您处理什么类型的集合,并且实现它们,您总是会知道如何依次处理元素。 这曾经需要一些相当笨拙的代码; 例如,在较早版本的 `Java` 中,您可以编写以下内容来打印集合内容的字符串表示形式: +迭代器的目的是提供一种统一访问集合元素的方法,因此无论您处理什么类型的集合,并且实现它们,您总是会知道如何依次处理元素。 这曾经需要一些相当笨拙的代 +码; 例如,在较早版本的 `Java` 中,您可以编写以下内容来打印集合内容的字符串表示形式: ```java - // coll是指实现Collection的对象 - // ----- 不是Java 5的首选成语 ------- - for (Iterator itr = coll.iterator() ; itr.hasNext() ; ) { - System.out.println(itr.next()); - } +// coll是指实现Collection的对象 +// ----- 不是Java 5的首选成语 ------- +for (Iterator itr = coll.iterator() ; itr.hasNext() ; ) { + System.out.println(itr.next()); +} ``` -这个奇怪的声明是 `Java 5` 之前的首选语言,因为通过将itr的范围限制在循环体中,它消除了在其他地方意外使用它的可能性。 这段代码的工作原理是任何实现 `Collection` 的类都有一个迭代器方法,该方法返回适合于该类对象的迭代器。 它已不再是已批准的惯用语,因为 `Java 5` 引入了更好的东西:`foreach` 语句,您在第I部分中遇到了。使用`foreach`,我们可以更简洁地编写前面的代码: +这个奇怪的声明是 `Java 5` 之前的首选语言,因为通过将 `itr` 的范围限制在循环体中,它消除了在其他地方意外使用它的可能性。 这段代码的工作原理是任何实现 +`Collection` 的类都有一个迭代器方法,该方法返回适合于该类对象的迭代器。 它已不再是已批准的惯用语,因为 `Java 5` 引入了更好的东西:`foreach` 语句, +您在第I部分中遇到了。使用`foreach`,我们可以更简洁地编写前面的代码: ```java - for (Object o : coll) { - System.out.println(o); - } +for (Object o : coll) { + System.out.println(o); +} ``` 这段代码可以与任何实现接口 `Iterable` 的任何东西一起工作 - 也就是说任何可以产生 `Iterator` 的东西。 这是 `Iterable` 的声明: ```java - public Iterable { - Iterator iterator(); // 在类型T的元素上返回一个迭代器 - } +public Iterable { + Iterator iterator(); // 在类型T的元素上返回一个迭代器 +} ``` -在 `Java 5` 中,`Collection` 接口用于扩展 `Iterable`,所以任何 `set`,`list` 或 `queue` 都可以成为 `foreach` 的目标,就像数组一样。 如果您编写自己的 `Iterable` 实现,那么也可以使用 `foreach`。 例 `11-1` 给出了 `Iterable` 如何直接实现的一个小例子。 `Counter` 对象用 `Integer` 对象的计数进行初始化; 它的迭代器以响应 `next()` 的调用的升序返回这些值。 +在 `Java 5` 中,`Collection` 接口用于扩展 `Iterable`,所以任何 `set`,`list` 或 `queue` 都可以成为 `foreach` 的目标,就像数组一样。 如果您编写 +自己的 `Iterable` 实现,那么也可以使用 `foreach`。 例 `11-1` 给出了 `Iterable` 如何直接实现的一个小例子。 `Counter` 对象用 `Integer` 对象的计数 +进行初始化; 它的迭代器以响应 `next()` 的调用的升序返回这些值。 现在,`Counter` 对象可以成为 `foreach` 语句的目标: ```java - int total = 0; - for (int i : new Counter(3)) { - total += i; - } - assert total == 6; +int total = 0; +for (int i : new Counter(3)) { + total += i; +} +assert total == 6; ``` 在实践中,以这种方式直接实现 `Iterable` 是很不寻常的,因为 `foreach` 最常用于数组和标准集合类。 -框架-`ArrayList`,`HashMap` 等通用集合的迭代器可以通过从单线程代码抛出 `ConcurrentModificationException` 来困扰新手用户。当这些迭代器检测到它们派生的集合已经在结构上发生了变化(广义地说,这些元素已被添加或删除)时,这些迭代器会抛出此异常。这种行为的动机是迭代器被实现为其底层集合的视图,因此,如果该集合在结构上发生了变化,则迭代器可能无法在到达集合的已更改部分时继续正常运行。通用集合框架迭代器可以快速失败,而不会让失败的表现延迟,使诊断变得困难。快速迭代器的方法检查自上次迭代器方法调用后,底层集合没有被结构性更改(由另一个迭代器或集合本身的方法)。如果他们检测到更改,则会抛出 `ConcurrentModificationException`。虽然这个限制排除了一些合理的程序,但它排除了更多不合适的程序。 +框架-`ArrayList`,`HashMap` 等通用集合的迭代器可以通过从单线程代码抛出 `ConcurrentModificationException` 来困扰新手用户。当这些迭代器检测到它们 +派生的集合已经在结构上发生了变化(广义地说,这些元素已被添加或删除)时,这些迭代器会抛出此异常。这种行为的动机是迭代器被实现为其底层集合的视图,因 +此,如果该集合在结构上发生了变化,则迭代器可能无法在到达集合的已更改部分时继续正常运行。通用集合框架迭代器可以快速失败,而不会让失败的表现延迟,使诊 +断变得困难。快速迭代器的方法检查自上次迭代器方法调用后,底层集合没有被结构性更改(由另一个迭代器或集合本身的方法)。如果他们检测到更改,则会抛出 +`ConcurrentModificationException`。虽然这个限制排除了一些合理的程序,但它排除了更多不合适的程序。 例11-1。直接实现 `Iterable` ```java - class Counter implements Iterable { - private int count; - public Counter(int count) { this.count = count; } - public Iterator iterator() { - return new Iterator() { - private int i = 0; - public boolean hasNext() { return i < count; } - public Integer next() { i++; return i; } - public void remove(){ - throw new UnsupportedOperationException(); - } - }; - } - } +class Counter implements Iterable { + private int count; + public Counter(int count) { + this.count = count; + } + public Iterator iterator() { + return new Iterator() { + private int i = 0; + public boolean hasNext() { + return i < count; + } + public Integer next() { + i++; + return i; + } + public void remove(){ + throw new UnsupportedOperationException(); + } + }; + } +} ``` 并发集合有处理并发修改的其他策略,例如弱一致的迭代器。 我们在第 `11.5` 节更详细地讨论它们。 《《《 [下一节](02_Implementations.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch11/02_Implementations.md b/ch11/02_Implementations.md index fa39c2bd..142bf30f 100644 --- a/ch11/02_Implementations.md +++ b/ch11/02_Implementations.md @@ -3,33 +3,36 @@ ### 实现 -我们简要地看了一下集合框架的接口,这些接口定义了我们可以预期的每个集合的行为。但正如我们在本章的介绍中提到的,有几种方法可以实现每个接口。为什么框架不会为每个接口使用最佳实现?那肯定会让生活变得更简单 - 事实上,过于简单就像生活一样。如果实施对某些行动来说是灰狗,墨菲定律告诉我们这对其他人来说将是一只乌龟。由于没有任何接口的“最佳”实现,所以您必须进行权衡,判断应用程序中哪些操作最常使用,并选择优化这些操作的实现。 +我们简要地看了一下集合框架的接口,这些接口定义了我们可以预期的每个集合的行为。但正如我们在本章的介绍中提到的,有几种方法可以实现每个接口。为什么框架不会 +为每个接口使用最佳实现?那肯定会让生活变得更简单 - 事实上,过于简单就像生活一样。如果实施对某些行动来说是灰狗,墨菲定律告诉我们这对其他人来说将是一只乌 +龟。由于没有任何接口的“最佳”实现,所以您必须进行权衡,判断应用程序中哪些操作最常使用,并选择优化这些操作的实现。 -大多数集合接口需要的三种主要操作是按位置插入和移除元素,按内容检索元素以及迭代集合元素。这些实现在这些操作上提供了许多变体,但它们之间的主要区别可以从它们如何实现这三个方面来讨论。在本节中,我们将简要考察用作实现基础的四个主要结构,随后,当我们需要它们时,我们将更详细地查看每个结构。这四种结构是: +大多数集合接口需要的三种主要操作是按位置插入和移除元素,按内容检索元素以及迭代集合元素。这些实现在这些操作上提供了许多变体,但它们之间的主要区别可以从它 +们如何实现这三个方面来讨论。在本节中,我们将简要考察用作实现基础的四个主要结构,随后,当我们需要它们时,我们将更详细地查看每个结构。这四种结构是: 数组 - - 这些是来自 `Java` 语言熟悉的结构 - 自 `Fortran` 以来几乎所有其他编程语言。 因为数组是直接在硬件中实现的,所以它们具有随机存取内存的特性:按位置 - 访问元素并迭代它们非常快,但在任意位置插入和移除元素的速度较慢(因为这可能需要调整位置 其他元素)。 在集合框架中中使用数组作为 `ArrayList`, - `CopyOnWriteArrayList`,`EnumSet` 和 `EnumMap` 以及许多 `Queue` 和 `Deque` 实现的支持结构。 它们也是实现散列表机制的一个重要部分(稍后讨论)。 +- 这些是来自 `Java` 语言熟悉的结构 - 自 `Fortran` 以来几乎所有其他编程语言。 因为数组是直接在硬件中实现的,所以它们具有随机存取内存的特性:按位置访问 +元素并迭代它们非常快,但在任意位置插入和移除元素的速度较慢(因为这可能需要调整位置 其他元素)。 在集合框架中中使用数组作为 `ArrayList`, +`CopyOnWriteArrayList`,`EnumSet` 和 `EnumMap` 以及许多 `Queue` 和 `Deque` 实现的支持结构。 它们也是实现散列表机制的一个重要部分(稍后讨论)。 链接列表 - - 顾名思义,它们由链式单元链组成。 每个单元格都包含对数据的引用和对列表中下一个单元格的引用(以及在某些实现中的前一个单元格)。 链接列表的执行方式与 - 数组完全不同:按位置访问元素的速度很慢,因为必须从列表的开始处跟随引用链,但可以通过重新排列单元格引用来在不变的时间内执行插入和删除操作。 链接列表 - 是用于类 `ConcurrentLinkedQueue`,`LinkedBlockingQueue` 和 `LinkedList` 以及新的跳过列表集合 `ConcurrentSkipListSet` 和 - `ConcurrentSkipListMap` 的主要支持结构。 它们也用于实现 `HashSet` 和 `LinkedHashSet`。 +- 顾名思义,它们由链式单元链组成。 每个单元格都包含对数据的引用和对列表中下一个单元格的引用(以及在某些实现中的前一个单元格)。 链接列表的执行方式与数组 +完全不同:按位置访问元素的速度很慢,因为必须从列表的开始处跟随引用链,但可以通过重新排列单元格引用来在不变的时间内执行插入和删除操作。 链接列表是用于类 +`ConcurrentLinkedQueue`,`LinkedBlockingQueue` 和 `LinkedList` 以及新的跳过列表集合 `ConcurrentSkipListSet` 和 `ConcurrentSkipListMap` 的主 +要支持结构。 它们也用于实现 `HashSet` 和 `LinkedHashSet`。 哈希表 - - 这些提供了一种存储在其内容上索引的元素的方法,而不是像列表那样在整数值索引上存储索引。 与数组和链表相比,散列表不支持按位置访问元素,但内容访问通常非 - 常快速,就像插入和删除一样。 哈希表是许多 `Set` 和 `Map` 实现的支持结构,包括 `HashSet` 和 `LinkedHashSet` 以及相应的映射 `HashMap` 和 - `LinkedHashMap`,以及 `WeakHashMap`,`IdentityHashMap` 和 `ConcurrentHashMap`。 +- 这些提供了一种存储在其内容上索引的元素的方法,而不是像列表那样在整数值索引上存储索引。 与数组和链表相比,散列表不支持按位置访问元素,但内容访问通常非 +常快速,就像插入和删除一样。 哈希表是许多 `Set` 和 `Map` 实现的支持结构,包括 `HashSet` 和 `LinkedHashSet` 以及相应的映射 `HashMap` 和 +`LinkedHashMap`,以及 `WeakHashMap`,`IdentityHashMap` 和 `ConcurrentHashMap`。 树 - - 这些也按内容来组织它们的元素,但重要的区别是它们可以按排序顺序存储和检索它们。它们对于插入和删除元素,通过内容访问它们并迭代它们的操作相对较快。 树是 - `TreeSet` 和 `TreeMap` 的支持结构。 在执行 `PriorityQueue` 和 `PriorityBlockingQueue` 时使用的优先堆是与树有关的结构。   +- 这些也按内容来组织它们的元素,但重要的区别是它们可以按排序顺序存储和检索它们。它们对于插入和删除元素,通过内容访问它们并迭代它们的操作相对较快。 树是 +`TreeSet` 和 `TreeMap` 的支持结构。 在执行 `PriorityQueue` 和 `PriorityBlockingQueue` 时使用的优先堆是与树有关的结构。   《《《 [下一节](03_Efficiency_and_the_O-Notation.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch11/03_Efficiency_and_the_O-Notation.md b/ch11/03_Efficiency_and_the_O-Notation.md index ffa08c57..a94142a5 100644 --- a/ch11/03_Efficiency_and_the_O-Notation.md +++ b/ch11/03_Efficiency_and_the_O-Notation.md @@ -3,15 +3,23 @@ ### 效率与Ο符号 -在最后一节中,我们讨论了不同的实现对于不同的操作是“好的”。一个好的算法在使用两种资源时很经济:时间和空间。集合的实现通常使用与集合大小成正比的空间,但是访问和更新所需的时间可能会有很大差异,所以这将是我们的主要关心。很难说一个程序执行的速度有多快,因为这取决于很多因素,包括程序员省外的一些因素,比如编译代码的质量和硬件的速度。即使我们忽略了这些并且仅仅考虑算法的执行时间如何依赖于它的数据,详细的分析可能是复杂的。在 `Donald Knuth` 的经典着作“排序和搜索”(`Addison-Wesley`)中提供了一个相对简单的例子, `Knuth` 的名义 `MIX` 机器上的多列表插入排序程序的最坏情况执行时间推导为 +在最后一节中,我们讨论了不同的实现对于不同的操作是“好的”。一个好的算法在使用两种资源时很经济:时间和空间。集合的实现通常使用与集合大小成正比的空间, +但是访问和更新所需的时间可能会有很大差异,所以这将是我们的主要关心。很难说一个程序执行的速度有多快,因为这取决于很多因素,包括程序员省外的一些因素, +比如编译代码的质量和硬件的速度。即使我们忽略了这些并且仅仅考虑算法的执行时间如何依赖于它的数据,详细的分析可能是复杂的。在 `Donald Knuth` 的经典着 +作“排序和搜索”(`Addison-Wesley`)中提供了一个相对简单的例子, `Knuth` 的名义 `MIX` 机器上的多列表插入排序程序的最坏情况执行时间推导为 ``` - 3.5N² + 24.5N + 4M + 2 +3.5N² + 24.5N + 4M + 2 ``` 其中N是正在排序的元素的数量,M是列表的数量。 -作为描述算法效率的简便方式,这不是很方便。很明显,我们需要更广泛的刷子以供一般使用。最常用的是 `Onotation`(发音为“big-oh notation”).`O-notation` 是一种以抽象的方式描述算法性能的方式,不需要详细预测特定程序的精确性能运行在特定的机器上我们使用它的主要原因是它给了我们一种描述算法的执行时间如何依赖于它的数据集的大小的方式,假设数据集足够大。例如,在以前的表达式中前两项对 `N` 的低值是可比较的;事实上,对于 `N <8`,第二项更大,但随着 `N` 增长,第一项越来越支配表达式,并且在达到 `100` 时,第一项是第二项的 `15` 倍,使用非常广泛的刷子,我们说这个算法的最坏情况需要时间 `O`(`N²`),我们不关心系数太多,因为它不对我们想要的唯一最重要的问题做出任何改变询问任何算法:数据大小增加时的运行时间会发生什么情况 - 例如,当它加倍时?对于最差的插入排序,答案是运行时间增加四倍。这使得`O`(`N²`)比我们在本书中实际使用的任何情况都糟糕。 +作为描述算法效率的简便方式,这不是很方便。很明显,我们需要更广泛的刷子以供一般使用。最常用的是 `Onotation`(发音为“big-oh notation”). +`O-notation` 是一种以抽象的方式描述算法性能的方式,不需要详细预测特定程序的精确性能运行在特定的机器上我们使用它的主要原因是它给了我们一种描述算法的 +执行时间如何依赖于它的数据集的大小的方式,假设数据集足够大。例如,在以前的表达式中前两项对 `N` 的低值是可比较的;事实上,对于 `N <8`,第二项更大,但 +随着 `N` 增长,第一项越来越支配表达式,并且在达到 `100` 时,第一项是第二项的 `15` 倍,使用非常广泛的刷子,我们说这个算法的最坏情况需要时间 +`O`(`N²`),我们不关心系数太多,因为它不对我们想要的唯一最重要的问题做出任何改变询问任何算法:数据大小增加时的运行时间会发生什么情况 - 例如,当它加 +倍时?对于最差的插入排序,答案是运行时间增加四倍。这使得`O`(`N²`)比我们在本书中实际使用的任何情况都糟糕。 表 `11-1`。 一些常见的运行时间 @@ -33,4 +41,4 @@ O(2^N)的算法来解决 - 对于这些,当N加倍时,运行时间是平 本为 `O(N)`,因此摊余成本为 `O(1)`。 《《《 [下一节](04_contract.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch11/05_Collections_and_Thread_Safety.md b/ch11/05_Collections_and_Thread_Safety.md index 53ab5684..d4084e92 100644 --- a/ch11/05_Collections_and_Thread_Safety.md +++ b/ch11/05_Collections_and_Thread_Safety.md @@ -3,56 +3,58 @@ ### 集合和线程安全 -当一个 `Java` 程序正在运行时,它正在执行一个或多个执行流或线程。 线程就像一个轻量级进程,所以同时执行多个线程的程序可以被认为是同时运行多个程序的计算 -机,但是有一个重要区别:不同的线程可以同时访问相同的内存位置和其他系统资源。 在具有多个处理器的机器上,可以通过为每个线程分配处理器来实现真正的并发线 +当一个 `Java` 程序正在运行时,它正在执行一个或多个执行流或线程。线程就像一个轻量级进程,所以同时执行多个线程的程序可以被认为是同时运行多个程序的计算 +机,但是有一个重要区别:不同的线程可以同时访问相同的内存位置和其他系统资源。在具有多个处理器的机器上,可以通过为每个线程分配处理器来实现真正的并发线 程执行。 但是,如果线程多于处理器 - 通常情况下 - 多线程是通过时间分片来实现的,其中处理器在切换到下一个线程之前依次执行来自每个线程的一些指令。 -使用多线程编程有两个很好的理由。对于多内核和多处理器机器来说,一个明显的例子就是分享工作并更快完成工作。 (随着硬件设计人员越来越多地将并行化作为提高 -整体性能的方式,这个原因变得越来越引人注目)。第二个原因是两个操作可能需要不同的(可能不知道的)时间量,而您不希望响应一个操作等待另一个完成。对于图形 -用户界面(`GUI`)尤其如此,其中用户单击按钮的响应应该是立即的,并且如果程序碰巧正在运行应用程序的计算密集型部分,则不应该延迟时间。 +使用多线程编程有两个很好的理由。对于多内核和多处理器机器来说,一个明显的例子就是分享工作并更快完成工作。(随着硬件设计人员越来越多地将并行化作为提高 +整体性能的方式,这个原因变得越来越引人注目)。第二个原因是两个操作可能需要不同的(可能不知道的)时间量,而您不希望响应一个操作等待另一个完成。对于图 +形用户界面(`GUI`)尤其如此,其中用户单击按钮的响应应该是立即的,并且如果程序碰巧正在运行应用程序的计算密集型部分,则不应该延迟时间。 -尽管并发性对于获得良好的性能可能是至关重要的,但它的价格是有限的。不同的线程同时访问相同的内存位置可能会产生意想不到的结果,除非您小心限制其访问。考虑 -示例 `11-2`,其中 `ArrayStack` 类使用数组和索引来实现接口 `Stack`,该接口 `Stack` 模拟 `int` 堆栈(尽管名称相似,但此示例与示例 `5-1` 不同)。为 -了使 `ArrayStack` 正常工作,无论向堆栈中添加或删除多少个元素,变量索引都应始终指向堆栈的顶层元素。这是一个不变的类。现在想想如果两个线程同时尝试将元 -素推入堆栈,会发生什么情况。作为 `push` 方法的一部分,每个方法都将执行行 `// 1` 和 `// 2`,这些行在单线程环境中是正确的,但是在多线程环境中可能会破 -坏不变量。例如,如果线程 `A` 执行行 `// 1`,则线程 `B` 执行行 `// 1`,然后执行行 `// 2`,最后线程 `A` 执行行 `// 2`,只有线程 `B` 添加的值现在将 -位于堆栈上,它会覆盖线程 `A` 添加的值。但是,堆栈指针会增加 `2`,因此堆栈顶部位置的值就是之前发生的任何事情。这被称为竞争条件,它会使程序处于不一致的 -状态,可能会失败,因为它的其他部分将取决于不变是真实的。 +尽管并发性对于获得良好的性能可能是至关重要的,但它的价格是有限的。不同的线程同时访问相同的内存位置可能会产生意想不到的结果,除非您小心限制其访问。考 +虑示例 `11-2`,其中 `ArrayStack` 类使用数组和索引来实现接口 `Stack`,该接口 `Stack` 模拟 `int` 堆栈(尽管名称相似,但此示例与示例 `5-1` 不同)。 +为了使 `ArrayStack` 正常工作,无论向堆栈中添加或删除多少个元素,变量索引都应始终指向堆栈的顶层元素。这是一个不变的类。现在想想如果两个线程同时尝试将 +元素推入堆栈,会发生什么情况。作为 `push` 方法的一部分,每个方法都将执行行 `// 1` 和 `// 2`,这些行在单线程环境中是正确的,但是在多线程环境中可能会 +破坏不变量。例如,如果线程 `A` 执行行 `// 1`,则线程 `B` 执行行 `// 1`,然后执行行 `// 2`,最后线程 `A` 执行行 `// 2`,只有线程 `B` 添加的值现在 +将位于堆栈上,它会覆盖线程 `A` 添加的值。但是,堆栈指针会增加 `2`,因此堆栈顶部位置的值就是之前发生的任何事情。这被称为竞争条件,它会使程序处于不一致 +的状态,可能会失败,因为它的其他部分将取决于不变是真实的。 例 `11-2`。 非线程安全的堆栈实现 ```java - interface Stack { - public void push(int elt); - public int pop(); - public boolean isEmpty(); - } - class ArrayStack implements Stack{ - private final int MAX_ELEMENTS = 10; - private int[] stack; - private int index; - public ArrayStack() { - stack = new int[MAX_ELEMENTS]; - index = -1; - } - public void push(int elt) { - if (index != stack.length - 1) { - index++; - stack[index] = elt; //2 - } else { - throw new IllegalStateException("stack overflow"); - } - } - public int pop() { - if (index != -1) { - return stack[index]; - index--; - } else { - throw new IllegalStateException("stack underflow"); - } - } - public boolean isEmpty() { return index == -1; } - } +interface Stack { + public void push(int elt); + public int pop(); + public boolean isEmpty(); +} +class ArrayStack implements Stack{ + private final int MAX_ELEMENTS = 10; + private int[] stack; + private int index; + public ArrayStack() { + stack = new int[MAX_ELEMENTS]; + index = -1; + } + public void push(int elt) { + if (index != stack.length - 1) { + index++; + stack[index] = elt; //2 + } else { + throw new IllegalStateException("stack overflow"); + } + } + public int pop() { + if (index != -1) { + return stack[index]; + index--; + } else { + throw new IllegalStateException("stack underflow"); + } + } + public boolean isEmpty() { + return index == -1; + } +} ``` 并发编程在 `Java` 生命周期中的重要性日益增加,集合库中对灵活和高效的并发策略的强调也相应增强。 作为 `Java` 集合的用户,您需要对不同集合的并发策略有 @@ -67,81 +69,125 @@ 成: ```java - public synchronized void push(int elt) { ... } +public synchronized void push(int elt) { ... } ``` -这称为同步代码的关键部分,在这种情况下是整个推送方法。在一个线程可以执行同步代码之前,它必须获得某个对象的锁定 - 默认情况下,就像在这种情况下一样,当前对象。当锁由一个线程持有时,另一个线程尝试进入在该锁上同步的任何关键部分将会阻塞 - 即,将被暂停 - 直到它可以获得锁。这种同步版本的推送是线程安全的;在多线程环境中,每个线程的行为与其在单线程环境中的行为保持一致。为了保证不变,并使 `ArrayStack` 作为一个整体线程安全,方法pop和isEmpty也必须在同一个对象上同步。方法 `isEmpty` 不写入共享数据,因此不需要同步它来防止竞争条件,但出于不同的原因。每个线程都可以使用单独的内存缓存,这意味着一个线程的写入操作可能不会被另一个线程看到,除非它们都发生在同一个锁上同步的块内,或者除非变量标记了 `volatile` 关键字。 +这称为同步代码的关键部分,在这种情况下是整个推送方法。在一个线程可以执行同步代码之前,它必须获得某个对象的锁定 - 默认情况下,就像在这种情况下一样,当 +前对象。当锁由一个线程持有时,另一个线程尝试进入在该锁上同步的任何关键部分将会阻塞 - 即,将被暂停 - 直到它可以获得锁。这种同步版本的推送是线程安全的; +在多线程环境中,每个线程的行为与其在单线程环境中的行为保持一致。为了保证不变,并使 `ArrayStack` 作为一个整体线程安全,方法pop和isEmpty也必须在同一 +个对象上同步。方法 `isEmpty` 不写入共享数据,因此不需要同步它来防止竞争条件,但出于不同的原因。每个线程都可以使用单独的内存缓存,这意味着一个线程的 +写入操作可能不会被另一个线程看到,除非它们都发生在同一个锁上同步的块内,或者除非变量标记了 `volatile` 关键字。 -实际上,完全方法同步是 `JDK1.0` 中提供的集合类的策略:`Vector`,`Hashtable` 及其子类;访问其实例数据的所有方法都是同步的。这些现在被视为遗留类,因为这种政策对这些类的所有客户提出高昂的价格是要避免的,无论它们是否需要线程安全。同步可能非常昂贵:强制线程逐个排队进入关键部分,会减慢程序的整体执行速度,如果经常发生争用,管理锁的开销可能非常高。 +实际上,完全方法同步是 `JDK1.0` 中提供的集合类的策略:`Vector`,`Hashtable` 及其子类;访问其实例数据的所有方法都是同步的。这些现在被视为遗留类,因 +为这种政策对这些类的所有客户提出高昂的价格是要避免的,无论它们是否需要线程安全。同步可能非常昂贵:强制线程逐个排队进入关键部分,会减慢程序的整体执行 +速度,如果经常发生争用,管理锁的开销可能非常高。 #### `JDK 1.2`:同步集合和失败快速迭代器 -在 `JDK 1.2` 中首次引入集合框架时,`JDK 1.0` 集合中内部同步的性能成本使得设计人员避免了这种情况。相反,接口 `List`,`Set` 和 `Map` 的平台实现扩大了程序员对并发策略的选择范围。为了为单线程执行提供最高性能,新的集合根本没有提供并发控制。 (最近,对同步类 `StringBuffer` 进行了相同的策略更改,在 `Java 5` 中通过其未同步的等效 `StringBuilder` 对其进行了补充。) +在 `JDK 1.2` 中首次引入集合框架时,`JDK 1.0` 集合中内部同步的性能成本使得设计人员避免了这种情况。相反,接口 `List`,`Set` 和 `Map` 的平台实现扩大 +了程序员对并发策略的选择范围。为了为单线程执行提供最高性能,新的集合根本没有提供并发控制。 (最近,对同步类 `StringBuffer` 进行了相同的策略更改,在 +`Java 5` 中通过其未同步的等效 `StringBuilder` 对其进行了补充。) -随着这一变化,为集合迭代器提供了一个新的并发策略。在多线程环境中,获取迭代器的线程通常会继续使用它,而其他线程修改原始集合。因此,迭代器行为必须被视为集合并发策略的组成部分。如第 `11.1` 节所述,`Java 2` 集合迭代器的策略是快速失败:每次访问后备集合时,都会检查它是否进行结构修改(通常意味着元素已添加或从中删除集合)。如果他们检测到结构修改,则立即失败,抛出 `ConcurrentModificationException` 而不是继续尝试迭代修改后的集合,结果不可预知。请注意,此故障快速行为用于帮助查找和诊断错误;它不作为收集合同的一部分得到保证。 +随着这一变化,为集合迭代器提供了一个新的并发策略。在多线程环境中,获取迭代器的线程通常会继续使用它,而其他线程修改原始集合。因此,迭代器行为必须被视 +为集合并发策略的组成部分。如第 `11.1` 节所述,`Java 2` 集合迭代器的策略是快速失败:每次访问后备集合时,都会检查它是否进行结构修改(通常意味着元素已 +添加或从中删除集合)。如果他们检测到结构修改,则立即失败,抛出 `ConcurrentModificationException` 而不是继续尝试迭代修改后的集合,结果不可预知。请 +注意,此故障快速行为用于帮助查找和诊断错误;它不作为收集合同的一部分得到保证。 -没有强制同步的 `Java` 集合的出现是一个值得欢迎的发展。 然而,在很多情况下仍然需要线程安全的集合,所以框架通过同步包装提供了一个选择,使用新的集合和旧的并发策略(请参阅第 `17` 章)。 这些是通过调用 `Collections` 类中的一个工厂方法创建的,它提供了一个将被封装的非同步集合。 例如,要创建一个同步列表,您可以提供一个要包装的 `ArrayList` 实例。 包装器通过将方法调用委托给您提供的集合来实现接口,但调用在包装器对象本身上同步。 例 `11-3` 显示了例 `11-2` 的接口 `Stack` 的一个同步封装器。 要得到一个线程安全的堆栈,你可以这样写: +没有强制同步的 `Java` 集合的出现是一个值得欢迎的发展。 然而,在很多情况下仍然需要线程安全的集合,所以框架通过同步包装提供了一个选择,使用新的集合和旧 +的并发策略(请参阅第 `17` 章)。 这些是通过调用 `Collections` 类中的一个工厂方法创建的,它提供了一个将被封装的非同步集合。 例如,要创建一个同步列 +表,您可以提供一个要包装的 `ArrayList` 实例。 包装器通过将方法调用委托给您提供的集合来实现接口,但调用在包装器对象本身上同步。 例 `11-3` 显示了例 +`11-2` 的接口 `Stack` 的一个同步封装器。 要得到一个线程安全的堆栈,你可以这样写: ```java - Stack threadSafe = new SynchronizedArrayStack(new ArrayStack()); +Stack threadSafe = new SynchronizedArrayStack(new ArrayStack()); ``` -这是使用同步包装的首选方式; 对包装对象的唯一引用是由包装器保存的,因此包装对象上的所有调用都将在属于包装器对象本身的同一个锁上进行同步。 让同步的包装器可用是非常重要的,但是你不会使用它们,因为它们与传统集合具有相同的性能缺点。 +这是使用同步包装的首选方式; 对包装对象的唯一引用是由包装器保存的,因此包装对象上的所有调用都将在属于包装器对象本身的同一个锁上进行同步。 让同步的包装 +器可用是非常重要的,但是你不会使用它们,因为它们与传统集合具有相同的性能缺点。 例 `11-3`。 一个 `ArrayStack` 的同步包装器 ```java - public class SynchronizedArrayStack implements Stack { - private final Stack stack; - public SynchronizedArrayStack(Stack stack) { - this.stack = stack; - } - public synchronized void push(int elt) { stack.push(elt); } - public synchronized int pop() { return stack.pop(); } - public synchronized boolean isEmpty() { return stack.isEmpty(); } - } +public class SynchronizedArrayStack implements Stack { + private final Stack stack; + public SynchronizedArrayStack(Stack stack) { + this.stack = stack; + } + public synchronized void push(int elt) { + stack.push(elt); + } + public synchronized int pop() { + return stack.pop(); + } + public synchronized boolean isEmpty() { + return stack.isEmpty(); + } +} ``` -安全地使用同步集合即使像 `SynchronizedArrayStack` 这样拥有完全同步方法并且本身是线程安全的类仍然必须在并发环境中小心使用。 例如,这个客户端代码不是线程安全的: +安全地使用同步集合即使像 `SynchronizedArrayStack` 这样拥有完全同步方法并且本身是线程安全的类仍然必须在并发环境中小心使用。 例如,这个客户端代码不是 +线程安全的: ```java - Stack stack = new SynchronizedArrayStack(new ArrayStack()); - ... - // 不要在多线程环境中执行此操作 - if (!stack.isEmpty()) { - stack.pop(); // 可以抛出IllegalStateException - } +Stack stack = new SynchronizedArrayStack(new ArrayStack()); +... +// 不要在多线程环境中执行此操作 +if (!stack.isEmpty()) { + stack.pop(); // 可以抛出IllegalStateException +} ``` -如果在评估 `isEmpty` 和执行 `pop` 之间的时间内堆栈中的最后一个元素被另一个线程删除,则会引发异常。 这是一个常见的并发程序错误的例子,有时称为 `test-then-act`,其中程序行为由在某些情况下会过时的信息指导。 为了避免它,测试和操作必须以原子方式执行。 对于同步集合(与旧集合一样),必须通过客户端锁定实施: +如果在评估 `isEmpty` 和执行 `pop` 之间的时间内堆栈中的最后一个元素被另一个线程删除,则会引发异常。 这是一个常见的并发程序错误的例子,有时称为 +`test-then-act`,其中程序行为由在某些情况下会过时的信息指导。 为了避免它,测试和操作必须以原子方式执行。 对于同步集合(与旧集合一样),必须通过客户 +端锁定实施: ```java - synchronized(stack) { - if (!stack.isEmpty()) { - stack.pop(); - } - } +synchronized(stack) { + if (!stack.isEmpty()) { + stack.pop(); + } +} ``` -为了使这种技术能够可靠地工作,客户端用来保护原子动作的锁定应该与同步包装器的方法所使用的相同。在本例中,与同步集合中一样,包装器的方法是 在包装器对象本身上同步。(另一种方法是在单个客户端中限制对集合的引用,从而强化自己的同步规则。但是这种策略的适用性有限。) +为了使这种技术能够可靠地工作,客户端用来保护原子动作的锁定应该与同步包装器的方法所使用的相同。在本例中,与同步集合中一样,包装器的方法是 在包装器对象 +本身上同步。(另一种方法是在单个客户端中限制对集合的引用,从而强化自己的同步规则。但是这种策略的适用性有限。) -客户端锁定确保线程安全,但代价是:由于其他线程在执行操作时不能使用任何集合的方法,因此守护长时间操作(例如遍历整个阵列)将影响吞吐量 如果同步方法大量使用,这种影响可能非常大; 除非您的应用程序需要同步集合的功能,例如独占锁定,否则 `Java 5` 并发集合几乎总是更好的选择。 +客户端锁定确保线程安全,但代价是:由于其他线程在执行操作时不能使用任何集合的方法,因此守护长时间操作(例如遍历整个阵列)将影响吞吐量 如果同步方法大量 +使用,这种影响可能非常大; 除非您的应用程序需要同步集合的功能,例如独占锁定,否则 `Java 5` 并发集合几乎总是更好的选择。 #### 并发集合:`Java 5` 和其他 -`Java 5` 引入了线程安全的并发集合,作为一组更大的并发实用程序的一部分,其中包括原语 - 原子变量和锁 - 它们使 `Java` 程序员能够访问管理并发线程的相对最新的硬件创新,特别是比较和交换操作,下面解释。并发集合删除了前面部分描述的客户端锁定的必要性 - 事实上,这些集合甚至不可能实现外部同步,因为没有一个对象在被锁定时会阻止所有方法。操作需要是原子操作的,例如,只有当元素当前不存在时才将元素插入到Map中 - 并发集合提供了一个指定为自动执行的方法 - 在本例中为 `ConcurrentMap.putIfAbsent`。 +`Java 5` 引入了线程安全的并发集合,作为一组更大的并发实用程序的一部分,其中包括原语 - 原子变量和锁 - 它们使 `Java` 程序员能够访问管理并发线程的相对 +最新的硬件创新,特别是比较和交换操作,下面解释。并发集合删除了前面部分描述的客户端锁定的必要性 - 事实上,这些集合甚至不可能实现外部同步,因为没有一个 +对象在被锁定时会阻止所有方法。操作需要是原子操作的,例如,只有当元素当前不存在时才将元素插入到Map中 - 并发集合提供了一个指定为自动执行的方法 - 在本例 +中为 `ConcurrentMap.putIfAbsent`。 -如果您需要线程安全性,并发集合通常提供比同步集合更好的性能。这主要是因为它们的吞吐量并未因需要序列化访问而降低,正如同步集合的情况一样。同步的集合也遭受管理锁的开销,如果争用太多,则锁可能很高。这些差异会导致超过几个线程的并发访问的两个数量级的效率差异。 +如果您需要线程安全性,并发集合通常提供比同步集合更好的性能。这主要是因为它们的吞吐量并未因需要序列化访问而降低,正如同步集合的情况一样。同步的集合也 +遭受管理锁的开销,如果争用太多,则锁可能很高。这些差异会导致超过几个线程的并发访问的两个数量级的效率差异。 -机制并发集合通过几种不同的机制实现线程安全。其中第一个是唯一不使用新基元的是写时复制。使用 `copy-on-write` 的类将它们的值存储在内部数组中,该数组实际上是不可变的;对集合值的任何更改都会导致创建一个新数组来表示新值。同步被这些类使用,尽管只是简单地在创建新数组期间使用;由于读取操作不需要同步,因此写入时复制集合在设计它们的情况下表现良好,其中读取在写入方面占据主导地位。`Copy-on-write` 由集合类 `CopyOnWriteArrayList` 和 `CopyOnWriteArraySet` 使用。 +机制并发集合通过几种不同的机制实现线程安全。其中第一个是唯一不使用新基元的是写时复制。使用 `copy-on-write` 的类将它们的值存储在内部数组中,该数组实 +际上是不可变的;对集合值的任何更改都会导致创建一个新数组来表示新值。同步被这些类使用,尽管只是简单地在创建新数组期间使用;由于读取操作不需要同步,因此 +写入时复制集合在设计它们的情况下表现良好,其中读取在写入方面占据主导地位。`Copy-on-write` 由集合类 `CopyOnWriteArrayList` 和 +`CopyOnWriteArraySet` 使用。 -第二组线程安全集合依赖于比较和交换(`CAS`),这是对传统同步的根本改进。为了看它是如何工作的,考虑一种计算,其中将单个变量的值用作长期运行计算的输入,该计算的最终结果用于更新变量。传统的同步使得整个计算为原子操作,排除其他任何线程同时访问变量。这减少了并行执行的机会并且损害了吞吐量。基于 `CAS` 的算法表现方式不同:它会生成变量的本地副本并执行计算而不会获得排他访问权限。只有当准备好更新变量时,它才会调用 `CAS`,`CAS` 在一次原子操作中将变量值与开始时的值进行比较,如果它们相同,则用新值更新它。如果它们不相同,则该变量必须由另一个线程修改;在这种情况下,`CAS` 线程可以使用新值再次尝试整个计算,或者放弃,或者在某些算法中继续执行,因为干扰实际上已经完成了它的工作!使用 `CAS` 的集合包括 `ConcurrentLinkedQueue` 和 `ConcurrentSkipListMap`。 +第二组线程安全集合依赖于比较和交换(`CAS`),这是对传统同步的根本改进。为了看它是如何工作的,考虑一种计算,其中将单个变量的值用作长期运行计算的输入, +该计算的最终结果用于更新变量。传统的同步使得整个计算为原子操作,排除其他任何线程同时访问变量。这减少了并行执行的机会并且损害了吞吐量。基于 `CAS` 的算 +法表现方式不同:它会生成变量的本地副本并执行计算而不会获得排他访问权限。只有当准备好更新变量时,它才会调用 `CAS`,`CAS` 在一次原子操作中将变量值与开 +始时的值进行比较,如果它们相同,则用新值更新它。如果它们不相同,则该变量必须由另一个线程修改;在这种情况下,`CAS` 线程可以使用新值再次尝试整个计算,或 +者放弃,或者在某些算法中继续执行,因为干扰实际上已经完成了它的工作!使用 `CAS` 的集合包括 `ConcurrentLinkedQueue` 和 `ConcurrentSkipListMap`。 -第三组使用 `java.util.concurrent.locks.Lock` 的实现,`Java 5` 中引入的接口作为经典同步的更灵活的替代方案。锁具有与传统同步相同的基本行为,但线程也可以在特殊情况下获得它:只有当锁没有被保持,或者超时,或者线程没有中断时。与代码块或方法执行时保持对象锁的同步代码不同,锁被保持,直到它的解锁方法被调用。该组中的一些集合类使用这些工具将集合划分为可以单独锁定的部分,从而提高了并发性。例如,`LinkedBlockingQueue` 对队列的头部和尾部分别具有锁定,以便可以并行添加和删除元素。其他使用这些锁的集合包括 `ConcurrentHashMap` 和 `BlockingQueue` 的大部分实现 +第三组使用 `java.util.concurrent.locks.Lock` 的实现,`Java 5` 中引入的接口作为经典同步的更灵活的替代方案。锁具有与传统同步相同的基本行为,但线程 +也可以在特殊情况下获得它:只有当锁没有被保持,或者超时,或者线程没有中断时。与代码块或方法执行时保持对象锁的同步代码不同,锁被保持,直到它的解锁方法 +被调用。该组中的一些集合类使用这些工具将集合划分为可以单独锁定的部分,从而提高了并发性。例如,`LinkedBlockingQueue` 对队列的头部和尾部分别具有锁 +定,以便可以并行添加和删除元素。其他使用这些锁的集合包括 `ConcurrentHashMap` 和 `BlockingQueue` 的大部分实现 -迭代器上面描述的机制导致迭代器策略更适合并发使用,而不是快速失败,这隐含地将并发修改视为要消除的问题。写时复制集合具有快照迭代器。这些集合由数组支持,这些数组一旦创建,就永远不会改变;如果集合中的值需要更改,则会创建一个新数组。所以迭代器可以读取其中一个数组中的值(但从不修改它们),而没有被另一个线程更改的危险。快照迭代器不会抛出 `ConcurrentModificationException`。 +迭代器上面描述的机制导致迭代器策略更适合并发使用,而不是快速失败,这隐含地将并发修改视为要消除的问题。写时复制集合具有快照迭代器。这些集合由数组支 +持,这些数组一旦创建,就永远不会改变;如果集合中的值需要更改,则会创建一个新数组。所以迭代器可以读取其中一个数组中的值(但从不修改它们),而没有被另一 +个线程更改的危险。快照迭代器不会抛出 `ConcurrentModificationException`。 -上述第三组也具有弱一致的迭代器。 在 `Java 6` 中,这包括 `DelayQueue` 和 `PriorityBlockingQueue`,它们在 `Java 5` 中具有快速迭代器。这意味着,除非这些队列是静态的,否则当不添加或插入元素时,您不能迭代这些队列的 `Java 5` 版本; 在其他时候,你必须使用 `toArray` 将它们的元素复制到一个数组中,然后遍历它。 +上述第三组也具有弱一致的迭代器。 在 `Java 6` 中,这包括 `DelayQueue` 和 `PriorityBlockingQueue`,它们在 `Java 5` 中具有快速迭代器。这意味着,除 +非这些队列是静态的,否则当不添加或插入元素时,您不能迭代这些队列的 `Java 5` 版本; 在其他时候,你必须使用 `toArray` 将它们的元素复制到一个数组中,然 +后遍历它。 《《《 [下一节](../ch12/00_The_Collection_Interface.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch12/00_The_Collection_Interface.md b/ch12/00_The_Collection_Interface.md index a4764f0c..01767df8 100644 --- a/ch12/00_The_Collection_Interface.md +++ b/ch12/00_The_Collection_Interface.md @@ -1,18 +1,19 @@ 《《《 [返回首页](../README.md)
《《《 [上一节](../ch11/05_Collections_and_Thread_Safety.md) -## 集合接口 +### 集合接口 接口集合(参见图 `12-1`)定义了我们期望的除地图以外的任何集合的核心功能。 它提供了四组中的方法。 **添加元素** ```java - boolean add(E e) // 添加元素e - boolean addAll(Collection c) // 添加c的内容 +boolean add(E e) // 添加元素e +boolean addAll(Collection c) // 添加c的内容 ``` -这些方法返回的布尔结果表明集合是否被调用改变了。 对于集合(如集合),这可能是错误的,如果要求添加已存在的元素,集合将保持不变。 但是方法契约指定了被添加的元素在执行后必须存在,因此,如果集合因任何其他原因(例如,某些集合不允许空元素)拒绝元素,则这些方法必须抛出异常。 +这些方法返回的布尔结果表明集合是否被调用改变了。 对于集合(如集合),这可能是错误的,如果要求添加已存在的元素,集合将保持不变。 但是方法契约指定了被 +添加的元素在执行后必须存在,因此,如果集合因任何其他原因(例如,某些集合不允许空元素)拒绝元素,则这些方法必须抛出异常。 ![](12_1.png) @@ -23,57 +24,63 @@ **删除元素** ```java - boolean remove(Object o) // remove the element o - void clear() // remove all elements - boolean removeAll(Collection c) // remove the elements in c - boolean retainAll(Collection c) // remove the elements *not* in c +boolean remove(Object o) // remove the element o +void clear() // remove all elements +boolean removeAll(Collection c) // remove the elements in c +boolean retainAll(Collection c) // remove the elements *not* in c ``` -如果元素 `0` 为空,则删除集合中的空值(如果存在)。 否则,如果存在一个元素 `e`,其等于(`e`),则它将其删除。 如果没有,它将保持集合不变。 如果此组中的方法返回布尔值,则如果集合因应用操作而发生更改则值为 `true`。 与添加元素的方法相比,这些方法(以及下一个组的方法)将接受任何类型的元素或元素集合。 稍后我们将解释这一点,当我们看看使用这些方法的例子时。 +如果元素 `0` 为空,则删除集合中的空值(如果存在)。 否则,如果存在一个元素 `e`,其等于(`e`),则它将其删除。 如果没有,它将保持集合不变。 如果此组 +中的方法返回布尔值,则如果集合因应用操作而发生更改则值为 `true`。 与添加元素的方法相比,这些方法(以及下一个组的方法)将接受任何类型的元素或元素集 +合。 稍后我们将解释这一点,当我们看看使用这些方法的例子时。 **查询集合的内容** ```java - boolean contains(Object o) // 如果o存在,则为true - boolean containsAll(Collection c) // 如果集合中存在c的所有元素,则返回 true - boolean isEmpty() // 如果没有元素存在,则返回 true - int size() // 返回元素数量(如果小于则返回 Integer.MAX_VALUE) +boolean contains(Object o) // 如果o存在,则为true +boolean containsAll(Collection c) // 如果集合中存在c的所有元素,则返回 true +boolean isEmpty() // 如果没有元素存在,则返回 true +int size() // 返回元素数量(如果小于则返回 Integer.MAX_VALUE) ``` -对于超大型集合,大小返回 `Integer.MAX_VALUE` 的决定很可能是基于这样的假设,即具有超过 `20` 亿个元素的集合很少会出现。即便如此,一种提出异常而不是返回任意值的替代设计也有一定的优点,即确保规模合同能够清楚地表明,如果它能成功返回一个值,那么这个值就是正确的。 +对于超大型集合,大小返回 `Integer.MAX_VALUE` 的决定很可能是基于这样的假设,即具有超过 `20` 亿个元素的集合很少会出现。即便如此,一种提出异常而不是返 +回任意值的替代设计也有一定的优点,即确保规模合同能够清楚地表明,如果它能成功返回一个值,那么这个值就是正确的。 **使集合的内容可用于进一步处理** ```java - Iterator iterator() // 在元素上返回一个迭代器 - Object[] toArray() // 将内容复制到 Object[] - T[] toArray(T[] t) // 将内容复制到 T [](对于任何 T) +Iterator iterator() // 在元素上返回一个迭代器 +Object[] toArray() // 将内容复制到 Object[] + T[] toArray(T[] t) // 将内容复制到 T [](对于任何 T) ``` 该组中的最后两种方法将集合转换为数组。 第一种方法将创建一个新的 `Object` 数组,第二个方法接受一个 `T` 数组并返回一个包含集合元素的相同类型的数组。 -这些方法非常重要,因为尽管现在应该将数组视为遗留数据类型(请参阅第 `6.9` 节),但许多 `API`,特别是早于 `Java` 集合框架的 `API`,都有接受或返回数组的方法。 +这些方法非常重要,因为尽管现在应该将数组视为遗留数据类型(请参阅第 `6.9` 节),但许多 `API`,特别是早于 `Java` 集合框架的 `API`,都有接受或返回数组 +的方法。 -正如在第 `6.4` 节中讨论的那样,为了在运行时提供数组的可调整类型,第二个方法的参数是必需的,尽管它也可以有另一个目的:如果有空间,集合的元素被放置在 否则,创建一个新类型的数组。 如果您想允许 `toArray` 方法重用您提供的数组,可以使用第一种情况; 这可以更有效率,特别是如果该方法被重复调用。 第二种情况更方便 - 一种常见而直接的用法是提供一个零长度的数组: +正如在第 `6.4` 节中讨论的那样,为了在运行时提供数组的可调整类型,第二个方法的参数是必需的,尽管它也可以有另一个目的:如果有空间,集合的元素被放置在 +否则,创建一个新类型的数组。 如果您想允许 `toArray` 方法重用您提供的数组,可以使用第一种情况; 这可以更有效率,特别是如果该方法被重复调用。 第二种情 +况更方便 - 一种常见而直接的用法是提供一个零长度的数组: ```java - Collection cs = ... - String[] sa = cs.toArray(new String[0]); +Collection cs = ... +String[] sa = cs.toArray(new String[0]); ``` 一个更有效的替代方法是,如果一个类不止一次地使用这个习语,那就是声明一个所需类型的空数组,然后可以根据需要多次使用它: ```java - private static final String[] EMPTY_STRING_ARRAY = new String[0]; - Collection cs = ... - String[] sa = cs.toArray(EMPTY_STRING_ARRAY); +private static final String[] EMPTY_STRING_ARRAY = new String[0]; +Collection cs = ... +String[] sa = cs.toArray(EMPTY_STRING_ARRAY); ``` 为什么在 `toArray` 的声明中允许任何类型的 `T`? 一个原因是如果集合碰巧包含这种类型的元素,则可以灵活地分配更具体的数组类型: ```java - List l = Array.asList("zero","one"); - String[] a = l.toArray(new String[0]); +List l = Array.asList("zero","one"); +String[] a = l.toArray(new String[0]); ``` 在这里,一个对象列表恰巧只包含字符串,所以它可以转换成一个字符串数组,类似于 `6.2` 节所述的提升方法。 @@ -81,30 +88,34 @@ 如果列表包含不是字符串的对象,则会在运行时捕获该错误,而不是编译期: ```java - List l = Array.asList("zero","one",2); - String[] a = l.toArray(new String[0]); // 运行期错误 +List l = Array.asList("zero","one",2); +String[] a = l.toArray(new String[0]); // 运行期错误 ``` 在这里,调用会引发 `ArrayStoreException`,这是在尝试将数组分配给具有不相容的指定类型的数组时发生的异常。 -一般来说,人们可能希望将给定类型的集合复制到更具体类型的数组中(例如,将对象列表复制到字符串数组中,如前所示)或更一般类型( 例如,将一个字符串列表复制到一个对象数组中)。 人们永远不想将给定类型的集合复制到完全不相关类型的数组中(例如,将整数列表复制到字符串数组中始终是错误的)。 但是,在 `Java` 中没有办法指定这个约束,所以这些错误在运行时被捕获而不是编译时。 +一般来说,人们可能希望将给定类型的集合复制到更具体类型的数组中(例如,将对象列表复制到字符串数组中,如前所示)或更一般类型( 例如,将一个字符串列表复 +制到一个对象数组中)。 人们永远不想将给定类型的集合复制到完全不相关类型的数组中(例如,将整数列表复制到字符串数组中始终是错误的)。 但是,在 `Java` +中没有办法指定这个约束,所以这些错误在运行时被捕获而不是编译时。 这种设计的一个缺点是它不适用于原始类型的数组: ```java - List l = Array.asList(0,1,2); - int[] a = l.toArray(new int[0]); // 编译错误 +List l = Array.asList(0,1,2); +int[] a = l.toArray(new int[0]); // 编译错误 ``` -这是非法的,因为方法调用中的类型参数T一如既往必须是引用类型。如果我们用 `int` 来替换两个 `int` 实例,调用将会起作用,但通常这不会执行,因为出于性能或兼容性的原因,我们需要一个原始类型的数组。 在这种情况下,除了显式复制数组外,没有其他用途。 +这是非法的,因为方法调用中的类型参数T一如既往必须是引用类型。如果我们用 `int` 来替换两个 `int` 实例,调用将会起作用,但通常这不会执行,因为出于性能 +或兼容性的原因,我们需要一个原始类型的数组。 在这种情况下,除了显式复制数组外,没有其他用途。 ```java - List l = Array.asList(0,1,2); - int[] a = new int[l.size()]; - for (int i=0; i l = Array.asList(0,1,2); +int[] a = new int[l.size()]; +for (int i=0; i -《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch12/01_Using_the_Methods_of_Collection.md b/ch12/01_Using_the_Methods_of_Collection.md index bdeb46ba..568acd3b 100644 --- a/ch12/01_Using_the_Methods_of_Collection.md +++ b/ch12/01_Using_the_Methods_of_Collection.md @@ -3,251 +3,278 @@ ### 使用集合方法 -为了说明集合类的用法,我们来构造一个小例子。 你的作者永远都想组织起来; 让我们想象一下,我们最新的努力涉及编写我们自己的待办事项经理。我们首先定义一个表示任务的类,然后用子类表示不同类型的任务,例如编写代码或拨打电话。 +为了说明集合类的用法,我们来构造一个小例子。 你的作者永远都想组织起来; 让我们想象一下,我们最新的努力涉及编写我们自己的待办事项经理。我们首先定义一个 +表示任务的类,然后用子类表示不同类型的任务,例如编写代码或拨打电话。 以下是我们将要使用的任务的定义: ```java - public abstract class Task implements Comparable { - protected Task() {} - public boolean equals(Object o) { - if (o instanceof Task) { - return toString().equals(o.toString()); - } else - return false; - } - public int compareTo(Task t) { - return toString().compareTo(t.toString()); - } - public int hashCode() { - return toString().hashCode(); - } - public abstract String toString(); - } +public abstract class Task implements Comparable { + protected Task() {} + public boolean equals(Object o) { + if (o instanceof Task) { + return toString().equals(o.toString()); + } else + return false; + } + public int compareTo(Task t) { + return toString().compareTo(t.toString()); + } + public int hashCode() { + return toString().hashCode(); + } + public abstract String toString(); +} ``` -我们只需要四个任务操作:`equals`,`compareTo`,`hashCode` 和 `toString`。 `Equality` 将用于测试集合是否包含给定任务,比较将被有序集合(如 `OrderedSet` 和 `OrderedMap`)使用,并且哈希代码将被基于哈希表(例如 `HashSet` 和 `HashMap`)的集合使用,并且每当我们显示集合的内容时,都会使用任务的字符串表示形式。前三种方法是根据 `toString` 方法定义的,它被声明为抽象的,所以它必须在 `Task` 的每个子类中定义。如果两个任务由同一个字符串表示,我们认为两个任务相等,任务的自然排序与其字符串排序相同。这保证了任务的自然顺序与平等一致,正如 `3.1` 节所讨论的 - 也就是说,当 `equals` 返回 `true` 时,`compareTo` 返回 `0`。 +我们只需要四个任务操作:`equals`,`compareTo`,`hashCode` 和 `toString`。 `Equality` 将用于测试集合是否包含给定任务,比较将被有序集合(如 +`OrderedSet` 和 `OrderedMap`)使用,并且哈希代码将被基于哈希表(例如 `HashSet` 和 `HashMap`)的集合使用,并且每当我们显示集合的内容时,都会使用任 +务的字符串表示形式。前三种方法是根据 `toString` 方法定义的,它被声明为抽象的,所以它必须在 `Task` 的每个子类中定义。如果两个任务由同一个字符串表示, +我们认为两个任务相等,任务的自然排序与其字符串排序相同。这保证了任务的自然顺序与平等一致,正如 `3.1` 节所讨论的 - 也就是说,当 `equals` 返回 `true` +时,`compareTo` 返回 `0`。 我们为两类任务定义子类,编写一些代码并拨打电话: ```java - public final class CodingTask extends Task { - private final String spec; - public CodingTask(String spec) { - this.spec = spec; - } - public String getSpec() { return spec; } - public String toString() { return "code " + spec; } - } - public final class PhoneTask extends Task { - private final String name; - private final String number; - public PhoneTask(String name, String number) { - this.name = name; - this.number = number; - } - public String getName() { return name; } - public String getNumber() { return number; } - public String toString() { return "phone " + name; } - } +public final class CodingTask extends Task { + private final String spec; + public CodingTask(String spec) { + this.spec = spec; + } + public String getSpec() { + return spec; + } + public String toString() { + return "code " + spec; + } +} +public final class PhoneTask extends Task { + private final String name; + private final String number; + public PhoneTask(String name, String number) { + this.name = name; + this.number = number; + } + public String getName() { + return name; + } + public String getNumber() { + return number; + } + public String toString() { + return "phone " + name; + } +} ``` -编码任务由字符串指定,电话任务由要调用的人员的姓名和号码指定。在每种情况下,我们都提供了类的构造函数,访问其字段的方法以及将其转换为字符串的方法。根据良好的实践,我们已经通过声明这些字段为 `final` 来使这两种任务不可变,并且我们已声明两个子类都是最终的,以便任何人稍后可以定义可变子类(参见“最小化可变性”/“最喜欢不变性“)由 `Joshua Bloch`,`Addison-Wesley` 撰写的 `Effective Java` 第 `4` 章)。 +编码任务由字符串指定,电话任务由要调用的人员的姓名和号码指定。在每种情况下,我们都提供了类的构造函数,访问其字段的方法以及将其转换为字符串的方法。根 +据良好的实践,我们已经通过声明这些字段为 `final` 来使这两种任务不可变,并且我们已声明两个子类都是最终的,以便任何人稍后可以定义可变子类(参见“最小化 +可变性”/“最喜欢不变性“)由 `Joshua Bloch`,`Addison-Wesley` 撰写的 `Effective Java` 第 `4` 章)。 -`toString` 方法将字符串“code”和每个电话任务的每个编码任务的前面加上字符串“phone”。由于第一个字母顺序是按照字母顺序排在第二位,并且由于任务按照 `toString` 返回的结果排序,编码任务在任务自然排序之前出现在电话任务之前,这符合我们的需求 - 毕竟我们是极客! +`toString` 方法将字符串“code”和每个电话任务的每个编码任务的前面加上字符串“phone”。由于第一个字母顺序是按照字母顺序排在第二位,并且由于任务按照 +`toString` 返回的结果排序,编码任务在任务自然排序之前出现在电话任务之前,这符合我们的需求 - 毕竟我们是极客! -为了紧凑,电话任务的 `toString` 方法仅返回要呼叫的人员的姓名,而不是电话号码。我们假设我们从不会创建两个具有相同名称和不同号码的电话任务;如果我们这样做了,使用 `toString` 返回的结果来测试相等性是错误的。 +为了紧凑,电话任务的 `toString` 方法仅返回要呼叫的人员的姓名,而不是电话号码。我们假设我们从不会创建两个具有相同名称和不同号码的电话任务;如果我们这 +样做了,使用 `toString` 返回的结果来测试相等性是错误的。 我们也定义一个空的任务: ```java - public class EmptyTask extends Task { - public EmptyTask() {} - public String toString() { return ""; } - } +public class EmptyTask extends Task { + public EmptyTask() {} + public String toString() { + return ""; + } +} ``` 例 `12-1`。 任务管理器的示例任务和任务集合 ```java - PhoneTask mikePhone = new PhoneTask("Mike", "987 6543"); - PhoneTask paulPhone = new PhoneTask("Paul", "123 4567"); - CodingTask databaseCode = new CodingTask("db"); - CodingTask interfaceCode = new CodingTask("gui"); - CodingTask logicCode = new CodingTask("logic"); - Collection phoneTasks = new ArrayList(); - Collection codingTasks = new ArrayList(); - Collection mondayTasks = new ArrayList(); - Collection tuesdayTasks = new ArrayList(); - Collections.addAll(phoneTasks, mikePhone, paulPhone); - Collections.addAll(codingTasks, databaseCode, interfaceCode, logicCode); - Collections.addAll(mondayTasks, logicCode, mikePhone); - Collections.addAll(tuesdayTasks, databaseCode, interfaceCode, paulPhone); - assert phoneTasks.toString().equals("[phone Mike, phone Paul]"); - assert codingTasks.toString().equals("[code db, code gui, code logic]"); - assert mondayTasks.toString().equals("[code logic, phone Mike]"); - assert tuesdayTasks.toString().equals("[code db, code gui, phone Paul]"); +PhoneTask mikePhone = new PhoneTask("Mike", "987 6543"); +PhoneTask paulPhone = new PhoneTask("Paul", "123 4567"); +CodingTask databaseCode = new CodingTask("db"); +CodingTask interfaceCode = new CodingTask("gui"); +CodingTask logicCode = new CodingTask("logic"); +Collection phoneTasks = new ArrayList(); +Collection codingTasks = new ArrayList(); +Collection mondayTasks = new ArrayList(); +Collection tuesdayTasks = new ArrayList(); +Collections.addAll(phoneTasks, mikePhone, paulPhone); +Collections.addAll(codingTasks, databaseCode, interfaceCode, logicCode); +Collections.addAll(mondayTasks, logicCode, mikePhone); +Collections.addAll(tuesdayTasks, databaseCode, interfaceCode, paulPhone); +assert phoneTasks.toString().equals("[phone Mike, phone Paul]"); +assert codingTasks.toString().equals("[code db, code gui, code logic]"); +assert mondayTasks.toString().equals("[code logic, phone Mike]"); +assert tuesdayTasks.toString().equals("[code db, code gui, phone Paul]"); ``` -由于空字符串在字符串上的自然排序中位于所有其他字符串之前,因此空任务在任务的自然排序中位于所有其他字符之前。当我们构造有序集合的范围视图时,这个任务会很有用(见 `13.2` 节)。 +由于空字符串在字符串上的自然排序中位于所有其他字符串之前,因此空任务在任务的自然排序中位于所有其他字符之前。当我们构造有序集合的范围视图时,这个任务 +会很有用(见 `13.2` 节)。 -例 `12-1` 展示了我们如何定义一系列要执行的任务(即使在真实系统中,他们更可能从数据库中检索)。我们选择了 `ArrayList` 作为本例中使用的 `Collection` 的实现,但我们不打算利用列表的任何特殊属性;我们将 `ArrayList` 视为 `Collection` 的实现,仅此而已。作为检索过程的一部分,我们已经使用 `1.4` 节中介绍的方法 `Collections.addAll` 将这些任务组织到由列表表示的各种类别中。 +例 `12-1` 展示了我们如何定义一系列要执行的任务(即使在真实系统中,他们更可能从数据库中检索)。我们选择了 `ArrayList` 作为本例中使用的 `Collection` +的实现,但我们不打算利用列表的任何特殊属性;我们将 `ArrayList` 视为 `Collection` 的实现,仅此而已。作为检索过程的一部分,我们已经使用 `1.4` 节中介 +绍的方法 `Collections.addAll` 将这些任务组织到由列表表示的各种类别中。 现在我们可以使用 `Collection` 的方法来处理这些类别。我们在这里介绍的例子按照前面介绍的顺序使用了这些方法。 **添加元素**我们可以将新任务添加到计划中: ```java - mondayTasks.add(new PhoneTask("Ruth", "567 1234")); - assert mondayTasks.toString().equals( - "[code logic, phone Mike, phone Ruth]"); +mondayTasks.add(new PhoneTask("Ruth", "567 1234")); +assert mondayTasks.toString().equals("[code logic, phone Mike, phone Ruth]"); ``` 或者我们可以将时间表组合在一起: ```java - Collection allTasks = new ArrayList(mondayTasks); - allTasks.addAll(tuesdayTasks); - assert allTasks.toString().equals( - "[code logic, phone Mike, phone Ruth, code db, code gui, phone Paul]"); +Collection allTasks = new ArrayList(mondayTasks); +allTasks.addAll(tuesdayTasks); +assert allTasks.toString().equals("[code logic, phone Mike, phone Ruth, code db, code gui, phone Paul]"); ``` **删除元素**任务完成后,我们可以从时间表中删除它: ```java - boolean wasPresent = mondayTasks.remove(mikePhone); - assert wasPresent; - assert mondayTasks.toString().equals("[code logic, phone Ruth]"); +boolean wasPresent = mondayTasks.remove(mikePhone); +assert wasPresent; +assert mondayTasks.toString().equals("[code logic, phone Ruth]"); ``` 我们可以完全清除一个时间表,因为它的所有任务都已完成(是的,正确的): ```java - mondayTasks.clear(); - assert mondayTasks.toString().equals("[]"); +mondayTasks.clear(); +assert mondayTasks.toString().equals("[]"); ``` 删除方法还允许我们以各种方式组合整个集合。 例如,要查看电话以外的其他任务是否安排在星期二,我们可以编写: ```java - Collection tuesdayNonphoneTasks = new ArrayList(tuesdayTasks); - tuesdayNonphoneTasks.removeAll(phoneTasks); - assert tuesdayNonphoneTasks.toString().equals("[code db, code gui]"); +Collection tuesdayNonphoneTasks = new ArrayList(tuesdayTasks); +tuesdayNonphoneTasks.removeAll(phoneTasks); +assert tuesdayNonphoneTasks.toString().equals("[code db, code gui]"); ``` 或查看当天计划拨打哪些电话: ```java - Collection phoneTuesdayTasks = new ArrayList(tuesdayTasks); - phoneTuesdayTasks.retainAll(phoneTasks); - assert phoneTuesdayTasks.toString().equals("[phone Paul]"); +Collection phoneTuesdayTasks = new ArrayList(tuesdayTasks); +phoneTuesdayTasks.retainAll(phoneTasks); +assert phoneTuesdayTasks.toString().equals("[phone Paul]"); ``` 最后一个例子可以通过不同的方式获得相同的结果: ```java - Collection tuesdayPhoneTasks = - new ArrayList(phoneTasks); - tuesdayPhoneTasks.retainAll(tuesdayTasks); - assert tuesdayPhoneTasks.toString().equals("[phone Paul]"); +Collection tuesdayPhoneTasks = new ArrayList(phoneTasks); +tuesdayPhoneTasks.retainAll(tuesdayTasks); +assert tuesdayPhoneTasks.toString().equals("[phone Paul]"); ``` 请注意,`phoneTuesdayTasks` 具有 `List` 类型,而星期二 `PhoneTasks` 具有更精确的 `List` 类型。 -这个例子提供了对这个组和下一个方法的签名的解释。我们已经讨论过(第 `2.6` 节),当添加到集合中的方法将它们的参数限制为它的参数类型时,他们为什么会接受 `Object` 或 `Collection` 类型的参数。以 `retainAll` 为例,它的合同要求删除这个集合中不存在于参数集合中的那些元素。这没有理由限制论证集合可能包含的内容;在前面的示例中,它可以包含任何种类的任务的实例,而不仅仅是 `PhoneTask`。即使将参数限制为参数类型的超类型的集合也太狭窄了;我们希望可能的限制性最小的类型是 `Collection。类似的推理适用于 `remove`,`removeAll`,`contains` 和 `containsAll`。 +这个例子提供了对这个组和下一个方法的签名的解释。我们已经讨论过(第 `2.6` 节),当添加到集合中的方法将它们的参数限制为它的参数类型时,他们为什么会接受 +`Object` 或 `Collection` 类型的参数。以 `retainAll` 为例,它的合同要求删除这个集合中不存在于参数集合中的那些元素。这没有理由限制论证集合可能包 +含的内容;在前面的示例中,它可以包含任何种类的任务的实例,而不仅仅是 `PhoneTask`。即使将参数限制为参数类型的超类型的集合也太狭窄了;我们希望可能的限制 +性最小的类型是 `Collection`。类似的推理适用于 `remove`,`removeAll`,`contains` 和 `containsAll`。 -**查询集合的内容**这些方法允许我们检查,例如,上述操作是否正常工作。我们将在这里使用断言来使系统检查我们的信念,即我们已经正确编程了以前的操作。例如,如果 `tuesdayPhoneTasks` 不包含 `paulPhone`,则第一条语句将抛出一个 `AssertionError`: +**查询集合的内容**这些方法允许我们检查,例如,上述操作是否正常工作。我们将在这里使用断言来使系统检查我们的信念,即我们已经正确编程了以前的操作。例 +如,如果 `tuesdayPhoneTasks` 不包含 `paulPhone`,则第一条语句将抛出一个 `AssertionError`: ```java - assert tuesdayPhoneTasks.contains(paulPhone); - assert tuesdayTasks.containsAll(tuesdayPhoneTasks); - assert mondayTasks.isEmpty(); - assert mondayTasks.size() == 0; +assert tuesdayPhoneTasks.contains(paulPhone); +assert tuesdayTasks.containsAll(tuesdayPhoneTasks); +assert mondayTasks.isEmpty(); +assert mondayTasks.size() == 0; ``` 使集合内容可用于进一步处理此组中的方法为集合提供迭代器或将其转换为数组。 -第 `11.1` 节展示了如何在 `Java 5` 中用 `foreach` 语句代替 `iterator` 的最简单和最常见的显式使用,`foreach` 语句隐式使用它们。 但是有一些 `foreach` 不能帮助迭代的用法; 如果要更改集合的结构而不遇到 `ConcurrentModificationException`,或者想要并行处理两个列表,则必须使用显式迭代器。 例如,假设我们决定在星期二没有时间进行电话任务。 使用 `foreach` 将它们从我们的任务列表中过滤出来可能会很诱人,但是这不会出于第 `11.1` 节中描述的原因: +第 `11.1` 节展示了如何在 `Java 5` 中用 `foreach` 语句代替 `iterator` 的最简单和最常见的显式使用,`foreach` 语句隐式使用它们。 但是有一些 +`foreach` 不能帮助迭代的用法; 如果要更改集合的结构而不遇到 `ConcurrentModificationException`,或者想要并行处理两个列表,则必须使用显式迭代器。 例 +如,假设我们决定在星期二没有时间进行电话任务。 使用 `foreach` 将它们从我们的任务列表中过滤出来可能会很诱人,但是这不会出于第 `11.1` 节中描述的原因: ```java - // throws ConcurrentModificationException - for (Task t : tuesdayTasks) { - if (t instanceof PhoneTask) { - tuesdayTasks.remove(t); - } - } +// throws ConcurrentModificationException +for (Task t : tuesdayTasks) { + if (t instanceof PhoneTask) { + tuesdayTasks.remove(t); + } +} ``` 如果仍然使用修改结构的 `Collection` 方法,则显式使用迭代器并不会有任何改进: ```java - // throws ConcurrentModificationException - for (Iterator it = tuesdayTasks.iterator() ; it.hasNext() ; ) { - Task t = it.next(); - if (t instanceof PhoneTask) { - tuesdayTasks.remove(t); - } - } +// throws ConcurrentModificationException +for (Iterator it = tuesdayTasks.iterator() ; it.hasNext() ; ) { + Task t = it.next(); + if (t instanceof PhoneTask) { + tuesdayTasks.remove(t); + } +} ``` 但是使用迭代器的结构变化方法给出了我们想要的结果: ```java - for (Iterator it = tuesdayTasks.iterator() ; it.hasNext() ; ) { - Task t = it.next(); - if (t instanceof PhoneTask) { - it.remove(); - } - } +for (Iterator it = tuesdayTasks.iterator() ; it.hasNext() ; ) { + Task t = it.next(); + if (t instanceof PhoneTask) { + it.remove(); + } +} ``` 例 `12-2`。 使用自然顺序合并集合 ```java - public class MergeCollections { - static > List merge(Collection c1, Collection c2) { - List mergedList = new ArrayList(); - Iterator itr1 = c1.iterator(); - Iterator itr2 = c2.iterator(); - T c1Element = getNextElement(itr1); - T c2Element = getNextElement(itr2); - // 每次迭代都会从迭代器中取一个任务;继续下去,直到迭代器都没有任何进一步的任务 - while (c1Element != null || c2Element != null) { - //使用当前的c1元素,如果当前c2 -        //元素为null,或者两者都是非空和c1元素 -        //以自然顺序在c2元素之前 - boolean useC1Element = c2Element == null || - c1Element != null && c1Element.compareTo(c2Element) < 0; - if (useC1Element) { - mergedList.add(c1Element); - c1Element = getNextElement(itr1); - } else { - mergedList.add(c2Element); - c2Element = getNextElement(itr2); - } - } - return mergedList; - } - static E getNextElement(Iterator itr) { - if (itr.hasNext()){ - E nextElement = itr.next(); - if (nextElement == null) throw new NullPointerException(); - return nextElement; - } else { - return null; - } - } - } -``` - -再举一个例子,假设我们是挑剔的人,喜欢按照升序排列我们所有的任务列表,并且我们希望将两个任务列表合并到一个列表中,同时保持顺序。 例 `12-2` 展示了如何将两个集合合并到第三个集合中,前提是每个集合的迭代器按照自然顺序返回它们的元素。 此方法依赖于要合并的集合不包含空元素的事实; 如果遇到一个,该方法抛出一个 `NullPointerException`。碰巧,例 `12-1` 中的集合 `mondayTasks` 和 `tuesdayTasks` 都是按升序排列的,我们可以按如下方式合并它们: - -```java - Collection mergedTasks = - MergeCollections.merge(mondayTasks, tuesdayTasks); - assert mergedTasks.toString().equals( - "[code db, code gui, code logic, phone Mike, phone Paul]"); +public class MergeCollections { + static > List merge(Collection c1, Collection c2) { + List mergedList = new ArrayList(); + Iterator itr1 = c1.iterator(); + Iterator itr2 = c2.iterator(); + T c1Element = getNextElement(itr1); + T c2Element = getNextElement(itr2); + // 每次迭代都会从迭代器中取一个任务;继续下去,直到迭代器都没有任何进一步的任务 + while (c1Element != null || c2Element != null) { + //使用当前的c1元素,如果当前c2 + //元素为null,或者两者都是非空和c1元素 + //以自然顺序在c2元素之前 + boolean useC1Element = c2Element == null ||c1Element != null && c1Element.compareTo(c2Element) < 0; + if (useC1Element) { + mergedList.add(c1Element); + c1Element = getNextElement(itr1); + } else { + mergedList.add(c2Element); + c2Element = getNextElement(itr2); + } + } + return mergedList; + } + static E getNextElement(Iterator itr) { + if (itr.hasNext()){ + E nextElement = itr.next(); + if (nextElement == null) + throw new NullPointerException(); + return nextElement; + } else { + return null; + } + } +} +``` + +再举一个例子,假设我们是挑剔的人,喜欢按照升序排列我们所有的任务列表,并且我们希望将两个任务列表合并到一个列表中,同时保持顺序。 例 `12-2` 展示了如何 +将两个集合合并到第三个集合中,前提是每个集合的迭代器按照自然顺序返回它们的元素。 此方法依赖于要合并的集合不包含空元素的事实; 如果遇到一个,该方法抛出 +一个 `NullPointerException`。碰巧,例 `12-1` 中的集合 `mondayTasks` 和 `tuesdayTasks` 都是按升序排列的,我们可以按如下方式合并它们: + +```java +Collection mergedTasks = MergeCollections.merge(mondayTasks, tuesdayTasks); +assert mergedTasks.toString().equals("[code db, code gui, code logic, phone Mike, phone Paul]"); ``` 《《《 [下一节](02_Implementing_Collection.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch12/02_Implementing_Collection.md b/ch12/02_Implementing_Collection.md index 13900825..a90ce47f 100644 --- a/ch12/02_Implementing_Collection.md +++ b/ch12/02_Implementing_Collection.md @@ -3,7 +3,9 @@ ### 集合实现 -`Collection` 没有具体的实现。 类 `AbstractCollection` 部分实现它,它是一系列骨架实现中的一个 - 包括 `AbstractSet`,`AbstractList`等 - 它们提供了每个接口的不同具体实现的通用功能。 这些框架实现可用于帮助框架接口的新实现的设计者。例如,`Collection` 可以作为包(无序列表)的接口,而实现包的程序员可以扩展 `AbstractCollection` 并查找大部分已经完成的实现工作。 +`Collection` 没有具体的实现。 类 `AbstractCollection` 部分实现它,它是一系列骨架实现中的一个 - 包括 `AbstractSet`,`AbstractList`等 - 它们提供了 +每个接口的不同具体实现的通用功能。 这些框架实现可用于帮助框架接口的新实现的设计者。例如,`Collection` 可以作为包(无序列表)的接口,而实现包的程序员可 +以扩展 `AbstractCollection` 并查找大部分已经完成的实现工作。 《《《 [下一节](03_Collection_Constructors.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch12/03_Collection_Constructors.md b/ch12/03_Collection_Constructors.md index 8c1ebcea..7f3c26c6 100644 --- a/ch12/03_Collection_Constructors.md +++ b/ch12/03_Collection_Constructors.md @@ -6,13 +6,17 @@ 我们将在接下来的三章中继续研究三种主要的集合,但我们首先应该解释大多数集合实现共享的两种常见的构造方法。 以 `HashSet` 为例,它们是: ```java - public HashSet() - public HashSet(Collection c) +public HashSet() +public HashSet(Collection c) ``` -其中第一个创建一个空集,第二个集包含任何参数类型或其子类型集合的元素。使用这个构造函数与使用默认构造函数创建一个空集具有相同的效果,然后使用 `addAll` 添加集合的内容。这有时被称为“复制构造函数”,但该术语应该保留给构造函数,该构造函数复制同一类的对象,而第二种形式的构造函数可以接受任何实现接口 `Collection 。 `Joshua Bloch` 建议使用术语“转换构造函数”。 +其中第一个创建一个空集,第二个集包含任何参数类型或其子类型集合的元素。使用这个构造函数与使用默认构造函数创建一个空集具有相同的效果,然后使用 `addAll` +添加集合的内容。这有时被称为“复制构造函数”,但该术语应该保留给构造函数,该构造函数复制同一类的对象,而第二种形式的构造函数可以接受任何实现接口 +`Collection `。 `Joshua Bloch` 建议使用术语“转换构造函数”。 -并非所有集合类都具有这两种形式的构造函数 - 例如,`ArrayBlockingQueue` 不能在未修复其容量的情况下创建,并且 `SynchronousQueue` 根本不能容纳任何元素,因此不需要第二种形式的构造函数。另外,许多集合类除了这两个以外还有其他构造函数,但是它们所拥有的不是它们所实现的接口,而是取决于底层实现;这些额外的构造函数用于配置实现。 +并非所有集合类都具有这两种形式的构造函数 - 例如,`ArrayBlockingQueue` 不能在未修复其容量的情况下创建,并且 `SynchronousQueue` 根本不能容纳任何元素, +因此不需要第二种形式的构造函数。另外,许多集合类除了这两个以外还有其他构造函数,但是它们所拥有的不是它们所实现的接口,而是取决于底层实现;这些额外的构造 +函数用于配置实现。 《《《 [下一节](../ch13/00_Sets.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch13/00_Sets.md b/ch13/00_Sets.md index 4bf8f768..67ea6fce 100644 --- a/ch13/00_Sets.md +++ b/ch13/00_Sets.md @@ -3,15 +3,18 @@ ## Sets -一个集合是不能包含重复项目的集合; 如果它已经存在于集合中,则添加它不起作用。`Set` 接口的方法与 `Collection` 的方法相同,但它是分开定义的,以允许以这种方式更改 `add`(和 `addAll`,这是用 `add` 定义的)合约。 回到上一章中的任务管理器示例,假设星期一您有空闲时间执行电话任务。 您可以通过将所有电话任务添加到星期一任务来进行相应的收集。 让 `mondayTasks` 和 `phone` 任务如例 `12-1` 中所声明的那样。 使用一个集合(再次选择一个方便常见的 `Set` 实现),你可以写: +一个集合是不能包含重复项目的集合; 如果它已经存在于集合中,则添加它不起作用。`Set` 接口的方法与 `Collection` 的方法相同,但它是分开定义的,以允许以这种 +方式更改 `add`(和 `addAll`,这是用 `add` 定义的)合约。 回到上一章中的任务管理器示例,假设星期一您有空闲时间执行电话任务。 您可以通过将所有电话任务添 +加到星期一任务来进行相应的收集。 让 `mondayTasks` 和 `phone` 任务如例 `12-1` 中所声明的那样。 使用一个集合(再次选择一个方便常见的 `Set` 实现),你 +可以写: ```java - Set phoneAndMondayTasks = new TreeSet(mondayTasks); - phoneAndMondayTasks.addAll(phoneTasks); - assert phoneAndMondayTasks.toString().equals("[code logic, phone Mike, phone Paul]"); +Set phoneAndMondayTasks = new TreeSet(mondayTasks); +phoneAndMondayTasks.addAll(phoneTasks); +assert phoneAndMondayTasks.toString().equals("[code logic, phone Mike, phone Paul]"); ``` -这是因为处理重复元素的方式。 在星期一任务和电话任务中的任务麦克电话,只有一次出现在 `phoneAndMondayTasks 中 - 你绝对不希望两次完成所有这些任务! +这是因为处理重复元素的方式。 在星期一任务和电话任务中的任务麦克电话,只有一次出现在 `phoneAndMondayTasks` 中 - 你绝对不希望两次完成所有这些任务! 《《《 [下一节](01_Implementing_Set.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch13/01_Implementing_Set.md b/ch13/01_Implementing_Set.md index d6d8f936..479df837 100644 --- a/ch13/01_Implementing_Set.md +++ b/ch13/01_Implementing_Set.md @@ -3,9 +3,13 @@ ### 实现Set -当我们在第 `12` 章的例子中使用 `Collection` 的方法时,我们强调他们可以使用 `Collection` 的任何实现。 如果我们决定使用集合框架中的一个 `Set` 实现呢? 我们必须在框架提供的各种具体实现之间进行选择,它们在执行添加,包含和迭代等基本操作的速度以及迭代器返回其元素的顺序上有所不同。 在本节和下一节中,我们将看看这些差异,然后在本章的最后我们将总结不同实现的比较性能。 +当我们在第 `12` 章的例子中使用 `Collection` 的方法时,我们强调他们可以使用 `Collection` 的任何实现。 如果我们决定使用集合框架中的一个 `Set` 实现 +呢? 我们必须在框架提供的各种具体实现之间进行选择,它们在执行添加,包含和迭代等基本操作的速度以及迭代器返回其元素的顺序上有所不同。 在本节和下一节 +中,我们将看看这些差异,然后在本章的最后我们将总结不同实现的比较性能。 -集合框架中有六个具体的 `Set` 实现。 图 `13-1` 显示了它们与 `Set` 及其子接口 `SortedSet` 和 `NavigableSet` 的关系。 在本节中,我们将看看 `HashSe`t,`LinkedHashSet`,`CopyOnWriteArraySet` 和 `EnumSet`。 我们将在 `13.2` 节讨论 `SortedSet` 和 `NavigableSet`,以及它们的实现 `TreeSet` 和 `ConcurrentSkipListSet`。 +集合框架中有六个具体的 `Set` 实现。 图 `13-1` 显示了它们与 `Set` 及其子接口 `SortedSet` 和 `NavigableSet` 的关系。 在本节中,我们将看看 +`HashSe`t,`LinkedHashSet`,`CopyOnWriteArraySet` 和 `EnumSet`。 我们将在 `13.2` 节讨论 `SortedSet` 和 `NavigableSet`,以及它们的实现 +`TreeSet` 和 `ConcurrentSkipListSet`。 ![](13_0.png) 图 `13-1`。`Set` 接口的实现 @@ -13,53 +17,71 @@ ### HashSet -这个类是 `Set` 的最常用的实现。 顾名思义,它是由一个哈希表实现的,这个哈希表是一个数组,其中元素存储在从其内容派生的位置。 由于哈希表按其内容存储和检索元素,因此它们非常适合于实现 `Set` 的操作(集合框架也将它们用于 `Map` 的各种实现)。 例如,要实现 `contains(Object o),你需要查找元素 `o`,如果发现它,则返回 `true`。 +这个类是 `Set` 的最常用的实现。 顾名思义,它是由一个哈希表实现的,这个哈希表是一个数组,其中元素存储在从其内容派生的位置。 由于哈希表按其内容存储和检 +索元素,因此它们非常适合于实现 `Set` 的操作(集合框架也将它们用于 `Map` 的各种实现)。 例如,要实现 `contains(Object o)`,你需要查找元素 `o`,如果 +发现它,则返回 `true`。 -散列表中元素的位置由其内容的散列函数计算。散列函数旨在尽可能地为结果(散列码)传递可能存储的元素值。 例如,这里是类似于 `String` 类中用于计算哈希码的代码: +散列表中元素的位置由其内容的散列函数计算。散列函数旨在尽可能地为结果(散列码)传递可能存储的元素值。 例如,这里是类似于 `String` 类中用于计算哈希码 +的代码: ```java - int hash = 0; - for (char ch : str.toCharArray()) { - hash = hash * 31 + ch; - } +int hash = 0; +for (char ch : str.toCharArray()) { + hash = hash * 31 + ch; +} ``` -传统上,散列表通过取除除表格长度后的余数来从散列码中获取索引。 集合框架类实际上使用位掩码而不是除法。 因为这意味着哈希码低端的比特模式很重要,所以在计算哈希码时使用素数(例如 `31` ),因为乘以素数不会倾向于将信息从低端 - 例如乘以 `2` 的幂。 +传统上,散列表通过取除除表格长度后的余数来从散列码中获取索引。 集合框架类实际上使用位掩码而不是除法。 因为这意味着哈希码低端的比特模式很重要,所以在 +计算哈希码时使用素数(例如 `31` ),因为乘以素数不会倾向于将信息从低端 - 例如乘以 `2` 的幂。 ![](13_1.png) 图 `13-2`。 带链式溢出的哈希表 -想一想,除非您的表的位置多于可能存储的位置,否则有时两个不同的值必须散列到散列表中的相同位置。 例如,没有 `int` 索引的表可以足够大来存储没有冲突的所有字符串值。我们可以通过一个好的散列函数来最小化问题 - 一个散列表中的元素 - 但是,当发生冲突时,我们 必须有一种方法可以将碰撞的元素保存在同一个表格位置或存储桶中。 这通常是通过将它们存储在一个链表中来完成的,如图 `13-2` 所示。 作为 `ConcurrentSkipListSet` 实现的一部分,我们将更详细地查看链表(请参阅第 `13.2.3` 节),但现在看到存储在同一个存储桶中的元素仍然可以被访问, 单元格引用链。 图 `13-2` 显示了在 `Sun` 的 `Java 5` 实现中运行此代码所导致的情况: +想一想,除非您的表的位置多于可能存储的位置,否则有时两个不同的值必须散列到散列表中的相同位置。 例如,没有 `int` 索引的表可以足够大来存储没有冲突的所 +有字符串值。我们可以通过一个好的散列函数来最小化问题 - 一个散列表中的元素 - 但是,当发生冲突时,我们 必须有一种方法可以将碰撞的元素保存在同一个表格位 +置或存储桶中。 这通常是通过将它们存储在一个链表中来完成的,如图 `13-2` 所示。 作为 `ConcurrentSkipListSet` 实现的一部分,我们将更详细地查看链表 +(请参阅第 `13.2.3` 节),但现在看到存储在同一个存储桶中的元素仍然可以被访问, 单元格引用链。 图 `13-2` 显示了在 `Sun` 的 `Java 5` 实现中运行此代 +码所导致的情况: ```java - Set s1 = new HashSet(8); - s1.add('a'); - s1.add('b'); - s1.add('j'); +Set s1 = new HashSet(8); +s1.add('a'); +s1.add('b'); +s1.add('j'); ``` -表格元素的索引值已经通过使用每个元素的哈希码的最低三位(对于长度为 `8` 的表格)来计算。 在这个实现中,一个 `Character` 的哈希码就是它所包含的字符的 `Unicode` 值。 (实际上,哈希表当然会比这个大得多,而且这个图是从实际情况中简化的;因为 `HashSet` 实际上是由专门的 `HashMap` 实现的,链中的每个单元都不包含一个,而是两个 引用一个键和一个值(见第 `16` 章)只有键被显示在这个图中,因为当一个哈希表被用来表示一个集合时,所有的值都是相同的 - 只有键的存在是显着的。) +表格元素的索引值已经通过使用每个元素的哈希码的最低三位(对于长度为 `8` 的表格)来计算。 在这个实现中,一个 `Character` 的哈希码就是它所包含的字符的 +`Unicode` 值。 (实际上,哈希表当然会比这个大得多,而且这个图是从实际情况中简化的;因为 `HashSet` 实际上是由专门的 `HashMap` 实现的,链中的每个单元 +都不包含一个,而是两个 引用一个键和一个值(见第 `16` 章)只有键被显示在这个图中,因为当一个哈希表被用来表示一个集合时,所有的值都是相同的 - 只有键的 +存在是显着的。) -只要没有冲突,插入或检索元素的成本就是不变的。随着哈希表填满,碰撞变得更可能;假设一个好的散列函数,轻载表中碰撞的概率与其载荷成比例,定义为表中元素的数量除以其容量(桶的数量)。如果碰撞确实发生,则必须创建并随后遍历链表,并且与列表中元素的数量成比例地增加额外的插入成本。如果哈希表的大小是固定的,随着更多的元素被添加并且负载增加,性能将恶化。为防止发生这种情况,当负载达到指定的阈值(其负载因子)时,通过重新哈希复制到新的更大的表来增加表的大小。 +只要没有冲突,插入或检索元素的成本就是不变的。随着哈希表填满,碰撞变得更可能;假设一个好的散列函数,轻载表中碰撞的概率与其载荷成比例,定义为表中元素的 +数量除以其容量(桶的数量)。如果碰撞确实发生,则必须创建并随后遍历链表,并且与列表中元素的数量成比例地增加额外的插入成本。如果哈希表的大小是固定的, +随着更多的元素被添加并且负载增加,性能将恶化。为防止发生这种情况,当负载达到指定的阈值(其负载因子)时,通过重新哈希复制到新的更大的表来增加表的大 +小。 -迭代散列表需要检查每个存储桶是否被占用,因此花费的时间与散列表的容量加上其包含的元素的数量成正比。由于迭代器依次检查每个桶,因此元素返回的顺序取决于它们的哈希代码,因此不能保证元素将返回的顺序。图 `13-2` 所示的哈希表以降序索引和链表的前向遍历顺序产生其元素。打印它会产生以下输出: +迭代散列表需要检查每个存储桶是否被占用,因此花费的时间与散列表的容量加上其包含的元素的数量成正比。由于迭代器依次检查每个桶,因此元素返回的顺序取决于 +它们的哈希代码,因此不能保证元素将返回的顺序。图 `13-2` 所示的哈希表以降序索引和链表的前向遍历顺序产生其元素。打印它会产生以下输出: ```java - [j,b,a] +[j,b,a] ``` 在本节的后面,我们将看看 `LinkedHashSet`,这是一个带有迭代器的实现的变体,它不会按照其插入顺序返回元素。 -哈希表实现集的主要吸引力是添加,删除,包含和大小的基本操作(理想情况下)的恒定时间性能。其主要缺点是其迭代性能;因为迭代遍历表涉及检查每个存储桶,其成本与表大小成正比,无论其包含的集大小如何。 +哈希表实现集的主要吸引力是添加,删除,包含和大小的基本操作(理想情况下)的恒定时间性能。其主要缺点是其迭代性能;因为迭代遍历表涉及检查每个存储桶,其成 +本与表大小成正比,无论其包含的集大小如何。 `HashSet` 具有我们在 `12.3` 节中介绍的标准构造函数,以及两个额外的构造函数: ```java - HashSet(int initialCapacity) - HashSet(int initialCapacity, float loadFactor) +HashSet(int initialCapacity) +HashSet(int initialCapacity, float loadFactor) ``` -这两个构造函数都会创建一个空集,但允许对基础表的大小进行一些控制,从而在所提供的容量之后创建一个长度为次最大的 `2` 的幂。 虽然框架的原始设计者 `Joshua Bloch` 告诉我们,新类将不再具有像加载因子这样的配置参数,但是集合框架中大多数基于散列表的实现具有相似的构造函数。 它们通常不是有用的,它们限制了以后改进实现的可能性。 +这两个构造函数都会创建一个空集,但允许对基础表的大小进行一些控制,从而在所提供的容量之后创建一个长度为次最大的 `2` 的幂。 虽然框架的原始设计者 +`Joshua Bloch` 告诉我们,新类将不再具有像加载因子这样的配置参数,但是集合框架中大多数基于散列表的实现具有相似的构造函数。 它们通常不是有用的,它们限 +制了以后改进实现的可能性。 `HashSet` 是不同步的并且不是线程安全的; 它的迭代器是快速失败的。 @@ -68,77 +90,97 @@ ![](13_2.png) 图 `13-3`。链接的哈希表 -这个类从 `HashSet` 继承,仍然只在一个方面实现 `Set` 和提炼它的超类的契约:它保证它的迭代器将按照它们第一次添加的顺序返回它们的元素。 它通过维护设置元素的链表来完成此操作,如图 `13-3` 中的曲线箭头所示。 该图中的情况将由此代码产生: +这个类从 `HashSet` 继承,仍然只在一个方面实现 `Set` 和提炼它的超类的契约:它保证它的迭代器将按照它们第一次添加的顺序返回它们的元素。 它通过维护设置 +元素的链表来完成此操作,如图 `13-3` 中的曲线箭头所示。 该图中的情况将由此代码产生: ```java - Set s2 = new LinkedHashSet(8); - Collections.addAll(s2, 'a', 'b', 'j'); - // LinkedHashSet的迭代器以正确的顺序返回它们的元素: - assert s2.toString().equals("[a, b, j]"); +Set s2 = new LinkedHashSet(8); +Collections.addAll(s2, 'a', 'b', 'j'); +// LinkedHashSet的迭代器以正确的顺序返回它们的元素: +assert s2.toString().equals("[a, b, j]"); ``` -链接结构在改进迭代性能方面也具有有用的结果:下一步在恒定时间内执行,因为链表可以用于依次访问每个元素。 这与 `HashSet` 相反,无论 `HashSet` 是否被占用,`HashSet` 中的每个桶都必须被访问,但维护链表所涉及的开销意味着只有 `orde`或 `HashSet` 才会选择 `LinkedHashSet` 而不是 `HashSet`。 迭代效率对于您的应用程序非常重要。 +链接结构在改进迭代性能方面也具有有用的结果:下一步在恒定时间内执行,因为链表可以用于依次访问每个元素。 这与 `HashSet` 相反,无论 `HashSet` 是否被占 +用,`HashSet` 中的每个桶都必须被访问,但维护链表所涉及的开销意味着只有 `orde`或 `HashSet` 才会选择 `LinkedHashSet` 而不是 `HashSet`。 迭代效率对 +于您的应用程序非常重要。 -`LinkedHashSet` 的构造函数提供了与 `HashSet` 相同的工具来配置底层哈希表。 像 `HashSet` 一样,它是不同步的,不是线程安全的; 它的迭代器是快速失败的。 +`LinkedHashSet` 的构造函数提供了与 `HashSet` 相同的工具来配置底层哈希表。 像 `HashSet` 一样,它是不同步的,不是线程安全的; 它的迭代器是快速失败 +的。 ### CopyOnWriteArraySet 在功能方面,`CopyOnWriteArraySet` 是 `Set` 约定的另一个直接实现,但与 `HashSet` 具有完全不同的性能特征。 -这个类是作为一个薄的包装器来实现的,这个包装器是一个 `CopyOnWriteArrayList` 实例,后者又由一个数组支持。这个数组被视为不可变的;对集合内容的更改会导致创建一个全新的阵列。因此,添加复杂度为 `O(n)`,正如包含一样,这必须通过线性搜索来实现。显然你不会在你期待许多搜索或插入的上下文中使用 `CopyOnWriteArraySet`。但是数组的实现意味着迭代每个元素花费 `O(1)` - 比 `HashSet` 快 - 它在一些应用程序中有一个非常有吸引力的优点:它提供了线程安全性(参见 `11.5` 节),而不增加读操作的开销。这与使用锁定来实现所有操作的线程安全的那些集合(例如,第 `17.3.1` 节的同步集合)形成对比。这样的集合是多线程使用的瓶颈,因为线程在以任何方式使用之前都必须独占访问集合对象。相比之下,在写入时复制集合上的读操作是在后备阵列上实现的,后者在创建后从不修改,所以它们可以被任何线程使用,而没有并发写入操作干扰的危险。 +这个类是作为一个薄的包装器来实现的,这个包装器是一个 `CopyOnWriteArrayList` 实例,后者又由一个数组支持。这个数组被视为不可变的;对集合内容的更改会导 +致创建一个全新的阵列。因此,添加复杂度为 `O(n)`,正如包含一样,这必须通过线性搜索来实现。显然你不会在你期待许多搜索或插入的上下文中使用 +`CopyOnWriteArraySet`。但是数组的实现意味着迭代每个元素花费 `O(1)` - 比 `HashSet` 快 - 它在一些应用程序中有一个非常有吸引力的优点:它提供了线程安 +全性(参见 `11.5` 节),而不增加读操作的开销。这与使用锁定来实现所有操作的线程安全的那些集合(例如,第 `17.3.1` 节的同步集合)形成对比。这样的集合是 +多线程使用的瓶颈,因为线程在以任何方式使用之前都必须独占访问集合对象。相比之下,在写入时复制集合上的读操作是在后备阵列上实现的,后者在创建后从不修 +改,所以它们可以被任何线程使用,而没有并发写入操作干扰的危险。 -你什么时候想要使用具有这些特征的集合?在相当特殊的情况下;一个很常见的问题是实现主题观察者设计模式(请参阅第 `9.5` 节),该模式要求将事件通知给一组观察者。在通知过程中不得更改此套件;通过锁定集实现,读取和写入操作共享确保此操作所需的开销,而使用 `CopyOnWriteArraySet` 时,开销完全由写操作执行。这对主题观察者是有意义的;在此模式的典型用法中,事件通知发生的频率要比侦听器集的更改频率高得多。 +你什么时候想要使用具有这些特征的集合?在相当特殊的情况下;一个很常见的问题是实现主题观察者设计模式(请参阅第 `9.5` 节),该模式要求将事件通知给一组观 +察者。在通知过程中不得更改此套件;通过锁定集实现,读取和写入操作共享确保此操作所需的开销,而使用 `CopyOnWriteArraySet` 时,开销完全由写操作执行。这 +对主题观察者是有意义的;在此模式的典型用法中,事件通知发生的频率要比侦听器集的更改频率高得多。 -`CopyOnWriteArraySet` 的迭代器只能用于读取集合。当它们被创建时,它们被附加到当时由该集合使用的支持数组的实例。由于这个数组的实例不应该被修改,因此迭代器的 `remove` 方法没有实现。这些是快照迭代器(参见 `11.5` 节);它们反映了创建时的集合的状态,并且随后可以遍历而不会受到修改其派生集合的线程干扰的任何危险。 +`CopyOnWriteArraySet` 的迭代器只能用于读取集合。当它们被创建时,它们被附加到当时由该集合使用的支持数组的实例。由于这个数组的实例不应该被修改,因此 +迭代器的 `remove` 方法没有实现。这些是快照迭代器(参见 `11.5` 节);它们反映了创建时的集合的状态,并且随后可以遍历而不会受到修改其派生集合的线程干扰 +的任何危险。 由于 `CopyOnWriteArraySet` 没有配置参数,构造函数只是第 `12.3` 节讨论的标准配置参数。 ### EnumSet -这个类的存在是为了利用当可能元素的数量是固定的并且可以为每个分配唯一索引时可能的有效实现。这两个条件适用于同一枚枚举的一组元素;键的数量由枚举类型的常量固定,并且序数方法返回的值保证对每个常量都是唯一的。另外,序数返回值形成一个紧凑的范围,从零理想开始,事实上,用作数组索引,或者在标准实现中,使用位向量的索引。所以添加,删除和包含都是作为位操作来实现的,并且具有恒定时间的性能。对单个单词的位操作非常快,并且可以使用长整型值来表示 `EnumSets`,而不是使用多达 `64` 个值的枚举类型。较大的枚举可以用类似的方式处理,但有一些开销,使用多于一个单词来表示。 +这个类的存在是为了利用当可能元素的数量是固定的并且可以为每个分配唯一索引时可能的有效实现。这两个条件适用于同一枚枚举的一组元素;键的数量由枚举类型的常 +量固定,并且序数方法返回的值保证对每个常量都是唯一的。另外,序数返回值形成一个紧凑的范围,从零理想开始,事实上,用作数组索引,或者在标准实现中,使用 +位向量的索引。所以添加,删除和包含都是作为位操作来实现的,并且具有恒定时间的性能。对单个单词的位操作非常快,并且可以使用长整型值来表示 `EnumSets`, +而不是使用多达 `64` 个值的枚举类型。较大的枚举可以用类似的方式处理,但有一些开销,使用多于一个单词来表示。 -`EnumSet` 是一个抽象类,它通过不同的包 - 私有子类来实现这些不同的表示。它隐藏了程序员的具体实现,而是暴露了调用适当子类的构造函数的工厂方法。下面一组工厂方法提供了创建具有不同初始内容的 `EnumSets` 的方法:仅为空,指定的元素或枚举的所有元素。 +`EnumSet` 是一个抽象类,它通过不同的包 - 私有子类来实现这些不同的表示。它隐藏了程序员的具体实现,而是暴露了调用适当子类的构造函数的工厂方法。下面一 +组工厂方法提供了创建具有不同初始内容的 `EnumSets` 的方法:仅为空,指定的元素或枚举的所有元素。 ```java - > EnumSet of(E first, E... rest) - // 创建一个最初包含指定元素的集合 - > EnumSet range(E from, E to) - // 创建一个最初包含由两个指定端点定义的范围内的所有元素的集合 - > EnumSet allOf(Class elementType) - // 创建一个最初包含elementType中所有元素的集合 - > EnumSet noneOf(Class elementType) - // 创建一组elementType,最初为空 +> EnumSet of(E first, E... rest) +// 创建一个最初包含指定元素的集合 +> EnumSet range(E from, E to) +// 创建一个最初包含由两个指定端点定义的范围内的所有元素的集合 +> EnumSet allOf(Class elementType) +// 创建一个最初包含elementType中所有元素的集合 +> EnumSet noneOf(Class elementType) +// 创建一组elementType,最初为空 ``` -`EnumSet` 包含其元素的指定类型,它在运行时用于检查新条目的有效性。 这种类型由上述工厂方法以两种不同的方式提供。 方法和范围至少接收一个枚举参数,可以查询它的声明类(即它所属的枚举)。 对于没有枚举参数的 `allOf` 和 `noneOf`,将提供一个类标记。 +`EnumSet` 包含其元素的指定类型,它在运行时用于检查新条目的有效性。 这种类型由上述工厂方法以两种不同的方式提供。 方法和范围至少接收一个枚举参数,可以 +查询它的声明类(即它所属的枚举)。 对于没有枚举参数的 `allOf` 和 `noneOf`,将提供一个类标记。 `EnumSet` 创建的常见情况通过第二组方法进行了优化,这些方法允许您使用枚举类型的一个,两个,三个,四个或五个元素高效地创建集合。 ```java - > EnumSet of(E e) - > EnumSet of(E e1, E e2) - > EnumSet of(E e1, E e2, E e3) - > EnumSet of(E e1, E e2, E e3, E e4) - > EnumSet of(E e1, E e2, E e3, E e4, E e5) +> EnumSet of(E e) +> EnumSet of(E e1, E e2) +> EnumSet of(E e1, E e2, E e3) +> EnumSet of(E e1, E e2, E e3, E e4) +> EnumSet of(E e1, E e2, E e3, E e4, E e5) ``` 第三组方法允许从现有集合创建 `EnumSet`: ```java - > EnumSet copyOf(EnumSet s) - // 使用与s相同的元素类型创建EnumSet,并使用相同的元素 - > EnumSet copyOf(Collection c) - // 从c的元素创建一个EnumSet,它必须包含至少一个元素 - > EnumSet complementOf(EnumSet s) - // 使用与s相同的元素类型创建一个EnumSet,其中包含不在s中的元素 +> EnumSet copyOf(EnumSet s) +// 使用与s相同的元素类型创建EnumSet,并使用相同的元素 +> EnumSet copyOf(Collection c) +// 从c的元素创建一个EnumSet,它必须包含至少一个元素 +> EnumSet complementOf(EnumSet s) +// 使用与s相同的元素类型创建一个EnumSet,其中包含不在s中的元素 ``` 作为参数提供给第二版 `copyOf` 的集合必须是非空的,以便可以确定元素类型。 -在使用中,`EnumSet` 服从 `Set` 的约定,并添加了它的迭代器将以自然顺序(声明 `enum` 常量的顺序)返回它们的元素的规范。 它不是线程安全的,但与不同步的通用集合不同,它的迭代器不是快速失败的。 它们可能是快照或弱一致的;保守,合约只能保证它们会弱一致(见第 `11.5` 节)。 +在使用中,`EnumSet` 服从 `Set` 的约定,并添加了它的迭代器将以自然顺序(声明 `enum` 常量的顺序)返回它们的元素的规范。 它不是线程安全的,但与不同步 +的通用集合不同,它的迭代器不是快速失败的。 它们可能是快照或弱一致的;保守,合约只能保证它们会弱一致(见第 `11.5` 节)。 ![](13_3.png) + 图 `13-4`。`SortedSet` 的 《《《 [下一节](02_SortedSet_and_NavigableSet.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch13/02_SortedSet_and_NavigableSet.md b/ch13/02_SortedSet_and_NavigableSet.md index 0c7201fa..1342ac73 100644 --- a/ch13/02_SortedSet_and_NavigableSet.md +++ b/ch13/02_SortedSet_and_NavigableSet.md @@ -3,75 +3,98 @@ ### SortedSet和NavigableSet -`Set` 有一个子接口 `SortedSet`(图 `13-4`),它向 `Set` 约束添加一个保证,它的迭代器将以升序元素顺序遍历集合。`SortedSet` 本身在接口 `NavigableSet` 的 `Java 6` 中扩展(见图 `13-5` ),它添加了一些方法来查找与目标元素最接近的匹配。`Java 6` 之前的 `SortedSet` 的唯一实现是 `TreeSet`,它已经用实现新接口所需的方法进行了改进。由于在 `Java 6` 中没有实现 `NavigableSet` 的 `SortedSet` 的平台实现,所以在同一部分讨论它们是有意义的。对于为 `Java 6` 平台开发的新客户端代码,根本不需要使用 `SortedSet` 接口,但是为了受限于使用 `Java 5` 的读者,我们将在本节中分别介绍这两个接口的方法。 +`Set` 有一个子接口 `SortedSet`(图 `13-4`),它向 `Set` 约束添加一个保证,它的迭代器将以升序元素顺序遍历集合。`SortedSet` 本身在接口 +`NavigableSet` 的 `Java 6` 中扩展(见图 `13-5` ),它添加了一些方法来查找与目标元素最接近的匹配。`Java 6` 之前的 `SortedSet` 的唯一实现是 +`TreeSet`,它已经用实现新接口所需的方法进行了改进。由于在 `Java 6` 中没有实现 `NavigableSet` 的 `SortedSet` 的平台实现,所以在同一部分讨论它们是 +有意义的。对于为 `Java 6` 平台开发的新客户端代码,根本不需要使用 `SortedSet` 接口,但是为了受限于使用 `Java 5` 的读者,我们将在本节中分别介绍这两 +个接口的方法。 在第 `3` 章中,我们看到元素排序可以由元素类本身定义,如果它实现了 `Comparable`,或者可以由 `TreeSet` 等构造函数提供的外部比较器来强制执行: ```java - TreeSet(Comparator comparator) +TreeSet(Comparator comparator) ``` -`Task` 确实实现了 `Comparable`(它的自然顺序是它的字符串表示的自然顺序),所以我们不需要提供一个单独的比较器。 现在合并两个有序列表,使用并行迭代器非常棘手,如果我们得到一个 `SortedSet` 来完成这项工作,那么这个列表就很简单。 使用例 `12-1` 的任务集合,它需要两行代码: +`Task` 确实实现了 `Comparable`(它的自然顺序是它的字符串表示的自然顺序),所以我们不需要提供一个单独的比较器。 现在合并两个有序列表,使用并行迭代器 +非常棘手,如果我们得到一个 `SortedSet` 来完成这项工作,那么这个列表就很简单。 使用例 `12-1` 的任务集合,它需要两行代码: ```java - Set naturallyOrderedTasks = new TreeSet(mondayTasks); - naturallyOrderedTasks.addAll(tuesdayTasks); - assert naturallyOrderedTasks.toString().equals ("[code db, code gui, code logic, phone Mike, phone Paul]"); +Set naturallyOrderedTasks = new TreeSet(mondayTasks); +naturallyOrderedTasks.addAll(tuesdayTasks); +assert naturallyOrderedTasks.toString().equals ("[code db, code gui, code logic, phone Mike, phone Paul]"); ``` 虽然这种简单性付出代价,将大小为n的两个排序列表合并为 `O(n)`,但将 `n` 个元素添加到大小为 `n` 的树结构中为 `O(n log n)`。 -我们可以使用 `SortedSet` 为待办事项管理器添加一些功能。到现在为止,收集和设置的方法已经给我们排序我们的任务没有任何帮助 - 肯定是待办事项管理者的核心要求之一。例 `13-1` 定义了一个优先级任务的类优先级任务。有三个优先级,`HIGH`,`MEDIUM` 和 `LOW`,被声明为高优先级在自然排序中排在第一位。为了比较两个 `PriorityTasks`,我们首先比较它们的优先级;如果优先级不相等,则优先级较高的任务首先出现,如果优先级相同,则我们使用基本任务的自然排序。为了测试两个 `PriorityTasks` 是否相等,我们检查它们是否具有相同的优先级和相同的任务。这些定义确保自然排序与等号一致(参见第 `3.1` 节)。当我们在 `12.1` 节中定义任务时,我们通过使 `PriorityTask` 不可变来遵循良好的实践。 +我们可以使用 `SortedSet` 为待办事项管理器添加一些功能。到现在为止,收集和设置的方法已经给我们排序我们的任务没有任何帮助 - 肯定是待办事项管理者的核心 +要求之一。例 `13-1` 定义了一个优先级任务的类优先级任务。有三个优先级,`HIGH`,`MEDIUM` 和 `LOW`,被声明为高优先级在自然排序中排在第一位。为了比较两 +个 `PriorityTasks`,我们首先比较它们的优先级;如果优先级不相等,则优先级较高的任务首先出现,如果优先级相同,则我们使用基本任务的自然排序。为了测试两 +个 `PriorityTasks` 是否相等,我们检查它们是否具有相同的优先级和相同的任务。这些定义确保自然排序与等号一致(参见第 `3.1` 节)。当我们在 `12.1` 节中 +定义任务时,我们通过使 `PriorityTask` 不可变来遵循良好的实践。 例 `13-1`。类 `PriorityTask` ```java - public enum Priority { HIGH, MEDIUM, LOW } - public final class PriorityTask implements Comparable { - private final Task task; - private final Priority priority; - PriorityTask(Task task, Priority priority) { - this.task = task; - this.priority = priority; - } - public Task getTask() { return task; } - public Priority getPriority() { return priority; } - public int compareTo(PriorityTask pt) { - int c = priority.compareTo(pt.priority); - return c != 0 ? c : task.compareTo(pt.task); - } - public boolean equals(Object o) { +public enum Priority { + HIGH, MEDIUM, LOW +} +public final class PriorityTask implements Comparable { + private final Task task; + private final Priority priority; + PriorityTask(Task task, Priority priority) { + this.task = task; + this.priority = priority; + } + public Task getTask() { + return task; + } + public Priority getPriority() { + return priority; + } + public int compareTo(PriorityTask pt) { + int c = priority.compareTo(pt.priority); + return c != 0 ? c : task.compareTo(pt.task); + } + public boolean equals(Object o) { if (o instanceof PriorityTask) { PriorityTask pt = (PriorityTask)o; return task.equals(pt.task) && priority.equals(pt.priority); - } else - return false; - } - public int hashCode() { return task.hashCode(); } - public String toString() { return task + ": " + priority; } + } else + return false; + } + public int hashCode() { + return task.hashCode(); } + public String toString() { + return task + ": " + priority; + } +} ``` -下面的代码显示了 `SortedSet` 与一组 `PriorityTasks` 一起工作(事实上,我们已经声明了一个 `NavigableSet`,以便我们可以在后面的示例中使用相同的集合。但是目前,我们将只使用 `SortedSet` 的方法): +下面的代码显示了 `SortedSet` 与一组 `PriorityTasks` 一起工作(事实上,我们已经声明了一个 `NavigableSet`,以便我们可以在后面的示例中使用相同的集 +合。但是目前,我们将只使用 `SortedSet` 的方法): ```java - NavigableSet priorityTasks = new TreeSet(); - priorityTasks.add(new PriorityTask(mikePhone, Priority.MEDIUM)); - priorityTasks.add(new PriorityTask(paulPhone, Priority.HIGH)); - priorityTasks.add(new PriorityTask(databaseCode, Priority.MEDIUM)); - priorityTasks.add(new PriorityTask(interfaceCode, Priority.LOW)); - assert(priorityTasks.toString()).equals("[phone Paul: HIGH, code db: MEDIUM, phone Mike: MEDIUM, code gui: LOW]"); + NavigableSet priorityTasks = new TreeSet(); + priorityTasks.add(new PriorityTask(mikePhone, Priority.MEDIUM)); + priorityTasks.add(new PriorityTask(paulPhone, Priority.HIGH)); + priorityTasks.add(new PriorityTask(databaseCode, Priority.MEDIUM)); + priorityTasks.add(new PriorityTask(interfaceCode, Priority.LOW)); + assert(priorityTasks.toString()).equals("[phone Paul: HIGH, code db: MEDIUM, phone Mike: MEDIUM, code gui: LOW]"); ``` -你能否简单地比较任务的优先级,而不使用字符串表示作为辅助键? 如果您想保留原始排序的某些方面,那么这样的偏序会很有用; 例如,您可能希望按优先级对任务进行排序,但在每个优先级内保留它们添加到该组的顺序。 但是 `SortedSet` 的契约(以及后面将会看到的 `SortedMap`)声明它将使用其比较器的比较方法 - 或者,如果它没有,则它的元素的 `compareTo` 方法 - 而不是元素的 `equals` 方法来确定何时元素是不同的。 这意味着如果多个元素的比较结果相同,则该集合会将它们视为重复对象,除了一个元素之外的所有元素都将被丢弃。 +你能否简单地比较任务的优先级,而不使用字符串表示作为辅助键? 如果您想保留原始排序的某些方面,那么这样的偏序会很有用; 例如,您可能希望按优先级对任务进 +行排序,但在每个优先级内保留它们添加到该组的顺序。 但是 `SortedSet` 的契约(以及后面将会看到的 `SortedMap`)声明它将使用其比较器的比较方法 - 或者, +如果它没有,则它的元素的 `compareTo` 方法 - 而不是元素的 `equals` 方法来确定何时元素是不同的。 这意味着如果多个元素的比较结果相同,则该集合会将它们 +视为重复对象,除了一个元素之外的所有元素都将被丢弃。 `SortedSet`接口定义的方法分为三组: **获取第一个和最后一个元素** ```java - E first() // 返回set里第一个元素 - E last() // 返回set里最后一个元素 +E first() // 返回set里第一个元素 +E last() // 返回set里最后一个元素 ``` 如果该集合为空,则这些操作会抛出 `NoSuchElementException`。 @@ -80,17 +103,18 @@ ```java - Comparator comparator() +Comparator comparator() ``` -如果在施工时已经给出了一个比较器,那么此方法将返回该集合的比较器。 使用 `Comparator` 是因为在 `E` 上参数化的 `SortedSet` 可以依赖于在 `E` 的任何超类型上定义的比较器的排序。 例如,回顾第 `3.3` 节,`Comparator` 可以与 `SortedSet` 一起使用。 +如果在施工时已经给出了一个比较器,那么此方法将返回该集合的比较器。 使用 `Comparator` 是因为在 `E` 上参数化的 `SortedSet` 可以依赖于在 +`E` 的任何超类型上定义的比较器的排序。 例如,回顾第 `3.3` 节,`Comparator` 可以与 `SortedSet` 一起使用。 **获取范围视图** ```java - SortedSet subSet(E fromElement, E toElement) - SortedSet headSet(E toElement) - SortedSet tailSet(E fromElement) + SortedSet subSet(E fromElement, E toElement) + SortedSet headSet(E toElement) + SortedSet tailSet(E fromElement) ``` `subSet` 方法返回一个包含原始集合中大于或等于 `fromElement` 且小于 `toElement` 的每个元素的集合。类似地,方法耳机返回小于 `toElement` 的每个元素,并且 `tailSet` 返回大于或等于 `fromElement` 的每个元素。请注意,这些操作的参数本身不一定是该组的成员。返回的集合是半开放的时间间隔:它们包含 `fromElement` - 当然提供它实际上是集合成员,并且不包含 `toElement`。 @@ -98,119 +122,132 @@ 在我们的例子中,这些方法可以用于提供 `priorityTasks` 中元素的不同视图。例如,我们可以使用 `headSet` 来获得高端中等优先级任务的视图。要做到这一点,我们需要在任务排序中的所有其他人之前完成一项特殊任务;幸运的是,我们在第 `12.1` 节中为此目的定义了一个类 `EmptyTask`。使用它,很容易提取出现在任何低优先级任务之前的所有任务: ```java - PriorityTask firstLowPriorityTask = new PriorityTask(new EmptyTask(), Priority.LOW); - SortedSet highAndMediumPriorityTasks = priorityTasks.headSet(firstLowPriorityTask); - assert highAndMediumPriorityTasks.toString().equals("[phone Paul: HIGH, code db: MEDIUM, phone Mike: MEDIUM]"); +PriorityTask firstLowPriorityTask = new PriorityTask(new EmptyTask(), Priority.LOW); +SortedSet highAndMediumPriorityTasks = priorityTasks.headSet(firstLowPriorityTask); +assert highAndMediumPriorityTasks.toString().equals("[phone Paul: HIGH, code db: MEDIUM, phone Mike: MEDIUM]"); ``` 实际上,因为我们知道具有空细节的任务通常不会发生,所以我们也可以在半开区间中使用一个作为第一个端点: ```java - PriorityTask firstMediumPriorityTask = new PriorityTask(new EmptyTask(), Priority.MEDIUM); - SortedSet mediumPriorityTasks = priorityTasks.subSet(firstMediumPriorityTask, firstLowPriorityTask); - assert mediumPriorityTasks.toString().equals("[code db: MEDIUM, phone Mike: MEDIUM]"); +PriorityTask firstMediumPriorityTask = new PriorityTask(new EmptyTask(), Priority.MEDIUM); +SortedSet mediumPriorityTasks = priorityTasks.subSet(firstMediumPriorityTask, firstLowPriorityTask); +assert mediumPriorityTasks.toString().equals("[code db: MEDIUM, phone Mike: MEDIUM]"); ``` -并非所有的订单都可以如此方便地处理; 例如,假设我们想要处理所有中等优先级任务(包括麦克风电话任务)的集合。 要将该集合定义为半开放间隔,`SortedSet` 的用户将需要构造紧接在 `PriorityTask` 排序中的 `mikePhone` 任务之后的任务,为此,您需要知道在自然中成功“Mike”的字符串 排序是“Mike \ 0”(即,添加了空字符的“Mike”)。 幸运的是,`NavigableSet` 的用户有一个更直观的方式来定义这个集合,就像我们稍后会看到的一样。 +并非所有的订单都可以如此方便地处理; 例如,假设我们想要处理所有中等优先级任务(包括麦克风电话任务)的集合。 要将该集合定义为半开放间隔,`SortedSet` +的用户将需要构造紧接在 `PriorityTask` 排序中的 `mikePhone` 任务之后的任务,为此,您需要知道在自然中成功“Mike”的字符串 排序是“Mike \ 0”(即,添加 +了空字符的“Mike”)。 幸运的是,`NavigableSet` 的用户有一个更直观的方式来定义这个集合,就像我们稍后会看到的一样。 请注意,这些操作返回的集合不是独立的集合,而是原始 `SortedSet` 的新视图。 因此,我们可以将元素添加到原始集中,并查看视图中反映的更改: ```java - PriorityTask logicCodeMedium = new PriorityTask(logicCode, Priority.MEDIUM); - priorityTasks.add(logicCodeMedium); - assert mediumPriorityTasks.toString().equals("[code db: MEDIUM, code logic: MEDIUM, phone Mike: MEDIUM]"); +PriorityTask logicCodeMedium = new PriorityTask(logicCode, Priority.MEDIUM); +priorityTasks.add(logicCodeMedium); +assert mediumPriorityTasks.toString().equals("[code db: MEDIUM, code logic: MEDIUM, phone Mike: MEDIUM]"); ``` 反过来也适用; 视图中的变化反映在原始集合中: ```java - mediumPriorityTasks.remove(logicCodeMedium); - assert priorityTasks.toString().equals("[phone Paul: HIGH, code db: MEDIUM, phone Mike: MEDIUM, code gui: LOW]"); +mediumPriorityTasks.remove(logicCodeMedium); +assert priorityTasks.toString().equals("[phone Paul: HIGH, code db: MEDIUM, phone Mike: MEDIUM, code gui: LOW]"); ``` -为了理解它是如何工作的,可以将所有可能的排列顺序想象成一条线,就像算术中使用的数字线一样。 范围被定义为该行的固定段,而不管哪些值实际上在原始集合中。 因此,在 `SortedSet` 和范围上定义的子集将允许您使用 `SortedSet` 当前位于范围内的任何元素。 +为了理解它是如何工作的,可以将所有可能的排列顺序想象成一条线,就像算术中使用的数字线一样。 范围被定义为该行的固定段,而不管哪些值实际上在原始集合中。 +因此,在 `SortedSet` 和范围上定义的子集将允许您使用 `SortedSet` 当前位于范围内的任何元素。 ### NavigableSet -`NavigableSet`(参见图 `13-5` )是在 `Java 6` 中引入的,用于补充 `SortedSet` 中的缺陷。 正如我们在本节开头提到的,新客户端代码应该优先于 `SortedSet` 使用它。 它增加了四个组的方法。 +`NavigableSet`(参见图 `13-5` )是在 `Java 6` 中引入的,用于补充 `SortedSet` 中的缺陷。 正如我们在本节开头提到的,新客户端代码应该优先于 +`SortedSet` 使用它。 它增加了四个组的方法。 **获取第一个和最后一个元素** ```java - E pollFirst() // 检索并移除第一个(最低)元素,如果此集合为空,则返回null - E pollLast() // 检索并移除最后一个(最高)元素,如果此集合为空,则返回null +E pollFirst() // 检索并移除第一个(最低)元素,如果此集合为空,则返回null +E pollLast() // 检索并移除最后一个(最高)元素,如果此集合为空,则返回null ``` ![](13_4.png) 图`13-5`。`NavigableSet` -这些类似于 `Deque` 中同名的方法(参见 `14.4` 节),并且有助于支持在需要队列功能的应用程序中使用 `NavigableSet`。 例如,在本节中待办事项管理器的版本中,我们可以通过以下方式从列表中获取最高优先级的任务:准备好执行的任务: +这些类似于 `Deque` 中同名的方法(参见 `14.4` 节),并且有助于支持在需要队列功能的应用程序中使用 `NavigableSet`。 例如,在本节中待办事项管理器的版 +本中,我们可以通过以下方式从列表中获取最高优先级的任务:准备好执行的任务: ```java - PriorityTask nextTask = priorityTasks.pollFirst(); - assert nextTask.toString().equals("phone Paul: HIGH"); +PriorityTask nextTask = priorityTasks.pollFirst(); +assert nextTask.toString().equals("phone Paul: HIGH"); ``` -注意虽然 `Deque` 也包含方法 `peekFirst` 和 `peekLast`,它们允许客户端在不删除它的情况下检索元素,但 `NavigableSet` 不需要它们,因为它们的函数已经由从 `SortedSet` 继承的第一个和最后一个方法提供。 +注意虽然 `Deque` 也包含方法 `peekFirst` 和 `peekLast`,它们允许客户端在不删除它的情况下检索元素,但 `NavigableSet` 不需要它们,因为它们的函数已经 +由从 `SortedSet` 继承的第一个和最后一个方法提供。 **获取范围视图** ```java - NavigableSet subSet(E fromElement, boolean fromInclusive, E toElement, boolean toInclusive) - NavigableSet headSet(E toElement, boolean inclusive) - NavigableSet tailSet(E fromElement, boolean inclusive) +NavigableSet subSet(E fromElement, boolean fromInclusive, E toElement, boolean toInclusive) +NavigableSet headSet(E toElement, boolean inclusive) +NavigableSet tailSet(E fromElement, boolean inclusive) ``` -该组是对 `SortedSet` 中相同名称的方法的改进,它返回总是包含下限并且不包括较高值的子集。 相比之下,`NavigableSet` 方法允许您为每个边界指定它应该是包含的还是独占的。 这使得定义某些集合的范围视图变得更容易。我们早些时候考虑过包含所有中等优先级任务直到包括(中等优先级)的 `mikePhone` 任务在内的集合。 要使用 `SortedSet` 获得该集合,我们必须使用字符串排序的鲜为人知的技术性将其定义为半开放间隔。 但是 `NavigableSet` 允许我们将它定义为一个闭区间,只需指定上限应该包含在内: +该组是对 `SortedSet` 中相同名称的方法的改进,它返回总是包含下限并且不包括较高值的子集。 相比之下,`NavigableSet` 方法允许您为每个边界指定它应该是包 +含的还是独占的。 这使得定义某些集合的范围视图变得更容易。我们早些时候考虑过包含所有中等优先级任务直到包括(中等优先级)的 `mikePhone` 任务在内的集 +合。 要使用 `SortedSet` 获得该集合,我们必须使用字符串排序的鲜为人知的技术性将其定义为半开放间隔。 但是 `NavigableSet` 允许我们将它定义为一个闭区 +间,只需指定上限应该包含在内: ```java - PriorityTask mikePhoneMedium = new PriorityTask(mikePhone, Priority.MEDIUM); - NavigableSet closedInterval = priorityTasks.subSet(firstMediumPriorityTask, true, mikePhoneMedium, true); - assert(closedInterval.toString()).equals("[code db: MEDIUM, phone Mike: MEDIUM]"); +PriorityTask mikePhoneMedium = new PriorityTask(mikePhone, Priority.MEDIUM); +NavigableSet closedInterval = priorityTasks.subSet(firstMediumPriorityTask, true, mikePhoneMedium, true); +assert(closedInterval.toString()).equals("[code db: MEDIUM, phone Mike: MEDIUM]"); ``` **获得最接近的值** ```java - E ceiling(E e) // 返回此集合中的最小元素大于或等于e;如果不存在这样的元素,则返回null - E floor(E e) // 返回此集合中最大的元素小于或等于e;如果没有这样的元素,则返回null - E higher(E e) // 返回此集合中的最小元素严格大于e,如果不存在这样的元素,则返回null - E lower(E e) // 返回此集合中最大的元素严格小于e,如果没有这样的元素,则返回null +E ceiling(E e) // 返回此集合中的最小元素大于或等于e;如果不存在这样的元素,则返回null +E floor(E e) // 返回此集合中最大的元素小于或等于e;如果没有这样的元素,则返回null +E higher(E e) // 返回此集合中的最小元素严格大于e,如果不存在这样的元素,则返回null +E lower(E e) // 返回此集合中最大的元素严格小于e,如果没有这样的元素,则返回null ``` -这些方法对于短距离导航很有用。 例如,假设我们想在一组有序的字符串中查找子集中最后三个由“x-ray”定界的字符串,包括该字符串本身(如果它存在于集合中)。`NavigableSet` 方法使这很简单: +这些方法对于短距离导航很有用。 例如,假设我们想在一组有序的字符串中查找子集中最后三个由“x-ray”定界的字符串,包括该字符串本身(如果它存在于集合中)。 +`NavigableSet` 方法使这很简单: ```java - NavigableSet stringSet = new TreeSet(); - Collections.addAll(stringSet, "abc", "cde", "x-ray" ,"zed"); - String last = stringSet.floor("x-ray"); - assert last.equals("x-ray"); - String secondToLast = last == null ? null : stringSet.lower(last); - String thirdToLast = secondToLast == null ? null : stringSet.lower(secondToLast); - assert thirdToLast.equals("abc"); +NavigableSet stringSet = new TreeSet(); +Collections.addAll(stringSet, "abc", "cde", "x-ray" ,"zed"); +String last = stringSet.floor("x-ray"); +assert last.equals("x-ray"); +String secondToLast = last == null ? null : stringSet.lower(last); +String thirdToLast = secondToLast == null ? null : stringSet.lower(secondToLast); +assert thirdToLast.equals("abc"); ``` -请注意,根据集合框架设计的一般趋势,`NavigableSet` 返回空值来表示没有元素,例如,`SortedSet` 的第一个和最后一个方法会抛出 `NoSuchElementException`。 出于这个原因,你应该避免在 `NavigableSets` 中使用空元素,实际上,较新的实现 `ConcurrentSkipListSet` 不允许它们(尽管为了向后兼容,`TreeSet` 必须继续这么做)。 +请注意,根据集合框架设计的一般趋势,`NavigableSet` 返回空值来表示没有元素,例如,`SortedSet` 的第一个和最后一个方法会抛出 +`NoSuchElementException`。 出于这个原因,你应该避免在 `NavigableSets` 中使用空元素,实际上,较新的实现 `ConcurrentSkipListSet` 不允许它们(尽管 +为了向后兼容,`TreeSet` 必须继续这么做)。 **以颠倒顺序浏览集合** ```java - NavigableSet descendingSet() // 返回此集合中元素的逆序视图 - Iterator descendingIterator() // 返回一个逆序迭代器 +NavigableSet descendingSet() // 返回此集合中元素的逆序视图 +Iterator descendingIterator() // 返回一个逆序迭代器 ``` -该组的方法使得在下降(即反向)排序中遍历 `NavigableSet` 同样简单。 作为一个简单的例子,让我们使用最接近的匹配方法概括上面的例子。 假设我们不是仅仅通过“x射线”找到有序的集合中的最后三个字符串,而是按照降序对该集合中的所有字符串进行迭代: +该组的方法使得在下降(即反向)排序中遍历 `NavigableSet` 同样简单。 作为一个简单的例子,让我们使用最接近的匹配方法概括上面的例子。 假设我们不是仅仅通 +过“x射线”找到有序的集合中的最后三个字符串,而是按照降序对该集合中的所有字符串进行迭代: ```java - NavigableSet headSet = stringSet.headSet(last, true); - NavigableSet reverseHeadSet = headSet.descendingSet(); - assert reverseHeadSet.toString().equals("[x-ray, cde, abc]"); - String conc = " "; - for (String s : reverseHeadSet) { - conc += s + " "; - } - assert conc.equals(" x-ray cde abc "); +NavigableSet headSet = stringSet.headSet(last, true); +NavigableSet reverseHeadSet = headSet.descendingSet(); +assert reverseHeadSet.toString().equals("[x-ray, cde, abc]"); +String conc = " "; +for (String s : reverseHeadSet) { + conc += s + " "; +} +assert conc.equals(" x-ray cde abc "); ``` ![](13_5.png) @@ -242,43 +279,54 @@ 如果您查看任何非叶节点(例如,包含以下单词的节点),则可以看到此树的最重要属性:左侧下方的所有节点都包含按字母顺序排列的单词,右侧的所有单词随之而来。要找到一个单词,您需要逐级开始,逐级下降,在每个级别进行字母比较,因此检索或插入元素的成本与树的深度成正比。 -那么,含有 `n` 个元素的树有多深?具有两个级别的完整二叉树具有三个元素(即 `2^2 -1`),并且具有三个级别的元素具有七个元素(`2^3 -1`)。通常,具有 `n` 个完整级别的二叉树将具有 `2^n -1`个元素。因此,具有 `n` 个元素的树的深度将以 `log n` 为界(因为 `2^log n = n`)。就像 `n` 的增长速度比 `2^n` 慢得多,`log n` 的增长速度比 `n` 慢得多。所以包含在一棵大树上比在包含相同元素的列表上快得多。它仍然不如散列表 - 它的操作理想情况下可以在不变的时间内工作 - 但是树比散列表有更大的优势,它的迭代器可以按排序顺序返回其元素。 +那么,含有 `n` 个元素的树有多深?具有两个级别的完整二叉树具有三个元素(即 `2^2 -1`),并且具有三个级别的元素具有七个元素(`2^3 -1`)。通常,具有 +`n` 个完整级别的二叉树将具有 `2^n -1`个元素。因此,具有 `n` 个元素的树的深度将以 `log n` 为界(因为 `2^log n = n`)。就像 `n` 的增长速度比 `2^n` +慢得多,`log n` 的增长速度比 `n` 慢得多。所以包含在一棵大树上比在包含相同元素的列表上快得多。它仍然不如散列表 - 它的操作理想情况下可以在不变的时间内 +工作 - 但是树比散列表有更大的优势,它的迭代器可以按排序顺序返回其元素。 -不过,并不是所有的二叉树都会有这么好的表现。图 `13-6` 显示了一个平衡二叉树 - 其中每个节点在每边有相同数量的后代(或尽可能接近)。不平衡的树可能会导致更糟的性能 - 在最坏的情况下,与链表一样差(参见图 `13-7`)。`TreeSet` 使用一种称为红黑树的数据类型,其优点是,如果通过插入或移除元素而导致其不平衡,则可以始终在 `O(log n)`时间内重新平衡它。 +不过,并不是所有的二叉树都会有这么好的表现。图 `13-6` 显示了一个平衡二叉树 - 其中每个节点在每边有相同数量的后代(或尽可能接近)。不平衡的树可能会导 +致更糟的性能 - 在最坏的情况下,与链表一样差(参见图 `13-7`)。`TreeSet` 使用一种称为红黑树的数据类型,其优点是,如果通过插入或移除元素而导致其不平 +衡,则可以始终在 `O(log n)`时间内重新平衡它。 `TreeSet` 的构造函数除了标准的构造函数之外,还包括允许您提供比较器(参见第 `3.4` 节)的构造函数和允许您从另一个 `SortedSet` 创建一个的构造函数: ```java - TreeSet(Comparator c) // 构造一个空集,它将使用指定的比较器进行排序 - TreeSet(SortedSet s) // 构造一个包含所提供集合的元素的新集合,按照相同的顺序排序 +TreeSet(Comparator c) // 构造一个空集,它将使用指定的比较器进行排序 +TreeSet(SortedSet s) // 构造一个包含所提供集合的元素的新集合,按照相同的顺序排序 ``` 其中的第二个在向标准“转换构造函数”的声明中相当接近(见第 `12.3` 节): ```java - TreeSet(Collection c) +TreeSet(Collection c) ``` -正如 `Joshua Bloch` 在 `Effective Java` 中所解释的那样(在 `Methods` 的章节中,项目“审慎地使用重载”),调用两个构造函数或方法重载中的一个,这些重载使用相关类型的参数会导致令人困惑的结果。 这是因为,在 `Java` 中,调用重载的构造函数和方法是在编译时根据参数的静态类型来解析的,因此将一个强制类型转换为参数会对调用的结果产生很大的影响,因为 以下代码显示: +正如 `Joshua Bloch` 在 `Effective Java` 中所解释的那样(在 `Methods` 的章节中,项目“审慎地使用重载”),调用两个构造函数或方法重载中的一个,这些重 +载使用相关类型的参数会导致令人困惑的结果。 这是因为,在 `Java` 中,调用重载的构造函数和方法是在编译时根据参数的静态类型来解析的,因此将一个强制类型转 +换为参数会对调用的结果产生很大的影响,因为 以下代码显示: ```java - // 构造并填充一个NavigableSet,其迭代器按照自然顺序的相反返回其元素: - NavigableSet base = new TreeSet(Collections.reverseOrder()); - Collections.addAll(base, "b", "a", "c"); - // 为TreeSet调用两个不同的构造函数,提供刚刚构建的集合,但使用不同的静态类型: - NavigableSet sortedSet1 = new TreeSet((Set)base); - NavigableSet sortedSet2 = new TreeSet(base); - // 并且这两组具有不同的迭代次序: - List forward = new ArrayList(); - forward.addAll(sortedSet1); - List backward = new ArrayList(); - backward.addAll(sortedSet2); - assert !forward.equals(backward); - Collections.reverse(forward); - assert forward.equals(backward); +// 构造并填充一个NavigableSet,其迭代器按照自然顺序的相反返回其元素: +NavigableSet base = new TreeSet(Collections.reverseOrder()); +Collections.addAll(base, "b", "a", "c"); +// 为TreeSet调用两个不同的构造函数,提供刚刚构建的集合,但使用不同的静态类型: +NavigableSet sortedSet1 = new TreeSet((Set)base); +NavigableSet sortedSet2 = new TreeSet(base); +// 并且这两组具有不同的迭代次序: +List forward = new ArrayList(); +forward.addAll(sortedSet1); +List backward = new ArrayList(); +backward.addAll(sortedSet2); +assert !forward.equals(backward); +Collections.reverse(forward); +assert forward.equals(backward); ``` -此问题折磨框架中所有已排序集合的构造函数(`TreeSet`,`TreeMap`,`ConcurrentSkipListSet` 和 `ConcurrentSkipListMap`)。为了避免出现在你自己的类设计中,为不同的重载选择参数类型,以便适合于一个重载的参数类型不能转换为适合不同类型的参数类型。如果这是不可能的,则应该将这两个重载设计为与同一个参数具有相同的行为,而不管其静态类型如何。例如,从集合构造的 `PriorityQueue`(第 `14.2.1` 节)使用原始的顺序,无论构造函数提供的静态类型是包含 `Comparator` 的类型 `PriorityQueue` 或 `SortedSet` 之一,还是纯粹的 `Collection`。为了达到这个目的,转换构造函数使用提供的集合的比较器,如果它没有自然顺序的话,它只会退化。 +此问题折磨框架中所有已排序集合的构造函数(`TreeSet`,`TreeMap`,`ConcurrentSkipListSet` 和 `ConcurrentSkipListMap`)。为了避免出现在你自己的类 +设计中,为不同的重载选择参数类型,以便适合于一个重载的参数类型不能转换为适合不同类型的参数类型。如果这是不可能的,则应该将这两个重载设计为与同一个参 +数具有相同的行为,而不管其静态类型如何。例如,从集合构造的 `PriorityQueue`(第 `14.2.1` 节)使用原始的顺序,无论构造函数提供的静态类型是包含 +`Comparator` 的类型 `PriorityQueue` 或 `SortedSet` 之一,还是纯粹的 `Collection`。为了达到这个目的,转换构造函数使用提供的集合的比较器,如果它没 +有自然顺序的话,它只会退化。 `TreeSet` 是不同步的并且不是线程安全的;它的迭代器是快速失败的。 @@ -292,13 +340,23 @@ 图 `13-9`。 搜索跳过列表 -`ConcurrentSkipListSet` 作为第一个并发集合实现在 `Java 6` 中引入。它由跳过列表支持,跳过列表是上一节二叉树的现代替代方案。一个集合的跳过列表是一系列链表,每个链表都是由两个字段组成的一连串单元格:一个用于保存一个值,另一个用于保存对下一个单元格的引用。通过指针重新排列,元素在常量时间内插入和移出链表,如图 `13-8` 部分(`a`)和(`b`)所示。 +`ConcurrentSkipListSet` 作为第一个并发集合实现在 `Java 6` 中引入。它由跳过列表支持,跳过列表是上一节二叉树的现代替代方案。一个集合的跳过列表是一系 +列链表,每个链表都是由两个字段组成的一连串单元格:一个用于保存一个值,另一个用于保存对下一个单元格的引用。通过指针重新排列,元素在常量时间内插入和移 +出链表,如图 `13-8` 部分(`a`)和(`b`)所示。 -图 `13-9` 显示了一个由三个链接列表组成的链接列表,标记为级别 `0`,`1` 和 `2`.集合的第一个链接列表(图中的级别 `0`)包含集合的元素,根据其自然顺序排序或由该组的比较器。 `0` 级以上的每个列表包含下面列表的一个子集,根据固定的概率随机选择。对于这个例子,我们假设概率是 `0.5` ;平均而言,每个列表将包含它下面列表的一半元素。在链接之间进行浏览需要一段固定的时间,因此找到一个元素最快的方法是从顶部列表的开头(左侧)开始,并尽可能远离每个列表,然后放到下面的列表中。 +图 `13-9` 显示了一个由三个链接列表组成的链接列表,标记为级别 `0`,`1` 和 `2`.集合的第一个链接列表(图中的级别 `0`)包含集合的元素,根据其自然顺序排 +序或由该组的比较器。 `0` 级以上的每个列表包含下面列表的一个子集,根据固定的概率随机选择。对于这个例子,我们假设概率是 `0.5` ;平均而言,每个列表将包 +含它下面列表的一半元素。在链接之间进行浏览需要一段固定的时间,因此找到一个元素最快的方法是从顶部列表的开头(左侧)开始,并尽可能远离每个列表,然后放 +到下面的列表中。 -图 `13-9` 的曲线箭头表示搜索元素 `55` 的进程。搜索从级别 `2` 的左上角的元素 `12` 开始,在该级别上步进到元素 `31`,然后发现下一个元素是 `61`,高于搜索值。所以它下降一个级别,然后重复这个过程;元素 `47` 仍然小于 `55`,但是 `61` 又是太大了,所以它再次下降一个级别并且在一个进一步的步骤中找到搜索值。将元素插入到跳过列表中总是涉及至少将其插入到级别 `0`.当已经完成了,是否也应该插入第 `1` 级?如果 `1` 级平均包含 `0` 级元素的一半,那么我们应该抛硬币(也就是随机选择 `0.5` 的概率)来决定是否应该插入 `1` 级。如果掷硬币导致它被插入到第 `1` 级,那么该过程将重复第 `2` 级,等等。要从跳过列表中删除元素,它将从其出现的每个级别中删除。 +图 `13-9` 的曲线箭头表示搜索元素 `55` 的进程。搜索从级别 `2` 的左上角的元素 `12` 开始,在该级别上步进到元素 `31`,然后发现下一个元素是 `61`,高于 +搜索值。所以它下降一个级别,然后重复这个过程;元素 `47` 仍然小于 `55`,但是 `61` 又是太大了,所以它再次下降一个级别并且在一个进一步的步骤中找到搜索 +值。将元素插入到跳过列表中总是涉及至少将其插入到级别 `0`.当已经完成了,是否也应该插入第 `1` 级?如果 `1` 级平均包含 `0` 级元素的一半,那么我们应该抛 +硬币(也就是随机选择 `0.5` 的概率)来决定是否应该插入 `1` 级。如果掷硬币导致它被插入到第 `1` 级,那么该过程将重复第 `2` 级,等等。要从跳过列表中删 +除元素,它将从其出现的每个级别中删除。 -如果抛硬币变得糟糕,我们最终可能会列出 `0` 以上的每个列表空白或满的情况,这同样会很糟糕。然而,这些结果的可能性非常低,分析表明,实际上,跳过列表的性能可以与二叉树相媲美:搜索,插入和删除都会花费 `O(log n)`。它们的并发使用的强大优势是它们具有高效的无锁插入和删除算法,而没有二叉树的已知方法。 +如果抛硬币变得糟糕,我们最终可能会列出 `0` 以上的每个列表空白或满的情况,这同样会很糟糕。然而,这些结果的可能性非常低,分析表明,实际上,跳过列表的性 +能可以与二叉树相媲美:搜索,插入和删除都会花费 `O(log n)`。它们的并发使用的强大优势是它们具有高效的无锁插入和删除算法,而没有二叉树的已知方法。 `ConcurrentSkipListSet` 的迭代器是弱一致的。 @@ -316,4 +374,4 @@ 在 `EnumSet` 实现中,对于多于 `64` 个值的枚举类型,下一个最坏情况下的复杂度为 `O(log m)`,其中 `m` 是枚举中元素的数量。 《《《 [下一节](../ch14/00_Queues.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch14/00_Queues.md b/ch14/00_Queues.md index 5cbb25d8..be973cdb 100644 --- a/ch14/00_Queues.md +++ b/ch14/00_Queues.md @@ -1,13 +1,21 @@ 《《《 [返回首页](../README.md)
《《《 [上一节](../ch13/02_SortedSet_and_NavigableSet.md) -## Queues +### Queues -队列是一个集合,用于保存要处理的元素,并按照它们要处理的顺序生成它们。相应的集合框架接口队列(参见图 `14-1`)有许多不同的实现,它们体现了关于这个顺序应该是什么的不同规则。许多实现使用的规则是按照提交顺序处理任务(先进先出或 `FIFO`),但也可以使用其他规则 - 例如,集合框架包括处理顺序为基于任务优先级。`Queue` 接口是在 `Java 5` 中引入的,部分原因是该版本中包含的并发实用程序中需要队列。看一下图 `14-2` 所示的实现层次结构,事实上,集合框架中几乎所有的 `Queue` 实现都在 `java.util.concurrent` 包中。 +队列是一个集合,用于保存要处理的元素,并按照它们要处理的顺序生成它们。相应的集合框架接口队列(参见图 `14-1`)有许多不同的实现,它们体现了关于这个顺序 +应该是什么的不同规则。许多实现使用的规则是按照提交顺序处理任务(先进先出或 `FIFO`),但也可以使用其他规则 - 例如,集合框架包括处理顺序为基于任务优先 +级。`Queue` 接口是在 `Java 5` 中引入的,部分原因是该版本中包含的并发实用程序中需要队列。看一下图 `14-2` 所示的实现层次结构,事实上,集合框架中几乎 +所有的 `Queue` 实现都在 `java.util.concurrent` 包中。 -并发系统中的队列的一个经典要求是当许多任务必须由多个并行工作的线程执行时。这种情况的一个日常例子是由一组登机运营商处理的单个航班乘客队列。每个操作员在处理单个乘客(或一组乘客)时工作,而剩余的乘客在队列中等待。当他们到达时,乘客加入队列的尾部,等到他们的头部,然后被分配给下一个空闲的运营商。实现这样的队列涉及到很多细节,必须防止运营商同时尝试处理同一名乘客,空队列必须得到正确处理,并且在计算机系统中必须有一种定义具有最大尺寸或界限的队列的方法。(这最后一项要求可能不会在航空公司终端中强制执行,但对于执行任务需要最长等待时间的系统来说,它可能非常有用。)`java.util.concurrent` 中的 `Queue` 实现将关注这些实现。 +并发系统中的队列的一个经典要求是当许多任务必须由多个并行工作的线程执行时。这种情况的一个日常例子是由一组登机运营商处理的单个航班乘客队列。每个操作员 +在处理单个乘客(或一组乘客)时工作,而剩余的乘客在队列中等待。当他们到达时,乘客加入队列的尾部,等到他们的头部,然后被分配给下一个空闲的运营商。实现 +这样的队列涉及到很多细节,必须防止运营商同时尝试处理同一名乘客,空队列必须得到正确处理,并且在计算机系统中必须有一种定义具有最大尺寸或界限的队列的方 +法。(这最后一项要求可能不会在航空公司终端中强制执行,但对于执行任务需要最长等待时间的系统来说,它可能非常有用。)`java.util.concurrent` 中的 +`Queue` 实现将关注这些实现。 -除了继承自 `Collection` 的操作之外,队列接口还包括将元素添加到队列尾部,在其头部检查元素或在其头部删除元素的操作。这三个操作中的每一个都有两种类型,一种返回一个值来表示失败,另一种抛出异常。 +除了继承自 `Collection` 的操作之外,队列接口还包括将元素添加到队列尾部,在其头部检查元素或在其头部删除元素的操作。这三个操作中的每一个都有两种类型, +一种返回一个值来表示失败,另一种抛出异常。 ![](14_1.png) @@ -17,33 +25,36 @@ 图 `14-2`。 集合框架中队列的实现 -**将元素添加到队列**此操作的异常抛出变体是从 `Collection` 继承的 `add` 方法。 虽然 `add` 不返回一个布尔值,表示它在插入元素方面的成功,但该值不能用于报告有界队列已满;`add` 的合约指定它只有在集合因为它已经存在而拒绝该元素时才会返回 `false`,否则它必须抛出异常。 +**将元素添加到队列**此操作的异常抛出变体是从 `Collection` 继承的 `add` 方法。 虽然 `add` 不返回一个布尔值,表示它在插入元素方面的成功,但该值不能 +用于报告有界队列已满;`add` 的合约指定它只有在集合因为它已经存在而拒绝该元素时才会返回 `false`,否则它必须抛出异常。 价值回归变体是提供: ```java - boolean offer (E e) // 如果可能的话插入给定的元素 +boolean offer (E e) // 如果可能的话插入给定的元素 ``` -`offer` 返回的值表示元素是否被成功插入。 请注意,如果元素在某些方面非法(例如,不允许 `null` 的队列的值为 `null`),`offer` 会抛出异常。 通常情况下,如果报价返回 `false`,则在已达到容量的有界队列上调用该报价。 +`offer` 返回的值表示元素是否被成功插入。 请注意,如果元素在某些方面非法(例如,不允许 `null` 的队列的值为 `null`),`offer` 会抛出异常。 通常情况 +下,如果报价返回 `false`,则在已达到容量的有界队列上调用该报价。 **从队列中检索元素**此组中的方法是 `peek` 和元素,用于检查 `head` 元素,以及 `poll` 和 `remove` 以将其从队列中移除并返回其值。 引发空队列异常的方法是: ```java - E element() // 检索但不要删除头元素 - E remove() // 检索并移除头部元素 +E element() // 检索但不要删除头元素 +E remove() // 检索并移除头部元素 ``` 请注意,这是与 `Collection` 方法 `remove(Object)` 不同的方法。 对于空队列返回 `null` 的方法是: ```java - E peek() // 检索但不要删除头元素 - E poll() // 检索并移除头部元素 +E peek() // 检索但不要删除头元素 +E poll() // 检索并移除头部元素 ``` -由于这些方法返回 `null` 以表示队列为空,因此应避免将 `null` 用作队列元素。 通常,`Queue` 接口不鼓励使用 `null` 作为队列元素,并且允许它的唯一标准实现是传统实现 `LinkedList`。 +由于这些方法返回 `null` 以表示队列为空,因此应避免将 `null` 用作队列元素。 通常,`Queue` 接口不鼓励使用 `null` 作为队列元素,并且允许它的唯一标准 +实现是传统实现 `LinkedList`。 《《《 [下一节](01_Using_the_Methods_of_Queue.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch14/01_Using_the_Methods_of_Queue.md b/ch14/01_Using_the_Methods_of_Queue.md index 19d58282..0d3b02e9 100644 --- a/ch14/01_Using_the_Methods_of_Queue.md +++ b/ch14/01_Using_the_Methods_of_Queue.md @@ -3,23 +3,25 @@ ### 使用队列方法 -我们来看看使用这些方法的例子。 队列应该提供一个实现任务管理器的好方法,因为它们的主要目的是生成要处理的元素,比如任务。 目前我们将使用 `ArrayDeque` 作为队列中最快和最直接的实现(当然也是 `Deque`)。 但是,像以前一样,我们只限于接口的方法 - 尽管您应该注意到,在选择队列实现时,您也正在选择一种排序。使用 `ArrayDeque`,您可以获得 `FIFO` 订购 - 当然,我们尝试使用花哨的调度方法进行组织,似乎无法很好地工作; 也许是时候尝试一些简单的事情了。 +我们来看看使用这些方法的例子。 队列应该提供一个实现任务管理器的好方法,因为它们的主要目的是生成要处理的元素,比如任务。 目前我们将使用 `ArrayDeque` +作为队列中最快和最直接的实现(当然也是 `Deque`)。 但是,像以前一样,我们只限于接口的方法 - 尽管您应该注意到,在选择队列实现时,您也正在选择一种排 +序。使用 `ArrayDeque`,您可以获得 `FIFO` 订购 - 当然,我们尝试使用花哨的调度方法进行组织,似乎无法很好地工作; 也许是时候尝试一些简单的事情了。 `ArrayDeque` 是无限的,所以我们可以使用 `add` 或 `offer` 来设置新任务的队列。 ```java - Queue taskQueue = new ArrayDeque(); - taskQueue.offer(mikePhone); - taskQueue.offer(paulPhone); +Queue taskQueue = new ArrayDeque(); +taskQueue.offer(mikePhone); +taskQueue.offer(paulPhone); ``` 任何时候我们都准备好去做任务,我们可以选择一个达到队列头的人: ```java - Task nextTask = taskQueue.poll(); - if (nextTask != null) { - // process nextTask - } +Task nextTask = taskQueue.poll(); +if (nextTask != null) { +// process nextTask +} ``` 使用民意调查和删除之间的选择取决于我们是否要将队列空虚视为特殊情况。 实际上 - 考虑到应用的性质 - 这可能是一个明智的假设,所以这是一个替代方案: @@ -36,18 +38,26 @@ 这种方案需要一些改进以适应不同类型任务的性质。电话任务适合相对较短的时间段,而我们不喜欢开始编码,除非有相当长的时间才能完成任务。 所以如果时间有限 - 比如说,直到下次会议 - 我们可能会希望在下班之前检查下一个任务是否正确。 ```java - Task nextTask = taskQueue.peek(); - if (nextTask instanceof PhoneTask) { - taskQueue.remove(); - // process nextTask - } +Task nextTask = taskQueue.peek(); +if (nextTask instanceof PhoneTask) { + taskQueue.remove(); + // process nextTask +} ``` -这些检查和删除方法是 `Queue` 界面的主要优点;集合没有像他们一样(虽然 `NavigableSet` 现在做)。我们为这个好处付出的代价是,只有头元素实际上是我们想要的元素时,`Queue` 的方法才对我们有用。诚然,类 `PriorityQueue` 允许我们提供一个比较器,它将排列队列元素,这样我们需要的队列元素就在头部,但这可能不是一种表达选择下一个任务的算法的特别好方法(例如,如果您需要了解所有未完成的任务,然后才能选择下一个)。所以在这种情况下,如果我们的待办事项管理者完全是基于队列的,我们最终可能会在开始会议前喝咖啡。作为替代方案,我们可以考虑使用List接口,它提供了访问其元素的更灵活的方法,但是它的缺点是它的实现对多线程的使用提供了更少的支持。 +这些检查和删除方法是 `Queue` 界面的主要优点;集合没有像他们一样(虽然 `NavigableSet` 现在做)。我们为这个好处付出的代价是,只有头元素实际上是我们想 +要的元素时,`Queue` 的方法才对我们有用。诚然,类 `PriorityQueue` 允许我们提供一个比较器,它将排列队列元素,这样我们需要的队列元素就在头部,但这可能 +不是一种表达选择下一个任务的算法的特别好方法(例如,如果您需要了解所有未完成的任务,然后才能选择下一个)。所以在这种情况下,如果我们的待办事项管理者 +完全是基于队列的,我们最终可能会在开始会议前喝咖啡。作为替代方案,我们可以考虑使用List接口,它提供了访问其元素的更灵活的方法,但是它的缺点是它的实现 +对多线程的使用提供了更少的支持。 -这听起来可能太悲观了;毕竟,`Queue` 是 `Collection` 的子接口,所以它继承了支持遍历的方法,比如迭代器。事实上,尽管实施了这些方法,但在正常情况下不建议使用这些方法。在队列类的设计中,遍历的效率已经与 `Queue` 方法的快速实现进行了交易;此外,队列迭代器不保证以正确的顺序返回它们的元素,并且对于某些并发队列,在正常条件下实际上会失败(请参见第 `14.3.2` 节)。 +这听起来可能太悲观了;毕竟,`Queue` 是 `Collection` 的子接口,所以它继承了支持遍历的方法,比如迭代器。事实上,尽管实施了这些方法,但在正常情况下不建 +议使用这些方法。在队列类的设计中,遍历的效率已经与 `Queue` 方法的快速实现进行了交易;此外,队列迭代器不保证以正确的顺序返回它们的元素,并且对于某些并 +发队列,在正常条件下实际上会失败(请参见第 `14.3.2` 节)。 -在下一节中,我们将看看 `Queue` - `Priority Queue` 和 `ConcurrentLinkedList` 的直接实现 - 以及 `14.3` 章节中的 `BlockingQueue` 及其实现。这两节中的课程在行为上差异很大。他们大多数是线程安全的;大多数提供阻塞设施(即等待条件的操作适合他们执行);一些支持优先级排序;一个 `DelayQueue` - 保留元素,直到它们的延迟已经过期,另一个 - 同步队列 - 完全是一个同步工具。在队列实现之间进行选择时,这些功能差异会比他们的表现更受影响。 +在下一节中,我们将看看 `Queue` - `Priority Queue` 和 `ConcurrentLinkedList` 的直接实现 - 以及 `14.3` 章节中的 `BlockingQueue` 及其实现。这两节 +中的课程在行为上差异很大。他们大多数是线程安全的;大多数提供阻塞设施(即等待条件的操作适合他们执行);一些支持优先级排序;一个 `DelayQueue` - 保留元 +素,直到它们的延迟已经过期,另一个 - 同步队列 - 完全是一个同步工具。在队列实现之间进行选择时,这些功能差异会比他们的表现更受影响。 《《《 [下一节](02_Implementing_Queue.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch14/02_Implementing_Queue.md b/ch14/02_Implementing_Queue.md index b3b67b61..6e5ab1f5 100644 --- a/ch14/02_Implementing_Queue.md +++ b/ch14/02_Implementing_Queue.md @@ -5,19 +5,24 @@ #### PriorityQueue -`PriorityQueue` 是两个非主题的 `Queue` 实现之一,主要不是为并发使用而设计的(另一个是 `ArrayDeque`)。它不是线程安全的,也不提供阻塞行为。它根据 `NavigableSet` 所使用的顺序放弃其处理元素 - 如果它们实现 `Comparable` 时其元素的自然顺序,或构造 `PriorityQueue` 时提供的比较器施加的顺序。因此,`PriorityQueue` 将成为我们在 `13.2` 节中使用 `NavigableSet` 概述的基于优先级的待办事项管理器的另一种设计选择(显然,以其名称为例)。您的应用程序将决定选择哪种替代方法:如果需要检查和操作一组等待任务,请使用 `NavigableSet`。如果其主要要求是有效访问要执行的下一个任务,请使用 `PriorityQueue`。 +`PriorityQueue` 是两个非主题的 `Queue` 实现之一,主要不是为并发使用而设计的(另一个是 `ArrayDeque`)。它不是线程安全的,也不提供阻塞行为。它根据 +`NavigableSet` 所使用的顺序放弃其处理元素 - 如果它们实现 `Comparable` 时其元素的自然顺序,或构造 `PriorityQueue` 时提供的比较器施加的顺序。因此, +`PriorityQueue` 将成为我们在 `13.2` 节中使用 `NavigableSet` 概述的基于优先级的待办事项管理器的另一种设计选择(显然,以其名称为例)。您的应用程序将 +决定选择哪种替代方法:如果需要检查和操作一组等待任务,请使用 `NavigableSet`。如果其主要要求是有效访问要执行的下一个任务,请使用 `PriorityQueue`。 -选择 `PriorityQueue` 允许我们重新考虑排序:因为它容纳重复项,所以它不会共享 `NavigableSet` 对与 `equals` 等效的排序的要求。为了强调这一点,我们将为我们的待办事项经理定义一个仅依赖于优先事项的新订单。与您所期望的相反,`PriorityQueue`不保证它如何呈现具有相同值的多个元素。因此,如果在我们的例子中,几个任务与队列中的最高优先级相关联,那么它将任意选择其中的一个作为头元素。 +选择 `PriorityQueue` 允许我们重新考虑排序:因为它容纳重复项,所以它不会共享 `NavigableSet` 对与 `equals` 等效的排序的要求。为了强调这一点,我们将 +为我们的待办事项经理定义一个仅依赖于优先事项的新订单。与您所期望的相反,`PriorityQueue`不保证它如何呈现具有相同值的多个元素。因此,如果在我们的例子 +中,几个任务与队列中的最高优先级相关联,那么它将任意选择其中的一个作为头元素。 `PriorityQueue`的构造函数是: ```java - PriorityQueue() // 自然排序,默认初始容量(11) - PriorityQueue(Collection c) // 从c取出的元素的自然顺序,除非c是PriorityQueue或SortedSet,在这种情况下,复制c的顺序 - PriorityQueue(int initialCapacity) // 自然排序,指定的初始容量 - PriorityQueue(int initialCapacity, Comparator comparator) // 比较器排序,指定初始容量 - PriorityQueue(PriorityQueue c) // 从c复制的顺序和元素 - PriorityQueue(SortedSet c) // 从c复制的顺序和元素 +PriorityQueue() // 自然排序,默认初始容量(11) +PriorityQueue(Collection c) // 从c取出的元素的自然顺序,除非c是PriorityQueue或SortedSet,在这种情况下,复制c的顺序 +PriorityQueue(int initialCapacity) // 自然排序,指定的初始容量 +PriorityQueue(int initialCapacity, Comparator comparator) // 比较器排序,指定初始容量 +PriorityQueue(PriorityQueue c) // 从c复制的顺序和元素 +PriorityQueue(SortedSet c) // 从c复制的顺序和元素 ``` ![](14_3.png) @@ -27,36 +32,48 @@ 请注意第二个构造函数如何避免第 `13.2.2` 节中讨论的重载 `TreeSet` 构造函数的问题。 我们可以使用 `PriorityQueue` 来简单地实现我们的待办事项管理器,其中使用了第 `13.2` 节中定义的 `PriorityTask` 类,而新的比较器仅取决于任务的优先级: ```java - final int INITIAL_CAPACITY = 10; - Comparator priorityComp = new Comparator() { - public int compare(PriorityTask o1, PriorityTask o2) { - return o1.getPriority().compareTo(o2.getPriority()); - } - }; - Queue priorityQueue = new PriorityQueue(INITIAL_CAPACITY, priorityComp); - priorityQueue.add(new PriorityTask(mikePhone, Priority.MEDIUM)); - priorityQueue.add(new PriorityTask(paulPhone, Priority.HIGH)); - ... - PriorityTask nextTask = priorityQueue.poll(); +final int INITIAL_CAPACITY = 10; +Comparator priorityComp = new Comparator() { + public int compare(PriorityTask o1, PriorityTask o2) { + return o1.getPriority().compareTo(o2.getPriority()); + } +}; +Queue priorityQueue = new PriorityQueue(INITIAL_CAPACITY, priorityComp); +priorityQueue.add(new PriorityTask(mikePhone, Priority.MEDIUM)); +priorityQueue.add(new PriorityTask(paulPhone, Priority.HIGH)); +... +PriorityTask nextTask = priorityQueue.poll(); ``` -优先级队列通常通过优先堆有效地实现。一个优先级堆是一个二叉树,有点像我们在 `13.2.2` 节中看到的那样实现 `TreeSet`,但有两点不同:首先,唯一的排序约束是树中的每个节点应该大于它的子节点,其次,除了可能的最低层之外,树的每一层都应该是完整的;如果最低级别不完整,它所包含的节点必须在左侧组合在一起。图 `14-3`(`a`)显示了一个小优先级堆,每个节点只显示包含其优先级的字段。要将一个新元素添加到优先堆中,它首先附加在最左边的空位上,如图 `14-3`(`b`)中圆圈所示。然后,它会与其父级重复交换,直至达到具有更高优先级的父级。在图中,这只需要将新元素与其父元素交换一次,如图 `14-3`(`c`)所示。 (图 `14-3` 和图 `14-4` 中圈出的节点刚好改变位置。) +优先级队列通常通过优先堆有效地实现。一个优先级堆是一个二叉树,有点像我们在 `13.2.2` 节中看到的那样实现 `TreeSet`,但有两点不同:首先,唯一的排序约束 +是树中的每个节点应该大于它的子节点,其次,除了可能的最低层之外,树的每一层都应该是完整的;如果最低级别不完整,它所包含的节点必须在左侧组合在一起。图 +`14-3`(`a`)显示了一个小优先级堆,每个节点只显示包含其优先级的字段。要将一个新元素添加到优先堆中,它首先附加在最左边的空位上,如图 `14-3`(`b`)中 +圆圈所示。然后,它会与其父级重复交换,直至达到具有更高优先级的父级。在图中,这只需要将新元素与其父元素交换一次,如图 `14-3`(`c`)所示。 (图 `14-3` +和图 `14-4` 中圈出的节点刚好改变位置。) -从优先级堆获取最高优先级的元素是微不足道的:它是树的根。但是,如果删除了这些结果,则必须重新组织这两个单独的树,以重新组织优先堆。这是通过首先将最底层的最右边的元素放到根位置来完成的。然后 - 与添加元素的过程相反 - 它会与其中较大的子元素重复交换,直到它具有比其中任何一个更高的优先级。图 `14-4` 显示了这个过程 - 再次只需要一次交换 - 从头部被移除后,从图 `14-3`(`c`)中的堆开始。 +从优先级堆获取最高优先级的元素是微不足道的:它是树的根。但是,如果删除了这些结果,则必须重新组织这两个单独的树,以重新组织优先堆。这是通过首先将最底 +层的最右边的元素放到根位置来完成的。然后 - 与添加元素的过程相反 - 它会与其中较大的子元素重复交换,直到它具有比其中任何一个更高的优先级。图 `14-4` 显 +示了这个过程 - 再次只需要一次交换 - 从头部被移除后,从图 `14-3`(`c`)中的堆开始。 ![](14_4.png) 图 `14-4`。 删除 `PriorityQueue` 的头部 -除了不变的开销之外,元素的添加和删除都需要一些与树高度成比例的操作。因此,`PriorityQueue` 为 `offer`,`poll`,`remove()` 和 `add` 提供 `O(log n)` 时间。`remove(Object)` 和 `contains` 方法可能需要遍历整个树,所以它们需要 `O(n)` 时间。方法 `peek` 和 `element` 只是在不删除它的情况下检索树的根,它需要一个不变的时间,就像 `size` 一样,它使用一个不断更新的对象字段。 +除了不变的开销之外,元素的添加和删除都需要一些与树高度成比例的操作。因此,`PriorityQueue` 为 `offer`,`poll`,`remove()` 和 `add` 提供 +`O(log n)` 时间。`remove(Object)` 和 `contains` 方法可能需要遍历整个树,所以它们需要 `O(n)` 时间。方法 `peek` 和 `element` 只是在不删除它的情况 +下检索树的根,它需要一个不变的时间,就像 `size` 一样,它使用一个不断更新的对象字段。 `PriorityQueue` 不适合并发使用。它的迭代器是快速失败的,它不支持客户端锁定。线程安全版本 `PriorityBlockingQueue`(请参阅第 `14.3.2` 节)。 ### ConcurrentLinkedQueue -另一个非阻塞队列实现是 `ConcurrentLinkedQueue`,它是一个无界的,线程安全的 `FIFO` 排序队列。它使用链接结构,类似于我们在第 `13.2.3` 节中看到的链接结构作为跳过列表的基础,在 `13.1.1` 节中用于散列表溢出链接。我们在那里注意到,链接结构的主要吸引力之一是通过指针重排实现的插入和移除操作在不变的时间内执行。这使得它们作为队列实现特别有用,其中这些操作总是在结构末端的单元上需要 - 也就是说,不需要使用链接结构的缓慢顺序搜索来定位单元。 +另一个非阻塞队列实现是 `ConcurrentLinkedQueue`,它是一个无界的,线程安全的 `FIFO` 排序队列。它使用链接结构,类似于我们在第 `13.2.3` 节中看到的链 +接结构作为跳过列表的基础,在 `13.1.1` 节中用于散列表溢出链接。我们在那里注意到,链接结构的主要吸引力之一是通过指针重排实现的插入和移除操作在不变的时 +间内执行。这使得它们作为队列实现特别有用,其中这些操作总是在结构末端的单元上需要 - 也就是说,不需要使用链接结构的缓慢顺序搜索来定位单元。 -`ConcurrentLinkedQueue` 使用基于 `CAS` 的无等待算法 - 也就是说,无论其他线程访问队列的状态如何,都可以确保任何线程始终可以完成其当前操作。它在一段时间内执行队列插入和删除操作,但需要线性时间来执行大小。这是因为依赖于线程之间的插入和移除协作的算法没有跟踪队列大小,并且必须在需要时迭代队列以计算它。 +`ConcurrentLinkedQueue` 使用基于 `CAS` 的无等待算法 - 也就是说,无论其他线程访问队列的状态如何,都可以确保任何线程始终可以完成其当前操作。它在一段 +时间内执行队列插入和删除操作,但需要线性时间来执行大小。这是因为依赖于线程之间的插入和移除协作的算法没有跟踪队列大小,并且必须在需要时迭代队列以计算 +它。 ![](14_5.png) @@ -65,4 +82,4 @@ `ConcurrentLinkedQueue` 有 `12.3` 节中讨论的两个标准构造函数。它的迭代器很弱一致。 《《《 [下一节](03_BlockingQueue.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch14/03_BlockingQueue.md b/ch14/03_BlockingQueue.md index b48791ab..6550a8d7 100644 --- a/ch14/03_BlockingQueue.md +++ b/ch14/03_BlockingQueue.md @@ -5,92 +5,124 @@ `Java 5` 为集合框架添加了许多类以供并发应用程序使用。其中大部分是 `Queue` 子接口 `BlockingQueue`(见图 `14-5`)的实现,主要用于生产者消费者队列。 -使用生产者 - 消费者队列的一个常见例子是在执行打印假脱机的系统中;客户端进程将打印作业添加到假脱机队列中,由一个或多个打印服务进程处理,每个打印服务进程重复“占用”队列头部的任务。 +使用生产者 - 消费者队列的一个常见例子是在执行打印假脱机的系统中;客户端进程将打印作业添加到假脱机队列中,由一个或多个打印服务进程处理,每个打印服务进 +程重复“占用”队列头部的任务。 -正如其名称所暗示的,`BlockingQueue` 为这些系统提供的关键功能是入队和出队方法,直到成功执行才会返回。因此,例如,打印服务器不需要持续轮询队列以发现是否有任何打印作业正在等待;它只需要调用轮询方法,提供超时,系统将暂停它,直到队列元素变为可用或超时过期。`BlockingQueue` 定义了七种新方法,分为三组: +正如其名称所暗示的,`BlockingQueue` 为这些系统提供的关键功能是入队和出队方法,直到成功执行才会返回。因此,例如,打印服务器不需要持续轮询队列以发现是 +否有任何打印作业正在等待;它只需要调用轮询方法,提供超时,系统将暂停它,直到队列元素变为可用或超时过期。`BlockingQueue` 定义了七种新方法,分为三组: **添加一个元素** ```java - boolean offer(E e, long timeout, TimeUnit unit) //插入e,等待超时 - void put(E e) // 添加e,只要需要等待 +boolean offer(E e, long timeout, TimeUnit unit) //插入e,等待超时 +void put(E e) // 添加e,只要需要等待 ``` -`Queue` 中定义的商品的无阻塞重载将返回 `false`,如果它不能立即插入该元素。 这个新的重载等待使用 `java.util.concurrent.TimeUnit` 指定的时间,`Enum` 允许以毫秒或秒为单位定义超时。 +`Queue` 中定义的商品的无阻塞重载将返回 `false`,如果它不能立即插入该元素。 这个新的重载等待使用 `java.util.concurrent.TimeUnit` 指定的时间, +`Enum` 允许以毫秒或秒为单位定义超时。 -将这些方法与从 `Queue` 继承的方法一起使用时,向 `BlockingQueue` 添加元素的方法可以采用四种方法:如果它不立即成功,则 `offer` 返回 `false`;如果在超时内未成功,则提供返回 `false`, 如果它不立即成功,则添加抛出异常,并放置块直到成功。 +将这些方法与从 `Queue` 继承的方法一起使用时,向 `BlockingQueue` 添加元素的方法可以采用四种方法:如果它不立即成功,则 `offer` 返回 `false`;如果在 +超时内未成功,则提供返回 `false`, 如果它不立即成功,则添加抛出异常,并放置块直到成功。 **移除一个元素** ```java - E poll(long timeout, TimeUnit unit) // 检索并移除头部,等待超时 - E take() // 检索并删除此队列的头部,并根据需要等待 +E poll(long timeout, TimeUnit unit) // 检索并移除头部,等待超时 +E take() // 检索并删除此队列的头部,并根据需要等待 ``` -再次将这些方法与从 `Queue` 继承的方法一起使用,有四种方法可用于从 `BlockingQueue` 中移除元素的方法:如果 `poll` 不能立即成功,`poll` 将返回 `null`,如果 `poll` 在其超时时间内未成功,则返回 `null`,如果它不立即成功,则抛出异常,并取块直到成功。 +再次将这些方法与从 `Queue` 继承的方法一起使用,有四种方法可用于从 `BlockingQueue` 中移除元素的方法:如果 `poll` 不能立即成功,`poll` 将返回 +`null`,如果 `poll` 在其超时时间内未成功,则返回 `null`,如果它不立即成功,则抛出异常,并取块直到成功。 **检索或查询队列的内容** ```java - int drainTo(Collection c)// 将队列清理成c - int drainTo(Collection c, int maxElements) // 至多将指定数量的元素清除为c - int remainingCapacity() // 返回将被接受而没有阻塞的元素的数量,或者如果无界的话返回Integer.MAX_VALUE +int drainTo(Collection c)// 将队列清理成c +int drainTo(Collection c, int maxElements) // 至多将指定数量的元素清除为c +int remainingCapacity() // 返回将被接受而没有阻塞的元素的数量,或者如果无界的话返回Integer.MAX_VALUE ``` -`drainTo` 方法自动且高效地执行,所以第二个重载在你知道你的处理能力对一定数量的元素立即可用的情况下是有用的,第一个是有用的 - 例如,当所有生产者线程都停止工作时。他们的返回值是转移的元素的数量。`RemainingCapacity` 报告队列的剩余容量,虽然与多线程上下文中的任何这样的值一样,调用的结果不应该被用作测试然后动作序列的一部分;在测试(剩余容量的调用)和一个线程的动作(向队列中添加一个元素)之间,另一个线程可能会介入以添加或删除元素。 +`drainTo` 方法自动且高效地执行,所以第二个重载在你知道你的处理能力对一定数量的元素立即可用的情况下是有用的,第一个是有用的 - 例如,当所有生产者线程 +都停止工作时。他们的返回值是转移的元素的数量。`RemainingCapacity` 报告队列的剩余容量,虽然与多线程上下文中的任何这样的值一样,调用的结果不应该被用作 +测试然后动作序列的一部分;在测试(剩余容量的调用)和一个线程的动作(向队列中添加一个元素)之间,另一个线程可能会介入以添加或删除元素。 -`BlockingQueue` 保证其实现的队列操作将是线程安全的和原子的。但是这种保证并没有扩展到从 `Collection-addAll`,`containsAll`,`retainAll` 和 `removeAll` 继承的批量操作 - 单个实现提供它。因此,例如,在添加集合中的一些元素之后,`addAll` 可能会失败并抛出异常。 +`BlockingQueue` 保证其实现的队列操作将是线程安全的和原子的。但是这种保证并没有扩展到从 `Collection-addAll`,`containsAll`,`retainAll` 和 +`removeAll` 继承的批量操作 - 单个实现提供它。因此,例如,在添加集合中的一些元素之后,`addAll` 可能会失败并抛出异常。 ### 使用BlockingQueue的方法 -一次只为一个人工作的待办事项经理非常有限;我们真的需要一个合作解决方案 - 这将允许我们共享任务的生产和处理。例 `14-1` 显示了 `StoppableTaskQueue`,它是一个基于 `PriorityBlockingQueue` 的并发任务管理器的简单版本,它允许其用户 - 我们 - 在我们发现需要它们的时候,将任务单独添加到任务队列中,并将它们取出来处理因为我们发现时间。`StoppableTaskQueue` 类有三个方法:`addTask`,`getTask` 和 `shutDown`。`StoppableTaskQueue` 正在工作或停止。方法 `addTask` 返回一个布尔值,指示它是否成功添加了一个任务;除非停止 `StoppableTaskQueue`,否则此值将为真。方法 `getTask` 从队列中返回头部任务。如果没有任何可用的任务,它不会阻塞但返回空值。方法 `shutDown` 停止 `StoppableTaskQueue`,等待所有挂起的 `addTask` 操作完成,然后排出 `StoppableTaskQueue` 并返回其内容。 +一次只为一个人工作的待办事项经理非常有限;我们真的需要一个合作解决方案 - 这将允许我们共享任务的生产和处理。例 `14-1` 显示了 `StoppableTaskQueue`, +它是一个基于 `PriorityBlockingQueue` 的并发任务管理器的简单版本,它允许其用户 - 我们 - 在我们发现需要它们的时候,将任务单独添加到任务队列中,并将 +它们取出来处理因为我们发现时间。`StoppableTaskQueue` 类有三个方法:`addTask`,`getTask` 和 `shutDown`。`StoppableTaskQueue` 正在工作或停止。方 +法 `addTask` 返回一个布尔值,指示它是否成功添加了一个任务;除非停止 `StoppableTaskQueue`,否则此值将为真。方法 `getTask` 从队列中返回头部任务。如 +果没有任何可用的任务,它不会阻塞但返回空值。方法 `shutDown` 停止 `StoppableTaskQueue`,等待所有挂起的 `addTask` 操作完成,然后排出 +`StoppableTaskQueue` 并返回其内容。 例 `14-1`。一个并发的基于队列的任务管理器 ```java - public class StoppableTaskQueue { - private final int MAXIMUM_PENDING_OFFERS = Integer.MAX_VALUE; - private final BlockingQueue taskQueue = new PriorityBlockingQueue(); - private boolean isStopped = false; - private Semaphore semaphore = new Semaphore(MAXIMUM_PENDING_OFFERS); - // 如果任务成功放入队列,则返回true;如果队列已关闭,则返回false。 - public boolean addTask(PriorityTask task) { - synchronized (this) { - if (isStopped) return false; - if (! semaphore.tryAcquire()) throw new Error("too many threads"); - } - try { - return taskQueue.offer(task); - } finally { - semaphore.release(); - } - } - // 返回队列中的头部任务;如果没有任何任务可用,则返回null - public PriorityTask getTask() { - return taskQueue.poll(); - } - // 停止队列,等待制作者完成,然后返回内容 - public Collection shutDown() { - synchronized(this) { isStopped = true; } - semaphore.acquireUninterruptibly(MAXIMUM_PENDING_OFFERS); - Set returnCollection = new HashSet(); - taskQueue.drainTo(returnCollection); - return returnCollection; - } - } +public class StoppableTaskQueue { + private final int MAXIMUM_PENDING_OFFERS = Integer.MAX_VALUE; + private final BlockingQueue taskQueue = new PriorityBlockingQueue(); + private boolean isStopped = false; + private Semaphore semaphore = new Semaphore(MAXIMUM_PENDING_OFFERS); + // 如果任务成功放入队列,则返回true;如果队列已关闭,则返回false。 + public boolean addTask(PriorityTask task) { + synchronized (this) { + if (isStopped) + return false; + if (! semaphore.tryAcquire()) + throw new Error("too many threads"); + } + try { + return taskQueue.offer(task); + } finally { + semaphore.release(); + } + } + // 返回队列中的头部任务;如果没有任何任务可用,则返回null + public PriorityTask getTask() { + return taskQueue.poll(); + } + // 停止队列,等待制作者完成,然后返回内容 + public Collection shutDown() { + synchronized(this) { + isStopped = true; + } + semaphore.acquireUninterruptibly(MAXIMUM_PENDING_OFFERS); + Set returnCollection = new HashSet(); + taskQueue.drainTo(returnCollection); + return returnCollection; + } +} ``` -在这个例子中,与 `java.util.concurrent` 集合的大多数用法一样,集合本身负责处理在添加或删除队列中的项目时不同线程的交互所产生的问题。实例14-1的大部分代码都是解决了提供有序关闭机制的问题。这种强调的原因是,当我们继续使用 `StoppableTaskQueue` 类作为更大系统中的组件时,我们需要能够停止日常任务队列而不会丢失任务信息。在并发系统中实现正常关闭常常是一个问题:有关更多详细信息,请参阅 `Brian Goetz et.al` 的“Java Concurrency in Practice”第 `7` 章。(`Addison-Wesley`出版社)。 +在这个例子中,与 `java.util.concurrent` 集合的大多数用法一样,集合本身负责处理在添加或删除队列中的项目时不同线程的交互所产生的问题。实例14-1的大部 +分代码都是解决了提供有序关闭机制的问题。这种强调的原因是,当我们继续使用 `StoppableTaskQueue` 类作为更大系统中的组件时,我们需要能够停止日常任务队 +列而不会丢失任务信息。在并发系统中实现正常关闭常常是一个问题:有关更多详细信息,请参阅 `Brian Goetz et.al` 的“Java Concurrency in Practice”第 +`7` 章。(`Addison-Wesley`出版社)。 -较大的系统将在下一年对每一天的计划任务进行建模,使消费者可以从每天的队列中处理任务。本节示例的一个隐含假设是,如果今天没有计划剩余任务,则消费者不会等待其中一个可用,但会立即继续在第二天的队列中查找任务。 (在现实世界中,我们现在会回家,或者更有可能出去庆祝。)这个假设简化了这个例子,因为我们不需要调用 `PriorityBlockingQueue` 的任何阻塞方法,尽管我们将使用一个方法,`drainTo`,来自 `BlockingQueue` 接口。 +较大的系统将在下一年对每一天的计划任务进行建模,使消费者可以从每天的队列中处理任务。本节示例的一个隐含假设是,如果今天没有计划剩余任务,则消费者不会 +等待其中一个可用,但会立即继续在第二天的队列中查找任务。 (在现实世界中,我们现在会回家,或者更有可能出去庆祝。)这个假设简化了这个例子,因为我们不需 +要调用 `PriorityBlockingQueue` 的任何阻塞方法,尽管我们将使用一个方法,`drainTo`,来自 `BlockingQueue` 接口。 -有很多方法可以关闭像这样的生产者 - 消费者队列;在我们为这个例子选择的一个队列中,管理者公开了一个可以由“主管”线程调用的关闭方法,以阻止生产者写入队列,排空它,并返回结果。`shutdown`方法设置布尔停止,在尝试将任务放入队列之前,哪些任务生成线程将读取它们。`Taskconsuming` 线程只是轮询队列,如果没有可用的任务,则返回 `null`。这个简单的想法的问题是,一个生产者线程可能会读取停止标志,发现它是错误的,但在将其值放入队列之前暂停一段时间。我们必须通过确保关闭方法来防止这种情况,停止队列,将等到所有未完成的值已被插入后再排空。 +有很多方法可以关闭像这样的生产者 - 消费者队列;在我们为这个例子选择的一个队列中,管理者公开了一个可以由“主管”线程调用的关闭方法,以阻止生产者写入队 +列,排空它,并返回结果。`shutdown`方法设置布尔停止,在尝试将任务放入队列之前,哪些任务生成线程将读取它们。`Taskconsuming` 线程只是轮询队列,如果没 +有可用的任务,则返回 `null`。这个简单的想法的问题是,一个生产者线程可能会读取停止标志,发现它是错误的,但在将其值放入队列之前暂停一段时间。我们必须通 +过确保关闭方法来防止这种情况,停止队列,将等到所有未完成的值已被插入后再排空。 -例 `14-1` 使用一个信号量来实现这个功能 - 一个线程安全的对象,它保持固定数量的许可证。例如,信号量通常用于调节对有限资源集的访问 - 例如数据库连接池。信号量随时可用的许可证代表目前未使用的资源。需要资源的线程从信号量获取许可证,并在释放资源时将其释放。如果所有资源都在使用,信号量将没有可用的许可证;此时,试图获得许可证的线程将阻塞,直到其他线程返回一个许可证。 +例 `14-1` 使用一个信号量来实现这个功能 - 一个线程安全的对象,它保持固定数量的许可证。例如,信号量通常用于调节对有限资源集的访问 - 例如数据库连接池。 +信号量随时可用的许可证代表目前未使用的资源。需要资源的线程从信号量获取许可证,并在释放资源时将其释放。如果所有资源都在使用,信号量将没有可用的许可证; +此时,试图获得许可证的线程将阻塞,直到其他线程返回一个许可证。 -这个例子中的信号量的用法是不同的。我们不想限制生产者线程写入队列 - 它是一个无限制的并发队列,毕竟,在没有我们帮助的情况下,它能够处理并发访问。我们只想保留当前正在进行的写入的计数。所以我们创建了信号量最大的许可证,这在实践中永远都不会被要求。生产者方法 `addTask` 检查队列是否停止 - 在这种情况下,它的合约说它应该返回 `false` - 如果没有,它使用信号量方法 `tryAcquire` 获得一个许可,它不会阻塞(不像更常用的如果没有许可证可用,`tryAcquire` 立即返回 `false`。这个测试然后行为序列是原子的,以确保程序在其他线程可见的任何点保持其不变:未写入值的数量不超过可用许可的数量。 +这个例子中的信号量的用法是不同的。我们不想限制生产者线程写入队列 - 它是一个无限制的并发队列,毕竟,在没有我们帮助的情况下,它能够处理并发访问。我们只 +想保留当前正在进行的写入的计数。所以我们创建了信号量最大的许可证,这在实践中永远都不会被要求。生产者方法 `addTask` 检查队列是否停止 - 在这种情况下, +它的合约说它应该返回 `false` - 如果没有,它使用信号量方法 `tryAcquire` 获得一个许可,它不会阻塞(不像更常用的如果没有许可证可用,`tryAcquire` 立即 +返回 `false`。这个测试然后行为序列是原子的,以确保程序在其他线程可见的任何点保持其不变:未写入值的数量不超过可用许可的数量。 -`shutdown` 方法将停止标志设置为同步块(通常的方式是确保由一个线程执行的变量写入对另一个线程的读取是可见的,这是因为写入和读取发生在同一个锁中同步的块内)。 现在,`addTask` 方法不能获得更多的许可证,并且关闭只需要等到所有先前获取的许可证都已返回。 要做到这一点,它要求获得,指明它需要所有的许可证; 该调用将被阻塞,直到它们全部由制作者线程释放。 此时,不变保证没有任何任务仍然写入队列,并且关闭可以完成。 +`shutdown` 方法将停止标志设置为同步块(通常的方式是确保由一个线程执行的变量写入对另一个线程的读取是可见的,这是因为写入和读取发生在同一个锁中同步的 +块内)。 现在,`addTask` 方法不能获得更多的许可证,并且关闭只需要等到所有先前获取的许可证都已返回。 要做到这一点,它要求获得,指明它需要所有的许可 +证; 该调用将被阻塞,直到它们全部由制作者线程释放。 此时,不变保证没有任何任务仍然写入队列,并且关闭可以完成。 ### 实现BlockingQueue @@ -110,65 +142,82 @@ #### ArrayBlockingQueue -该实现基于圆形阵列 - 一种线性结构,其中第一个元素和最后一个元素在逻辑上相邻。图 `14-6`(`a`)显示了这个想法。标有“head”的位置表示队列的头部;每次头元素被从队列中移除时,头索引被提前。同样,每个新元素都添加在尾部位置,导致该索引被提前。当任何一个索引需要超过数组的最后一个元素时,它将得到值 `0`.如果两个索引具有相同的值,则队列可以是满或空的,因此实现必须分别跟踪元素的数量在队列中。 +该实现基于圆形阵列 - 一种线性结构,其中第一个元素和最后一个元素在逻辑上相邻。图 `14-6`(`a`)显示了这个想法。标有“head”的位置表示队列的头部;每次头元 +素被从队列中移除时,头索引被提前。同样,每个新元素都添加在尾部位置,导致该索引被提前。当任何一个索引需要超过数组的最后一个元素时,它将得到值 `0`.如果 +两个索引具有相同的值,则队列可以是满或空的,因此实现必须分别跟踪元素的数量在队列中。 -头部和尾部可以以这种方式连续前进的圆形阵列比非圆形的更适合作为队列实现(例如,`ArrayList` 的标准实现,我们将在第 `15.2` 节中介绍),其中删除头元素需要改变所有剩余元素的位置,以便新头位于位置 `0`.注意,只有队列末尾的元素可以在固定时间内插入和移除。如果要从中间移除一个元素,这可以通过 `Iterator.remove` 方法为队列完成,那么必须从一端移动所有元素以保持紧凑的表示形式。图 `14-6`(`b`)显示了索引 `6` 处的元素被从队列中移除。结果,插入和移除队列中间的元素的时间复杂度为 `O(n)`。 +头部和尾部可以以这种方式连续前进的圆形阵列比非圆形的更适合作为队列实现(例如,`ArrayList` 的标准实现,我们将在第 `15.2` 节中介绍),其中删除头元素需 +要改变所有剩余元素的位置,以便新头位于位置 `0`.注意,只有队列末尾的元素可以在固定时间内插入和移除。如果要从中间移除一个元素,这可以通过 +`Iterator.remove` 方法为队列完成,那么必须从一端移动所有元素以保持紧凑的表示形式。图 `14-6`(`b`)显示了索引 `6` 处的元素被从队列中移除。结果,插 +入和移除队列中间的元素的时间复杂度为 `O(n)`。 ![](14_6.png) 图 `14-6`。 圆形阵列 -数组支持的集合类的构造函数通常具有单个配置参数,即数组的初始长度。对于像 `ArrayBlockingQueue` 这样的固定大小的类,此参数对于定义集合的容量是必需的。(对于像 `ArrayList` 这样的可变大小的类,可以使用默认的初始容量,因此提供了不需要容量的构造函数。)对于 `ArrayBlockingQueue`,三个构造函数是: +数组支持的集合类的构造函数通常具有单个配置参数,即数组的初始长度。对于像 `ArrayBlockingQueue` 这样的固定大小的类,此参数对于定义集合的容量是必需 +的。(对于像 `ArrayList` 这样的可变大小的类,可以使用默认的初始容量,因此提供了不需要容量的构造函数。)对于 `ArrayBlockingQueue`,三个构造函数是: ```java - ArrayBlockingQueue(int capacity) - ArrayBlockingQueue(int capacity, boolean fair) - ArrayBlockingQueue(int capacity, boolean fair, Collection c) +ArrayBlockingQueue(int capacity) +ArrayBlockingQueue(int capacity, boolean fair) +ArrayBlockingQueue(int capacity, boolean fair, Collection c) ``` -这些参数的最后一个允许 `ArrayBlockingQueue` 初始化为指定集合的​​内容,并按集合迭代器的遍历顺序添加。对于此构造函数,指定的容量必须至少与提供的集合的容量一样大,或者如果提供的集合为空,则至少为 `1`.除了配置 `backing` 数组之外,最后两个构造函数还需要一个布尔参数来控制该队列将处理多个被阻止的请求。当多个线程尝试从空队列中移除项目或将项目排入完整队列时,会发生这些情况。当队列变得能够处理这些请求中的一个时,它应该选择哪一个?替代方法是要求保证队列将选择等待时间最长的队列 - 即实现公平的调度策略 - 或者允许实现选择一个。公平调度听起来是更好的选择,因为它避免了不幸的线程可能无限延迟的可能性,但实际上,它提供的好处很少足够重要,足以证明它会给队列操作带来很大的开销。如果公平没有指定调度,`ArrayBlockingQueue` 通常近似公平操作,但没有保证。 +这些参数的最后一个允许 `ArrayBlockingQueue` 初始化为指定集合的内容,并按集合迭代器的遍历顺序添加。对于此构造函数,指定的容量必须至少与提供的集合的 +容量一样大,或者如果提供的集合为空,则至少为 `1`.除了配置 `backing` 数组之外,最后两个构造函数还需要一个布尔参数来控制该队列将处理多个被阻止的请求。 +当多个线程尝试从空队列中移除项目或将项目排入完整队列时,会发生这些情况。当队列变得能够处理这些请求中的一个时,它应该选择哪一个?替代方法是要求保证队 +列将选择等待时间最长的队列 - 即实现公平的调度策略 - 或者允许实现选择一个。公平调度听起来是更好的选择,因为它避免了不幸的线程可能无限延迟的可能性,但 +实际上,它提供的好处很少足够重要,足以证明它会给队列操作带来很大的开销。如果公平没有指定调度,`ArrayBlockingQueue` 通常近似公平操作,但没有保证。 `ArrayBlockingQueue` 施加的顺序是 `FIFO`。队列插入和移除是在一定的时间内执行的;诸如需要遍历阵列的包含的操作需要线性时间。迭代器是弱一致的。 #### PriorityBlockingQueue -这个实现是一个线程安全的 `PriorityQueue` 阻塞版本(参见 `14.2` 节),具有类似的排序和性能特征。 它的迭代器快速失败,所以在正常使用中它们会抛出 `ConcurrentModificationException`; 只有队列静止才能成功。要在 `PriorityBlockingQueue` 上安全地进行迭代,请将元素传递给数组,然后对其进行迭代。 +这个实现是一个线程安全的 `PriorityQueue` 阻塞版本(参见 `14.2` 节),具有类似的排序和性能特征。 它的迭代器快速失败,所以在正常使用中它们会抛出 +`ConcurrentModificationException`; 只有队列静止才能成功。要在 `PriorityBlockingQueue` 上安全地进行迭代,请将元素传递给数组,然后对其进行迭代。 #### DelayQueue -这是一个专门的优先级队列,其中排序基于每个元素的延迟时间 - 元素准备从队列中取出之前的剩余时间。 如果所有元素都有一个正的延迟时间 - 也就是说,没有任何关联的延迟时间已过期,那么轮询队列的尝试将返回 `null`(尽管 `peek` 仍然允许您查看第一个未到期的元素)。 如果一个或多个元素的延迟时间已过期,则延迟时间最长的元素将位于队列头部。`DelayQueue` 的元素属于实现 `java.util.concurrent.Delayed` 的类: +这是一个专门的优先级队列,其中排序基于每个元素的延迟时间 - 元素准备从队列中取出之前的剩余时间。 如果所有元素都有一个正的延迟时间 - 也就是说,没有任何 +关联的延迟时间已过期,那么轮询队列的尝试将返回 `null`(尽管 `peek` 仍然允许您查看第一个未到期的元素)。 如果一个或多个元素的延迟时间已过期,则延迟时 +间最长的元素将位于队列头部。`DelayQueue` 的元素属于实现 `java.util.concurrent.Delayed` 的类: ```java - interface Delayed extends Comparable { - long getDelay(TimeUnit unit); - } +interface Delayed extends Comparable { + long getDelay(TimeUnit unit); +} ``` -`Delayed` 对象的 `getDelay` 方法返回与该对象关联的其余延迟。 必须定义 `Comparable` 的 `compareTo` 方法(请参阅第 `3.1` 节),以提供与所比较对象的延迟一致的结果。 这意味着它很少与 `equals` 相兼容,所以 `Delayed` 对象不适合用于 `SortedSet` 和 `SortedMap` 的实现。 +`Delayed` 对象的 `getDelay` 方法返回与该对象关联的其余延迟。 必须定义 `Comparable` 的 `compareTo` 方法(请参阅第 `3.1` 节),以提供与所比较对象 +的延迟一致的结果。 这意味着它很少与 `equals` 相兼容,所以 `Delayed` 对象不适合用于 `SortedSet` 和 `SortedMap` 的实现。 -例如,在我们的待办事项管理人员中,我们可能需要提醒任务,以确保我们跟进尚未回复的电子邮件和电话信息。 我们可以像例 `14-2` 那样定义一个新类 `DelayedTask`,并用它来实现提醒队列。 +例如,在我们的待办事项管理人员中,我们可能需要提醒任务,以确保我们跟进尚未回复的电子邮件和电话信息。 我们可以像例 `14-2` 那样定义一个新类 +`DelayedTask`,并用它来实现提醒队列。 ```java - BlockingQueue reminderQueue = new DelayQueue(); - reminderQueue.offer(new DelayedTask (databaseCode, 1)); - reminderQueue.offer(new DelayedTask (interfaceCode, 2)); - ... - // 现在获取准备好处理的第一个提醒任务 - DelayedTask t1 = reminderQueue.poll(); - if (t1 == null) { - // 没有提醒准备好了 - } else { - // process t1 - } +BlockingQueue reminderQueue = new DelayQueue(); +reminderQueue.offer(new DelayedTask (databaseCode, 1)); +reminderQueue.offer(new DelayedTask (interfaceCode, 2)); +... +// 现在获取准备好处理的第一个提醒任务 +DelayedTask t1 = reminderQueue.poll(); +if (t1 == null) { +// 没有提醒准备好了 +} else { +// process t1 +} ``` -大多数队列操作都遵循延迟值,并且会将没有未过期元素的队列看作是空的。 例外是偷看和删除,这可能令人惊讶的是,将允许您检查 `DelayQueue` 的头元素是否已过期。 像他们一样,与 `Queue` 的其他方法不同,`DelayQueue` 上的收集操作不考虑延迟值。 例如,以下是将 `reminderQueue` 的元素复制到集合中的两种方法: +大多数队列操作都遵循延迟值,并且会将没有未过期元素的队列看作是空的。 例外是偷看和删除,这可能令人惊讶的是,将允许您检查 `DelayQueue` 的头元素是否已 +过期。 像他们一样,与 `Queue` 的其他方法不同,`DelayQueue` 上的收集操作不考虑延迟值。 例如,以下是将 `reminderQueue` 的元素复制到集合中的两种方 +法: ```java - Set delayedTaskSet1 = new HashSet(); - delayedTaskSet1.addAll(reminderQueue); - Set delayedTaskSet2 = new HashSet(); - reminderQueue.drainTo(delayedTaskSet2); +Set delayedTaskSet1 = new HashSet(); +delayedTaskSet1.addAll(reminderQueue); +Set delayedTaskSet2 = new HashSet(); +reminderQueue.drainTo(delayedTaskSet2); ``` 集合 `delayedTaskSet1` 将包含队列中的所有提醒,而集合 `delayedTaskSet2` 将只包含准备使用的提醒。 @@ -177,40 +226,47 @@ #### SynchronousQueue -乍看之下,您可能会认为没有内部容量的队列没有多大意义,这是 `SynchronousQueue` 的简短描述。 但是,实际上,它可能非常有用;想要向 `SynchronousQueue` 添加元素的线程必须等到另一个线程准备好同时将其取出,并且相反,对于想要 从队列中取出一个元素。 因此,`SynchronousQueue` 具有名称所暗示的功能,即集合点 - 一种用于同步两个线程的机制。 (不要混淆以这种方式同步线程的概念 - 允许它们通过交换数据来协作 - 与 `Java` 的关键字 `synchronized` 同步,这会阻止不同线程同时执行代码。)`SynchronousQueue` 有两个构造函数: +乍看之下,您可能会认为没有内部容量的队列没有多大意义,这是 `SynchronousQueue` 的简短描述。 但是,实际上,它可能非常有用;想要向 `SynchronousQueue` +添加元素的线程必须等到另一个线程准备好同时将其取出,并且相反,对于想要 从队列中取出一个元素。 因此,`SynchronousQueue` 具有名称所暗示的功能,即集合 +点 - 一种用于同步两个线程的机制。 (不要混淆以这种方式同步线程的概念 - 允许它们通过交换数据来协作 - 与 `Java` 的关键字 `synchronized` 同步,这会阻 +止不同线程同时执行代码。)`SynchronousQueue` 有两个构造函数: ```java - SynchronousQueue() - SynchronousQueue(boolean fair) +SynchronousQueue() +SynchronousQueue(boolean fair) ``` 例 `14-2`。 类 `DelayedTask` ```java - public class DelayedTask implements Delayed { - public final static long MILLISECONDS_IN_DAY = 60 * 60 * 24 * 1000; - private long endTime; // in milliseconds after January 1, 1970 - private Task task; - DelayedTask(Task t, int daysDelay) { - endTime = System.currentTimeMillis() + daysDelay * MILLISECONDS_IN_DAY; - task = t; - } - public long getDelay(TimeUnit unit) { - long remainingTime = endTime - System.currentTimeMillis(); - return unit.convert(remainingTime, TimeUnit.MILLISECONDS); - } - public int compareTo(Delayed t) { - long thisDelay = getDelay(TimeUnit.MILLISECONDS); - long otherDelay = t.getDelay(TimeUnit.MILLISECONDS); - return (thisDelay < otherDelay) ? -1 : (thisDelay > otherDelay) ? 1 : 0; - } - Task getTask() { return task; } - } +public class DelayedTask implements Delayed { + public final static long MILLISECONDS_IN_DAY = 60 * 60 * 24 * 1000; + private long endTime; // in milliseconds after January 1, 1970 + private Task task; + DelayedTask(Task t, int daysDelay) { + endTime = System.currentTimeMillis() + daysDelay * MILLISECONDS_IN_DAY; + task = t; + } + public long getDelay(TimeUnit unit) { + long remainingTime = endTime - System.currentTimeMillis(); + return unit.convert(remainingTime, TimeUnit.MILLISECONDS); + } + public int compareTo(Delayed t) { + long thisDelay = getDelay(TimeUnit.MILLISECONDS); + long otherDelay = t.getDelay(TimeUnit.MILLISECONDS); + return (thisDelay < otherDelay) ? -1 : (thisDelay > otherDelay) ? 1 : 0; + } + Task getTask() { + return task; + } +} ``` -`SynchronousQueue` 的一个常见应用是工作共享系统,其中设计确保有足够的消费者线程来确保生产者线程无需等待即可完成任务。 在这种情况下,它允许在线程之间安全传输任务数据,而不会导致入队的 `BlockingQueue` 开销,然后出队,传输每个任务。 +`SynchronousQueue` 的一个常见应用是工作共享系统,其中设计确保有足够的消费者线程来确保生产者线程无需等待即可完成任务。 在这种情况下,它允许在线程之间 +安全传输任务数据,而不会导致入队的 `BlockingQueue` 开销,然后出队,传输每个任务。 -就 `Collection` 方法而言,`SynchronousQueue` 表现得像一个空的 `Collection`; `Queue` 和 `BlockingQueue` 方法的行为与您对容量为零的队列的期望值相同,因此它始终为空。 迭代器方法返回一个空的迭代器,其中 `hasNext` 总是返回 `false`。 +就 `Collection` 方法而言,`SynchronousQueue` 表现得像一个空的 `Collection`; `Queue` 和 `BlockingQueue` 方法的行为与您对容量为零的队列的期望值 +相同,因此它始终为空。 迭代器方法返回一个空的迭代器,其中 `hasNext` 总是返回 `false`。 《《《 [下一节](04_Deque.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch14/04_Deque.md b/ch14/04_Deque.md index 7d902e64..917f3499 100644 --- a/ch14/04_Deque.md +++ b/ch14/04_Deque.md @@ -3,11 +3,17 @@ ### Deque -`deque`(发音为“deck”)是一个双端队列。与只能在尾部插入元素并仅在头部进行检查或删除的队列不同,`deque` 可以接受用于插入的元素并将其呈现在任一端进行检查或删除。与 `Queue` 不同的是,`Deque` 的合约指定了它在呈现元素时使用的顺序:它是一种线性结构,其中在尾部添加的元素在头部以相同的顺序排列。用作队列,那么 `Deque` 总是一个 `FIFO` 结构;合同不允许优先考虑。如果从添加元素的同一端(头部或尾部)删除元素,则 `Deque` 将用作堆栈或 `LIFO`(后进先出)结构。 +`deque`(发音为“deck”)是一个双端队列。与只能在尾部插入元素并仅在头部进行检查或删除的队列不同,`deque` 可以接受用于插入的元素并将其呈现在任一端进行 +检查或删除。与 `Queue` 不同的是,`Deque` 的合约指定了它在呈现元素时使用的顺序:它是一种线性结构,其中在尾部添加的元素在头部以相同的顺序排列。用作队 +列,那么 `Deque` 总是一个 `FIFO` 结构;合同不允许优先考虑。如果从添加元素的同一端(头部或尾部)删除元素,则 `Deque` 将用作堆栈或 `LIFO`(后进先出) +结构。 -`Deque` 及其子接口 `BlockingDeque` 在 `Java 6` 中引入。快速 `Deque` 实现 `ArrayDeque` 使用循环数组(请参见第 `14.3.2` 节),现在是堆栈和队列的选择实现。并发 `deques` 在并行化中扮演着特殊的角色,在第 `14.4.2` 节讨论。 +`Deque` 及其子接口 `BlockingDeque` 在 `Java 6` 中引入。快速 `Deque` 实现 `ArrayDeque` 使用循环数组(请参见第 `14.3.2` 节),现在是堆栈和队列的 +选择实现。并发 `deques` 在并行化中扮演着特殊的角色,在第 `14.4.2` 节讨论。 -`Deque` 接口(参见图 `14-7` )使用关于头部和尾部对称的方法扩展队列。为了明确命名,隐式引用队列一端的 `Queue` 方法获取一个同义词,使其行为对 `Deque` 是显式的。例如,从 `Queue` 继承的方法 `peek` 和 `offer`,等同于 `peekFirst` 和 `offerLast`。(第一个和最后一个是指 `deque` 的头部和尾部; `Deque` 的 `JavaDoc` 也使用“front”和“end”。) +`Deque` 接口(参见图 `14-7` )使用关于头部和尾部对称的方法扩展队列。为了明确命名,隐式引用队列一端的 `Queue` 方法获取一个同义词,使其行为对 +`Deque` 是显式的。例如,从 `Queue` 继承的方法 `peek` 和 `offer`,等同于 `peekFirst` 和 `offerLast`。(第一个和最后一个是指 `deque` 的头部和尾 +部; `Deque` 的 `JavaDoc` 也使用“front”和“end”。) ![](14_7.png) @@ -17,23 +23,26 @@ ### 集合方法 ```java - void addFirst(E e) // 如果有足够的空间,将e插入头部 - void addLast(E e) // 如果有足够的空间,请在尾部插入e - void push(E e) // 如果有足够的空间,将e插入头部 - boolean removeFirstOccurrence(Object o); // 删除第一个出现的o - boolean removeLastOccurrence(Object o); // 删除最后一次出现的o - Iterator descendingIterator() // 得到一个迭代器,以相反的顺序返回deque元素 +void addFirst(E e) // 如果有足够的空间,将e插入头部 +void addLast(E e) // 如果有足够的空间,请在尾部插入e +void push(E e) // 如果有足够的空间,将e插入头部 +boolean removeFirstOccurrence(Object o); // 删除第一个出现的o +boolean removeLastOccurrence(Object o); // 删除最后一次出现的o +Iterator descendingIterator() // 得到一个迭代器,以相反的顺序返回deque元素 ``` -`addFirst` 和 `addLast` 方法的约定与 `Collection` 的 `add` 方法的约定类似,但另外指定要添加的元素的位置,如果不能添加,则引发的异常为 `IllegalStateException`。与有界队列一样,有界 `deques` 的用户应该避免使用这些方法来支持 `offerFirst` 和 `offerLast`,它可以通过返回的布尔值报告“正常”失败。 +`addFirst` 和 `addLast` 方法的约定与 `Collection` 的 `add` 方法的约定类似,但另外指定要添加的元素的位置,如果不能添加,则引发的异常为 +`IllegalStateException`。与有界队列一样,有界 `deques` 的用户应该避免使用这些方法来支持 `offerFirst` 和 `offerLast`,它可以通过返回的布尔值报 +告“正常”失败。 -方法名称 `push` 是 `addFirst` 的同义词,用于将 `Deque` 用作堆栈。 `removeFirstOccurrence` 和 `removeLastOccurrence` 是类似于 `Collection.remove` 的类似方法,但还要指定哪些元素应该被删除。 返回值表示元素是否因调用而被删除。 +方法名称 `push` 是 `addFirst` 的同义词,用于将 `Deque` 用作堆栈。 `removeFirstOccurrence` 和 `removeLastOccurrence` 是类似于 +`Collection.remove` 的类似方法,但还要指定哪些元素应该被删除。 返回值表示元素是否因调用而被删除。 ### 队列方法 ```java - boolean offerFirst(E e) // 如果这个deque有空间,就在头部插入e - boolean offerLast(E e) // 如果deque有空间,请在尾部插入e + boolean offerFirst(E e) // 如果这个deque有空间,就在头部插入e + boolean offerLast(E e) // 如果deque有空间,请在尾部插入e ``` 方法 `offerLast` 是 `Queue` 接口上等效方法报价的重命名。 @@ -41,10 +50,10 @@ 对于空双端队列返回 `null` 的方法是: ```java - E peekFirst() // 检索但不要删除第一个元素 - E peekLast() // 检索但不删除最后一个元素 - E pollFirst() // 检索并删除第一个元素 - E pollLast() // 检索并删除最后一个元素 + E peekFirst() // 检索但不要删除第一个元素 + E peekLast() // 检索但不删除最后一个元素 + E pollFirst() // 检索并删除第一个元素 + E pollLast() // 检索并删除最后一个元素 ``` `peekFirst` 和 `pollFirst` 方法是队列接口上等价方法 `peek` 和 `poll` 的重命名。 @@ -52,20 +61,23 @@ 为空双端队列引发异常的方法是: ```java - E getFirst() // 检索但不要删除第一个元素 - E getLast() // 检索但不删除最后一个元素 - E removeFirst() // 检索并删除第一个元素 - E removeLast() // 检索并删除最后一个元素 - E pop() // 检索并删除第一个元素 + E getFirst() // 检索但不要删除第一个元素 + E getLast() // 检索但不删除最后一个元素 + E removeFirst() // 检索并删除第一个元素 + E removeLast() // 检索并删除最后一个元素 + E pop() // 检索并删除第一个元素 ``` 方法 `getFirst` 和 `removeFirst` 是等效方法元素的重命名并在队列接口上删除。 方法名pop是一个同义词,用于删除首先,再次提供了栈的使用。 -#### 实现Deque +#### 实现 Deque #### ArrayDeque -除了接口 `Deque` 之外,`Java 6` 还引入了一个非常高效的实现 `ArrayDeque`,它基于像 `ArrayBlockingQueue` 这样的循环数组(参见 `14.3.2` 节)。 它填补了 `Queue` 类之间的空白。 以前,如果您希望在单线程环境中使用 `FIFO` 队列,您将不得不使用类 `LinkedList`(我们将在后面介绍,但应该避免将其作为通用 `Queue` 实现),或者支付 使用并发类 `ArrayBlockingQueue` 或 `LinkedBlockingQueue` 之一的线程安全不必要的开销。对于 `deques` 和 `FIFO` 队列,`ArrayDeque` 现在是选择的通用实现。 它具有圆形阵列的性能特征:在头部或尾部添加或移除元素需要一段时间。 迭代器快速失败。 +除了接口 `Deque` 之外,`Java 6` 还引入了一个非常高效的实现 `ArrayDeque`,它基于像 `ArrayBlockingQueue` 这样的循环数组(参见 `14.3.2` 节)。 它 +填补了 `Queue` 类之间的空白。 以前,如果您希望在单线程环境中使用 `FIFO` 队列,您将不得不使用类 `LinkedList`(我们将在后面介绍,但应该避免将其作为 +通用 `Queue` 实现),或者支付 使用并发类 `ArrayBlockingQueue` 或 `LinkedBlockingQueue` 之一的线程安全不必要的开销。对于 `deques` 和 `FIFO` 队 +列,`ArrayDeque` 现在是选择的通用实现。 它具有圆形阵列的性能特征:在头部或尾部添加或移除元素需要一段时间。 迭代器快速失败。 #### LinkedList @@ -73,31 +85,45 @@ 图 `14-8`. 一个双向链表 -在 `Deque` 实现中,`LinkedList` 是一个古怪的东西;例如,单独允许 `null` 元素,`Queue` 接口不鼓励使用空元素,因为 `null` 通常用作特殊值。它从一开始就一直处于集合框架中,最初是作为 `List` 的标准实现之一(参见第 `15.2` 节),并且用 `Queue for Java 5` 的方法和 `Deque for Java 6` 的方法进行了改进。它基于一个链表结构类似于我们在 `13.2.3` 节中看到的链表结构作为跳过列表的基础,但是在每个单元中有一个额外的字段,指向前一个条目(见图 `14-8` )。这些指针允许列表向后遍历 - 例如,用于反向迭代,或者从列表的末尾删除元素。 +在 `Deque` 实现中,`LinkedList` 是一个古怪的东西;例如,单独允许 `null` 元素,`Queue` 接口不鼓励使用空元素,因为 `null` 通常用作特殊值。它从一开始 +就一直处于集合框架中,最初是作为 `List` 的标准实现之一(参见第 `15.2` 节),并且用 `Queue for Java 5` 的方法和 `Deque for Java 6` 的方法进行了改 +进。它基于一个链表结构类似于我们在 `13.2.3` 节中看到的链表结构作为跳过列表的基础,但是在每个单元中有一个额外的字段,指向前一个条目(见图 `14-8` )。 +这些指针允许列表向后遍历 - 例如,用于反向迭代,或者从列表的末尾删除元素。 -作为 `Deque` 的一个实现,`LinkedList` 不太可能非常流行。它的主要优点,即恒定时间的插入和删除,在 `Java 6` 中可以与队列和 `deques` 相媲美 - 由其他更高级的 `ArrayDeque` 所支持。以前,如果线程安全不是问题,并且不需要阻止行为,那么您就可以使用它。现在,将 `LinkedList` 用作队列或deque实现的唯一可能原因是您还需要随机访问要素。有了 `LinkedList`,即使这样做价格也很高;由于随机访问必须通过线性搜索来实现,因此其时间复杂度为 `O(n)`。 +作为 `Deque` 的一个实现,`LinkedList` 不太可能非常流行。它的主要优点,即恒定时间的插入和删除,在 `Java 6` 中可以与队列和 `deques` 相媲美 - 由其他 +更高级的 `ArrayDeque` 所支持。以前,如果线程安全不是问题,并且不需要阻止行为,那么您就可以使用它。现在,将 `LinkedList` 用作队列或 `deque` 实现的 +唯一可能原因是您还需要随机访问要素。有了 `LinkedList`,即使这样做价格也很高;由于随机访问必须通过线性搜索来实现,因此其时间复杂度为 `O(n)`。 `LinkedList` 的构造函数只是第 `12.3` 节中的标准函数。它的迭代器快速失败。 ### BlockingDeque -图 `14-9` 显示了 `BlockingDeque` 添加到 `BlockingQueue` 的方法(请参见图 `14-5`)。`BlockingQueue` 的两个阻塞插入方法和两个删除方法都有一个同义词,用于明确它所修饰的 `deque` 的哪一端,以及匹配方法以在另一端提供相同的动作。 因此,报价,例如,获取同义词 `offerLast` 和匹配方法报价首先。 因此,为 `BlockingQueue` 定义的相同的四个基本行为 - 在失败时返回一个特殊值,在超时后返回失败时的特殊值,在失败时抛出异常并阻塞直到成功 - 可以应用于元素插入或 在 `deque` 的任一端去除。 +图 `14-9` 显示了 `BlockingDeque` 添加到 `BlockingQueue` 的方法(请参见图 `14-5`)。`BlockingQueue` 的两个阻塞插入方法和两个删除方法都有一个同义 +词,用于明确它所修饰的 `deque` 的哪一端,以及匹配方法以在另一端提供相同的动作。 因此,报价,例如,获取同义词 `offerLast` 和匹配方法报价首先。 因 +此,为 `BlockingQueue` 定义的相同的四个基本行为 - 在失败时返回一个特殊值,在超时后返回失败时的特殊值,在失败时抛出异常并阻塞直到成功 - 可以应用于元 +素插入或 在 `deque` 的任一端去除。 ![](14_9.png) 图 `14-9`. BlockingDeque -随着多核和多处理器架构成为标准,良好的负载平衡算法将变得越来越重要。并发 `deques` 是最好的负载平衡方法之一,盗窃工作的基础。为了理解窃取工作,设想一种负载平衡算法,它以某种方式分配任务 - 循环 - 比如说 - 一系列队列,每个队列都有一个专用的消费者线程,可以从队列的头部重复执行任务,处理它,并返回另一个。尽管这种方案确实通过并行提供了加速,但它有一个主要缺点:我们可以想象两个相邻的队列,一个长期任务积压和一个消费者线程努力跟上它们,并且在它旁边有一个空闲空闲队列消费者等待工作。如果我们允许空闲线程从另一个队列的头部执行任务,它将明显提高吞吐量。工作窃取进一步提高了这个想法;观察到空闲线程从另一个队列的头部窃取工作会冒险争夺头元素,它会更改队列中的队列并指示空闲线程从另一个线程的双端队列的尾部执行任务。事实证明这是一种高效的机制,并且正在被广泛使用。 +随着多核和多处理器架构成为标准,良好的负载平衡算法将变得越来越重要。并发 `deques` 是最好的负载平衡方法之一,盗窃工作的基础。为了理解窃取工作,设想一 +种负载平衡算法,它以某种方式分配任务 - 循环 - 比如说 - 一系列队列,每个队列都有一个专用的消费者线程,可以从队列的头部重复执行任务,处理它,并返回另一 +个。尽管这种方案确实通过并行提供了加速,但它有一个主要缺点:我们可以想象两个相邻的队列,一个长期任务积压和一个消费者线程努力跟上它们,并且在它旁边有 +一个空闲空闲队列消费者等待工作。如果我们允许空闲线程从另一个队列的头部执行任务,它将明显提高吞吐量。工作窃取进一步提高了这个想法;观察到空闲线程从另一 +个队列的头部窃取工作会冒险争夺头元素,它会更改队列中的队列并指示空闲线程从另一个线程的双端队列的尾部执行任务。事实证明这是一种高效的机制,并且正在被 +广泛使用。 -#### 实现BlockingDeque +#### 实现 BlockingDeque -接口 `BlockingDeque` 具有单个实现 `LinkedBlockingDeque`。`LinkedBlockingDeque` 基于类似 `LinkedList` 的双链表结构。除了两个标准构造函数之外,它可以有选择地被限制,它提供了第三个可以用来指定其容量的第三个构造函数: +接口 `BlockingDeque` 具有单个实现 `LinkedBlockingDeque`。`LinkedBlockingDeque` 基于类似 `LinkedList` 的双链表结构。除了两个标准构造函数之外, +它可以有选择地被限制,它提供了第三个可以用来指定其容量的第三个构造函数: ```java - LinkedBlockingDeque(int capacity) +LinkedBlockingDeque(int capacity) ``` 它具有与 `LinkedBlockingQueue` 类似的性能特征 - 队列插入和删除需要不断的时间和操作,比如包含需要遍历队列的操作,需要线性时间。迭代器是弱一致的。 《《《 [下一节](05_Comparing_Queue_Implementations.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch14/05_Comparing_Queue_Implementations.md b/ch14/05_Comparing_Queue_Implementations.md index 08b0b8cd..5d2f2f3b 100644 --- a/ch14/05_Comparing_Queue_Implementations.md +++ b/ch14/05_Comparing_Queue_Implementations.md @@ -3,11 +3,14 @@ ### 比较队列实现 -表 `14-1` 显示了我们讨论的 `Deque` 和 `Queue` 实现的一些示例操作的顺序性能,不考虑锁定和 `CAS` 开销。这些结果对于理解您选择的实现的行为而言应该很有意思,但正如我们在本章开头提到的那样,它们不可能是决定性因素。 您的选择更可能取决于应用程序的功能和并发性要求。 +表 `14-1` 显示了我们讨论的 `Deque` 和 `Queue` 实现的一些示例操作的顺序性能,不考虑锁定和 `CAS` 开销。这些结果对于理解您选择的实现的行为而言应该很 +有意思,但正如我们在本章开头提到的那样,它们不可能是决定性因素。 您的选择更可能取决于应用程序的功能和并发性要求。 -在选择队列时,首先要问的问题是你选择的实现是否需要支持并发访问; 如果不是,你的选择很简单。 对于 `FIFO` 排序,请选择 `ArrayDeque`; 优先级排序,`PriorityQueue`。 +在选择队列时,首先要问的问题是你选择的实现是否需要支持并发访问; 如果不是,你的选择很简单。 对于 `FIFO` 排序,请选择 `ArrayDeque`; 优先级排序, +`PriorityQueue`。 -如果您的应用程序确实需要线程安全性,则您需要考虑订购。 如果您需要优先级或延迟排序,则显然必须分别选择 `PriorityBlockingQueue` 或 `DelayQueue`。另一方面,如果 `FIFO` 排序是可以接受的,则第三 +如果您的应用程序确实需要线程安全性,则您需要考虑订购。 如果您需要优先级或延迟排序,则显然必须分别选择 `PriorityBlockingQueue` 或 `DelayQueue`。另 +一方面,如果 `FIFO` 排序是可以接受的,则第三 表 `14-1`。不同 `Queue` 和 `Deque` 实现的比较性能 @@ -23,11 +26,18 @@ LinkedList |O(1) | O(1) | O(1) | O(1) ArrayDeque |O(1) | O(1) | O(1) | O(1) LinkedBlockingDeque |O(1) | O(1) | O(1) | O(1) -问题在于你是否需要阻塞方法,就像你通常为生产者 - 消费者问题所做的那样(要么是因为消费者必须通过等待来处理一个空队列,要么是因为想通过限制队列限制对它们的需求,然后生产者有时必须等待)。如果您不需要阻塞方法或队列大小的限制,请选择高效且免等待的 `ConcurrentLinkedQueue`。 +问题在于你是否需要阻塞方法,就像你通常为生产者 - 消费者问题所做的那样(要么是因为消费者必须通过等待来处理一个空队列,要么是因为想通过限制队列限制对它 +们的需求,然后生产者有时必须等待)。如果您不需要阻塞方法或队列大小的限制,请选择高效且免等待的 `ConcurrentLinkedQueue`。 -如果您确实需要阻塞队列,因为您的应用程序需要支持生产者与消费者的协作,请暂停以考虑是否真的需要缓冲数据,或者您是否需要在线程之间安全地交换数据。如果您可以不使用缓冲(通常是因为您确信会有足够的消费者来防止数据堆积),那么 `SynchronousQueue` 是剩余 `FIFO` 阻止实现 `LinkedBlockingQueue` 和 `ArrayBlockingQueue` 的有效替代方案。 +如果您确实需要阻塞队列,因为您的应用程序需要支持生产者与消费者的协作,请暂停以考虑是否真的需要缓冲数据,或者您是否需要在线程之间安全地交换数据。如果 +您可以不使用缓冲(通常是因为您确信会有足够的消费者来防止数据堆积),那么 `SynchronousQueue` 是剩余 `FIFO` 阻止实现 `LinkedBlockingQueue` 和 +`ArrayBlockingQueue` 的有效替代方案。 -否则,我们终于留下了这两者之间的选择。如果无法修复队列大小的实际上限,则必须选择 `LinkedBlockingQueue`,因为 `ArrayBlockingQueue` 总是有界的。对于有限使用,您将根据性能在两者之间进行选择。它们在图 `14-1` 中的性能特点是相同的,但这些只是顺序访问的公式;它们在并发使用中的表现是一个不同的问题。正如我们上面提到的,如果超过三个或四个线程正在服务,`LinkedBlockingQueue` 总体上比 `ArrayBlockingQueue` 更好。这符合 `LinkedBlockingQueue` 的头部和尾部被独立锁定的事实,允许同时更新两端。另一方面,`ArrayBlockingQueue` 不必为每个插入分配新的对象。如果队列性能对于您的应用程序的成功至关重要,那么您应该使用对您来说最重要的基准来衡量这两个实现:您的应用程序本身。 +否则,我们终于留下了这两者之间的选择。如果无法修复队列大小的实际上限,则必须选择 `LinkedBlockingQueue`,因为 `ArrayBlockingQueue` 总是有界的。对 +于有限使用,您将根据性能在两者之间进行选择。它们在图 `14-1` 中的性能特点是相同的,但这些只是顺序访问的公式;它们在并发使用中的表现是一个不同的问题。正 +如我们上面提到的,如果超过三个或四个线程正在服务,`LinkedBlockingQueue` 总体上比 `ArrayBlockingQueue` 更好。这符合 `LinkedBlockingQueue` 的头部 +和尾部被独立锁定的事实,允许同时更新两端。另一方面,`ArrayBlockingQueue` 不必为每个插入分配新的对象。如果队列性能对于您的应用程序的成功至关重要,那 +么您应该使用对您来说最重要的基准来衡量这两个实现:您的应用程序本身。 《《《 [下一节](../ch15/00_Lists.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch15/00_Lists.md b/ch15/00_Lists.md index e84a4ee6..24d38d76 100644 --- a/ch15/00_Lists.md +++ b/ch15/00_Lists.md @@ -1,9 +1,10 @@ 《《《 [返回首页](../README.md)
《《《 [上一节](../ch14/05_Comparing_Queue_Implementations.md) -## Lists +### Lists -列表可能是实践中使用最广泛的 `Java` 集合。 一个列表是一个集合,与集合不同 - 它可以包含重复项,而不像一个队列 - 可以使用户完全可见并控制其元素的排序。 +列表可能是实践中使用最广泛的 `Java` 集合。 一个列表是一个集合,与集合不同 - 它可以包含重复项,而不像一个队列 - 可以使用户完全可见并控制其元素的排 +序。 集合框架接口是 `List`(请参见图 `15-1`)。除了从 `Collection` 继承的操作外,`List` 接口还包含以下操作: @@ -14,57 +15,66 @@ **位置访问**方法,根据它们在数字中的位置访问元素 ```java - void add(int index, E e) // 在给定索引处添加元素e - boolean addAll(int index, Collection c) // 在给定索引处添加c的内容 - E get(int index) // 返回具有给定索引的元素 - E remove(int index) // 删除具有给定索引的元素 - E set(int index, E e) // 用e替换给定索引的元素 +void add(int index, E e) // 在给定索引处添加元素e +boolean addAll(int index, Collection c) // 在给定索引处添加c的内容 +E get(int index) // 返回具有给定索引的元素 +E remove(int index) // 删除具有给定索引的元素 +E set(int index, E e) // 用e替换给定索引的元素 ``` **搜索**列表中搜索指定对象并返回其数字位置的搜索方法。 如果对象不存在,则这些方法返回 `-1`: ```java - int indexOf(Object o) // o的第一次出现的返回索引 - int lastIndexOf(Object o) // 返回最后一次出现的索引 +int indexOf(Object o) // o的第一次出现的返回索引 +int lastIndexOf(Object o) // 返回最后一次出现的索引 ``` **范围视图**一种获取列表范围视图的方法: ```java - List subList(int fromIndex, int toIndex) // 返回列表的一部分视图 +List subList(int fromIndex, int toIndex) // 返回列表的一部分视图 ``` -方法 `subList` 的工作方式与 `SortedSet` 上的 `subSet` 操作类似(参见 `13.2` 节),但使用列表中元素的位置而不是它们的值:返回的列表包含从 `fromIndex` 开始的列表元素,直到但不包括 `toIndex`。 返回的列表没有单独的存在 - 它只是从中获取它的列表的一部分的视图,因此其中的更改反映在原始列表中。 但是,与 `subSet` 有一个重要的区别, 您对子列表所做的更改会写入后备列表,但反过来并不总是如此。 如果通过直接调用其中一个“结构更改”方法(第 `12.1` 节)将元素插入到支持列表或从支持列表中删除,则任何后续使用该子列表的尝试都将导致 `ConcurrentModificationException` 异常。 +方法 `subList` 的工作方式与 `SortedSet` 上的 `subSet` 操作类似(参见 `13.2` 节),但使用列表中元素的位置而不是它们的值:返回的列表包含从 +`fromIndex` 开始的列表元素,直到但不包括 `toIndex`。 返回的列表没有单独的存在 - 它只是从中获取它的列表的一部分的视图,因此其中的更改反映在原始列表 +中。 但是,与 `subSet` 有一个重要的区别, 您对子列表所做的更改会写入后备列表,但反过来并不总是如此。 如果通过直接调用其中一个“结构更改”方法(第 +`12.1` 节)将元素插入到支持列表或从支持列表中删除,则任何后续使用该子列表的尝试都将导致 `ConcurrentModificationException` 异常。 **listIterator** 返回 `ListIterator` 的方法,它是一个具有扩展语义的 `Iterator`,以利用列表的顺序特性: ```java - ListIterator listIterator() // 为此列表返回一个ListIterator,最初位于索引0处 - ListIterator listIterator(int indx) // 为此列表返回一个ListIterator,最初位于index indx +ListIterator listIterator() // 为此列表返回一个ListIterator,最初位于索引0处 +ListIterator listIterator(int indx) // 为此列表返回一个ListIterator,最初位于index indx ``` -`ListIterator` 添加的方法支持以相反顺序遍历列表,更改列表元素或添加新元素,并获取迭代器的当前位置。`ListIterator` 的当前位置始终位于两个元素之间,因此在长度为n的列表中,有 `n + 1` 个有效列表迭代器位置,从 `0`(第一个元素之前)到 `n`(最后一个之后)。`listIterator` 的第二个重载使用提供的值将 `listIterator` 的初始位置设置为这些位置之一(调用没有参数的 `listIterator` 与提供参数 `0` 相同) +`ListIterator` 添加的方法支持以相反顺序遍历列表,更改列表元素或添加新元素,并获取迭代器的当前位置。`ListIterator` 的当前位置始终位于两个元素之间, +因此在长度为n的列表中,有 `n + 1` 个有效列表迭代器位置,从 `0`(第一个元素之前)到 `n`(最后一个之后)。`listIterator` 的第二个重载使用提供的值将 +`listIterator` 的初始位置设置为这些位置之一(调用没有参数的 `listIterator` 与提供参数 `0` 相同) 向 `Iterator` 方法 `hasNext`,`next` 和 `remove`,`ListIterator` 添加以下方法: ```java - public interface ListIterator extends Iterator { - void add(E e); // 将指定的元素插入列表中 - boolean hasPrevious(); // 如果此列表迭代器具有相反方向的其他元素,则返回true - int nextIndex(); // 返回下一次调用返回的元素的索引 - E previous(); // 返回列表中的前一个元素 - int previousIndex(); // 返回后续调用返回的元素索引 - void set(E e); // 用指定的元素替换next或previous返回的最后一个元素 - } +public interface ListIterator extends Iterator { + void add(E e); // 将指定的元素插入列表中 + boolean hasPrevious(); // 如果此列表迭代器具有相反方向的其他元素,则返回true + int nextIndex(); // 返回下一次调用返回的元素的索引 + E previous(); // 返回列表中的前一个元素 + int previousIndex(); // 返回后续调用返回的元素索引 + void set(E e); // 用指定的元素替换next或previous返回的最后一个元素 +} ``` ![](15_1.png) 图 `15-2`. `ListIterator` 操作 -图 `15-2` 显示了三个元素的列表。考虑位置 `2` 处的迭代器,它可以从其他位置移动,或者通过调用 `listIterator(2)` 创建。这个迭代器的大部分操作的效果是直观的;在当前迭代器位置(位于索引 `1` 和 `2` 之间的元素之间)添加插入元素,`hasPrevious` 和 `hasNext` 返回 `true`,`previous` 和 `next` 分别返回索引 `1` 和 `2` 处的元素,而 `previousIndex` 和 `nextIndex` 自身返回这些索引。在列表的极端位置,图中的 `0` 和 `3`,`previousIndex` 和 `nextIndex` 分别返回 `-1` 和 `3`(列表的大小),分别返回前一个或下一个,将引发 `NoSuchElementException`。 +图 `15-2` 显示了三个元素的列表。考虑位置 `2` 处的迭代器,它可以从其他位置移动,或者通过调用 `listIterator(2)` 创建。这个迭代器的大部分操作的效果是 +直观的;在当前迭代器位置(位于索引 `1` 和 `2` 之间的元素之间)添加插入元素,`hasPrevious` 和 `hasNext` 返回 `true`,`previous` 和 `next` 分别返 +回索引 `1` 和 `2` 处的元素,而 `previousIndex` 和 `nextIndex` 自身返回这些索引。在列表的极端位置,图中的 `0` 和 `3`,`previousIndex` 和 +`nextIndex` 分别返回 `-1` 和 `3`(列表的大小),分别返回前一个或下一个,将引发 `NoSuchElementException`。 -操作设置和删除工作的方式不同。它们的效果不取决于迭代器的当前位置,而是取决于它的“当前元素”,使用下一个或上一个遍历的最后一个:`set` 替换当前元素,`remove` 将其删除。如果没有当前元素,或者因为迭代器刚刚创建,或者因为当前元素已被删除,这些方法将抛出 `IllegalStateException`。 +操作设置和删除工作的方式不同。它们的效果不取决于迭代器的当前位置,而是取决于它的“当前元素”,使用下一个或上一个遍历的最后一个:`set` 替换当前元素, +`remove` 将其删除。如果没有当前元素,或者因为迭代器刚刚创建,或者因为当前元素已被删除,这些方法将抛出 `IllegalStateException`。 《《《 [下一节](01_Using_the_Methods_of_List.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch15/01_Using_the_Methods_of_List.md b/ch15/01_Using_the_Methods_of_List.md index 10447796..3a273a96 100644 --- a/ch15/01_Using_the_Methods_of_List.md +++ b/ch15/01_Using_the_Methods_of_List.md @@ -3,73 +3,90 @@ ### 使用List的方法 -让我们看看在待办事项管理器中使用其中一些方法的例子。 在上一章中,我们考虑使用关闭功能在一个基于队列的类中组织一天的任务。 扩大应用范围的一个有用的方法是拥有许多这种类型的对象,每个对象代表未来一天计划的任务。 我们将在一个 `List` 中存储对这些对象的引用,以便将它表示的将来的天数编入索引(保持简单并避免处理 `java.util.Calendar` 的令人厌恶的细节)。 因此,今天计划的任务队列将存储在列表的元素 `0` 处,明天排队等待在元素1处等待。 例 `15-1` 显示了调度程序。 +让我们看看在待办事项管理器中使用其中一些方法的例子。 在上一章中,我们考虑使用关闭功能在一个基于队列的类中组织一天的任务。 扩大应用范围的一个有用的方 +法是拥有许多这种类型的对象,每个对象代表未来一天计划的任务。 我们将在一个 `List` 中存储对这些对象的引用,以便将它表示的将来的天数编入索引(保持简单并 +避免处理 `java.util.Calendar` 的令人厌恶的细节)。 因此,今天计划的任务队列将存储在列表的元素 `0` 处,明天排队等待在元素1处等待。 例 `15-1` 显示了 +调度程序。 例 `15-1`。 基于列表的任务调度程序 ```java - public class TaskScheduler { - private List schedule; - private final int FORWARD_PLANNING_DAYS = 365; - public TaskScheduler() { - List temp = new ArrayList(); - for (int i = 0 ; i < FORWARD_PLANNING_DAYS ; i++) { - temp.add(new StoppableTaskQueue()); - } - schedule = new CopyOnWriteArrayList(temp); //1 - } - public PriorityTask getTask() { - for (StoppableTaskQueue daysTaskQueue : schedule) { - PriorityTask topTask = daysTaskQueue.getTask(); - if (topTask != null) return topTask; - } - return null; // no outstanding tasks - at all!? - } - // 在午夜时分,移除并关闭第0天的队列,将其任务分配到新的第0天,并在计划范围内创建新的一天队列 - public void rollOver() throws InterruptedException{ - StoppableTaskQueue oldDay = schedule.remove(0); - Collection remainingTasks = oldDay.shutDown(); - StoppableTaskQueue firstDay = schedule.get(0); - for (PriorityTask t : remainingTasks) { - firstDay.addTask(t); - } - StoppableTaskQueue lastDay = new StoppableTaskQueue(); - schedule.add(lastDay); - } - public void addTask(PriorityTask task, int day) { - if (day < 0 || day >= FORWARD_PLANNING_DAYS) - throw new IllegalArgumentException("day out of range"); - StoppableTaskQueue daysTaskQueue = schedule.get(day); - if (daysTaskQueue.addTask(task)) return; //2 - // StoppableTaskQueue.addTask仅在已关闭的队列上调用时返回false。 在这种情况下,它现在也会被移除,所以再次尝试是安全的。 - if (! schedule.get(0).addTask(task)) { - throw new IllegalStateException("failed to add task " + task); - } - } - } +public class TaskScheduler { + private List schedule; + private final int FORWARD_PLANNING_DAYS = 365; + public TaskScheduler() { + List temp = new ArrayList(); + for (int i = 0 ; i < FORWARD_PLANNING_DAYS ; i++) { + temp.add(new StoppableTaskQueue()); + } + schedule = new CopyOnWriteArrayList(temp); //1 + } + public PriorityTask getTask() { + for (StoppableTaskQueue daysTaskQueue : schedule) { + PriorityTask topTask = daysTaskQueue.getTask(); + if (topTask != null) + return topTask; + } + return null; // no outstanding tasks - at all!? + } + // 在午夜时分,移除并关闭第0天的队列,将其任务分配到新的第0天,并在计划范围内创建新的一天队列 + public void rollOver() throws InterruptedException{ + StoppableTaskQueue oldDay = schedule.remove(0); + Collection remainingTasks = oldDay.shutDown(); + StoppableTaskQueue firstDay = schedule.get(0); + for (PriorityTask t : remainingTasks) { + firstDay.addTask(t); + } + StoppableTaskQueue lastDay = new StoppableTaskQueue(); + schedule.add(lastDay); + } + public void addTask(PriorityTask task, int day) { + if (day < 0 || day >= FORWARD_PLANNING_DAYS) + throw new IllegalArgumentException("day out of range"); + StoppableTaskQueue daysTaskQueue = schedule.get(day); + if (daysTaskQueue.addTask(task)) return; //2 + // StoppableTaskQueue.addTask仅在已关闭的队列上调用时返回false。 在这种情况下,它现在也会被移除,所以再次尝试是安全的。 + if (! schedule.get(0).addTask(task)) { + throw new IllegalStateException("failed to add task " + task); + } + } +} ``` -尽管这个例子主要是为了展示使用 `List` 接口方法而不是去探索任何特定的实现,但是我们不能在没有选择的情况下进行设置。因为选择中的主要因素是应用程序的并发需求,所以我们需要现在考虑他们。它们非常简单:消费或制作任务的客户只会读取表示日程表的 `List`,因此(一旦构建完成),它唯一的写作场合就是在一天结束时。此时,当天的队列将从计划中删除,并在最后添加一个新的队列(“计划范围”,我们在示例中将其设置为一年)。在这种情况发生之前,我们不需要排除客户端使用当天的队列,因为示例 `14.1` 的 `StoppableTaskQueue` 设计确保了一旦队列停止后它们能够以有序的方式完成。因此,唯一需要排除的是确保客户端在滚动过程正在改变其值时不会尝试读取日程表。 +尽管这个例子主要是为了展示使用 `List` 接口方法而不是去探索任何特定的实现,但是我们不能在没有选择的情况下进行设置。因为选择中的主要因素是应用程序的并 +发需求,所以我们需要现在考虑他们。它们非常简单:消费或制作任务的客户只会读取表示日程表的 `List`,因此(一旦构建完成),它唯一的写作场合就是在一天结束 +时。此时,当天的队列将从计划中删除,并在最后添加一个新的队列(“计划范围”,我们在示例中将其设置为一年)。在这种情况发生之前,我们不需要排除客户端使用 +当天的队列,因为示例 `14.1` 的 `StoppableTaskQueue` 设计确保了一旦队列停止后它们能够以有序的方式完成。因此,唯一需要排除的是确保客户端在滚动过程正 +在改变其值时不会尝试读取日程表。 -如果您回想一下第 `11.5.3` 节中对 `CopyOnWriteArrayList` 的讨论,您可能会发现它很好地满足了这些要求。它优化了阅读权限,符合我们的一项要求。在写操作的情况下,它同步的时间足够长,以创建其内部支持阵列的新副本,从而满足了防止读和写操作之间干扰的其他要求。 +如果您回想一下第 `11.5.3` 节中对 `CopyOnWriteArrayList` 的讨论,您可能会发现它很好地满足了这些要求。它优化了阅读权限,符合我们的一项要求。在写操作 +的情况下,它同步的时间足够长,以创建其内部支持阵列的新副本,从而满足了防止读和写操作之间干扰的其他要求。 选择实现后,我们可以理解示例 `15.1` 的构造函数;写入列表的代价很高,因此使用转换构造函数在一次操作中设置一年的任务队列(行// 1)是明智的。 -`getTask` 方法很简单;我们只需从今天的队列开始迭代任务队列,查找计划任务。如果该方法找不到任何未完成的任务,则返回 `null` - 并且如果发现无任务的日期值得注意,我们应该如何庆祝一个无任务的年份? +`getTask` 方法很简单;我们只需从今天的队列开始迭代任务队列,查找计划任务。如果该方法找不到任何未完成的任务,则返回 `null` - 并且如果发现无任务的日期 +值得注意,我们应该如何庆祝一个无任务的年份? -每天午夜,系统会调用 `rollOver` 方法,实现关闭旧日任务队列的悲伤仪式,并将其中剩余的任务转移到新的一天。这里的一系列事件很重要。`rollOver` 首先从列表中删除队列,此时生产者和消费者可能仍然要插入或删除元素。然后它调用 `StoppableTaskQueue.shutDown`,正如我们在例 `14-1` 中看到的那样,返回队列中剩余的任务并保证不会再添加更多任务。根据它们进展的程度,`addTask` 的调用将完成或将返回 `false`,表示由于队列关闭而失败。 +每天午夜,系统会调用 `rollOver` 方法,实现关闭旧日任务队列的悲伤仪式,并将其中剩余的任务转移到新的一天。这里的一系列事件很重要。`rollOver` 首先从列 +表中删除队列,此时生产者和消费者可能仍然要插入或删除元素。然后它调用 `StoppableTaskQueue.shutDown`,正如我们在例 `14-1` 中看到的那样,返回队列中剩 +余的任务并保证不会再添加更多任务。根据它们进展的程度,`addTask` 的调用将完成或将返回 `false`,表示由于队列关闭而失败。 -这激发了 `addTask` 的逻辑:`StoppableTaskQueue` 的 `addTask` 方法可以返回 `false` 的唯一情况是被调用的队列已经停止。由于停止的唯一队列是第 `0` 天的队列,因此 `addTask` 的返回值 `false` 必须来自生产者线程在午夜翻转前获取对该队列的引用。在这种情况下,列表的元素 `0` 的当前值现在是新的第 `0` 天,并且再次尝试是安全的。如果第二次尝试失败,该线程已被暂停 `24` 小时! +这激发了 `addTask` 的逻辑:`StoppableTaskQueue` 的 `addTask` 方法可以返回 `false` 的唯一情况是被调用的队列已经停止。由于停止的唯一队列是第 `0` +天的队列,因此 `addTask` 的返回值 `false` 必须来自生产者线程在午夜翻转前获取对该队列的引用。在这种情况下,列表的元素 `0` 的当前值现在是新的第 `0` +天,并且再次尝试是安全的。如果第二次尝试失败,该线程已被暂停 `24` 小时! -请注意,`rollOver` 方法非常昂贵;它将两次写入日程表,并且由于日程表由 `CopyOnWriteArrayList` 表示(请参见第 `15.2.3` 节),因此每次写入都会导致整个备份数组被复制。赞成这种实现选择的论点是,与在 `getTask` 上进行的调用次数相比,`rollOver` 非常少被调用,该调用迭代了计划。`CopyOnWriteArrayList` 的替代方案将是一个 `BlockingQueue` 实现,但是在很少使用的 `rollOver` 方法中所提供的改进将以减慢经常使用的 `getTask` 方法为代价,因为队列迭代器不打算用于性能 - 危急情况。 +请注意,`rollOver` 方法非常昂贵;它将两次写入日程表,并且由于日程表由 `CopyOnWriteArrayList` 表示(请参见第 `15.2.3` 节),因此每次写入都会导致整 +个备份数组被复制。赞成这种实现选择的论点是,与在 `getTask` 上进行的调用次数相比,`rollOver` 非常少被调用,该调用迭代了计划。 +`CopyOnWriteArrayList` 的替代方案将是一个 `BlockingQueue` 实现,但是在很少使用的 `rollOver` 方法中所提供的改进将以减慢经常使用的 `getTask` 方法 +为代价,因为队列迭代器不打算用于性能 - 危急情况。 ```java - listIterator getSubSchedule(int startDay, int endDay) { - return schedule.subList(startDay, endDay).listIterator(); - } +listIterator getSubSchedule(int startDay, int endDay) { + return schedule.subList(startDay, endDay).listIterator(); +} ``` 这种观点对今天来说可能会很好,但是我们必须记得在午夜时分抛弃它,因为删除和添加条目的结构性变化将使其无效。 《《《 [下一节](02_Implementing_List.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch15/02_Implementing_List.md b/ch15/02_Implementing_List.md index 22deddfe..536326ba 100644 --- a/ch15/02_Implementing_List.md +++ b/ch15/02_Implementing_List.md @@ -1,52 +1,74 @@ 《《《 [返回首页](../README.md)
《《《 [上一节](01_Using_the_Methods_of_List.md) -### 实现List +### 实现 List -在集合框架中有三个具体的 `List` 实现(参见图 `15-3`),它们执行界面定义的各种操作的速度以及它们在并发修改时的行为方式有所不同; 但是,与 `Set` 和 `Queue` 不同,`List` 没有子接口来指定功能行为的差异。 在本节和下一节中,我们依次查看每个实现并提供性能比较。 +在集合框架中有三个具体的 `List` 实现(参见图 `15-3`),它们执行界面定义的各种操作的速度以及它们在并发修改时的行为方式有所不同; 但是,与 `Set` 和 +`Queue` 不同,`List` 没有子接口来指定功能行为的差异。 在本节和下一节中,我们依次查看每个实现并提供性能比较。 ![](15_3.png) 图 `15-3` `List`接口的实现 -### ArrayList +#### ArrayList -数组作为 `Java` 语言的一部分提供,并且语法非常方便,但是它们的关键缺点 - 一旦创建,它们不能调整大小 - 使它们不如 `List` 实现更受欢迎,它们(如果可调整大小)是无限可扩展的。实际上,`List` 最常用的实现是 `ArrayList`,即一个由数组支持的 `List`。 +数组作为 `Java` 语言的一部分提供,并且语法非常方便,但是它们的关键缺点 - 一旦创建,它们不能调整大小 - 使它们不如 `List` 实现更受欢迎,它们(如果可 +调整大小)是无限可扩展的。实际上,`List` 最常用的实现是 `ArrayList`,即一个由数组支持的 `List`。 -`ArrayList` 的标准实现将 `List` 元素存储在连续的数组位置中,第一个元素始终存储在数组中的索引 `0` 处。它需要一个至少足够大(有足够容量)的数组来容纳这些元素,以及一种跟踪“占用”位置数量(列表大小)的方法。如果一个 `ArrayList` 已经增长到了它的大小等于它的容量的地步,试图添加另一个元素将需要它用一个能够容纳旧内容和新元素的较大容器替换后备数组,进一步扩展(标准实现实际上使用了一个新的数组,它是旧数组的两倍)。正如我们在第 `11.3` 节中解释的那样,这导致 `O(1)` 的摊销成本。 +`ArrayList` 的标准实现将 `List` 元素存储在连续的数组位置中,第一个元素始终存储在数组中的索引 `0` 处。它需要一个至少足够大(有足够容量)的数组来容纳 +这些元素,以及一种跟踪“占用”位置数量(列表大小)的方法。如果一个 `ArrayList` 已经增长到了它的大小等于它的容量的地步,试图添加另一个元素将需要它用一 +个能够容纳旧内容和新元素的较大容器替换后备数组,进一步扩展(标准实现实际上使用了一个新的数组,它是旧数组的两倍)。正如我们在第 `11.3` 节中解释的那 +样,这导致 `O(1)` 的摊销成本。 -`ArrayList` 的性能反映了“随机访问”操作的数组性能:`set` 和 `get` 获取常量时间。数组实现的缺点是在任意位置插入或移除元素,因为这可能需要调整其他元素的位置。 我们已经用基于数组的队列的迭代器的 `remove` 方法来解决这个问题 - 例如,`ArrayBlockingQueue`(见 `14.3.2` 节)。但是对于列表来说位置添加和移除方法的性能比迭代器更重要.`remove` 是为队列。) +`ArrayList` 的性能反映了“随机访问”操作的数组性能:`set` 和 `get` 获取常量时间。数组实现的缺点是在任意位置插入或移除元素,因为这可能需要调整其他元素 +的位置。 我们已经用基于数组的队列的迭代器的 `remove` 方法来解决这个问题 - 例如,`ArrayBlockingQueue`(见 `14.3.2` 节)。但是对于列表来说位置添加 +和移除方法的性能比迭代器更重要.`remove` 是为队列。) 例如,图 `15-4`(`a`)通过以下语句添加了三个元素后显示了一个新的 `ArrayList`: ```java - List; charList = new ArrayList(); - Collections.addAll(charList, 'a', 'b', 'c'); +List; charList = new ArrayList(); +Collections.addAll(charList, 'a', 'b', 'c'); ``` -如果我们现在想要移除数组的索引 `1` 处的元素,那么实现必须保留剩余元素的顺序并确保数组的占用区域仍然从索引 `0` 开始。因此索引2处的元素必须是 移至索引 `1`,索引 `3` 移至索引 `2`,依此类推。图 `15-4`(`b`)显示了这个操作执行后的样本 `ArrayList`。 由于每个元素都必须依次移动,因此此操作的时间复杂度与列表的大小成正比(即使由于此操作通常可以用硬件实现,常数因子很低)。 +如果我们现在想要移除数组的索引 `1` 处的元素,那么实现必须保留剩余元素的顺序并确保数组的占用区域仍然从索引 `0` 开始。因此索引2处的元素必须是 移至索引 +`1`,索引 `3` 移至索引 `2`,依此类推。图 `15-4`(`b`)显示了这个操作执行后的样本 `ArrayList`。 由于每个元素都必须依次移动,因此此操作的时间复杂度 +与列表的大小成正比(即使由于此操作通常可以用硬件实现,常数因子很低)。 ![](15_4.png) 图 `15-4`。 从 `ArrayList` 中移除元素 -即便如此,提醒读者回顾了用于实现 `ArrayBlockingQueue` 和 `ArrayDeque`(参见 `14.4.1` 节)的循环数组的讨论可能会想知道为什么还没有选择循环数组来实现 `ArrayList`。确实,循环数组的添加和删除方法只有在索引参数为0时才会显示出更好的性能,但这是一个常见的情况,并且使用循环数组的开销非常小,以至于问题依旧。 +即便如此,提醒读者回顾了用于实现 `ArrayBlockingQueue` 和 `ArrayDeque`(参见 `14.4.1` 节)的循环数组的讨论可能会想知道为什么还没有选择循环数组来实 +现 `ArrayList`。确实,循环数组的添加和删除方法只有在索引参数为0时才会显示出更好的性能,但这是一个常见的情况,并且使用循环数组的开销非常小,以至于问 +题依旧。 -事实上,`Heinz Kabutz` 在 [`The Java Specialists'Newsletter`](http://www.javaspecialists.co.za/archive/Issue027.html) 中提出了一个循环数组列表的大纲实现。原则上,仍然有可能的 `ArrayList` 可以通过这种方式来重新实现,这可能导致许多现有的 `Java applications.A` 可能的替代真正的性能提升是圆形 `ArrayDeque` 可改进来实现列表的方法。与此同时,如果您的应用程序使用 `List`,其中从列表开始的元素插入和删除的性能比 `randomaccess` 操作的性能更重要,请考虑写入 `Deque` 接口并利用其非常高效的 `ArrayDeque` 实现。 +事实上,`Heinz Kabutz` 在 [`The Java Specialists'Newsletter`](http://www.javaspecialists.co.za/archive/Issue027.html) 中提出了一个循环数组 +列表的大纲实现。原则上,仍然有可能的 `ArrayList` 可以通过这种方式来重新实现,这可能导致许多现有的 `Java applications.A` 可能的替代真正的性能提升是 +圆形 `ArrayDeque` 可改进来实现列表的方法。与此同时,如果您的应用程序使用 `List`,其中从列表开始的元素插入和删除的性能比 `randomaccess` 操作的性能 +更重要,请考虑写入 `Deque` 接口并利用其非常高效的 `ArrayDeque` 实现。 -正如我们在讨论 `ArrayBlockingQueue`(第 `14.2` 节)时所提到的,可变大小的数组支持的集合类可以有一个配置参数:数组的初始长度。因此,除了标准集合框架构造函数之外,`ArrayList` 还有一个允许您选择初始容量的值足够大以容纳集合的元素,而无需频繁的创建 - 复制操作。由默认构造函数创建的 `ArrayList` 的初始容量为 `10`,使用另一个集合的元素初始化的初始容量是该集合大小的 `110%`。 +正如我们在讨论 `ArrayBlockingQueue`(第 `14.2` 节)时所提到的,可变大小的数组支持的集合类可以有一个配置参数:数组的初始长度。因此,除了标准集合框架 +构造函数之外,`ArrayList` 还有一个允许您选择初始容量的值足够大以容纳集合的元素,而无需频繁的创建 - 复制操作。由默认构造函数创建的 `ArrayList` 的初 +始容量为 `10`,使用另一个集合的元素初始化的初始容量是该集合大小的 `110%`。 `ArrayList` 的迭代器是快速失败的。 -### LinkedList +#### LinkedList -我们在 `14.4.1` 节中讨论了 `LinkedList` 作为 `Deque` 实现。 如果您的应用程序大量使用随机访问,您将避免它作为 `List` 实现; 由于列表必须在内部进行迭代以达到所需的位置,位置添加和移除平均具有线性时间复杂度。 其中 `LinkedList` 确实比 `ArrayList` 具有更好的性能优势,它可以添加和移除列表末尾以外的任何元素; 对于 `LinkedList`,这需要一段时间,而非非圆形数组实现所需的线性时间。 +我们在 `14.4.1` 节中讨论了 `LinkedList` 作为 `Deque` 实现。 如果您的应用程序大量使用随机访问,您将避免它作为 `List` 实现; 由于列表必须在内部进行 +迭代以达到所需的位置,位置添加和移除平均具有线性时间复杂度。 其中 `LinkedList` 确实比 `ArrayList` 具有更好的性能优势,它可以添加和移除列表末尾以外 +的任何元素; 对于 `LinkedList`,这需要一段时间,而非非圆形数组实现所需的线性时间。 -### CopyOnWriteArrayList +#### CopyOnWriteArrayList -在第 `13.1` 节中,我们遇到了 `CopyOnWriteArraySet`,这是一个集合实现,旨在提供线程安全性以及非常快速的读取访问。`CopyOnWriteArrayList` 是一个具有相同设计目标的 `List` 实现。线程安全与快速读取访问的组合对于某些并发程序非常有用,尤其是当观察者对象的集合需要接收频繁的事件通知时。成本是支持集合的数组必须被视为不可变的,所以每当对集合进行任何更改时都会创建一个新副本。如果观察员组的变化很少发生,这个成本可能不会太高。 +在第 `13.1` 节中,我们遇到了 `CopyOnWriteArraySet`,这是一个集合实现,旨在提供线程安全性以及非常快速的读取访问。`CopyOnWriteArrayList` 是一个具 +有相同设计目标的 `List` 实现。线程安全与快速读取访问的组合对于某些并发程序非常有用,尤其是当观察者对象的集合需要接收频繁的事件通知时。成本是支持集合 +的数组必须被视为不可变的,所以每当对集合进行任何更改时都会创建一个新副本。如果观察员组的变化很少发生,这个成本可能不会太高。 -`CopyOnWriteArraySet` 类实际上将其所有操作委托给 `CopyOnWriteArrayList` 实例,利用后者提供的原子操作 `addIfAbsent` 和 `addAllAbsent` 来启用 `Set` 方法 `add` 和 `addAll`,以避免向该集合中引入重复项。除了两个标准的构造函数(参见 `12.3` 节)之外,`CopyOnWriteArrayList` 还有一个允许使用提供的数组元素作为其初始内容创建的额外元素。它的迭代器是快照迭代器,反映了它们创建时的列表状态。 +`CopyOnWriteArraySet` 类实际上将其所有操作委托给 `CopyOnWriteArrayList` 实例,利用后者提供的原子操作 `addIfAbsent` 和 `addAllAbsent` 来启用 +`Set` 方法 `add` 和 `addAll`,以避免向该集合中引入重复项。除了两个标准的构造函数(参见 `12.3` 节)之外,`CopyOnWriteArrayList` 还有一个允许使用 +提供的数组元素作为其初始内容创建的额外元素。它的迭代器是快照迭代器,反映了它们创建时的列表状态。 《《《 [下一节](03_Comparing_List_Implementations.md)
《《《 [返回首页](../README.md) diff --git a/ch15/03_Comparing_List_Implementations.md b/ch15/03_Comparing_List_Implementations.md index ddbf7a61..c4051f1b 100644 --- a/ch15/03_Comparing_List_Implementations.md +++ b/ch15/03_Comparing_List_Implementations.md @@ -1,11 +1,17 @@ 《《《 [返回首页](../README.md)
《《《 [上一节](02_Implementing_List.md) -### 比较List实现 +### 比较 List 实现 -表 `15-1` 给出了 `List` 类的一些样例操作的比较性能。即使这里的选择比队列甚至集合要窄得多,也可以使用相同的消除过程。与队列一样,首先要问的问题是您的应用程序是否需要线程安全。如果是这样,你应该使用 `CopyOnWriteArrayList`,如果可以的话 - 也就是说,如果写入列表会相对不频繁。如果没有,你将不得不围绕 `ArrayList` 或 `LinkedList` 使用一个同步包装器(见 `17.3.1` 节)。 +表 `15-1` 给出了 `List` 类的一些样例操作的比较性能。即使这里的选择比队列甚至集合要窄得多,也可以使用相同的消除过程。与队列一样,首先要问的问题是您的应 +用程序是否需要线程安全。如果是这样,你应该使用 `CopyOnWriteArrayList`,如果可以的话 - 也就是说,如果写入列表会相对不频繁。如果没有,你将不得不围绕 +`ArrayList` 或 `LinkedList` 使用一个同步包装器(见 `17.3.1` 节)。 -对于大多数列表应用程序,选择在 `ArrayList` 和 `LinkedList` 之间,是否同步。再一次,你的决定将取决于在实践中如何使用列表。如果设置并获得主导地位,或者元素插入和移除主要在列表的末尾,那么 `ArrayList` 将是最佳选择。相反,如果您的应用程序需要频繁插入和移除列表开始附近的元素作为使用迭代的进程的一部分,那么 `LinkedList` 可能会更好。如果您有疑问,请在每次实施时测试性能。在最后一种情况下值得考虑的单线程代码的 `Java 6` 替代方案(如果插入和删除实际上位于列表的起始处)是使用非常高效的 `ArrayDeque` 实现写入 `Deque` 接口。对于相对不频繁的随机访问,请使用迭代器,或使用 `toArray` 将 `ArrayDeque` 元素复制到数组中。 +对于大多数列表应用程序,选择在 `ArrayList` 和 `LinkedList` 之间,是否同步。再一次,你的决定将取决于在实践中如何使用列表。如果设置并获得主导地位,或者 +元素插入和移除主要在列表的末尾,那么 `ArrayList` 将是最佳选择。相反,如果您的应用程序需要频繁插入和移除列表开始附近的元素作为使用迭代的进程的一部分,那 +么 `LinkedList` 可能会更好。如果您有疑问,请在每次实施时测试性能。在最后一种情况下值得考虑的单线程代码的 `Java 6` 替代方案(如果插入和删除实际上位于列 +表的起始处)是使用非常高效的 `ArrayDeque` 实现写入 `Deque` 接口。对于相对不频繁的随机访问,请使用迭代器,或使用 `toArray` 将 `ArrayDeque` 元素复制到 +数组中。 表 `15-1`。不同列表实现的比较性能 @@ -18,4 +24,4 @@ CopyOnWrite-ArrayList |O(1) | O(n) | O(n) |O(1) | O(n) | O(n) 有可能在未来的版本中,`ArrayDeque` 将被改进以实现 `List` 接口;如果发生这种情况,它将成为单线程环境中队列和列表的选择。 《《《 [下一节](../ch16/00_Maps.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch16/00_Maps.md b/ch16/00_Maps.md index 9c1f0b08..b2daca1d 100644 --- a/ch16/00_Maps.md +++ b/ch16/00_Maps.md @@ -1,15 +1,16 @@ 《《《 [返回首页](../README.md)
《《《 [上一节](../ch15/03_Comparing_List_Implementations.md) -## Maps +### Maps -`Map` 接口是主要集合框架接口中的最后一个,也是唯一不能从 `Collection` 继承的接口。 它定义了一组键值对关联所支持的操作,其中键是唯一的。 这些操作如图 `16-1` 所示,分为以下四组,大致平行于 `Collection-adding` 元素的四个操作组,删除元素,查询集合内容以及提供集合内容的不同视图。 +`Map` 接口是主要集合框架接口中的最后一个,也是唯一不能从 `Collection` 继承的接口。 它定义了一组键值对关联所支持的操作,其中键是唯一的。 这些操作如 +图 `16-1` 所示,分为以下四组,大致平行于 `Collection-adding` 元素的四个操作组,删除元素,查询集合内容以及提供集合内容的不同视图。 **添加关联** ```java - V put(K key, V value) // 如果密钥存在,则添加或替换键值关联返回旧值(可能为空); 否则返回null - void putAll(Map m) // 将提供的映射中的每个键值关联添加到接收器中 +V put(K key, V value) // 如果密钥存在,则添加或替换键值关联返回旧值(可能为空); 否则返回null +void putAll(Map m) // 将提供的映射中的每个键值关联添加到接收器中 ``` 该组中的操作是可选的; 在不可修改的映射上调用它们将导致 `UnsupportedOperationException`。 @@ -17,65 +18,49 @@ **删除关联** ```java - void clear() // 从此地图中删除所有关联 - V remove(Object key) // 使用给定的密钥去除关联(如果有的话); 返回与其关联的值,或返回null +void clear() // 从此地图中删除所有关联 +V remove(Object key) // 使用给定的密钥去除关联(如果有的话); 返回与其关联的值,或返回null ``` -`Map.remove` 的签名与 `Collection.remove` 的签名相似(请参阅第 `12.1` 节),因为它采用 `Object` 类型的参数而不是泛型类型。 我们讨论过了 `2.6` 节中的这个设计的替代方案。 +`Map.remove` 的签名与 `Collection.remove` 的签名相似(请参阅第 `12.1` 节),因为它采用 `Object` 类型的参数而不是泛型类型。 我们讨论过了 `2.6` 节 +中的这个设计的替代方案。 像前一组的添加操作一样,这些删除操作是可选的。 **查询 Map 的内容** ```java - V get(Object k) // 返回对应于k的值;如果k不存在,则返回null - boolean containsKey(Object k) // 如果k作为键存在,则返回true - boolean containsValue(Object v) // 如果v作为值存在,则返回true - int size() // 返回关联的数量 - boolean isEmpty() // 如果没有关联,则返回true +V get(Object k) // 返回对应于k的值;如果k不存在,则返回null +boolean containsKey(Object k) // 如果k作为键存在,则返回true +boolean containsValue(Object v) // 如果v作为值存在,则返回true +int size() // 返回关联的数量 +boolean isEmpty() // 如果没有关联,则返回true ``` ![](16_1.png) 图 `16-1` `Map` -对于允许空键或值(分别)的 `Map` 实现,`containsKey` 和 `containsValue` 的参数可能为 `null`。 如果为这些方法提供了 `null` 参数,则不允许为 `null` 的实现将抛出 `NullPointerException`。 +对于允许空键或值(分别)的 `Map` 实现,`containsKey` 和 `containsValue` 的参数可能为 `null`。 如果为这些方法提供了 `null` 参数,则不允许为 +`null` 的实现将抛出 `NullPointerException`。 与 `Collection` 的 `size` 方法一样,可以报告的最大元素数量是 `Integer.MAX_VALUE`。 提供键,值或关联的集合视图: ```java - Set> entrySet() // 返回关联的Set视图 - Set keySet() // 返回键的Set视图 - Collection values() // 返回值的集合视图 +Set> entrySet() // 返回关联的Set视图 +Set keySet() // 返回键的Set视图 +Collection values() // 返回值的集合视图 ``` -这些方法返回的集合由地图支持,因此对地图的任何更改都会反映在地图本身中,反之亦然。事实上,通过视图只能进行有限的更改:可以直接或通过视图上的迭代器删除元素,但不能添加元素;如果你尝试,你会得到一个 `UnsupportedOperationException`。删除键可删除单个相应的键值关联;另一方面,删除值只会删除映射到其中的一个关联;该值可能仍然作为与不同密钥关联的一部分存在。如果支持地图被同时修改,则视图上的迭代器将变为未定义。 +这些方法返回的集合由地图支持,因此对地图的任何更改都会反映在地图本身中,反之亦然。事实上,通过视图只能进行有限的更改:可以直接或通过视图上的迭代器删 +除元素,但不能添加元素;如果你尝试,你会得到一个 `UnsupportedOperationException`。删除键可删除单个相应的键值关联;另一方面,删除值只会删除映射到其中 +的一个关联;该值可能仍然作为与不同密钥关联的一部分存在。如果支持地图被同时修改,则视图上的迭代器将变为未定义。 -`entrySet` 返回的集合的成员实现了接口 `Map.Entry`,它表示键值关联并提供了一个 `setValue` 方法,可用于更改备份映射中的值。`Map.Entry` 的文档在指定实现接口的对象时只能在通过 `entrySet` 调用产生的视图迭代期间创建,并且如果在此迭代过程中修改了支持映射,则这些对象变为无效。在 `Java 6` 中,创建 `Map.Entry` 对象的这种限制性方案不够充分,因为它是 `NavigableMap` 的许多方法的返回类型(请参见第 `16.3` 节)。 +`entrySet` 返回的集合的成员实现了接口 `Map.Entry`,它表示键值关联并提供了一个 `setValue` 方法,可用于更改备份映射中的值。`Map.Entry` 的文档在指定 +实现接口的对象时只能在通过 `entrySet` 调用产生的视图迭代期间创建,并且如果在此迭代过程中修改了支持映射,则这些对象变为无效。在 `Java 6` 中,创建 +`Map.Entry` 对象的这种限制性方案不够充分,因为它是 `NavigableMap` 的许多方法的返回类型(请参见第 `16.3` 节)。 《《《 [下一节](01_Using_the_Methods_of_Map.md)
《《《 [返回首页](../README.md) - - - - - - - - - - - - - - - - - - - - - - diff --git a/ch16/01_Using_the_Methods_of_Map.md b/ch16/01_Using_the_Methods_of_Map.md index cc16a1ea..6ea5e7bd 100644 --- a/ch16/01_Using_the_Methods_of_Map.md +++ b/ch16/01_Using_the_Methods_of_Map.md @@ -3,70 +3,74 @@ ### 使用Map的方法 -正如我们在前两章中所做的那样,将待办事项管理器置于优先级队列中的一个问题是优先级队列无法保留将元素添加到它们的顺序(除非可以按优先级顺序 例如作为时间戳或序列号)。 为了避免这种情况,我们可以使用一系列 `FIFO 队列作为替代模型,每个队列分配一个优先级。 一个 `Map` 适合于保持优先级与任务队列之间的关联; 特别是 `EnumMap` 是一个高效的 `Map` 实现,专门用于与枚举成员关键字一起使用。 +正如我们在前两章中所做的那样,将待办事项管理器置于优先级队列中的一个问题是优先级队列无法保留将元素添加到它们的顺序(除非可以按优先级顺序 例如作为时间 +戳或序列号)。 为了避免这种情况,我们可以使用一系列 `FIFO` 队列作为替代模型,每个队列分配一个优先级。 一个 `Map` 适合于保持优先级与任务队列之间的关 +联; 特别是 `EnumMap` 是一个高效的 `Map` 实现,专门用于与枚举成员关键字一起使用。 该模型将依赖于保持 `FIFO` 排序的队列实现。 为了关注 `Map` 方法的使用,我们假设一个单线程客户端并使用一系列 `ArrayDeques` 作为实现: ```java - Map> taskMap = new EnumMap>(Priority.class); - for (Priority p : Priority.values()) { - taskMap.put(p, new ArrayDeque()); - } - // 填充列表,例如: - taskMap.get(Priority.MEDIUM).add(mikePhone); - taskMap.get(Priority.HIGH).add(databaseCode); +Map> taskMap = new EnumMap>(Priority.class); +for (Priority p : Priority.values()) { + taskMap.put(p, new ArrayDeque()); +} +// 填充列表,例如: +taskMap.get(Priority.MEDIUM).add(mikePhone); +taskMap.get(Priority.HIGH).add(databaseCode); ``` 现在,要进入任务队列之一 - 比如说具有最高优先级任务的队列 - 我们可以这样写: ```java - Queue highPriorityTaskList = taskMap.get(Priority.HIGH); +Queue highPriorityTaskList = taskMap.get(Priority.HIGH); ``` 轮询此队列现在将按照它们进入系统的顺序为我们提供高优先级待办事项。 -为了看到其他一些 `Map` 方法的使用,让我们稍微扩展一下这个例子,以允许这些任务中的一些实际上可以通过计费赚取一些钱。 表示这种方法的一种方法是定义一个类 `Client`: +为了看到其他一些 `Map` 方法的使用,让我们稍微扩展一下这个例子,以允许这些任务中的一些实际上可以通过计费赚取一些钱。 表示这种方法的一种方法是定义一个 +类 `Client`: ```java - class Client {...} - Client acme = new Client("Acme Corp.",...); +class Client {...} +Client acme = new Client("Acme Corp.",...); ``` 并创建从任务到客户的映射: ```java - Map billingMap = new HashMap(); - billingMap.put(interfaceCode, acme); +Map billingMap = new HashMap(); +billingMap.put(interfaceCode, acme); ``` -我们需要确保系统仍然可以处理无法填写的任务。我们在这里有一个选择:我们可以简单地将不可填写任务的名称添加到 `billingMap` 中,也可以将其映射为 `null`。无论哪种情况,作为处理任务 `t` 的代码的一部分,我们可以写出: +我们需要确保系统仍然可以处理无法填写的任务。我们在这里有一个选择:我们可以简单地将不可填写任务的名称添加到 `billingMap` 中,也可以将其映射为 +`null`。无论哪种情况,作为处理任务 `t` 的代码的一部分,我们可以写出: ```java - Task t = ... - Client client = billingMap.get(t); - if (client != null) { - client.bill(t); - } +Task t = ... +Client client = billingMap.get(t); +if (client != null) { + client.bill(t); +} ``` 当我们终于完成了我们的客户 `Acme Corp`.承包的所有工作时,可以删除将任务与 `Acme` 关联的 `Map` 条目: ```java - Collection clients = billingMap.values(); - for (Iterator iter = clients.iterator() ; iter.hasNext() ; ) { - if (iter.next().equals(acme)) { - iter.remove(); - } - } +Collection clients = billingMap.values(); +for (Iterator iter = clients.iterator() ; iter.hasNext() ; ) { + if (iter.next().equals(acme)) { + iter.remove(); + } +} ``` 整洁的替代方法利用方法 `Collections.singleton`(参见 `17.2` 节),该方法返回一个只包含指定元素的不可变 `Set`: ```java - clients.removeAll(Collections.singleton(acme)); +clients.removeAll(Collections.singleton(acme)); ``` 这两种方法的成本都是 `O(n)`,`Sun` 在当前的实施中具有类似的常数因子。 《《《 [下一节](02_Implementing_Map.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch16/02_Implementing_Map.md b/ch16/02_Implementing_Map.md index 2da9f5e4..e38917a1 100644 --- a/ch16/02_Implementing_Map.md +++ b/ch16/02_Implementing_Map.md @@ -3,16 +3,20 @@ ### 实现Map -图 `16-2` 显示了集合框架为 `Map` 提供的实现,共八个实例。 我们将在这里讨论 `HashMap`,`LinkedHashMap`,`WeakHashMap`,`IdentityHashMap` 和 `EnumMap`; 讨论 `NavigableMap`,`ConcurrentMap` 和 `ConcurrentNavigableMap` 的接口及其实现,以下部分。 +图 `16-2` 显示了集合框架为 `Map` 提供的实现,共八个实例。 我们将在这里讨论 `HashMap`,`LinkedHashMap`,`WeakHashMap`,`IdentityHashMap` 和 +`EnumMap`; 讨论 `NavigableMap`,`ConcurrentMap` 和 `ConcurrentNavigableMap` 的接口及其实现,以下部分。 -对于构造函数,`Map` 实现的一般规则类似于 `Collection` 实现(参见 `12.3` 节)。 除 `EnumMap` 以外的每个实现都至少有两个构造函数; 以 `HashMap` 为例,它们是: +对于构造函数,`Map` 实现的一般规则类似于 `Collection` 实现(参见 `12.3` 节)。 除 `EnumMap` 以外的每个实现都至少有两个构造函数; 以 `HashMap` 为 +例,它们是: ```java - public HashMap() - public HashMap(Map m) +public HashMap() +public HashMap(Map m) ``` -其中第一个创建一个空映射,第二个映射将包含提供的映射 `m` 中包含的键 - 值映射。 `map m`的键和值必须具有与正在创建的映射的键和值分别相同(或者是其子类型)的类型。 使用第二个构造函数与使用默认构造函数创建空映射的效果相同,然后使用 `putAll` 添加 `map m` 的内容。 除了这两者之外,标准实现还有其他用于配置目的的构造函数。 +其中第一个创建一个空映射,第二个映射将包含提供的映射 `m` 中包含的键 - 值映射。 `map m`的键和值必须具有与正在创建的映射的键和值分别相同(或者是其子类 +型)的类型。 使用第二个构造函数与使用默认构造函数创建空映射的效果相同,然后使用 `putAll` 添加 `map m` 的内容。 除了这两者之外,标准实现还有其他用于 +配置目的的构造函数。 ![](16_2.png) @@ -20,15 +24,18 @@ ### HashMap -在讨论第 `13.1.1` 节中的 `HashSet` 时,我们提到它将其所有操作委托给 `HashMap` 的私有实例。图 `16-3`(`a`)与图 `13-2` 类似,但没有从映射中移除值元素的简化(`HashSet` 中的所有元素都以具有相同常数值的键存储)。哈希表第 `13.1` 节中的讨论及其性能同样适用于 `HashMap`。特别是,`HashMap` 为 `put` 和 `get` 提供了恒定的性能。尽管原则上恒定时间的性能只能在没有碰撞的情况下实现,但可以通过使用重新哈希来控制负载并由此最小化碰撞次数来紧密接近它。 +在讨论第 `13.1.1` 节中的 `HashSet` 时,我们提到它将其所有操作委托给 `HashMap` 的私有实例。图 `16-3`(`a`)与图 `13-2` 类似,但没有从映射中移除值 +元素的简化(`HashSet` 中的所有元素都以具有相同常数值的键存储)。哈希表第 `13.1` 节中的讨论及其性能同样适用于 `HashMap`。特别是,`HashMap` 为 +`put` 和 `get` 提供了恒定的性能。尽管原则上恒定时间的性能只能在没有碰撞的情况下实现,但可以通过使用重新哈希来控制负载并由此最小化碰撞次数来紧密接近 +它。 对键或值的集合进行迭代需要的时间与映射的容量加上包含的键 - 值映射的数量成正比。迭代器快速失败。 两个构造函数允许程序员配置一个新的 `HashMap` 实例: ```java - public HashMap(int initialCapacity) - public HashMap(int initialCapacity, float loadFactor) +public HashMap(int initialCapacity) +public HashMap(int initialCapacity, float loadFactor) ``` 这些构造函数与 `HashSet` 的构造函数类似,允许指定初始容量,并且可选地指定表将被重新映射的加载因子。 @@ -39,112 +46,162 @@ 图 `16-3`.`HashMap` 和 `WeakHashMap` -像 `LinkedHashSet`( `13.1.2` 节)一样,类 `LinkedHashMap` 通过保证迭代器返回其元素的顺序来优化其父类 `HashMap` 的约定。但是,与 `LinkedHashSet` 不同,`LinkedHashMap` 提供迭代次序的选择; 元素可以按照它们在地图中插入的顺序或按照它们的访问顺序(从最近到最近访问的顺序)返回。访问顺序 `LinkedHashMap` 是通过提供 `true` 的参数 构造函数的最后一个参数: +像 `LinkedHashSet`( `13.1.2` 节)一样,类 `LinkedHashMap` 通过保证迭代器返回其元素的顺序来优化其父类 `HashMap` 的约定。但是,与 +`LinkedHashSet` 不同,`LinkedHashMap` 提供迭代次序的选择; 元素可以按照它们在地图中插入的顺序或按照它们的访问顺序(从最近到最近访问的顺序)返回。访 +问顺序 `LinkedHashMap` 是通过提供 `true` 的参数 构造函数的最后一个参数: ```java - public LinkedHashMap(int initialCapacity, - float loadFactor, - boolean accessOrder) +public LinkedHashMap(int initialCapacity, +float loadFactor, +boolean accessOrder) ``` -提供 `false` 将提供插入顺序的地图。其他的构造函数,就像 `HashMap` 的构造函数一样,也生成插入顺序的地图。与 `LinkedHashSet` 一样,迭代 `LinkedHashMap` 所需的时间只与地图中元素的数量成正比,而不是其容量。 +提供 `false` 将提供插入顺序的地图。其他的构造函数,就像 `HashMap` 的构造函数一样,也生成插入顺序的地图。与 `LinkedHashSet` 一样,迭代 +`LinkedHashMap` 所需的时间只与地图中元素的数量成正比,而不是其容量。 -访问顺序映射对于构建最近最少使用(`LRU`)缓存特别有用。高速缓存是存储经常访问的数据以便快速访问的内存区域。在设计高速缓存时,关键问题是选择哪种算法来决定要删除哪些数据以节省内存。当需要找到来自缓存数据集的项目时,将首先搜索缓存。通常,如果在高速缓存中找不到该项目,则会从主存储中检索该项目并将其添加到高速缓存中。但是不能允许缓存继续无限增长,因此必须选择一种策略,以便在添加新缓存时从缓存中删除最不有用的项目。如果选择的策略是LRU,则删除的条目将是最近使用的条目。这种简单的策略适用于元素的访问增加了在相同元素的近期进一步访问的可能性的情况。它的简单性和速度使其成为最受欢迎的缓存策略。 +访问顺序映射对于构建最近最少使用(`LRU`)缓存特别有用。高速缓存是存储经常访问的数据以便快速访问的内存区域。在设计高速缓存时,关键问题是选择哪种算法来 +决定要删除哪些数据以节省内存。当需要找到来自缓存数据集的项目时,将首先搜索缓存。通常,如果在高速缓存中找不到该项目,则会从主存储中检索该项目并将其添 +加到高速缓存中。但是不能允许缓存继续无限增长,因此必须选择一种策略,以便在添加新缓存时从缓存中删除最不有用的项目。如果选择的策略是LRU,则删除的条目将 +是最近使用的条目。这种简单的策略适用于元素的访问增加了在相同元素的近期进一步访问的可能性的情况。它的简单性和速度使其成为最受欢迎的缓存策略。 使用 `LinkedHashMap` 进行缓存构建可以通过 `removeEldestEntry` 进一步辅助,它是从 `HashMap` 继承的单一方法 ```java - protected boolean removeEldestEntry(Map.Entry eldest) +protected boolean removeEldestEntry(Map.Entry eldest) ``` -`removeEldestEntry` 的合约规定,只要向地图添加新条目,`put` 和 `putAll` 方法就会调用 `removeEldestEntry`,并将“最老”条目传递给它。在插入顺序的地图中,最年长的条目将是最近最少添加到地图的条目,但在访问顺序映射中,它是最近最少访问的映射(如果某些条目从未访问过,则是其中最近最少添加的这些之一)。在 `LinkedHashMap` 本身中,`removeEldestEntry` 不做任何事情并返回 `false`,但是子类可以覆盖它以在某些情况下返回 `true`。此方法的契约指定虽然它本身可以移除最老的条目,但如果它已经这样做,它必须返回 `false`,因为预计 `true` 的返回值将导致其调用方法执行删除操作。`removeEldestEntry` 的一个简单示例将允许地图增长到给定的最大大小,然后通过在每次添加新地图时删除最老的条目来保持该大小: +`removeEldestEntry` 的合约规定,只要向地图添加新条目,`put` 和 `putAll` 方法就会调用 `removeEldestEntry`,并将“最老”条目传递给它。在插入顺序的地 +图中,最年长的条目将是最近最少添加到地图的条目,但在访问顺序映射中,它是最近最少访问的映射(如果某些条目从未访问过,则是其中最近最少添加的这些之 +一)。在 `LinkedHashMap` 本身中,`removeEldestEntry` 不做任何事情并返回 `false`,但是子类可以覆盖它以在某些情况下返回 `true`。此方法的契约指定虽 +然它本身可以移除最老的条目,但如果它已经这样做,它必须返回 `false`,因为预计 `true` 的返回值将导致其调用方法执行删除操作。`removeEldestEntry` 的一 +个简单示例将允许地图增长到给定的最大大小,然后通过在每次添加新地图时删除最老的条目来保持该大小: ```java - class BoundedSizeMap extends LinkedHashMap { - private int maxEntries; - public BoundedSizeMap(int maxEntries) { - super(16, 0.75f, true); - this.maxEntries = maxEntries; - } - protected boolean removeEldestEntry(Map.Entry eldest) { - return size() > maxEntries; - } - } +class BoundedSizeMap extends LinkedHashMap { + private int maxEntries; + public BoundedSizeMap(int maxEntries) { + super(16, 0.75f, true); + this.maxEntries = maxEntries; + } + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > maxEntries; + } +} ``` 这个简单示例的改进可以考虑作为 `removeEldestEntry` 的参数提供的条目。 例如,目录高速缓存可能有一组保留名称,即使缓存继续增长,也不应删除该名称。 -请注意,如上所示重写 `removeEldestEntry` 的插入顺序 `LinkedHashMap` 将实现 `FIFO` 策略。`FIFO` 缓存通常优先于 `LRU` 使用,因为在不提供访问顺序的地图中实现起来要简单得多。 然而,`LRU` 通常比 `FIFO` 更有效,因为缓存刷新的成本降低超过维护访问排序的开销。 +请注意,如上所示重写 `removeEldestEntry` 的插入顺序 `LinkedHashMap` 将实现 `FIFO` 策略。`FIFO` 缓存通常优先于 `LRU` 使用,因为在不提供访问顺序的 +地图中实现起来要简单得多。 然而,`LRU` 通常比 `FIFO` 更有效,因为缓存刷新的成本降低超过维护访问排序的开销。 对 `LinkedHashMap` 返回的键或值的集合的迭代在元素数量上是线性的。这些集合上的迭代器快速失败。 ### WeakHashMap -一个普通的 `Map` 保持对它所包含的所有对象的普通(“强”)引用。这意味着,即使通过地图本身以外的任何方式都无法获得密钥,也无法收集垃圾。通常,这正是我们想要的;在本章开头的示例中,我们将任务映射到客户端,我们不希望映射消失,仅仅是因为我们没有持有对我们放入 `HashMap` 的任务对象的引用。要查找与提供的键相关联的值,`HashMap` 将查找与提供的键相匹配的键(在等同意义上) - 它们不必是物理上相同的对象。 +一个普通的 `Map` 保持对它所包含的所有对象的普通(“强”)引用。这意味着,即使通过地图本身以外的任何方式都无法获得密钥,也无法收集垃圾。通常,这正是我们 +想要的;在本章开头的示例中,我们将任务映射到客户端,我们不希望映射消失,仅仅是因为我们没有持有对我们放入 `HashMap` 的任务对象的引用。要查找与提供的键 +相关联的值,`HashMap` 将查找与提供的键相匹配的键(在等同意义上) - 它们不必是物理上相同的对象。 -但是,假设关键类的对象是唯一的 - 即对象相等与对象标识相同。例如,每个对象可能包含一个唯一的序列号。在这种情况下,一旦我们不再有一个从 `Map` 之外引用的对象 - 被用作关键字的对象,我们再也不能查看它了,因为我们永远无法重新创建它。因此,地图可能会摆脱键值对,事实上,如果地图很大且内存不足,可能会有很强的优势。这是 `WeakHashMap` 实现的想法。 +但是,假设关键类的对象是唯一的 - 即对象相等与对象标识相同。例如,每个对象可能包含一个唯一的序列号。在这种情况下,一旦我们不再有一个从 `Map` 之外引用 +的对象 - 被用作关键字的对象,我们再也不能查看它了,因为我们永远无法重新创建它。因此,地图可能会摆脱键值对,事实上,如果地图很大且内存不足,可能会有很 +强的优势。这是 `WeakHashMap` 实现的想法。 -`WeakHashMap` 内部通过引用类 `java.lang.ref.WeakReference` 来保存对其关键对象的引用(见图 `16-3`(`b`))。 `WeakReference` 在达到一个对象时引入了额外的间接级别。例如,为了对字符串“code gui”做一个弱引用,你可以这样写: +`WeakHashMap` 内部通过引用类 `java.lang.ref.WeakReference` 来保存对其关键对象的引用(见图 `16-3`(`b`))。 `WeakReference` 在达到一个对象时引 +入了额外的间接级别。例如,为了对字符串“code gui”做一个弱引用,你可以这样写: ```java - WeakReference wref = new WeakReference("code gui"); +WeakReference wref = new WeakReference("code gui"); ``` 稍后,您将使用 `WeakReference` 的 `get` 方法恢复强引用: ```java - String recoveredStringRef = wref.get(); +String recoveredStringRef = wref.get(); ``` -如果没有强制引用字符串“code gui”(或从其 `subString` 方法返回的任何子字符串),则弱引用的存在本身不会阻止垃圾回收器回收对象。因此,恢复的参考值 `recoveredStringRef` 可能会或可能不会为 `null`。 +如果没有强制引用字符串“code gui”(或从其 `subString` 方法返回的任何子字符串),则弱引用的存在本身不会阻止垃圾回收器回收对象。因此,恢复的参考值 +`recoveredStringRef` 可能会或可能不会为 `null`。 -要了解 `WeakHashMap` 如何有用,可以考虑通过确定哪些对象可访问并收回所有其他对象来工作的跟踪垃圾回收器。可达性搜索的起点包括当前加载的类的静态变量和当前位于范围内的局部变量。只有强大的参考才能确定可达性,所以如果 `WeakHashMap` 的关键字无法通过任何其他路由到达,它们将可用于回收。请注意,如果从相应的值强烈引用密钥,则无法回收密钥。 (包括它们对应的值)。 +要了解 `WeakHashMap` 如何有用,可以考虑通过确定哪些对象可访问并收回所有其他对象来工作的跟踪垃圾回收器。可达性搜索的起点包括当前加载的类的静态变量和 +当前位于范围内的局部变量。只有强大的参考才能确定可达性,所以如果 `WeakHashMap` 的关键字无法通过任何其他路由到达,它们将可用于回收。请注意,如果从相 +应的值强烈引用密钥,则无法回收密钥。 (包括它们对应的值)。 -在执行 `WeakHashMap` 上的大多数操作之前,映射会检查哪些键已被回收。(检查一个键是否为空是不够的,因为 `null` 是 `WeakHashMap` 中键的合法值。`WeakReference` 机制允许你告诉垃圾收集器在每次它回收一个弱引用对象时在 `ReferenceQueue` 中留下信息。) `WeakHashMap` 然后删除垃圾收集器已经收回密钥的每个条目。 +在执行 `WeakHashMap` 上的大多数操作之前,映射会检查哪些键已被回收。(检查一个键是否为空是不够的,因为 `null` 是 `WeakHashMap` 中键的合法值。 +`WeakReference` 机制允许你告诉垃圾收集器在每次它回收一个弱引用对象时在 `ReferenceQueue` 中留下信息。) `WeakHashMap` 然后删除垃圾收集器已经收回密 +钥的每个条目。 -`WeakHashMap` 的优点是什么?想象你有一个程序,可以根据客户的请求分配一些暂时的系统资源 - 例如缓冲区。除了将对资源的引用传递回客户端,您的程序可能还需要在本地存储关于它的信息 - 例如,将缓冲区与请求它的客户端相关联。这可以通过从资源到客户端对象的映射来实现。但是现在,即使在客户端已经处理了资源之后,地图仍然会保存一个引用,以防止资源对象被垃圾回收 - 如果引用强。内存将逐渐被不再使用的资源耗尽。但是如果引用很弱,由 `WeakHashMap` 保存,垃圾收集器将能够在最后的强引用消失后回收对象,并且防止内存泄漏。 +`WeakHashMap` 的优点是什么?想象你有一个程序,可以根据客户的请求分配一些暂时的系统资源 - 例如缓冲区。除了将对资源的引用传递回客户端,您的程序可能还 +需要在本地存储关于它的信息 - 例如,将缓冲区与请求它的客户端相关联。这可以通过从资源到客户端对象的映射来实现。但是现在,即使在客户端已经处理了资源之 +后,地图仍然会保存一个引用,以防止资源对象被垃圾回收 - 如果引用强。内存将逐渐被不再使用的资源耗尽。但是如果引用很弱,由 `WeakHashMap` 保存,垃圾收集 +器将能够在最后的强引用消失后回收对象,并且防止内存泄漏。 -更常用的用途是在那些应用程序中 - 例如缓存 - 在内存不足的情况下,您不介意信息会消失。在这里,无论密钥是否唯一,`WeakHashMap` 都很有用,因为如果需要的话,您总是可以重新创建一个密钥,以查看相应的值是否仍在缓存中。`WeakHashMap` 对于这个目的并不完美;它的缺点之一是它弱地引用地图的键而不是它的值,这通常占用更多的内存。因此,即使垃圾收集器已经回收了一个密钥,在可用内存方面的真正好处也不会体现出来,除非地图已经删除了旧的条目。第二个缺点是弱引用太弱;垃圾收集器在任何时候都有可能回收弱可达的对象,程序员不能以任何方式影响这个。 (`WeakReference` 的一个姐妹类,`java.lang.ref.SoftReference`,被区别对待:垃圾回收器应该推迟回收它们,直到它受到严重的内存压力。`HeinzKabutz` 使用泛型编写了一个基于 `SoftReference` 的 `Map`;参见http:// www.javaspecialists.co.za/archive/Issue098.html。) +更常用的用途是在那些应用程序中 - 例如缓存 - 在内存不足的情况下,您不介意信息会消失。在这里,无论密钥是否唯一,`WeakHashMap` 都很有用,因为如果需要的 +话,您总是可以重新创建一个密钥,以查看相应的值是否仍在缓存中。`WeakHashMap` 对于这个目的并不完美;它的缺点之一是它弱地引用地图的键而不是它的值,这通 +常占用更多的内存。因此,即使垃圾收集器已经回收了一个密钥,在可用内存方面的真正好处也不会体现出来,除非地图已经删除了旧的条目。第二个缺点是弱引用太弱; +垃圾收集器在任何时候都有可能回收弱可达的对象,程序员不能以任何方式影响这个。 (`WeakReference` 的一个姐妹类,`java.lang.ref.SoftReference`,被区 +别对待:垃圾回收器应该推迟回收它们,直到它受到严重的内存压力。`HeinzKabutz` 使用泛型编写了一个基于 `SoftReference` 的 `Map`;参见 +http:// www.javaspecialists.co.za/archive/Issue098.html。) -`WeakHashMap` 执行类似于 `HashMap`,但由于密钥额外间接级别的间接开销较慢。在每次操作之前清除不需要的键值关联的成本与需要删除的关联数成比例。`WeakHashMap` 返回的键和值集合上的迭代器快速失败。 +`WeakHashMap` 执行类似于 `HashMap`,但由于密钥额外间接级别的间接开销较慢。在每次操作之前清除不需要的键值关联的成本与需要删除的关联数成比例。 +`WeakHashMap` 返回的键和值集合上的迭代器快速失败。 ### IdentityHashMap -`IdentityHashMap` 与普通的 `HashMap` 的不同之处在于,只有两个键在物理上是相同的对象时才被认为是相同的:使用 `identity` 而不是 `equals` 来进行键比较。这为 `IdentityHashMap` 的契约设置与 `Map` 的契约(它实现的接口)的契约不一致,它指定应该使用相等性来进行密钥比较。另一种设计可以通过为 `Map` 提供较弱的合同来避免此问题,其中两个不同的子接口加强合同以指定要使用的密钥比较的类型。这是我们在第 `11.4` 节中讨论的关于平衡框架复杂性与其在合同执行中的精确度之间的权衡的问题的另一个例子。 +`IdentityHashMap` 与普通的 `HashMap` 的不同之处在于,只有两个键在物理上是相同的对象时才被认为是相同的:使用 `identity` 而不是 `equals` 来进行键 +比较。这为 `IdentityHashMap` 的契约设置与 `Map` 的契约(它实现的接口)的契约不一致,它指定应该使用相等性来进行密钥比较。另一种设计可以通过为 `Map` +提供较弱的合同来避免此问题,其中两个不同的子接口加强合同以指定要使用的密钥比较的类型。这是我们在第 `11.4` 节中讨论的关于平衡框架复杂性与其在合同执行 +中的精确度之间的权衡的问题的另一个例子。 ![](16_4.png) 图 `16-4`。 线性探测解决冲突 -`IdentityHashMap` 是一个专门的类,常用于序列化等操作中,其中必须遍历一个图并存储有关每个节点的信息。用于遍历 `Map` 的算法必须能够检查它遇到的每个节点是否已经看到该节点;否则,`Map` 循环可能会无限期地跟随。对于循环图,我们必须使用身份而不是相等来检查节点是否相同。计算两个图形节点对象之间的相等性需要计算其字段的相等性,这反过来意味着计算它们的所有后继 - 并且我们又回到了原始问题。相比之下,`IdentityHashMap` 只有在先前已将同一节点放入地图中时才会报告节点存在。 +`IdentityHashMap` 是一个专门的类,常用于序列化等操作中,其中必须遍历一个图并存储有关每个节点的信息。用于遍历 `Map` 的算法必须能够检查它遇到的每个节 +点是否已经看到该节点;否则,`Map` 循环可能会无限期地跟随。对于循环图,我们必须使用身份而不是相等来检查节点是否相同。计算两个图形节点对象之间的相等性需 +要计算其字段的相等性,这反过来意味着计算它们的所有后继 - 并且我们又回到了原始问题。相比之下,`IdentityHashMap` 只有在先前已将同一节点放入地图中时才 +会报告节点存在。 -`IdentityHashMap` 的标准实现处理冲突的方式与图 `13-2` 中所示的链接方法不同,并且由所有其他 `HashSet` 和 `HashMap` 变体使用。该实现使用称为线性探测的技术,其中键和值引用直接存储在表本身中的相邻位置而不是从其引用的单元中。通过线性探测,碰撞通过简单地沿着台面进行处理,直到找到第一对空闲位置。图 `16-4` 显示了填充容量为 `8` 的 `IdentityHashMap` 的三个阶段。在(`a`)中,我们存储的密钥值对的密钥哈希值为 `0`,(`b`)密钥哈希值为 `4` 的密钥值对。第三(`c`)中添加的密钥也哈希为 `4`,所以算法沿着该表逐步走向,直到找到未使用的位置。在这种情况下,它尝试的第一个索引是 `6`,可以使用。删除比链接更复杂;如果 `key2` 和 `value2` 从图 `13-2` 中的表格中删除,`key3` 和 `value3` 将不得不移动以代替它们。 +`IdentityHashMap` 的标准实现处理冲突的方式与图 `13-2` 中所示的链接方法不同,并且由所有其他 `HashSet` 和 `HashMap` 变体使用。该实现使用称为线性探 +测的技术,其中键和值引用直接存储在表本身中的相邻位置而不是从其引用的单元中。通过线性探测,碰撞通过简单地沿着台面进行处理,直到找到第一对空闲位置。图 +`16-4` 显示了填充容量为 `8` 的 `IdentityHashMap` 的三个阶段。在(`a`)中,我们存储的密钥值对的密钥哈希值为 `0`,(`b`)密钥哈希值为 `4` 的密钥值 +对。第三(`c`)中添加的密钥也哈希为 `4`,所以算法沿着该表逐步走向,直到找到未使用的位置。在这种情况下,它尝试的第一个索引是 `6`,可以使用。删除比链 +接更复杂;如果 `key2` 和 `value2` 从图 `13-2` 中的表格中删除,`key3` 和 `value3` 将不得不移动以代替它们。 -在所有集合框架哈希实现中,为什么当所有其他人使用链接时,`IdentityHashMap` 本身都使用线性探测? 使用探测的动机是它比下面的链表快一些,但只有当对数值的引用可以直接放在数组中时才是真实的,如图16-4所示。 对于所有其他基于散列的集合来说这是不实际的,因为它们存储哈希码以及值。 这是出于效率的原因:`get` 操作必须检查它是否找到了正确的密钥,并且由于 `equal` 是一个昂贵的操作,因此首先检查它是否具有正确的 `hashcode` 是有意义的。 当然,这个推理不适用于 `IdentityHashMap`,它检查对象身份而不是对象相等。 +在所有集合框架哈希实现中,为什么当所有其他人使用链接时,`IdentityHashMap` 本身都使用线性探测? 使用探测的动机是它比下面的链表快一些,但只有当对数值 +的引用可以直接放在数组中时才是真实的,如图16-4所示。 对于所有其他基于散列的集合来说这是不实际的,因为它们存储哈希码以及值。 这是出于效率的原因: +`get` 操作必须检查它是否找到了正确的密钥,并且由于 `equal` 是一个昂贵的操作,因此首先检查它是否具有正确的 `hashcode` 是有意义的。 当然,这个推理不 +适用于 `IdentityHashMap`,它检查对象身份而不是对象相等。 `IdentityHashMap` 有三个构造函数: ```java - public IdentityHashMap() - public IdentityHashMap(Map m) - public IdentityHashMap(int expectedMaxSize) +public IdentityHashMap() +public IdentityHashMap(Map m) +public IdentityHashMap(int expectedMaxSize) ``` -前两个是每个通用 `Map` 实现中的标准构造函数。 第三个取代了两个构造函数,它们在其他 `HashMaps` 中允许用户控制表的初始容量和重新加载的负载因子。`IdentityHashMap` 不允许这样做,而是修改它(在标准实现中为 `.67`),以保护用户免受不适当地设置加载因子的影响:而使用链接在表中查找键的成本与负载因子 `l` 在使用线性探测的表中与 `1/(1-l)` 成比例。因此,避免高负载因素对于程序员来说太重要了!这个决定符合前面提到的策略,即在将新类引入框架时不再提供配置参数。 +前两个是每个通用 `Map` 实现中的标准构造函数。 第三个取代了两个构造函数,它们在其他 `HashMaps` 中允许用户控制表的初始容量和重新加载的负载因子。 +`IdentityHashMap` 不允许这样做,而是修改它(在标准实现中为 `.67`),以保护用户免受不适当地设置加载因子的影响:而使用链接在表中查找键的成本与负载因 +子 `l` 在使用线性探测的表中与 `1/(1-l)` 成比例。因此,避免高负载因素对于程序员来说太重要了!这个决定符合前面提到的策略,即在将新类引入框架时不再提供 +配置参数。 ### EnumMap -由于类似于针对 `EnumSet` 描述的原因,实现枚举类型的映射非常直接且非常高效(请参阅第 `13.1.4` 节); 在数组实现中,每个枚举类型常量的序数值可以作为相应值的索引。`get` 和 `put` 的基本操作可以以数组访问的形式实现。对键集合的迭代器需要的时间与枚举类型中的常量数量成正比,并按照其自然顺序(声明枚举常量的顺序)返回键。尽管和 `EnumSet` 一样,这个类不是线程安全的,但它的集合视图上的迭代器不是快速失败,而是一致性较弱。 +由于类似于针对 `EnumSet` 描述的原因,实现枚举类型的映射非常直接且非常高效(请参阅第 `13.1.4` 节); 在数组实现中,每个枚举类型常量的序数值可以作为相 +应值的索引。`get` 和 `put` 的基本操作可以以数组访问的形式实现。对键集合的迭代器需要的时间与枚举类型中的常量数量成正比,并按照其自然顺序(声明枚举常 +量的顺序)返回键。尽管和 `EnumSet` 一样,这个类不是线程安全的,但它的集合视图上的迭代器不是快速失败,而是一致性较弱。 `EnumMap` 有三个公共构造函数: ```java - EnumMap(Class keyType) // 创建一个空的枚举 Map - EnumMap(EnumMap m) // 创建一个枚举 Map,使用与 m 相同的键类型和元素 - EnumMap(Map m) // 使用提供的 Map 的元素创建枚举映射(除非它是 EnumMap)必须至少包含一个关联 +EnumMap(Class keyType) // 创建一个空的枚举 Map +EnumMap(EnumMap m) // 创建一个枚举 Map,使用与 m 相同的键类型和元素 +EnumMap(Map m) // 使用提供的 Map 的元素创建枚举映射(除非它是 EnumMap)必须至少包含一个关联 ``` -`EnumMap` 包含一个特定的键类型,它在运行时用于检查添加到地图的新条目的有效性。 这种类型由三种构造函数以不同的方式提供。 首先,它是作为一个类别令牌提供的; 在第二个中,它是从指定的 `EnumMap` 复制的。 在第三种情况下,有两种可能性:指定的 `Map` 实际上是一个 `EnumMap`,在这种情况下,它的行为与第二个构造函数类似,或者使用指定 `Map` 的第一个键的类(这就是为什么提供的 `Map` 可能不会空着)。 +`EnumMap` 包含一个特定的键类型,它在运行时用于检查添加到地图的新条目的有效性。 这种类型由三种构造函数以不同的方式提供。 首先,它是作为一个类别令牌提 +供的; 在第二个中,它是从指定的 `EnumMap` 复制的。 在第三种情况下,有两种可能性:指定的 `Map` 实际上是一个 `EnumMap`,在这种情况下,它的行为与第二 +个构造函数类似,或者使用指定 `Map` 的第一个键的类(这就是为什么提供的 `Map` 可能不会空着)。 《《《 [下一节](03_SortedMap_and_NavigableMap.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch16/03_SortedMap_and_NavigableMap.md b/ch16/03_SortedMap_and_NavigableMap.md index c65090eb..bedac240 100644 --- a/ch16/03_SortedMap_and_NavigableMap.md +++ b/ch16/03_SortedMap_and_NavigableMap.md @@ -7,17 +7,21 @@ 图 `16-5` SortedMap -像 `SortedSet` 一样,子接口 `SortedMap`(参见图 `16-5`)增加了它的合约,保证其迭代器将按照升序键顺序遍历映射。它的定义类似于 `SortedSet` 的定义,`firstKey` 和 `headMap` 方法对应于 `SortedSet` 方法 `first` 和 `headSet`。同 `SortedSet` 一样,`SortedMap` 接口已经在 `Java 6` 中通过子接口 `NavigableMap` 进行了扩展(参见图 `16-6`)。由于相同的原因,此部分的结构类似于第 `13.2` 节:`SortedMap` 已被 `NavigableMap` 过时,但可能有帮助对于目前阻止开发人员使用 `Java 6` 来分别处理两个接口。 +像 `SortedSet` 一样,子接口 `SortedMap`(参见图 `16-5`)增加了它的合约,保证其迭代器将按照升序键顺序遍历映射。它的定义类似于 `SortedSet` 的定义, +`firstKey` 和 `headMap` 方法对应于 `SortedSet` 方法 `first` 和 `headSet`。同 `SortedSet` 一样,`SortedMap` 接口已经在 `Java 6` 中通过子接口 +`NavigableMap` 进行了扩展(参见图 `16-6`)。由于相同的原因,此部分的结构类似于第 `13.2` 节:`SortedMap` 已被 `NavigableMap` 过时,但可能有帮助对 +于目前阻止开发人员使用 `Java 6` 来分别处理两个接口。 -`SortedMap` 对其按键进行排序,无论是自然排序还是比较器排序;但无论哪种情况,比较方法都必须与 `equals` 相一致,因为 `SortedMap` 将使用比较来确定密钥何时已经位于地图中。 +`SortedMap` 对其按键进行排序,无论是自然排序还是比较器排序;但无论哪种情况,比较方法都必须与 `equals` 相一致,因为 `SortedMap` 将使用比较来确定密钥 +何时已经位于地图中。 由 `SortedMap` 接口定义的额外方法分为三组: **获取第一个和最后一个要素** ```java - K firstKey() - K lastKey() +K firstKey() +K lastKey() ``` 如果该集合为空,则这些操作会引发 `NoSuchElementException`。 @@ -25,7 +29,7 @@ **检索比较器** ```java - Comparator comparator() +Comparator comparator() ``` 此方法返回映射的键比较器(如果它已被赋予),而不是依赖于键的自然排序。 否则,它返回 `null`。 @@ -33,12 +37,13 @@ **查找子序列** ```java - SortedMap subMap(K fromKey, K toKey) - SortedMap headMap(K toKey) - SortedMap tailMap(K fromKey) +SortedMap subMap(K fromKey, K toKey) +SortedMap headMap(K toKey) +SortedMap tailMap(K fromKey) ``` -这些操作就像 `SortedSet` 中的相应操作一样:关键参数本身不必存在于映射中,并且返回的集合包含 `fromKey` - 如果实际上它存在于映射中 - 并且不包含 `toKey`。 +这些操作就像 `SortedSet` 中的相应操作一样:关键参数本身不必存在于映射中,并且返回的集合包含 `fromKey` - 如果实际上它存在于映射中 - 并且不包含 +`toKey`。 ### NavigableMap @@ -46,76 +51,88 @@ 图 `16-6` NavigableMap -`NavigableMap`(参见图 `16-6` )扩展并替换 `SortedMap`,就像 `NavigableSet` 替换 `SortedSet` 一样。 其方法几乎与 `NavigableSet` 的方法完全一致,将地图视为一组由 `Map.Entry` 对象表示的键值关联。 因此,当 `NavigableSet` 方法返回该集合的元素时,相应的 `NavigableMap` 方法将返回 `Map.Entry` 类型的结果。 到目前为止,这种类型的对象只能通过遍历由 `Map.entrySet` 返回的集合来获得,并且在映射的并发修改面前被指定为无效。该规范不适用于新方法返回的 `Map.Entry` 对象,`NavigableMap` 的契约通过指定由其方法返回的 `Map.Entry` 对象是反映地图在生成时的状态的快照对象,并且不支持 `setValue`。 +`NavigableMap`(参见图 `16-6` )扩展并替换 `SortedMap`,就像 `NavigableSet` 替换 `SortedSet` 一样。 其方法几乎与 `NavigableSet` 的方法完全一 +致,将地图视为一组由 `Map.Entry` 对象表示的键值关联。 因此,当 `NavigableSet` 方法返回该集合的元素时,相应的 `NavigableMap` 方法将返回 +`Map.Entry` 类型的结果。 到目前为止,这种类型的对象只能通过遍历由 `Map.entrySet` 返回的集合来获得,并且在映射的并发修改面前被指定为无效。该规范不适 +用于新方法返回的 `Map.Entry` 对象,`NavigableMap` 的契约通过指定由其方法返回的 `Map.Entry` 对象是反映地图在生成时的状态的快照对象,并且不支持 +`setValue`。 `NavigableMap` 添加的方法可以分为四组。 **获取第一个和最后一个要素** ```java - Map.Entry pollFirstEntry() - Map.Entry pollLastEntry() - Map.Entry firstEntry() - Map.Entry lastEntry() +Map.Entry pollFirstEntry() +Map.Entry pollLastEntry() +Map.Entry firstEntry() +Map.Entry lastEntry() ``` -前两种方法类似于 `NavigableSet` 中的 `pollFirst` 和 `pollLast`。后两个是因为 `NavigableMap` 中关于使地图条目可用的重点需要与从 `SortedMap` 继承的第一个和最后一个密钥返回方法相对应的入口返回方法。 +前两种方法类似于 `NavigableSet` 中的 `pollFirst` 和 `pollLast`。后两个是因为 `NavigableMap` 中关于使地图条目可用的重点需要与从 `SortedMap` 继承 +的第一个和最后一个密钥返回方法相对应的入口返回方法。 **获取范围视图** ```java - NavigableMap subMap( - K fromKey, boolean fromInclusive, K toKey, boolean toInclusive) - NavigableMap headMap(K toKey, boolean inclusive) - NavigableMap tailMap(K fromKey, boolean inclusive) +NavigableMap subMap( +K fromKey, boolean fromInclusive, K toKey, boolean toInclusive) +NavigableMap headMap(K toKey, boolean inclusive) +NavigableMap tailMap(K fromKey, boolean inclusive) ``` -像 `NavigableSet` 方法一样,这些方法比 `SortedMap` 的范围视图方法提供了更大的灵活性。 这些方法不是总是返回一个半开的间隔,而是接受用于确定是否包含定义间隔的一个或多个键的布尔参数。 +像 `NavigableSet` 方法一样,这些方法比 `SortedMap` 的范围视图方法提供了更大的灵活性。 这些方法不是总是返回一个半开的间隔,而是接受用于确定是否包含 +定义间隔的一个或多个键的布尔参数。 **获得最接近的比赛** ```java - Map.Entry ceilingEntry(K Key) - K ceilingKey(K Key) - Map.Entry floorEntry(K Key) - K floorKey(K Key) - Map.Entry higherEntry(K Key) - K higherKey(K Key) - Map.Entry lowerEntry(K Key) - K lowerKey(K Key) +Map.Entry ceilingEntry(K Key) +K ceilingKey(K Key) +Map.Entry floorEntry(K Key) +K floorKey(K Key) +Map.Entry higherEntry(K Key) +K higherKey(K Key) +Map.Entry lowerEntry(K Key) +K lowerKey(K Key) ``` -这些与 `NavigableSet` 的相应最接近匹配方法类似,但它们返回 `Map.Entry` 对象。 如果您想要属于这些条目之一的密钥,请使用相应的便捷键返回方法,同时允许地图避免不必要的创建 `Map.Entry` 对象的性能优势。 +这些与 `NavigableSet` 的相应最接近匹配方法类似,但它们返回 `Map.Entry` 对象。 如果您想要属于这些条目之一的密钥,请使用相应的便捷键返回方法,同时允 +许 `Map` 避免不必要的创建 `Map.Entry` 对象的性能优势。 **浏览 Map** ```java - NavigableMap descendingMap() // 返回 Map 的逆序视图 - NavigableSet descendingKeySet() // 返回一个反序键集 +NavigableMap descendingMap() // 返回 Map 的逆序视图 +NavigableSet descendingKeySet() // 返回一个反序键集 ``` 还有一个新的方法被定义为获得键的 `NavigableSet`: ```java - NavigableSet navigableKeySet() // 返回一个前向顺序键集 +NavigableSet navigableKeySet() // 返回一个前向顺序键集 ``` -您可能想知道为什么从 `Map` 继承的方法 `keySet` 不能简单地使用协变返回类型来重写,以返回 `NavigableSet`。 事实上,`NavigableMap.keySet` 的平台实现返回一个 `NavigableSet`。但是存在一个兼容性问题:如果 `TreeMap.keySet` 的返回类型从 `Set` 更改为 `NavigableSet`,则覆盖该方法的任何现有 `TreeMap` 子类现在将无法编译,除非它们也更改了返回类型。(这一点与 `8.4` 节中讨论的相似。) +您可能想知道为什么从 `Map` 继承的方法 `keySet` 不能简单地使用协变返回类型来重写,以返回 `NavigableSet`。 事实上,`NavigableMap.keySet` 的平台实 +现返回一个 `NavigableSet`。但是存在一个兼容性问题:如果 `TreeMap.keySet` 的返回类型从 `Set` 更改为 `NavigableSet`,则覆盖该方法的任何现有 +`TreeMap` 子类现在将无法编译,除非它们也更改了返回类型。(这一点与 `8.4` 节中讨论的相似。) ### TreeMap -`SortedMap` 是通过 `TreeMap` 在集合框架中实现的。 当我们讨论 `TreeSet` 时,我们遇到了树作为存储元素的数据结构(请参阅第 `13.2.2` 节)。 实际上,`TreeSet` 的内部表示只是一个 `TreeMap`,其中每个键都与相同的标准值相关联,因此第 `13.2.2` 节中给出的红黑树的机制和性能的解释同样适用于此处。 +`SortedMap` 是通过 `TreeMap` 在集合框架中实现的。 当我们讨论 `TreeSet` 时,我们遇到了树作为存储元素的数据结构(请参阅第 `13.2.2` 节)。 实际上, +`TreeSet` 的内部表示只是一个 `TreeMap`,其中每个键都与相同的标准值相关联,因此第 `13.2.2` 节中给出的红黑树的机制和性能的解释同样适用于此处。 -`TreeMap` 的构造函数除了标准的构造函数之外,还包括一个允许您提供一个 `Comparator` 的构造函数,另一个允许您从另一个 `SortedMap` 创建一个,同时使用相同的比较器和相同的映射: +`TreeMap` 的构造函数除了标准的构造函数之外,还包括一个允许您提供一个 `Comparator` 的构造函数,另一个允许您从另一个 `SortedMap` 创建一个,同时使用 +相同的比较器和相同的映射: ```java - public TreeMap(Comparator comparator) - public TreeMap(SortedMap m) +public TreeMap(Comparator comparator) +public TreeMap(SortedMap m) ``` -请注意,这些构造函数中的第二个与 `TreeSet` 的相应构造函数有类似的问题(请参阅第 `13.2.2` 节),因为标准转换构造函数始终使用键的自然顺序,即使其参数实际上是 `SortedMap`。 +请注意,这些构造函数中的第二个与 `TreeSet` 的相应构造函数有类似的问题(请参阅第 `13.2.2` 节),因为标准转换构造函数始终使用键的自然顺序,即使其参数 +实际上是 `SortedMap`。 `TreeMap` 与 `TreeSet` 具有相似的性能特征:基本操作(`get`,`put` 和 `remove`)在 `O(log n)` 时间执行)。 集合视图迭代器是快速失败的。 《《《 [下一节](04_ConcurrentMap.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch16/04_ConcurrentMap.md b/ch16/04_ConcurrentMap.md index 19702597..7f1f71c5 100644 --- a/ch16/04_ConcurrentMap.md +++ b/ch16/04_ConcurrentMap.md @@ -3,33 +3,42 @@ ### ConcurrentMap -`Map` 通常用于高性能服务器应用程序中 - 例如,用作缓存实现 - 因此高吞吐量线程安全的地图实现是 `Java` 平台的重要组成部分。 同步地图(例如由 `Collections.synchronizedMap` 提供的 `Map`)无法满足此要求,因为在完全同步的情况下,每个操作都需要在整个地图上获得排他锁,从而有效地序列化对它的访问。 我们很快就会看到 `ConcurrentHashMap`,可以在吞吐量上获得非常大的增益,只锁定一部分集合。 但是因为客户端没有单独的锁来获得独占访问权限,所以客户端锁定不再起作用,并且客户端需要来自集合本身的帮助来执行原子动作。 +`Map` 通常用于高性能服务器应用程序中 - 例如,用作缓存实现 - 因此高吞吐量线程安全的 `Map` 实现是 `Java` 平台的重要组成部分。 同步 `Map`(例如由 +`Collections.synchronizedMap` 提供的 `Map`)无法满足此要求,因为在完全同步的情况下,每个操作都需要在整个地图上获得排他锁,从而有效地序列化对它的访 +问。 我们很快就会看到 `ConcurrentHashMap`,可以在吞吐量上获得非常大的增益,只锁定一部分集合。 但是因为客户端没有单独的锁来获得独占访问权限,所以客 +户端锁定不再起作用,并且客户端需要来自集合本身的帮助来执行原子动作。 这就是 `ConcurrentMap` 接口的目的。 它为以原子方式执行复合操作的方法提供声明。 有四种方法: ```java - V putIfAbsent(K key, V value) // 仅当密钥当前不存在时才将密钥与值关联。 如果键存在,则返回旧值(可以为空),否则返回 null - boolean remove(Object key, Object value) // 只有在当前映射为值的情况下才移除键。 如果该值已删除,则返回 true,否则返回 false - V replace(K key, V value) // 仅当密钥当前存在时才替换密钥的条目。 如果密钥存在,则返回旧值(可以为 null),否则返回 null - boolean replace(K key, V oldValue, V newValue) // 只有在当前映射到 oldValue 的情况下才替换键的条目。 如果值被替换则返回 true,否则返回 false +V putIfAbsent(K key, V value) // 仅当密钥当前不存在时才将密钥与值关联。 如果键存在,则返回旧值(可以为空),否则返回 null +boolean remove(Object key, Object value) // 只有在当前映射为值的情况下才移除键。 如果该值已删除,则返回 true,否则返回 false +V replace(K key, V value) // 仅当密钥当前存在时才替换密钥的条目。 如果密钥存在,则返回旧值(可以为 null),否则返回 null +boolean replace(K key, V oldValue, V newValue) // 只有在当前映射到 oldValue 的情况下才替换键的条目。 如果值被替换则返回 true,否则返回 false ``` ### ConcurrentHashMap -`ConcurrentHashMap` 提供了 `ConcurrentMap` 的实现,并提供了一个解决吞吐量与线程安全性问题的高效解决方案。 它针对阅读进行了优化,因此即使在更新表时检索也不会被阻止(为此,合同规定检索结果将反映在检索开始之前完成的最新更新操作)。 更新通常也可以不受阻塞地进行,因为 `ConcurrentHashMap` 不是由一个而是由一组表组成,称为段,它们可以独立锁定。 如果段的数量相对于访问表的线程数量足够大,那么每个段的任何时间通常不会有多个更新正在进行。`ConcurrentHashMap` 的构造函数类似于 `HashMap` 的构造函数,但是还有一个额外的提供程序员控制 `Map` 将使用的段数(其并发级别): +`ConcurrentHashMap` 提供了 `ConcurrentMap` 的实现,并提供了一个解决吞吐量与线程安全性问题的高效解决方案。 它针对阅读进行了优化,因此即使在更新表时 +检索也不会被阻止(为此,合同规定检索结果将反映在检索开始之前完成的最新更新操作)。 更新通常也可以不受阻塞地进行,因为 `ConcurrentHashMap` 不是由一 +个而是由一组表组成,称为段,它们可以独立锁定。 如果段的数量相对于访问表的线程数量足够大,那么每个段的任何时间通常不会有多个更新正在进行。 +`ConcurrentHashMap` 的构造函数类似于 `HashMap` 的构造函数,但是还有一个额外的提供程序员控制 `Map` 将使用的段数(其并发级别): ```java - ConcurrentHashMap() - ConcurrentHashMap(int initialCapacity) - ConcurrentHashMap(int initialCapacity, float loadFactor) - ConcurrentHashMap( - int initialCapacity, float loadFactor, int concurrencyLevel) - ConcurrentHashMap(Map m) +ConcurrentHashMap() +ConcurrentHashMap(int initialCapacity) +ConcurrentHashMap(int initialCapacity, float loadFactor) +ConcurrentHashMap( +int initialCapacity, float loadFactor, int concurrencyLevel) +ConcurrentHashMap(Map m) ``` -`ConcurrentHashMap` 类是在任何不需要锁定整个表的应用程序中使用 `Map` 的有用实现;这是 `SynchronizedMap` 的一个不支持的功能。这有时会带来问题:例如,`size` 方法尝试在不使用锁的情况下对地图中的条目进行计数。但是,如果地图正在同时更新,则尺寸方法将无法获得一致的结果。在这种情况下,它通过锁定所有分段获得对地图的独占访问权,从每个分段中获取入口数,然后再次解锁它们。与此相关的性能成本对于常见操作的高度优化性能(尤其是读取)来说是合理的折衷。总体而言,`ConcurrentHashMap` 在高度并发的上下文中是不可或缺的,它比任何可用的替代方法都要好得多。 +`ConcurrentHashMap` 类是在任何不需要锁定整个表的应用程序中使用 `Map` 的有用实现;这是 `SynchronizedMap` 的一个不支持的功能。这有时会带来问题:例 +如,`size` 方法尝试在不使用锁的情况下对地图中的条目进行计数。但是,如果地图正在同时更新,则尺寸方法将无法获得一致的结果。在这种情况下,它通过锁定所有 +分段获得对地图的独占访问权,从每个分段中获取入口数,然后再次解锁它们。与此相关的性能成本对于常见操作的高度优化性能(尤其是读取)来说是合理的折衷。总 +体而言,`ConcurrentHashMap` 在高度并发的上下文中是不可或缺的,它比任何可用的替代方法都要好得多。 忽略刚刚描述的锁定开销,`ConcurrentHashMap` 的操作成本类似于 `HashMap` 的操作成本。集合视图返回弱一致的迭代器。 《《《 [下一节](05_ConcurrentNavigableMap.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch16/05_ConcurrentNavigableMap.md b/ch16/05_ConcurrentNavigableMap.md index 0fbbf165..657ae163 100644 --- a/ch16/05_ConcurrentNavigableMap.md +++ b/ch16/05_ConcurrentNavigableMap.md @@ -7,11 +7,16 @@ 图 `16-7` `ConcurrentNavigableMap` -`ConcurrentNavigableMap`(参见图 `16-7`)从 `ConcurrentMap` 和 `NavigableMap` 都继承,并且只包含这两个接口的方法,并进行一些更改以使返回类型更加精确。从 `SortedMap` 和 `NavigableMap` 继承的范围视图方法现在返回 `ConcurrentNavigableMap` 类型的视图。兼容性问题阻止 `NavigableMap` 覆盖 `SortedMap` 的方法,这里不适用于覆盖 `NavigableMap` 或 `SortedMap` 的 `rangeview` 方法; 因为这些都没有任何已经改进到新接口的实现,所以不会出现打破实现子类的危险。 出于同样的原因,现在可以覆盖 `keySet` 以返回 `NavigableSet`。 +`ConcurrentNavigableMap`(参见图 `16-7`)从 `ConcurrentMap` 和 `NavigableMap` 都继承,并且只包含这两个接口的方法,并进行一些更改以使返回类型更加精 +确。从 `SortedMap` 和 `NavigableMap` 继承的范围视图方法现在返回 `ConcurrentNavigableMap` 类型的视图。兼容性问题阻止 `NavigableMap` 覆盖 +`SortedMap` 的方法,这里不适用于覆盖 `NavigableMap` 或 `SortedMap` 的 `rangeview` 方法; 因为这些都没有任何已经改进到新接口的实现,所以不会出现打破 +实现子类的危险。 出于同样的原因,现在可以覆盖 `keySet` 以返回 `NavigableSet`。 ### ConcurrentSkipListMap -`ConcurrentSkipListMap` 和 `ConcurrentSkipListSet` 之间的关系类似于 `TreeMap` 和 `TreeSet` 之间的关系;`ConcurrentSkipListSet` 由 `ConcurrentSkipListMap` 实现,其中每个键与相同的标准值相关联,所以第 `13.2.3` 节中给出的跳过列表实现的机制和性能在这里同样适用:基本操作(`get`,`put` 和 `remove`) 在 `O(log n)` 时间执行); 集合视图上的迭代器会在下一个时间内执行。这些迭代器是快速失败的。 +`ConcurrentSkipListMap` 和 `ConcurrentSkipListSet` 之间的关系类似于 `TreeMap` 和 `TreeSet` 之间的关系;`ConcurrentSkipListSet` 由 +`ConcurrentSkipListMap` 实现,其中每个键与相同的标准值相关联,所以第 `13.2.3` 节中给出的跳过列表实现的机制和性能在这里同样适用:基本操作(`get`, +`put` 和 `remove`) 在 `O(log n)` 时间执行); 集合视图上的迭代器会在下一个时间内执行。这些迭代器是快速失败的。 《《《 [下一节](06_Comparing_Map_Implementations.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch16/06_Comparing_Map_Implementations.md b/ch16/06_Comparing_Map_Implementations.md index a2ecd745..1e03db2a 100644 --- a/ch16/06_Comparing_Map_Implementations.md +++ b/ch16/06_Comparing_Map_Implementations.md @@ -21,7 +21,8 @@ TreeMap |O(log n) |O(log n)| O(log n)| ConcurrentHashMap |O(1) |O(1) |O(h/n)| h是表格容量 ConcurrentSkipListMap| O(log n)| O(log n)| O(1)| -这就为通用地图留下了实施的选择。 对于并发应用程序,`ConcurrentHashMap` 是唯一的选择。 否则,如果您需要使用映射的插入或访问顺序(例如,将其用作缓存),则可以优先使用 `HashMap` 上的 `LinkedHashMap`(并接受其稍差的性能)。 +这就为通用地图留下了实施的选择。 对于并发应用程序,`ConcurrentHashMap` 是唯一的选择。 否则,如果您需要使用映射的插入或访问顺序(例如,将其用作缓存), +则可以优先使用 `HashMap` 上的 `LinkedHashMap`(并接受其稍差的性能)。 《《《 [下一节](../ch17/00_The_Collections_Class.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch17/00_The_Collections_Class.md b/ch17/00_The_Collections_Class.md index 5fabed45..3ce9c5c5 100644 --- a/ch17/00_The_Collections_Class.md +++ b/ch17/00_The_Collections_Class.md @@ -3,9 +3,10 @@ ## 集合类 -`java.util.Collections` 类完全由静态方法组成,这些静态方法对集合进行操作或返回集合。 有三个主要类别:泛型算法,返回空或预填充集合的方法以及创建包装器的方法。 我们依次讨论这三个类别,然后是其他一些不适合整洁分类的方法。 +`java.util.Collections` 类完全由静态方法组成,这些静态方法对集合进行操作或返回集合。 有三个主要类别:泛型算法,返回空或预填充集合的方法以及创建包装器 +的方法。 我们依次讨论这三个类别,然后是其他一些不适合整洁分类的方法。 集合的所有方法都是公共和静态的,所以为了可读性,我们将从各个声明中省略这些修饰符。 《《《 [下一节](01_Generic_Algorithms.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch17/01_Generic_Algorithms.md b/ch17/01_Generic_Algorithms.md index 71fcf981..4c1865da 100644 --- a/ch17/01_Generic_Algorithms.md +++ b/ch17/01_Generic_Algorithms.md @@ -3,11 +3,13 @@ ### 通用算法 -通用算法分为四个主要类别:更改列表中的元素顺序,更改列表内容,查找集合中的极端值以及查找列表中的特定值。它们表示可重用的功能,因为它们可以应用于任何类型的列表(或在某些情况下适用于集合)。生成这些方法的类型导致了一些相当复杂的声明,因此每个部分都在声明之后简要地讨论这些声明。 +通用算法分为四个主要类别:更改列表中的元素顺序,更改列表内容,查找集合中的极端值以及查找列表中的特定值。它们表示可重用的功能,因为它们可以应用于任何 +类型的列表(或在某些情况下适用于集合)。生成这些方法的类型导致了一些相当复杂的声明,因此每个部分都在声明之后简要地讨论这些声明。 ### 更改列表元素的顺序 -有七种方法以各种方式对列表进行重新排序。 其中最简单的是 `swap`,它交换两个元素,并且在实现 `RandomAccess` 的 `List` 的情况下,执行时间不变。 最复杂的是排序,它将元素传输到数组中,在时间 `O(n log n)` 中对它们应用合并排序,然后将它们返回到 `List`。 所有其余的方法在时间 `O(n)` 中执行。 +有七种方法以各种方式对列表进行重新排序。 其中最简单的是 `swap`,它交换两个元素,并且在实现 `RandomAccess` 的 `List` 的情况下,执行时间不变。 最 +复杂的是排序,它将元素传输到数组中,在时间 `O(n log n)` 中对它们应用合并排序,然后将它们返回到 `List`。 所有其余的方法在时间 `O(n)` 中执行。 ```java void reverse(List list) // 颠倒元素的顺序 @@ -19,32 +21,40 @@ void shuffle(List list, Random rnd) // 用随机性源rnd随机排列列表 void swap(List list, int i, int j) // 交换指定位置的元素 ``` -对于这些方法中的每一种,除了排序和交换,都有两种算法,一种使用迭代,另一种使用随机访问。 方法 `sort` 将 `List` 元素传递给一个数组,在当前实现中它们使用 `n log n` 性能的 `mergesort` 算法进行排序。 方法交换总是使用随机访问。本节中其他方法的标准实现使用迭代或随机访问,具体取决于列表是否实现了 `RandomAccess` 接口(请参阅第 `8.3` 节)。 如果是这样,则实现选择随机访问算法; 即使对于没有实现 `RandomAccess` 的列表,但是,如果列表大小低于给定阈值,则使用随机访问算法,这是通过性能测试在每个方法基础上确定的。 +对于这些方法中的每一种,除了排序和交换,都有两种算法,一种使用迭代,另一种使用随机访问。 方法 `sort` 将 `List` 元素传递给一个数组,在当前实现中它们 +使用 `n log n` 性能的 `mergesort` 算法进行排序。 方法交换总是使用随机访问。本节中其他方法的标准实现使用迭代或随机访问,具体取决于列表是否实现了 +`RandomAccess` 接口(请参阅第 `8.3` 节)。 如果是这样,则实现选择随机访问算法; 即使对于没有实现 `RandomAccess` 的列表,但是,如果列表大小低于给定 +阈值,则使用随机访问算法,这是通过性能测试在每个方法基础上确定的。 ### 更改列表的内容 -这些方法改变了列表的一些或全部元素。 该方法拷贝将源列表中的元素转移到目的地列表的初始子列表中(其必须足够长以容纳它们),从而使目的地列表的任何剩余元素保持不变。方法 `fill` 使用指定对象替换列表中的每个元素,并且 `replaceAll` 将列表中每个值的每次出现替换为另一个值,其中旧值或新值可以为空 - 如果进行了替换,则返回 `true`。 +这些方法改变了列表的一些或全部元素。 该方法拷贝将源列表中的元素转移到目的地列表的初始子列表中(其必须足够长以容纳它们),从而使目的地列表的任何剩余元 +素保持不变。方法 `fill` 使用指定对象替换列表中的每个元素,并且 `replaceAll` 将列表中每个值的每次出现替换为另一个值,其中旧值或新值可以为空 - 如果进 +行了替换,则返回 `true`。 ```java - void copy(List dest, List src) // 将所有元素从一个列表复制到另一个列表中 - void fill(List list, T obj) // 用obj替换列表中的每个元素 - boolean replaceAll(List list, T oldVal, T newVal) // 用newVal替换列表中出现的所有oldVal + void copy(List dest, List src) // 将所有元素从一个列表复制到另一个列表中 + void fill(List list, T obj) // 用obj替换列表中的每个元素 + boolean replaceAll(List list, T oldVal, T newVal) // 用newVal替换列表中出现的所有oldVal ``` -这些方法的签名可以使用 `Get` 和 `Put` 原则来解释(参见第 `2.4` 节)。`2.3` 节讨论了拷贝的签名。它从源列表中获取元素并将它们放入目标中,因此这些列表的类型分别是 `? extends T` 和 `? super T`。 这符合直觉,源列表元素的类型应该是目标列表的子类型。 虽然复制签名有更简单的选择,但 `2.3` 节规定尽可能使用通配符扩大了允许的呼叫范围。 +这些方法的签名可以使用 `Get` 和 `Put` 原则来解释(参见第 `2.4` 节)。`2.3` 节讨论了拷贝的签名。它从源列表中获取元素并将它们放入目标中,因此这些列表 +的类型分别是 `? extends T` 和 `? super T`。 这符合直觉,源列表元素的类型应该是目标列表的子类型。 虽然复制签名有更简单的选择,但 `2.3` 节规定尽可能 +使用通配符扩大了允许的呼叫范围。 -对于填充,`Get` 和 `Put` 原则规定,如果要将值放入参数化集合中,应该使用 `super`;对于 `replaceAll`,它指出如果要将值放入并从同一结构中获取值,则不应使用完全通配符。 +对于填充,`Get` 和 `Put` 原则规定,如果要将值放入参数化集合中,应该使用 `super`;对于 `replaceAll`,它指出如果要将值放入并从同一结构中获取值,则不 +应使用完全通配符。 ### 在集合中查找极端值 方法 `min` 和 `max` 是为此目的提供的,每个方法都有两个重载 - 一个使用元素的自然顺序,一个接受比较器强制排序。 它们以线性时间执行。 ```java - > T max(Collection coll) // 使用自然顺序返回最大元素 - T max(Collection coll, Comparator comp) // 使用提供的比较器返回最大元素 - > - T min(Collection coll) // 使用自然顺序返回最小元素 - T min(Collection coll, Comparator comp) // 使用提供的比较器返回最小元素 +> T max(Collection coll) // 使用自然顺序返回最大元素 + T max(Collection coll, Comparator comp) // 使用提供的比较器返回最大元素 +> +T min(Collection coll) // 使用自然顺序返回最小元素 + T min(Collection coll, Comparator comp) // 使用提供的比较器返回最小元素 ``` 在列表中查找特定值 @@ -52,17 +62,21 @@ void swap(List list, int i, int j) // 交换指定位置的元素 该组中的方法在 `List` 中找到元素或元素组,再次根据列表的大小以及是否实现 `RandomAccess` 在替代算法之间进行选择。 ```java - int binarySearch(List> list, T key) // 使用二分查找搜索密钥 - int binarySearch(List list, T key, Comparator c) // 使用二分查找搜索密钥 - int indexOfSubList(List source, List target) // 找到匹配目标的源的第一个子列表 - int lastIndexOfSubList(List source, List target) // 找到与目标匹配的源的最后一个子列表 + int binarySearch(List> list, T key) // 使用二分查找搜索密钥 + int binarySearch(List list, T key, Comparator c) // 使用二分查找搜索密钥 +int indexOfSubList(List source, List target) // 找到匹配目标的源的第一个子列表 +int lastIndexOfSubList(List source, List target) // 找到与目标匹配的源的最后一个子列表 ``` -第一个 `binarySearch` 重载的签名表示可以使用它在对象列表中搜索类型为 `T` 的键,该对象列表中可以具有任何类型,可以与T类型的对象进行比较。第二种类似于比较器 `min` 和 `max` 的重载,除了在这种情况下,`Collection` 的类型参数必须是键类型的子类型,而该类型又必须是 `Comparator` 的类型参数的子类型。 +第一个 `binarySearch` 重载的签名表示可以使用它在对象列表中搜索类型为 `T` 的键,该对象列表中可以具有任何类型,可以与T类型的对象进行比较。第二种类似 +于比较器 `min` 和 `max` 的重载,除了在这种情况下,`Collection` 的类型参数必须是键类型的子类型,而该类型又必须是 `Comparator` 的类型参数的子类型。 -二进制搜索需要一个排序列表来进行操作。在搜索开始时,搜索值可能出现的索引范围对应于整个列表。二分搜索算法使用采样元素的值来采样该范围中间的元素,以确定新范围是上一个旧范围的一部分还是元素索引下的部分。第三种可能性是采样值等于搜索值,在这种情况下搜索完成。由于每个步骤将范围的大小减半,因此需要m个步骤才能在长度为 `2 m` 的列表中找到搜索值,并且长度为n的列表的时间复杂度为 `O(log n)`。 +二进制搜索需要一个排序列表来进行操作。在搜索开始时,搜索值可能出现的索引范围对应于整个列表。二分搜索算法使用采样元素的值来采样该范围中间的元素,以确 +定新范围是上一个旧范围的一部分还是元素索引下的部分。第三种可能性是采样值等于搜索值,在这种情况下搜索完成。由于每个步骤将范围的大小减半,因此需要m个步 +骤才能在长度为 `2 m` 的列表中找到搜索值,并且长度为n的列表的时间复杂度为 `O(log n)`。 -`indexOfSubList` 和 `lastIndexOfSubList` 方法的操作不需要排序列表。他们的签名允许源列表和目标列表包含任何类型的元素(请记住,这两个通配符可能代表两种不同的类型)。这些签名背后的设计决策与 `Collection` 方法 `containsAll`,`retainAll`和removeAll背后的设计决策相同(参见第 `2.6` 节)。 +`indexOfSubList` 和 `lastIndexOfSubList` 方法的操作不需要排序列表。他们的签名允许源列表和目标列表包含任何类型的元素(请记住,这两个通配符可能代表 +两种不同的类型)。这些签名背后的设计决策与 `Collection` 方法 `containsAll`,`retainAll`和 `removeAll` 背后的设计决策相同(参见第 `2.6` 节)。 《《《 [下一节](02_Collection_Factories.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch17/02_Collection_Factories.md b/ch17/02_Collection_Factories.md index 3bf07ff7..d5b6a58a 100644 --- a/ch17/02_Collection_Factories.md +++ b/ch17/02_Collection_Factories.md @@ -6,19 +6,21 @@ `Collections` 类提供了创建某些包含零个或多个对同一对象的引用的集合的简便方法。 最简单的这种集合是空的: ```java - List emptyList() // 返回空列表(不可变) - Map emptyMap() // 返回空映射(不可变) - Set emptySet() // 返回空集(不可变的) + List emptyList() // 返回空列表(不可变) + Map emptyMap() // 返回空映射(不可变) + Set emptySet() // 返回空集(不可变的) ``` -空集合在实现返回值集合的方法时很有用,它们可以用来表示没有值返回。 每个方法都返回一个对 `Collection` 的单例内部类的实例的引用。 因为这些实例是不可变的,所以它们可以安全地共享,所以调用其中一个工厂方法不会导致创建对象。 在泛型之前,`Collections` 字段 `EMPTY_SET`,`EMPTY_LIST` 和 `EMPTY_MAP` 通常用于 `Java` 中的相同目的,但现在用处不大,因为它们的原始类型在使用时会生成未经检查的警告。 +空集合在实现返回值集合的方法时很有用,它们可以用来表示没有值返回。 每个方法都返回一个对 `Collection` 的单例内部类的实例的引用。 因为这些实例是不可变 +的,所以它们可以安全地共享,所以调用其中一个工厂方法不会导致创建对象。 在泛型之前,`Collections` 字段 `EMPTY_SET`,`EMPTY_LIST` 和 `EMPTY_MAP` 通常 +用于 `Java` 中的相同目的,但现在用处不大,因为它们的原始类型在使用时会生成未经检查的警告。 `Collections` 类还为您提供了创建仅包含单个成员的集合对象的方法: ```java - Set singleton(T o) // 返回只包含指定对象的不可变集合 - List singletonList(T o) // 返回只包含指定对象的不可变列表 - Map singletonMap(K key, V value) // 返回一个不可变的映射,只将键 K 映射到值 V. + Set singleton(T o) // 返回只包含指定对象的不可变集合 + List singletonList(T o) // 返回只包含指定对象的不可变列表 + Map singletonMap(K key, V value) // 返回一个不可变的映射,只将键 K 映射到值 V. ``` 同样,这些可以用于为接受值集合的方法提供单个输入值。 @@ -26,10 +28,11 @@ 最后,可以创建一个包含给定对象的许多副本的列表。 ```java - List nCopies(int n, T o) // 返回一个不可变列表,其中包含对对象 o 的 n 个引用 + List nCopies(int n, T o) // 返回一个不可变列表,其中包含对对象 o 的 n 个引用 ``` -由于 `nCopies` 生成的列表是不可变的,它只需要包含一个物理元素来提供所需长度的列表视图。这样的列表通常被用作构建进一步集合的基础 - 例如,作为构造函数或 `addAll` 方法的参数。 +由于 `nCopies` 生成的列表是不可变的,它只需要包含一个物理元素来提供所需长度的列表视图。这样的列表通常被用作构建进一步集合的基础 - 例如,作为构造函数或 +`addAll` 方法的参数。 《《《 [下一节](03_Wrappers.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch17/03_Wrappers.md b/ch17/03_Wrappers.md index ed17d21c..ec2d5f3e 100644 --- a/ch17/03_Wrappers.md +++ b/ch17/03_Wrappers.md @@ -3,56 +3,68 @@ ### 包装 -`Collections` 类提供了包装对象,通过以下三种方式之一来修改标准集合类的行为:通过同步它们,使其不可修改,或通过检查添加到它们的元素的类型。这些包装器对象实现与包装对象相同的接口,并将它们的工作委托给它们。他们的目的是限制在哪些情况下进行这项工作。这些是使用保护代理的例子(参见 `Gamma`,`Helm`,`Johnson` 和 `Vlissides`,`Addison-Wesley` 的设计模式),代理模式的一个变体,代理控制对真实主体的访问。 +`Collections` 类提供了包装对象,通过以下三种方式之一来修改标准集合类的行为:通过同步它们,使其不可修改,或通过检查添加到它们的元素的类型。这些包装器 +对象实现与包装对象相同的接口,并将它们的工作委托给它们。他们的目的是限制在哪些情况下进行这项工作。这些是使用保护代理的例子(参见 `Gamma`,`Helm`, +`Johnson` 和 `Vlissides`,`Addison-Wesley` 的设计模式),代理模式的一个变体,代理控制对真实主体的访问。 -代理可以用不同的方式创建。在这里,它们是由工厂方法创建的,这些方法将提供的集合对象包装在实现集合接口的集合的内部类中。随后,对代理的方法调用(主要)委托给集合对象,但代理控制调用的条件:在同步包装器的情况下,所有方法调用都被委派,但代理使用同步来确保集合一次只能由一个线程访问。在不可修改和检查集合的情况下,打破代理合同的方法调用失败,抛出相应的异常。 +代理可以用不同的方式创建。在这里,它们是由工厂方法创建的,这些方法将提供的集合对象包装在实现集合接口的集合的内部类中。随后,对代理的方法调用(主要) +委托给集合对象,但代理控制调用的条件:在同步包装器的情况下,所有方法调用都被委派,但代理使用同步来确保集合一次只能由一个线程访问。在不可修改和检查集 +合的情况下,打破代理合同的方法调用失败,抛出相应的异常。 ### 同步集合 -正如我们在第 `11.5` 节中解释的那样,大多数框架类都不是线程安全的 - 通过设计 - 为了避免不必要的同步开销(如传统类 `Vector` 和 `Hashtable` 引起的)。 但有时您需要编程多个线程才能访问相同的集合,并且这些同步的包装由 `Collections` 类为这种情况提供。 +正如我们在第 `11.5` 节中解释的那样,大多数框架类都不是线程安全的 - 通过设计 - 为了避免不必要的同步开销(如传统类 `Vector` 和 `Hashtable` 引起 +的)。 但有时您需要编程多个线程才能访问相同的集合,并且这些同步的包装由 `Collections` 类为这种情况提供。 -有六个同步包装工厂方法,对应于集合框架的六个预 `Java 6` 接口。 (在 `Java 6` 中没有为 `NavigableSet` 或 `NavigableMap` 提供同步包装器,如果它们已经提供,那么在极少数情况下,您会选择它们而不是线程安全集合 `ConcurrentSkipListSet` 和 `ConcurrentSkipListMap`。) +有六个同步包装工厂方法,对应于集合框架的六个预 `Java 6` 接口。 (在 `Java 6` 中没有为 `NavigableSet` 或 `NavigableMap` 提供同步包装器,如果它们 +已经提供,那么在极少数情况下,您会选择它们而不是线程安全集合 `ConcurrentSkipListSet` 和 `ConcurrentSkipListMap`。) ```java - Collection synchronizedCollection(Collection c); - Set synchronizedSet(Set s); - List synchronizedList(List list); - Map synchronizedMap(Map m); - SortedSet synchronizedSortedSet(SortedSet s); - SortedMap synchronizedSortedMap(SortedMap m); + Collection synchronizedCollection(Collection c); + Set synchronizedSet(Set s); + List synchronizedList(List list); + Map synchronizedMap(Map m); + SortedSet synchronizedSortedSet(SortedSet s); + SortedMap synchronizedSortedMap(SortedMap m); ``` -提供这些同步视图的类是有条件线程安全的(参见 `11.5` 节); 尽管他们的每个操作都是原子操作,但您可能需要同步多个方法调用才能获得一致的行为。 特别是,迭代器必须在集合中同步的代码块中完全创建和使用; 否则,`ConcurrentModificationException` 结果最多只会失败。这是非常粗粒度的同步; 如果您的应用程序大量使用同步集合,则其有效并发性将大大降低。 +提供这些同步视图的类是有条件线程安全的(参见 `11.5` 节); 尽管他们的每个操作都是原子操作,但您可能需要同步多个方法调用才能获得一致的行为。 特别是, +迭代器必须在集合中同步的代码块中完全创建和使用; 否则,`ConcurrentModificationException` 结果最多只会失败。这是非常粗粒度的同步; 如果您的应用程序大 +量使用同步集合,则其有效并发性将大大降低。 ### 不可修改的集合 -一个不可修改的集合将抛出 `UnsupportedOperationException` 来响应任何尝试更改其结构或构成它的元素。当您希望允许客户端读取内部数据结构时,这会很有用。以不可修改的包装传递结构将阻止客户更改它。它不会阻止客户更改其包含的对象,如果它们是可修改的。在某些情况下,您可能必须通过为客户提供为此目的而制作的防御性副本或通过将这些对象置于不可修改的包装中来保护内部数据结构。 +一个不可修改的集合将抛出 `UnsupportedOperationException` 来响应任何尝试更改其结构或构成它的元素。当您希望允许客户端读取内部数据结构时,这会很有 +用。以不可修改的包装传递结构将阻止客户更改它。它不会阻止客户更改其包含的对象,如果它们是可修改的。在某些情况下,您可能必须通过为客户提供为此目的而制 +作的防御性副本或通过将这些对象置于不可修改的包装中来保护内部数据结构。 有六个不可修改的包装工厂方法,对应于集合框架的六个主要接口: ```java - Collection unmodifiableCollection(Collection c) - Set unmodifiableSet(Set s) - List unmodifiableList(List list) - Map unmodifiableMap(Map m) - SortedSet unmodifiableSortedSet(SortedSet s) - SortedMap unmodifiableSortedMap(SortedMap m) + Collection unmodifiableCollection(Collection c) + Set unmodifiableSet(Set s) + List unmodifiableList(List list) + Map unmodifiableMap(Map m) + SortedSet unmodifiableSortedSet(SortedSet s) + SortedMap unmodifiableSortedMap(SortedMap m) ``` ### 检查集合 -来自编译器的未经检查的警告是我们要特别注意避免运行时类型违规的信号。 例如,在我们将一个类型化的集合引用传递给 `ungenerified` 库方法后,我们无法确定它只向集合中添加了正确类型的元素。 我们可以传入一个选中的包装器,而不是失去对集合类型安全性的置信度,它将测试每个添加到集合中的元素,以获取创建时提供的类型的成员资格。`8.2` 节给出了这种技术的一个例子。 +来自编译器的未经检查的警告是我们要特别注意避免运行时类型违规的信号。 例如,在我们将一个类型化的集合引用传递给 `ungenerified` 库方法后,我们无法确定 +它只向集合中添加了正确类型的元素。 我们可以传入一个选中的包装器,而不是失去对集合类型安全性的置信度,它将测试每个添加到集合中的元素,以获取创建时提供 +的类型的成员资格。`8.2` 节给出了这种技术的一个例子。 为主界面提供检查包装: ```java - static Collection checkedCollection(Collection c, Class elementType) - static List checkedList(List c, Class elementType) - static Set checkedSet(Set c, Class elementType) - static SortedSet checkedSortedSet(SortedSet c, Class elementType) - static Map checkedMap(Map c, Class keyType, Class valueType) - static SortedMap checkedSortedMap(SortedMap c, Class keyType,Class valueType) +static Collection checkedCollection(Collection c, Class elementType) +static List checkedList(List c, Class elementType) +static Set checkedSet(Set c, Class elementType) +static SortedSet checkedSortedSet(SortedSet c, Class elementType) +static Map checkedMap(Map c, Class keyType, Class valueType) +static SortedMap checkedSortedMap(SortedMap c, Class keyType,Class valueType) ``` 《《《 [下一节](04_Other_Methods.md)
-《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git a/ch17/04_Other_Methods.md b/ch17/04_Other_Methods.md index e5595f38..3e911ba5 100644 --- a/ch17/04_Other_Methods.md +++ b/ch17/04_Other_Methods.md @@ -8,7 +8,7 @@ **addAll** ```java - boolean addAll(Collection c, T... elements) // 将所有指定的元素添加到指定的集合中。 + boolean addAll(Collection c, T... elements) // 将所有指定的元素添加到指定的集合中。 ``` 我们已经多次使用这种方法作为初始化具有单独元素的集合或数组内容的方便和有效的方式。 @@ -16,31 +16,35 @@ **asLifoQueue** ```java - Queue asLifoQueue(Deque deque) // 返回 Deque 作为后进先出(Lifo)队列的视图。 + Queue asLifoQueue(Deque deque) // 返回 Deque 作为后进先出(Lifo)队列的视图。 ``` -回顾第 `14` 章,虽然队列可以对其元素施加各种不同的排序,但没有提供 `LIFO` 排序的标准队列实现。另一方面,排队实现都支持 `LIFO` 排序,如果元素从出列队列的同一端移除因为他们被添加。`asLifo` `Queue` 方法允许您通过简洁的 `Queue` 界面使用此功能。 +回顾第 `14` 章,虽然队列可以对其元素施加各种不同的排序,但没有提供 `LIFO` 排序的标准队列实现。另一方面,排队实现都支持 `LIFO` 排序,如果元素从出列 +队列的同一端移除因为他们被添加。`asLifo` `Queue` 方法允许您通过简洁的 `Queue` 界面使用此功能。 **disjoint** ```java - boolean disjoint(Collection c1, Collection c2) // 如果 c1 和 c2 没有共同的元素,则返回 true +boolean disjoint(Collection c1, Collection c2) // 如果 c1 和 c2 没有共同的元素,则返回 true ``` -使用这种方法需要小心; 实现可以迭代两个集合,在另一个集合中测试一个元素用于包含。 因此,如果两个集合以不同方式确定包含,则此方法的结果是未定义的。例如,如果一个集合是一个 `SortedSet`,其中包含是由自然顺序或比较器决定的,另一个集合是一个集合,其中包含由其元素的 `equals` 方法决定,则可能出现这种情况。 +使用这种方法需要小心; 实现可以迭代两个集合,在另一个集合中测试一个元素用于包含。 因此,如果两个集合以不同方式确定包含,则此方法的结果是未定义的。例 +如,如果一个集合是一个 `SortedSet`,其中包含是由自然顺序或比较器决定的,另一个集合是一个集合,其中包含由其元素的 `equals` 方法决定,则可能出现这种 +情况。 **enumeration** ```java - Enumeration enumeration(Collection c) // 返回指定集合上的枚举 + Enumeration enumeration(Collection c) // 返回指定集合上的枚举 ``` -这个方法提供了与 `API` 的互操作,这些 `API` 的方法接受 `Enumeration` 类型的参数,这是 `Iterator` 的一个传统版本。 它返回的 `Enumeration` 与 `C` 提供的 `Iterator` 以相同的顺序产生相同的元素。 此方法与方法列表形成一对,方法列表将 `Enumeration` 值转换为 `ArrayList`。 +这个方法提供了与 `API` 的互操作,这些 `API` 的方法接受 `Enumeration` 类型的参数,这是 `Iterator` 的一个传统版本。 它返回的 `Enumeration` 与 `C` +提供的 `Iterator` 以相同的顺序产生相同的元素。 此方法与方法列表形成一对,方法列表将 `Enumeration` 值转换为 `ArrayList`。 **frequency** ```java - int frequency(Collection c, Object o) // 返回 c 中等于 o 的元素数目 +int frequency(Collection c, Object o) // 返回 c 中等于 o 的元素数目 ``` 如果提供的值 `o` 为空,则频率返回集合 `c` 中的空元素数。 @@ -48,48 +52,55 @@ **list** ```java - ArrayList list(Enumeration e) // 返回一个 ArrayList,其中包含由指定的 Enumeration 返回的元素 + ArrayList list(Enumeration e) // 返回一个 ArrayList,其中包含由指定的 Enumeration 返回的元素 ``` -提供此方法的 `API` 与其方法返回 `Enumeration` 类型(`Iterator` 旧版本)的结果的 `API` 互操作。 它返回的 `ArrayList` 包含与 `e` 提供的相同顺序的相同元素。 此方法与枚举方法形成一对,该枚举将框架集合转换为 `Enumeration`。 +提供此方法的 `API` 与其方法返回 `Enumeration` 类型(`Iterator` 旧版本)的结果的 `API` 互操作。 它返回的 `ArrayList` 包含与 `e` 提供的相同顺序的 +相同元素。 此方法与枚举方法形成一对,该枚举将框架集合转换为 `Enumeration`。 **newSetFromMap** ```java - Set newSetFromMap(Map map) // 返回由指定 map 支持的集合。 + Set newSetFromMap(Map map) // 返回由指定 map 支持的集合。 ``` -正如我们前面看到的,许多集合(如 `TreeSet` 和 `NavigableSkipListSet`)都是由映射实现的,并共享其排序,并发性和性能特征。 然而,某些地图(如 `WeakHashMap` 和 `IdentityHashMap`)没有标准的 `Set` 等价物。`newSetFromMap` 方法的目的是为这些地图提供等效的 `Set` 实现。`newSetFromMap` 方法包装其参数,该参数在提供时必须为空,并且不应直接访问。 此代码显示了使用它创建弱 `HashSet` 的标准习惯用法,其中元素通过弱引用保存: +正如我们前面看到的,许多集合(如 `TreeSet` 和 `NavigableSkipListSet`)都是由映射实现的,并共享其排序,并发性和性能特征。 然而,某些地图(如 +`WeakHashMap` 和 `IdentityHashMap`)没有标准的 `Set` 等价物。`newSetFromMap` 方法的目的是为这些地图提供等效的 `Set` 实现。`newSetFromMap` 方法 +包装其参数,该参数在提供时必须为空,并且不应直接访问。 此代码显示了使用它创建弱 `HashSet` 的标准习惯用法,其中元素通过弱引用保存: ```java - Set weakHashSet = Collections.newSetFromMap(new WeakHashMap()); +Set weakHashSet = Collections.newSetFromMap(new WeakHashMap()); ``` **reverseOrder** ```java - Comparator reverseOrder() // 返回一个反转自然顺序的比较器 + Comparator reverseOrder() // 返回一个反转自然顺序的比较器 ``` 此方法提供了一种按照自然顺序排序或维护 `Comparable` 对象集合的简单方法。 这是一个使用的例子: ```java - SortedSet s = new TreeSet(Collections.reverseOrder()); - Collections.addAll(s, 1, 2, 3); - assert s.toString().equals("[3, 2, 1]"); +SortedSet s = new TreeSet(Collections.reverseOrder()); +Collections.addAll(s, 1, 2, 3); +assert s.toString().equals("[3, 2, 1]"); ``` 这种方法还有另一种形式。 ```java - Comparator reverseOrder(Comparator cmp) + Comparator reverseOrder(Comparator cmp) ``` -此方法与前一个方法相似,但不是颠倒对象集合的自然顺序,而是颠倒比较器作为其参数提供的顺序。 它提供 `null` 的行为对于 `Collections` 类的方法来说是不寻常的。`Collections` 的约定指出,如果提供给它们的集合或类对象为 `null`,则它的方法抛出 `NullPointerException`,但如果此方法提供 `null`,则返回与 `reverseOrder()` 的调用相同的结果 - 也就是说,它返回一个 `Comparator`,它颠倒了一组对象的自然顺序。 +此方法与前一个方法相似,但不是颠倒对象集合的自然顺序,而是颠倒比较器作为其参数提供的顺序。 它提供 `null` 的行为对于 `Collections` 类的方法来说是不 +寻常的。`Collections` 的约定指出,如果提供给它们的集合或类对象为 `null`,则它的方法抛出 `NullPointerException`,但如果此方法提供 `null`,则返回 +与 `reverseOrder()` 的调用相同的结果 - 也就是说,它返回一个 `Comparator`,它颠倒了一组对象的自然顺序。 -结论这完成了我们对由 `Collections` 类提供的便捷方法的介绍,以及我们对集合框架的讨论。 我们提供了集合,集合,列表,队列和地图,并为您提供了所需的信息,以选择最适合您需求的接口和实现。 +结论这完成了我们对由 `Collections` 类提供的便捷方法的介绍,以及我们对集合框架的讨论。 我们提供了集合,集合,列表,队列和地图,并为您提供了所需的信 +息,以选择最适合您需求的接口和实现。 -泛型和改进的集合框架可能是 `Java` 自成立以来最重大的变化。我们对这些变化感到非常兴奋,并希望我们已将这些兴奋传达给了您。我们希望您会看到泛型和集合很好地融合在一起为您的 `Java` 编程技巧提供强大的补充。 +泛型和改进的集合框架可能是 `Java` 自成立以来最重大的变化。我们对这些变化感到非常兴奋,并希望我们已将这些兴奋传达给了您。我们希望您会看到泛型和集合很 +好地融合在一起为您的 `Java` 编程技巧提供强大的补充。 -《《《 [返回首页](../README.md) \ No newline at end of file +《《《 [返回首页](../README.md) diff --git "a/\345\213\230\350\257\257.md" "b/\345\213\230\350\257\257.md" new file mode 100644 index 00000000..9ecdce53 --- /dev/null +++ "b/\345\213\230\350\257\257.md" @@ -0,0 +1,5 @@ +## 原 `pdf` 勘误 + +- ch02/07_Wildcard_Capture.md + - `public static void reverse(List list);` + - `public static void reverse(List list);`