@@ -2254,11 +2254,12 @@ Note: Recompile with -Xlint:unchecked for details.
22542254
22552255果然,标准库会产生很多警告。如果你使用过 C 语言,尤其是使用 ANSI C 之前的语言,你会记住警告的特殊效果:发现警告后,可以忽略它们。因此,除非程序员必须对其进行处理,否则最好不要从编译器发出任何类型的消息。
22562256
2257- Neal Gafter (Java 5 的主要开发人员之一 )在他的博客中[^ 2 ]指出,他在重写 Java 库时是很随意、马虎的,我们不应该像他那样做。Neal 还指出,他在不破坏现有接口的情况下无法修复某些 Java 库代码。因此,即使在 Java 库源代码中出现了一些习惯用法,它们也不一定是正确的做法。当查看库代码时,我们不能认为这就是要在自己代码中必须遵循的示例。
2257+ Neal Gafter (Java 5 的主要开发人员之一 )在他的博客中[^ 2 ]指出,他在重写 Java 库时是很随意、马虎的,我们不应该像他那样做。Neal 还指出,他在不破坏现有接口的情况下无法修复某些 Java 库代码。因此,即使在 Java 库源代码中出现了一些习惯用法,它们也不一定是正确的做法。当查看库代码时,我们不能认为这就是要在自己代码中必须遵循的示例。
22582258
22592259请注意,在 Java 文献中推荐使用类型标记技术,例如 Gilad Bracha 的论文《Generics in the Java Programming Language 》[^ 3 ],他指出:"例如,这种用法已广泛用于新的 API 中以处理注解。" 我发现此技术在人们对于舒适度的看法方面存在一些不一致之处;有些人强烈喜欢本章前面介绍的工厂方法。
22602260
22612261< ! -- Bounds -- >
2262+ 22622263## 边界
22632264
22642265* 边界* (bounds)在本章的前面进行了简要介绍。边界允许我们对泛型使用的参数类型施加约束。尽管这可以强制执行有关应用了泛型类型的规则,但潜在的更重要的效果是我们可以在绑定的类型中调用方法。
@@ -2567,10 +2568,200 @@ public class EpicBattle {
25672568接下来将要研究的通配符将会把范围限制在单个类型。
25682569
25692570< ! -- Wildcards -- >
2571+ 25702572## 通配符
25712573
2574+ 你已经在 [集合](. / 12 - Collections . md) 章节中看到了一些简单示例使用了通配符——在泛型参数表达式中的问号,在 [类型信息](. / 19 - Type - Information . md) 一章中这种示例更多。本节将更深入地探讨这个特性。
2575+ 2576+ 我们的起始示例要展示数组的一种特殊行为:你可以将派生类的数组赋值给基类的引用:
2577+ 2578+ ```java
2579+ // generics/CovariantArrays.java
2580+ 2581+ class Fruit {}
2582+ 2583+ class Apple extends Fruit {}
2584+ 2585+ class Jonathan extends Apple {}
2586+ 2587+ class Orange extends Fruit {}
2588+ 2589+ public class CovariantArrays {
2590+ 2591+ public static void main (String [] args ) {
2592+ Fruit [] fruit = new Apple [10 ];
2593+ fruit[0 ] = new Apple (); // OK
2594+ fruit[1 ] = new Jonathan (); // OK
2595+ // Runtime type is Apple[], not Fruit[] or Orange[]:
2596+ try {
2597+ // Compiler allows you to add Fruit:
2598+ fruit[0 ] = new Fruit (); // ArrayStoreException
2599+ } catch (Exception e) {
2600+ System . out. println(e);
2601+ }
2602+ try {
2603+ // Compiler allows you to add Oranges:
2604+ fruit[0 ] = new Orange (); // ArrayStoreException
2605+ } catch (Exception e) {
2606+ System . out. println(e);
2607+ }
2608+ }
2609+ }
2610+ /* Output:
2611+ java.lang.ArrayStoreException: Fruit
2612+ java.lang.ArrayStoreException: Orange
2613+ ```
2614+
2615+ `main()` 中的第一行创建了 **Apple** 数组,并赋值给一个 **Fruit** 数组引用。这是有意义的,因为 **Apple** 也是一种 **Fruit**,因此 **Apple** 数组应该也是一个 **Fruit** 数组。
2616+
2617+ 但是,如果实际的数组类型是 **Apple[]**,你可以在其中放置 **Apple** 或 **Apple** 的子类型,这在编译期和运行时都可以工作。但是你也可以在数组中放置 **Fruit** 对象。这对编译器来说是有意义的,因为它有一个 **Fruit[]** 引用——它有什么理由不允许将 **Fruit** 对象或任何从 **Fruit** 继承出来的对象(比如 **Orange**),放置到这个数组中呢?因此在编译期,这是允许的。然而,运行时的数组机制知道它处理的是 **Apple[]**,因此会在向数组中放置异构类型时抛出异常。
2618+
2619+ 向上转型用在这里不合适。你真正在做的是将一个数组赋值给另一个数组。数组的行为是持有其他对象,这里只是因为我们能够向上转型而已,所以很明显,数组对象可以保留有关它们包含的对象类型的规则。看起来就像数组对它们持有的对象是有意识的,因此在编译期检查和运行时检查之间,你不能滥用它们。
2620+
2621+ 数组的这种赋值并不是那么可怕,因为在运行时你可以发现插入了错误的类型。但是泛型的主要目标之一是将这种错误检测移到编译期。所以当我们试图使用泛型集合代替数组时,会发生什么呢?
2622+
2623+ ```java
2624+ // generics/NonCovariantGenerics.java
2625+ // {WillNotCompile}
2626+
2627+ import java.util.*;
2628+
2629+ public class NonCovariantGenerics {
2630+ // Compile Error: incompatible types:
2631+ List<Fruit> flist = new ArrayList<Apple>();
2632+ }
2633+ ```
2634+
2635+ 尽管你在首次阅读这段代码时会认为"不能将一个 **Apple** 集合赋值给一个 **Fruit** 集合"。记住,泛型不仅仅是关于集合,它真正要表达的是"不能把一个涉及 **Apple** 的泛型赋值给一个涉及 **Fruit** 的泛型"。如果像在数组中的情况一样,编译器对代码的了解足够多,可以确定所涉及到的集合,那么它可能会留下一些余地。但是它不知道任何有关这方面的信息,因此它拒绝向上转型。然而实际上这也不是向上转型—— **Apple** 的 **List** 不是 **Fruit** 的 **List**。**Apple** 的 **List** 将持有 **Apple** 和 **Apple** 的子类型,**Fruit** 的 **List** 将持有任何类型的 **Fruit**。是的,这包括 **Apple**,但是它不是一个 **Apple** 的 **List**,它仍然是 **Fruit** 的 **List**。**Apple** 的 **List** 在类型上不等价于 **Fruit** 的 **List**,即使 **Apple** 是一种 **Fruit** 类型。
2636+
2637+ 真正的问题是我们在讨论的集合类型,而不是集合持有对象的类型。与数组不同,泛型没有内建的协变类型。这是因为数组是完全在语言中定义的,因此可以具有编译期和运行时的内建检查,但是在使用泛型时,编译器和运行时系统不知道你想用类型做什么,以及应该采用什么规则。
2638+
2639+ 但是,有时你想在两个类型间建立某种向上转型关系。通配符可以产生这种关系。
2640+
2641+ ```java
2642+ // generics/GenericsAndCovariance.java
2643+
2644+ import java.util.*;
2645+
2646+ public class GenericsAndCovariance {
2647+
2648+ public static void main(String[] args) {
2649+ // Wildcards allow covariance:
2650+ List<? extends Fruit> flist = new ArrayList<>();
2651+ // Compile Error: can't add any type of object:
2652+ // flist.add(new Apple());
2653+ // flist.add(new Fruit());
2654+ // flist.add(new Object());
2655+ flist.add(null); // Legal but uninteresting
2656+ // We know it returns at least Fruit:
2657+ Fruit f = flist.get(0);
2658+ }
2659+
2660+ }
2661+ ```
2662+
2663+ **flist** 的类型现在是 `List<? extends Fruit>`,你可以读作"一个具有任何从 **Fruit** 继承的类型的列表"。然而,这实际上并不意味着这个 **List** 将持有任何类型的 **Fruit**。通配符引用的是明确的类型,因此它意味着"某种 **flist** 引用没有指定的具体类型"。因此这个被赋值的 **List** 必须持有诸如 **Fruit** 或 **Apple** 这样的指定类型,但是为了向上转型为 **Fruit**,这个类型是什么没人在意。
2664+
2665+ **List** 必须持有一种具体的 **Fruit** 或 **Fruit** 的子类型,但是如果你不关心具体的类型是什么,那么你能对这样的 **List** 做什么呢?如果不知道 **List** 中持有的对象是什么类型,你怎能保证安全地向其中添加对象呢?就像在 **CovariantArrays.java** 中向上转型一样,你不能,除非编译器而不是运行时系统可以阻止这种操作的发生。你很快就会发现这个问题。
2666+
2667+ 你可能认为事情开始变得有点走极端了,因为现在你甚至不能向刚刚声明过将持有 **Apple** 对象的 **List** 中放入一个 **Apple** 对象。是的,但编译器并不知道这一点。`List<? extends Fruit>` 可能合法地指向一个 `List<Orange>`。一旦执行这种类型的向上转型,你就丢失了向其中传递任何对象的能力,甚至传递 **Object** 也不行。
2668+
2669+ 另一方面,如果你调用了一个返回 **Fruit** 的方法,则是安全的,因为你知道这个 **List** 中的任何对象至少具有 **Fruit** 类型,因此编译器允许这么做。
2670+
2671+ ### 编译器有多聪明
2672+
2673+ 现在你可能会猜想自己不能去调用任何接受参数的方法,但是考虑下面的代码:
2674+
2675+ ```java
2676+ // generics/CompilerIntelligence.java
2677+
2678+ import java.util.*;
2679+
2680+ public class CompilerIntelligence {
2681+
2682+ public static void main(String[] args) {
2683+ List<? extends Fruit> flist = Arrays.asList(new Apple());
2684+ Apple a = (Apple) flist.get(0); // No warning
2685+ flist.contains(new Apple()); // Argument is 'Object'
2686+ flist.indexOf(new Apple()); // Argument is 'Object'
2687+ }
2688+
2689+ }
2690+ ```
2691+
2692+ 这里对 `contains()` 和 `indexOf()` 的调用接受 **Apple** 对象作为参数,执行没问题。这是否意味着编译器实际上会检查代码,以查看是否有某个特定的方法修改了它的对象?
2693+
2694+ 通过查看 **ArrayList** 的文档,我们发现编译器没有那么聪明。尽管 `add()` 接受一个泛型参数类型的参数,但 `contains()` 和 `indexOf()` 接受的参数类型是 **Object**。因此当你指定一个 `ArrayList<? extends Fruit>` 时,`add()` 的参数就变成了"**? extends Fruit**"。从这个描述中,编译器无法得知这里需要 **Fruit** 的哪个具体子类型,因此它不会接受任何类型的 **Fruit**。如果你先把 **Apple** 向上转型为 **Fruit**,也没有关系——编译器仅仅会拒绝调用像 `add()` 这样参数列表中涉及通配符的方法。
2695+
2696+ `contains()` 和 `indexOf()` 的参数类型是 **Object**,不涉及通配符,所以编译器允许调用它们。这意味着将由泛型类的设计者来决定哪些调用是"安全的",并使用 **Object** 类作为它们的参数类型。为了禁止对类型中使用了通配符的方法调用,需要在参数列表中使用类型参数。
2697+
2698+ 下面展示一个简单的 **Holder** 类:
2699+
2700+ ```java
2701+ public class Holder<T> {
2702+
2703+ private T value;
2704+
2705+ public Holder() {}
2706+
2707+ public Holder(T val) {
2708+ value = val;
2709+ }
2710+
2711+ public void set(T val) {
2712+ value = val;
2713+ }
2714+
2715+ public T get() {
2716+ return value;
2717+ }
2718+
2719+ @Override
2720+ public boolean equals(Object o) {
2721+ return o instanceof Holder && Objects.equals(value, ((Holder) o).value);
2722+ }
2723+
2724+ @Override
2725+ public int hashCode() {
2726+ return Objects.hashCode(value);
2727+ }
2728+
2729+ public static void main(String[] args) {
2730+ Holder<Apple> apple = new Holder<>(new Apple());
2731+ Apple d = apple.get();
2732+ apple.set(d);
2733+ // Holder<Fruit> fruit = apple; // Cannot upcast
2734+ Holder<? extends Fruit> fruit = apple; // OK
2735+ Fruit p = fruit.get();
2736+ d = (Apple) fruit.get();
2737+ try {
2738+ Orange c = (Orange) fruit.get(); // No warning
2739+ } catch (Exception e) {
2740+ System.out.println(e);
2741+ }
2742+ // fruit.set(new Apple()); // Cannot call set()
2743+ // fruit.set(new Fruit()); // Cannot call set()
2744+ System.out.println(fruit.equals(d)); // OK
2745+ }
2746+ }
2747+ /* Output
2748+ java.lang.ClassCastException: Apple cannot be cast to Orange
2749+ false
2750+ */
2751+ ```
2752+ 2753+ ** Holder ** 有一个接受 ** T ** 类型对象的 `set()` 方法,一个返回 T 对象的 `get()` 方法和一个接受 Object 对象的 `equals()` 方法。正如你所见,如果创建了一个 `Holder<Apple > `,就不能将其向上转型为 `Holder<Fruit > `,但是可以向上转型为 `Holder<? extends Fruit > `。如果调用 `get()`,只能返回一个 ** Fruit ** ——这就是在给定"任何;额扩展自 ** Fruit ** 的对象"这一边界后,它所能知道的一切了。如果你知道更多的信息,就可以将其转型到某种具体的 ** Fruit ** 而不会导致任何警告,但是存在得到 ** ClassCastException ** 的风险。`set()` 方法不能工作在 ** Apple ** 和 ** Fruit ** 上,因为 `set()` 的参数也是" **? extends Fruit**" ,意味着它可以是任何事物,编译器无法验证"任何事物"的类型安全性。
2754+ 2755+ 但是,`equals()` 方法可以正常工作,因为它接受的参数是 ** Object ** 而不是 ** T ** 类型。因此,编译器只关注传递进来和要返回的对象类型。它不会分析代码,以查看是否执行了任何实际的写入和读取操作。
2756+ 2757+ Java 7 引入了 ** java.util. Objects ** 库,使创建 `equals()` 和 `hashCode()` 方法变得更加容易,当然还有很多其他功能。`equals()` 方法的标准形式参考 [附录:理解 equals 和 hashCode 方法](. / Appendix - Understanding - equals- and- hashCode) 一章。
2758+ 2759+ ### 逆变
2760+ 2761+ 25722762
25732763< ! -- Issues -- >
2764+ 25742765## 问题
25752766
25762767
0 commit comments