From 475518cc7d1cd44e3148d2c53b52052cbc4ee720 Mon Sep 17 00:00:00 2001 From: xiangflight Date: Mon, 25 Nov 2019 09:49:44 +0800 Subject: [PATCH] =?UTF-8?q?[revision=2020](=E6=88=AA=E6=AD=A2=20=E6=B3=9B?= =?UTF-8?q?=E5=9E=8B=E6=95=B0=E7=BB=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/book/20-Generics.md | 527 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 511 insertions(+), 16 deletions(-) diff --git a/docs/book/20-Generics.md b/docs/book/20-Generics.md index 2e6623bb..3228a45a 100644 --- a/docs/book/20-Generics.md +++ b/docs/book/20-Generics.md @@ -611,12 +611,12 @@ GenericMethods 尽管可以同时对类及其方法进行参数化,但这里未将 **GenericMethods** 类参数化。只有方法 `f()` 具有类型参数,该参数由方法返回类型之前的参数列表指示。 -对于泛型类,必须在实例化该类时指定类型参数。使用泛型方法时,通常不需要指定参数类型,因为编译器会找出这些类型。 这称为 *类型参数推断*。因此,对`f()` 的调用看起来像普通的方法调用,并且 `f()` 看起来像被重载了无数次一样。它甚至会接受 **GenericMethods** 类型的参数。 +对于泛型类,必须在实例化该类时指定类型参数。使用泛型方法时,通常不需要指定参数类型,因为编译器会找出这些类型。 这称为 *类型参数推断*。因此,对 `f()` 的调用看起来像普通的方法调用,并且 `f()` 看起来像被重载了无数次一样。它甚至会接受 **GenericMethods** 类型的参数。 如果使用基本类型调用 `f()` ,自动装箱就开始起作用,自动将基本类型包装在它们对应的包装类型中。 -### 变量和泛型方法 +### 变长参数和泛型方法 泛型方法和变长参数列表可以很好地共存: @@ -658,7 +658,7 @@ S, T, U, V, W, X, Y, Z] `@SafeVarargs` 注解保证我们不会对变长参数列表进行任何修改,这是正确的,因为我们只从中读取。如果没有此注解,编译器将无法知道这些并会发出警告。 -### 一个泛型的Supplier +### 一个泛型的 Supplier 这是一个为任意具有无参构造方法的类生成 **Supplier** 的类。为了减少键入,它还包括一个用于生成 **BasicSupplier** 的泛型方法: @@ -834,10 +834,10 @@ public class TupleTest2 { */ ``` -请注意,`f()` 返回一个参数化的 **Tuple2** 对象,而 `f2()` 返回一个未参数化的 **Tuple2** 对象。编译器不会在这里警告 `f2()` ,因为返回值未以参数化方式使用。从某种意义上说,它被“向上转型”为一个未参数化的 **Tuple2** 。 但是,如果如果尝试将 `f2()` 的结果放入到参数化的 **Tuple2** 中,则编译器将发出警告。 +请注意,`f()` 返回一个参数化的 **Tuple2** 对象,而 `f2()` 返回一个未参数化的 **Tuple2** 对象。编译器不会在这里警告 `f2()` ,因为返回值未以参数化方式使用。从某种意义上说,它被“向上转型”为一个未参数化的 **Tuple2** 。 但是,如果尝试将 `f2()` 的结果放入到参数化的 **Tuple2** 中,则编译器将发出警告。 -### 一个Set工具 +### 一个 Set 工具 对于泛型方法的另一个示例,请考虑由 **Set** 表示的数学关系。这些被方便地定义为可用于所有不同类型的泛型方法: @@ -898,7 +898,7 @@ public enum Watercolors { } ``` -为了方便起见(不必全限定所有名称),将其静态导入到以下示例中。本示例使用 **EnumSet** 轻松从 **enum** 中创建 **Set** 。(可以在[第二十二章 枚举](https://github.com/LingCoder/OnJava8/blob/master/docs/book/22-Enumerations.md)一章中了解有关 **EnumSet** 的更多信息。)在这里,静态方法 `EnumSet.range()` 要求提供所要在结果 **Set** 中创建的元素范围的第一个和最后一个元素: +为了方便起见(不必全限定所有名称),将其静态导入到以下示例中。本示例使用 **EnumSet** 轻松从 **enum** 中创建 **Set** 。(可以在[第二十二章 枚举](./22-Enumerations.md)一章中了解有关 **EnumSet** 的更多信息。)在这里,静态方法 `EnumSet.range()` 要求提供所要在结果 **Set** 中创建的元素范围的第一个和最后一个元素: ```java // generics/WatercolorSets.java @@ -1089,9 +1089,10 @@ Serializable] 在[第十二章 集合](./12-Collections.md)的[本章小结](./12-Collections.md#本章小结)部分将会用到这里的输出结果。 + ## 构建复杂模型 -泛型的一个重要好处是能够简单安全地创建复杂模型。例如,我们可以地轻松创建一个元组列表: +泛型的一个重要好处是能够简单安全地创建复杂模型。例如,我们可以轻松地创建一个元组列表: ```java // generics/TupleList.java @@ -1232,8 +1233,505 @@ public class Store extends ArrayList { ## 泛型擦除 +当你开始更深入地钻研泛型时,会发现有大量的东西初看起来是没有意义的。例如,尽管可以说 `ArrayList.class`,但不能说成 `ArrayList.class`。考虑下面的情况: + +```java +// generics/ErasedTypeEquivalence.java + +import java.util.*; + +public class ErasedTypeEquivalence { + + public static void main(String[] args) { + Class c1 = new ArrayList().getClass(); + Class c2 = new ArrayList().getClass(); + System.out.println(c1 == c2); + } + +} +/* Output: +true +*/ +``` + +`ArrayList` 和 `ArrayList` 应该是不同的类型。不同的类型会有不同的行为。例如,如果尝试向 `ArrayList` 中放入一个 `Integer`,所得到的行为(失败)和向 `ArrayList` 中放入一个 `Integer` 所得到的行为(成功)完全不同。然而上面的程序认为它们是相同的类型。 + +下面的例子是对该谜题的补充: + +```java +// generics/LostInformation.java + +import java.util.*; + +class Frob {} +class Fnorkle {} +class Quark {} + +class Particle {} + +public class LostInformation { + + public static void main(String[] args) { + List list = new ArrayList<>(); + Map map = new HashMap<>(); + Quark quark = new Quark<>(); + Particle p = new Particle<>(); + System.out.println(Arrays.toString(list.getClass().getTypeParameters())); + System.out.println(Arrays.toString(map.getClass().getTypeParameters())); + System.out.println(Arrays.toString(quark.getClass().getTypeParameters())); + System.out.println(Arrays.toString(p.getClass().getTypeParameters())); + } + +} +/* Output: +[E] +[K,V] +[Q] +[POSITION,MOMENTUM] +``` + +根据 JDK 文档,**Class.getTypeParameters()** “返回一个 **TypeVariable** 对象数组,表示泛型声明中声明的类型参数...” 这暗示你可以发现这些参数类型。但是正如上例中输出所示,你只能看到用作参数占位符的标识符,这并非有用的信息。 + +残酷的现实是: + +在泛型代码内部,无法获取任何有关泛型参数类型的信息。 + +因此,你可以知道如类型参数标识符和泛型边界这些信息,但无法得知实际的类型参数从而用来创建特定的实例。如果你曾是 C++ 程序员,那么这个事实会让你很沮丧,在使用 Java 泛型工作时,它是必须处理的最基本的问题。 + +Java 泛型是使用擦除实现的。这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。因此,`List` 和 `List` 在运行时实际上是相同的类型。它们都被擦除成原生类型 `List`。 + +理解擦除并知道如何处理它,是你在学习 Java 泛型时面临的最大障碍之一。这也是本节将要探讨的内容。 + +### C++ 的方式 + +下面是使用模版的 C++ 示例。你会看到类型参数的语法十分相似,因为 Java 是受 C++ 启发的: + +```c++ +// generics/Templates.cpp + +#include +using namespace std; + +template class Manipulator { + T obj; +public: + Manipulator(T x) { obj = x; } + void manipulate() { obj.f(); } +}; + +class HasF { +public: + void f() { cout << "HasF::f()" << endl; } +}; + +int main() { + HasF hf; + Manipulator manipulator(hf); + manipulator.manipulate(); +} +/* Output: +HasF::f() +*/ +``` + +**Manipulator** 类存储了一个 **T** 类型的对象。`manipulate()` 方法会调用 **obj** 上的 `f()` 方法。它是如何知道类型参数 **T** 中存在 `f()` 方法的呢?C++ 编译器会在你实例化模版时进行检查,所以在 `Manipulator` 实例化的那一刻,它看到 **HasF** 中含有一个方法 `f()`。如果情况并非如此,你就会得到一个编译期错误,保持类型安全。 + +用 C++ 编写这种代码很简单,因为当模版被实例化时,模版代码就知道模版参数的类型。Java 泛型就不同了。下面是 **HasF** 的 Java 版本: + +```java +// generics/HasF.java + +public class HasF { + public void f() { + System.out.println("HasF.f()"); + } +} +``` + +如果我们将示例的其余代码用 Java 实现,就不会通过编译: + +```java +// generics/Manipulation.java +// {WillNotCompile} + +class Manipulator { + private T obj; + + Manipulator(T x) { + obj = x; + } + + // Error: cannot find symbol: method f(): + public void manipulate() { + obj.f(); + } +} + +public class Manipulation { + public static void main(String[] args) { + HasF hf = new HasF(); + Manipulator manipulator = new Manipulator<>(hf); + manipulator.manipulate(); + } +} +``` + +因为擦除,Java 编译器无法将 `manipulate()` 方法必须能调用 **obj** 的 `f()` 方法这一需求映射到 HasF 具有 `f()` 方法这个事实上。为了调用 `f()`,我们必须协助泛型类,给定泛型类一个边界,以此告诉编译器只能接受遵循这个边界的类型。这里重用了 **extends** 关键字。由于有了边界,下面的代码就能通过编译: + +```java +public class Manipulator2 { + private T obj; + + Manipulator2(T x) { + obj = x; + } + + public void manipulate() { + obj.f(); + } +} +``` + +边界 `` 声明 T 必须是 HasF 类型或其子类。如果情况确实如此,就可以安全地在 **obj** 上调用 `f()` 方法。 + +我们说泛型类型参数会擦除到它的第一个边界(可能有多个边界,稍后你将看到)。我们还提到了类型参数的擦除。编译器实际上会把类型参数替换为它的擦除,就像上面的示例,**T** 擦除到了 **HasF**,就像在类的声明中用 **HasF** 替换了 **T** 一样。 + +你可能正确地观察到了泛型在 **Manipulator2.java** 中没有贡献任何事。你可以很轻松地自己去执行擦除,生成没有泛型的类: + +```java +// generics/Manipulator3.java + +class Manipulator3 { + private HasF obj; + + Manipulator3(HasF x) { + ojb = x; + } + + public void manipulate() { + obj.f(); + } +} +``` + +这提出了很重要的一点:泛型只有在类型参数比某个具体类型(以及其子类)更加“泛化”——代码能跨多个类工作时才有用。因此,类型参数和它们在有用的泛型代码中的应用,通常比简单的类替换更加复杂。但是,不能因此认为使用 `` 形式就是有缺陷的。例如,如果某个类有一个返回 **T** 的方法,那么泛型就有所帮助,因为它们之后将返回确切的类型: + +```java +// generics/ReturnGenericType.java + +public class ReturnGenericType { + private T obj; + + ReturnGenericType(T x) { + obj = x; + } + + public T get() { + return obj; + } +} +``` + +你必须查看所有的代码,从而确定代码是否复杂到必须使用泛型的程度。 + +我们将在本章稍后看到有关边界的更多细节。 + +### 迁移兼容性 + +为了减少潜在的关于擦除的困惑,你必须清楚地认识到这不是一个语言特性。它是 Java 实现泛型的一种妥协,因为泛型不是 Java 语言出现时就有的,所以就有了这种妥协。它会使你痛苦,因此你需要尽早习惯它并了解为什么它会这样。 + +如果 Java 1.0 就含有泛型的话,那么这个特性就不会使用擦除来实现——它会使用具体化,保持参数类型为第一类实体,因此你就能在类型参数上执行基于类型的语言操作和反射操作。本章稍后你会看到,擦除减少了泛型的泛化性。泛型在 Java 中仍然是有用的,只是不如它们本来设想的那么有用,而原因就是擦除。 + +在基于擦除的实现中,泛型类型被当作第二类类型处理,即不能在某些重要的上下文使用泛型类型。泛型类型只有在静态类型检测期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换为它们的非泛型上界。例如, `List` 这样的类型注解会被擦除为 **List**,普通的类型变量在未指定边界的情况下会被擦除为 **Object**。 + +擦除的核心动机是你可以在泛化的客户端上使用非泛型的类库,反之亦然。这经常被称为“迁移兼容性”。在理想情况下,所有事物将在指定的某天被泛化。在现实中,即使程序员只编写泛型代码,他们也必须处理 Java 5 之前编写的非泛型类库。这些类库的作者可能从没想过要泛化他们的代码,或许他们可能刚刚开始接触泛型。 + +因此 Java 泛型不仅必须支持向后兼容性——现有的代码和类文件仍然合法,继续保持之前的含义——而且还必须支持迁移兼容性,使得类库能按照它们自己的步调变为泛型,当某个类库变为泛型时,不会破坏依赖于它的代码和应用。在确定了这个目标后,Java 设计者们和从事此问题相关工作的各个团队决策认为擦除是唯一可行的解决方案。擦除使得这种向泛型的迁移成为可能,允许非泛型的代码和泛型代码共存。 + +例如,假设一个应用使用了两个类库 **X** 和 **Y**,**Y** 使用了类库 **Z**。随着 Java 5 的出现,这个应用和这些类库的创建者最终可能希望迁移到泛型上。但是当进行迁移时,它们有着不同的动机和限制。为了实现迁移兼容性,每个类库与应用必须与其他所有的部分是否使用泛型无关。因此,它们不能探测其他类库是否使用了泛型。因此,某个特定的类库使用了泛型这样的证据必须被”擦除“。 + +如果没有某种类型的迁移途径,所有已经构建了很长时间的类库就需要与希望迁移到 Java 泛型上的开发者们说再见了。类库毫无争议是编程语言的一部分,对生产效率有着极大的影响,所以这种代码无法接受。擦除是否是最佳的活唯一的迁移途径,还待时间来证明。 + +### 擦除的问题 + +因此,擦除主要的正当理由是从非泛化代码到泛化代码的转变过程,以及在不破坏现有类库的情况下将泛型融入到语言中。擦除允许你继续使用现有的非泛型客户端代码,直至客户端准备好用泛型重写这些代码。这是一个崇高的动机,因为它不会骤然破坏所有现有的代码。 + +擦除的代码是显著的。泛型不能用于显式地引用运行时类型的操作中,例如转型、**instanceof** 操作和 **new** 表达式。因为所有关于参数的类型信息都丢失了,当你在编写泛型代码时,必须时刻提醒自己,你只是看起来拥有有关参数的类型信息而已。 + +考虑如下的代码段: + +```java +class Foo { + T var; +} +``` + +看上去当你创建一个 **Foo** 实例时: + +```java +Foo f = new Foo<>(); +``` + +**class** **Foo** 中的代码应该知道现在工作于 **Cat** 之上。泛型语法也在强烈暗示整个类中所有 T 出现的地方都被替换,就像在 C++ 中一样。但是事实并非如此,当你在编写这个类的代码时,必须提醒自己:“不,这只是一个 **Object**“。 + +另外,擦除和迁移兼容性意味着,使用泛型并不是强制的,尽管你可能希望这样: + +```java +// generics/ErasureAndInheritance.java + +class GenericBase { + private T element; + + public void set(T arg) { + element = arg; + } + + public T get() { + return element; + } +} + +class Derived1 extends GenericBase {} + +class Derived2 extends GenericBase {} // No warning + +// class Derived3 extends GenericBase {} +// Strange error: +// unexpected type +// required: class or interface without bounds +public class ErasureAndInteritance { + @SuppressWarnings("unchecked") + public static void main(String[] args) { + Derived2 d2 = new Derived2(); + Object obj = d2.get(); + d2.set(obj); // Warning here! + } +} +``` + +**Derived2** 继承自 **GenericBase**,但是没有任何类型参数,编译器没有发出任何警告。直到调用 `set()` 方法时才出现警告。 + +为了关闭警告,Java 提供了一个注解,我们可以在列表中看到它: + +```java +@SuppressWarnings("unchecked") +``` + +这个注解放置在产生警告的方法上,而不是整个类上。当你要关闭警告时,最好尽可能地“聚焦”,这样就不会因为过于宽泛地关闭警告,而导致意外地遮蔽掉真正的问题。 + +可以推断,**Derived3** 产生的错误意味着编译器期望得到一个原生基类。 + +当你希望将类型参数不仅仅当作 Object 处理时,就需要付出额外努力来管理边界,并且与在 C++、Ada 和 Eiffel 这样的语言中获得参数化类型相比,你需要付出多得多的努力来获得少得多的回报。这并不是说,对于大多数的编程问题而言,这些语言通常都会比 Java 更得心应手,只是说它们的参数化类型机制相比 Java 更灵活、更强大。 + +### 边界处的动作 + +因为擦除,我发现了泛型最令人困惑的方面是可以表示没有任何意义的事物。例如: + +```java +// generics/ArrayMaker.java + +import java.lang.reflect.*; +import java.util.*; + +public class ArrayMaker { + private Class kind; + + public ArrayMaker(Class kind) { + this.kind = kind; + } + + @SuppressWarnings("unchecked") + T[] create(int size) { + return (T[]) Array.newInstance(kind, size); + } + + public static void main(String[] args) { + ArrayMaker stringMaker = new ArrayMaker<>(String.class); + String[] stringArray = stringMaker.create(9); + System.out.println(Arrays.toString(stringArray)); + } +} +/* Output +[null,null,null,null,null,null,null,null,null] +*/ +``` + +即使 **kind** 被存储为 `Class`,擦除也意味着它实际被存储为没有任何参数的 **Class**。因此,当你在使用它时,例如创建数组,`Array.newInstance()` 实际上并未拥有 **kind** 所蕴含的类型信息。所以它不会产生具体的结果,因而必须转型,这会产生一条令你无法满意的警告。 + +注意,对于在泛型中创建数组,使用 `Array.newInstance()` 是推荐的方式。 + +如果我们创建一个集合而不是数组,情况就不同了: + +```java +// generics/ListMaker.java + +import java.util.*; + +public class ListMaker { + List create() { + return new ArrayList<>(); + } + + public static void main(String[] args) { + ListMaker stringMaker = new ListMaker<>(); + List stringList = stringMaker.create(); + } +} +``` + +编译器不会给出任何警告,尽管我们知道(从擦除中)在 `create()` 内部的 `new ArrayList<>()` 中的 `` 被移除了——在运行时,类内部没有任何 ``,因此这看起来毫无意义。但是如果你遵从这种思路,并将这个表达式改为 `new ArrayList()`,编译器就会发出警告。 + +本例中这么做真的毫无意义吗?如果在创建 **List** 的同时向其中放入一些对象呢,像这样: + +```java +// generics/FilledList.java + +import java.util.*; +import java.util.function.*; +import onjava.*; + +public class FilledList extends ArrayList { + FilledList gen, int size) { + Suppliers.fill(this, gen, size); + } + + public FilledList(T t, int size) { + for (int i = 0; i < size; i++) { + this.add(t); + } + } + + public static void main(String[] args) { + List list = new FilledList<>("Hello", 4); + System.out.println(list); + // Supplier version: + List ilist = new FilledList<>(() -> 47, 4); + System.out.println(ilist); + } +} +/* Output: +[Hello,Hello,Hello,Hello] +[47,47,47,47] +``` + +即使编译器无法得知 `add()` 中的 **T** 的任何信息,但它仍可以在编译期确保你放入 **FilledList** 中的对象是 **T** 类型。因此,即使擦除移除了方法或类中的实际类型的信息,编译器仍可以确保方法或类中使用的类型的内部一致性。 + +因为擦除移除了方法体中的类型信息,所以在运行时的问题就是*边界*:即对象进入和离开方法的地点。这些正是编译器在编译期执行类型检查并插入转型代码的地点。 + +考虑如下这段非泛型示例: + +```java +// generics/SimpleHolder.java + +public class SimpleHolder { + private Object obj; + + public void set(Object obj) { + this.obj = obj; + } + + public Object get() { + return obj; + } + + public static void main(String[] args) { + SimpleHolder holder = new SimpleHolder(); + holder.set("Item"); + String s = (String) holder.get(); + } +} +``` + +如果用 **javap -c SimpleHolder** 反编译这个类,会得到如下内容(经过编辑): + +```java +public void set(java.lang.Object); + 0: aload_0 + 1: aload_1 + 2: putfield #2; // Field obj:Object; + 5: return + +public java.lang.Object get(); + 0: aload_0 + 1: getfield #2; // Field obj:Object; + 4: areturn + +public static void main(java.lang.String[]); + 0: new #3; // class SimpleHolder + 3: dup + 4: invokespecial #4; // Method "":()V + 7: astore_1 + 8: aload_1 + 9: ldc #5; // String Item + 11: invokevirtual #6; // Method set:(Object;)V + 14: aload_1 + 15: invokevirtual #7; // Method get:()Object; + 18: checkcast #8; // class java/lang/String + 21: astore_2 + 22: return +``` + +`set()` 和 `get()` 方法存储和产生值,转型在调用 `get()` 时接受检查。 + +现在将泛型融入上例代码中: + +```java +// generics/GenericHolder2.java + +public class GenericHolder2 { + private T obj; + + public void set(T obj) { + this.obj = obj; + } + + public T get() { + return obj; + } + + public static void main(String[] args) { + GenericHolder2 holder = new GenericHolder2<>(); + holder.set("Item"); + String s = holder.get(); + } +} +``` + +从 `get()` 返回后的转型消失了,但是我们还知道传递给 `set()` 的值在编译期会被检查。下面是相关的字节码: + +```java +public void set(java.lang.Object); + 0: aload_0 + 1: aload_1 + 2: putfield #2; // Field obj:Object; + 5: return + +public java.lang.Object get(); + 0: aload_0 + 1: getfield #2; // Field obj:Object; + 4: areturn + +public static void main(java.lang.String[]); + 0: new #3; // class GenericHolder2 + 3: dup + 4: invokespecial #4; // Method "":()V + 7: astore_1 + 8: aload_1 + 9: ldc #5; // String Item + 11: invokevirtual #6; // Method set:(Object;)V + 14: aload_1 + 15: invokevirtual #7; // Method get:()Object; + 18: checkcast #8; // class java/lang/String + 21: astore_2 + 22: return +``` + +所产生的字节码是相同的。对进入 `set()` 的类型进行检查是不需要的,因为这将由编译器执行。而对 `get()` 返回的值进行转型仍然是需要的,只不过不需要你来操作,它由编译器自动插入,这样你就不用编写(阅读)杂乱的代码。 + +`get()` 和 `set()` 产生了相同的字节码,这就告诉我们泛型的所有动作都发生在边界处——对入参的编译器检查和对返回值的转型。这有助于澄清对擦除的困惑,记住:“边界就是动作发生的地方”。 + ## 补偿擦除 因为擦除,我们将失去执行泛型代码中某些操作的能力。无法在运行时知道确切类型: @@ -1246,17 +1744,13 @@ public class Erased { private final int SIZE = 100; public void f(Object arg) { - // error: illegal generic type for instanceof if (arg instanceof T) { } - // error: unexpected type T var = new T(); - // error: generic array creation T[] array = new T[SIZE]; - // warning: [unchecked] unchecked cast T[] array = (T[]) new Object[SIZE]; @@ -1266,7 +1760,7 @@ public class Erased { 有时,我们可以对这些问题进行编程,但是有时必须通过引入类型标签来补偿擦除。这意味着为所需的类型显式传递一个 **Class** 对象,以在类型表达式中使用它。 -例如,由于删除了类型信息,因此在上一个程序中尝试使用 **instanceof** 将会失败。类型标签可以使用动态 `isInstance()` : +例如,由于擦除了类型信息,因此在上一个程序中尝试使用 **instanceof** 将会失败。类型标签可以使用动态 `isInstance()` : ```java // generics/ClassTypeCapture.java @@ -1386,7 +1880,7 @@ java.lang.InstantiationException: java.lang.Integer */ ``` -这样可以编译,但对于 `ClassAsFactory \` 会失败,这是因为 **Integer** 没有无参构造函数。由于错误不是在编译时捕获的,因此语言创建者不赞成这种方法。他们建议使用显式工厂(**Supplier**)并约束类型,以便只有实现该工厂的类可以这样创建对象。这是创建工厂的两种不同方法: +这样可以编译,但对于 `ClassAsFactory` 会失败,这是因为 **Integer** 没有无参构造函数。由于错误不是在编译时捕获的,因此语言创建者不赞成这种方法。他们建议使用显式工厂(**Supplier**)并约束类型,以便只有实现该工厂的类可以这样创建对象。这是创建工厂的两种不同方法: ```java // generics/FactoryConstraint.java @@ -1469,7 +1963,7 @@ public class FactoryConstraint { */ ``` -**IntegerFactory** 本身就是通过实现 `Supplier\` 的工厂。 **Widget** 包含一个内部类,它是一个工厂。还要注意,**Fudge** 并没有做任何类似于工厂的操作,并且传递 `Fudge::new` 仍然会产生工厂行为,因为编译器将对函数方法 `::new` 的调用转换为对 `get()` 的调用。 +**IntegerFactory** 本身就是通过实现 `Supplier` 的工厂。 **Widget** 包含一个内部类,它是一个工厂。还要注意,**Fudge** 并没有做任何类似于工厂的操作,并且传递 `Fudge::new` 仍然会产生工厂行为,因为编译器将对函数方法 `::new` 的调用转换为对 `get()` 的调用。 另一种方法是模板方法设计模式。在以下示例中,`create()` 是模板方法,在子类中被重写以生成该类型的对象: @@ -1515,6 +2009,7 @@ X **GenericWithCreate** 包含 `element` 字段,并通过无参构造函数强制其初始化,该构造函数又调用抽象的 `create()` 方法。这种创建方式可以在子类中定义,同时建立 **T** 的类型。 + ### 泛型数组 正如在 **Erased.java** 中所看到的,我们无法创建泛型数组。通用解决方案是在试图创建泛型数组的时候使用 **ArrayList** : @@ -1584,7 +2079,7 @@ Generic[] */ ``` -问题在于数组会跟踪其实际类型,而该类型是在创建数组时建立的。因此,即使 `gia` 被强制转换为 `Generic\[]` ,该信息也仅在编译时存在(并且没有 **@SuppressWarnings** 注解,将会收到有关该强制转换的警告)。在运行时,它仍然是一个对象数组,这会引起问题。成功创建泛型类型的数组的唯一方法是创建一个已擦除类型的新数组,并将其强制转换。 +问题在于数组会跟踪其实际类型,而该类型是在创建数组时建立的。因此,即使 `gia` 被强制转换为 `Generic[]` ,该信息也仅在编译时存在(并且没有 **@SuppressWarnings** 注解,将会收到有关该强制转换的警告)。在运行时,它仍然是一个 **Object** 数组,这会引起问题。成功创建泛型类型的数组的唯一方法是创建一个已擦除类型的新数组,并将其强制转换。 让我们看一个更复杂的示例。考虑一个包装数组的简单泛型包装器: @@ -1631,7 +2126,7 @@ public class GenericArray { 和以前一样,我们不能说 `T[] array = new T[sz]` ,所以我们创建了一个 **Object** 数组并将其强制转换。 -`rep()` 方法返回一个 `T[]` ,在主方法中它应该是 `gai` 的 `Integer[]` ,但是如果调用它并尝试将结果转换为 `Integer[]` 引用,则会得到 **ClassCastException** ,这再次是因为实际的运行时类型为 `Object[]` 。 +`rep()` 方法返回一个 `T[]` ,在主方法中它应该是 `gai` 的 `Integer[]`,但是如果调用它并尝试将结果转换为 `Integer[]` 引用,则会得到 **ClassCastException** ,这再次是因为实际的运行时类型为 `Object[]` 。 如果再注释掉 **@SuppressWarnings** 注解后编译 **GenericArray.java** ,则编译器会产生警告: