🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
## 简单泛型 促成泛型出现的最主要的动机之一是为了创建*集合类*,参见 [集合](book/12-Collections.md) 章节。集合用于存放要使用到的对象。数组也是如此,不过集合比数组更加灵活,功能更丰富。几乎所有程序在运行过程中都会涉及到一组对象,因此集合是可复用性最高的类库之一。 我们先看一个只能持有单个对象的类。这个类可以明确指定其持有的对象的类型: ```java // generics/Holder1.java class Automobile {} public class Holder1 { private Automobile a; public Holder1(Automobile a) { this.a = a; } Automobile get() { return a; } } ``` 这个类的可复用性不高,它无法持有其他类型的对象。我们可不希望为碰到的每个类型都编写一个新的类。 在 Java 5 之前,我们可以让这个类直接持有 `Object` 类型的对象: ```java // generics/ObjectHolder.java public class ObjectHolder { private Object a; public ObjectHolder(Object a) { this.a = a; } public void set(Object a) { this.a = a; } public Object get() { return a; } public static void main(String[] args) { ObjectHolder h2 = new ObjectHolder(new Automobile()); Automobile a = (Automobile)h2.get(); h2.set("Not an Automobile"); String s = (String)h2.get(); h2.set(1); // 自动装箱为 Integer Integer x = (Integer)h2.get(); } } ``` 现在,`ObjectHolder` 可以持有任何类型的对象,在上面的示例中,一个 `ObjectHolder` 先后持有了三种不同类型的对象。 一个集合中存储多种不同类型的对象的情况很少见,通常而言,我们只会用集合存储同一种类型的对象。泛型的主要目的之一就是用来约定集合要存储什么类型的对象,并且通过编译器确保规约得以满足。 因此,与其使用 `Object` ,我们更希望先指定一个类型占位符,稍后再决定具体使用什么类型。要达到这个目的,需要使用*类型参数*,用尖括号括住,放在类名后面。然后在使用这个类时,再用实际的类型替换此类型参数。在下面的例子中,`T` 就是类型参数: ```java // generics/GenericHolder.java public class GenericHolder<T> { private T a; public GenericHolder() {} public void set(T a) { this.a = a; } public T get() { return a; } public static void main(String[] args) { GenericHolder<Automobile> h3 = new GenericHolder<Automobile>(); h3.set(new Automobile()); // 此处有类型校验 Automobile a = h3.get(); // 无需类型转换 //- h3.set("Not an Automobile"); // 报错 //- h3.set(1); // 报错 } } ``` 创建 `GenericHolder` 对象时,必须指明要持有的对象的类型,将其置于尖括号内,就像 `main()` 中那样使用。然后,你就只能在 `GenericHolder` 中存储该类型(或其子类,因为多态与泛型不冲突)的对象了。当你调用 `get()` 取值时,直接就是正确的类型。 这就是 Java 泛型的核心概念:你只需告诉编译器要使用什么类型,剩下的细节交给它来处理。 你可能注意到 `h3` 的定义非常繁复。在 `=` 左边有 `GenericHolder<Automobile>`, 右边又重复了一次。在 Java 5 中,这种写法被解释成“必要的”,但在 Java 7 中设计者修正了这个问题(新的简写语法随后成为备受欢迎的特性)。以下是简写的例子: ```java // generics/Diamond.java class Bob {} public class Diamond<T> { public static void main(String[] args) { GenericHolder<Bob> h3 = new GenericHolder<>(); h3.set(new Bob()); } } ``` 注意,在 `h3` 的定义处,`=` 右边的尖括号是空的(称为“钻石语法”),而不是重复左边的类型信息。在本书剩余部分都会使用这种语法。 一般来说,你可以认为泛型和其他类型差不多,只不过它们碰巧有类型参数罢了。在使用泛型时,你只需要指定它们的名称和类型参数列表即可。 ### 一个元组类库 有时一个方法需要能返回多个对象。而 **return** 语句只能返回单个对象,解决方法就是创建一个对象,用它打包想要返回的多个对象。当然,可以在每次需要的时候,专门创建一个类来完成这样的工作。但是有了泛型,我们就可以一劳永逸。同时,还获得了编译时的类型安全。 这个概念称为*元组*,它是将一组对象直接打包存储于单一对象中。可以从该对象读取其中的元素,但不允许向其中存储新对象(这个概念也称为 *数据传输对象* 或 *信使* )。 通常,元组可以具有任意长度,元组中的对象可以是不同类型的。不过,我们希望能够为每个对象指明类型,并且从元组中读取出来时,能够得到正确的类型。要处理不同长度的问题,我们需要创建多个不同的元组。下面是一个可以存储两个对象的元组: ```java // onjava/Tuple2.java package onjava; public class Tuple2<A, B> { public final A a1; public final B a2; public Tuple2(A a, B b) { a1 = a; a2 = b; } public String rep() { return a1 + ", " + a2; } @Override public String toString() { return "(" + rep() + ")"; } } ``` 构造函数传入要存储的对象。这个元组隐式地保持了其中元素的次序。 初次阅读上面的代码时,你可能认为这违反了 Java 编程的封装原则。`a1` 和 `a2` 应该声明为 **private**,然后提供 `getFirst()` 和 `getSecond()` 取值方法才对呀?考虑下这样做能提供的“安全性”是什么:元组的使用程序可以读取 `a1` 和 `a2` 然后对它们执行任何操作,但无法对 `a1` 和 `a2` 重新赋值。例子中的 `final` 可以实现同样的效果,并且更为简洁明了。 另一种设计思路是允许元组的用户给 `a1` 和 `a2` 重新赋值。然而,采用上例中的形式无疑更加安全,如果用户想存储不同的元素,就会强制他们创建新的 `Tuple2` 对象。 我们可以利用继承机制实现长度更长的元组。添加更多的类型参数就行了: ```java // onjava/Tuple3.java package onjava; public class Tuple3<A, B, C> extends Tuple2<A, B> { public final C a3; public Tuple3(A a, B b, C c) { super(a, b); a3 = c; } @Override public String rep() { return super.rep() + ", " + a3; } } // onjava/Tuple4.java package onjava; public class Tuple4<A, B, C, D> extends Tuple3<A, B, C> { public final D a4; public Tuple4(A a, B b, C c, D d) { super(a, b, c); a4 = d; } @Override public String rep() { return super.rep() + ", " + a4; } } // onjava/Tuple5.java package onjava; public class Tuple5<A, B, C, D, E> extends Tuple4<A, B, C, D> { public final E a5; public Tuple5(A a, B b, C c, D d, E e) { super(a, b, c, d); a5 = e; } @Override public String rep() { return super.rep() + ", " + a5; } } ``` 演示需要,再定义两个类: ```java // generics/Amphibian.java public class Amphibian {} // generics/Vehicle.java public class Vehicle {} ``` 使用元组时,你只需要定义一个长度适合的元组,将其作为返回值即可。注意下面例子中方法的返回类型: ```java // generics/TupleTest.java import onjava.*; public class TupleTest { static Tuple2<String, Integer> f() { // 47 自动装箱为 Integer return new Tuple2<>("hi", 47); } static Tuple3<Amphibian, String, Integer> g() { return new Tuple3<>(new Amphibian(), "hi", 47); } static Tuple4<Vehicle, Amphibian, String, Integer> h() { return new Tuple4<>(new Vehicle(), new Amphibian(), "hi", 47); } static Tuple5<Vehicle, Amphibian, String, Integer, Double> k() { return new Tuple5<>(new Vehicle(), new Amphibian(), "hi", 47, 11.1); } public static void main(String[] args) { Tuple2<String, Integer> ttsi = f(); System.out.println(ttsi); // ttsi.a1 = "there"; // 编译错误,因为 final 不能重新赋值 System.out.println(g()); System.out.println(h()); System.out.println(k()); } } /* 输出: (hi, 47) (Amphibian@1540e19d, hi, 47) (Vehicle@7f31245a, Amphibian@6d6f6e28, hi, 47) (Vehicle@330bedb4, Amphibian@2503dbd3, hi, 47, 11.1) */ ``` 有了泛型,你可以很容易地创建元组,令其返回一组任意类型的对象。 通过 `ttsi.a1 = "there"` 语句的报错,我们可以看出,**final** 声明确实可以确保 **public** 字段在对象被构造出来之后就不能重新赋值了。 在上面的程序中,`new` 表达式有些啰嗦。本章稍后会介绍,如何利用 *泛型方法* 简化它们。 ### 一个堆栈类 接下来我们看一个稍微复杂一点的例子:堆栈。在 [集合](book/12-Collections.md) 一章中,我们用 `LinkedList` 实现了 `onjava.Stack` 类。在那个例子中,`LinkedList` 本身已经具备了创建堆栈所需的方法。`Stack` 是通过两个泛型类 `Stack<T>` 和 `LinkedList<T>` 的组合来创建。我们可以看出,泛型只不过是一种类型罢了(稍后我们会看到一些例外的情况)。 这次我们不用 `LinkedList` 来实现自己的内部链式存储机制。 ```java // generics/LinkedStack.java // 用链式结构实现的堆栈 public class LinkedStack<T> { private static class Node<U> { U item; Node<U> next; Node() { item = null; next = null; } Node(U item, Node<U> next) { this.item = item; this.next = next; } boolean end() { return item == null && next == null; } } private Node<T> top = new Node<>(); // 栈顶 public void push(T item) { top = new Node<>(item, top); } public T pop() { T result = top.item; if (!top.end()) { top = top.next; } return result; } public static void main(String[] args) { LinkedStack<String> lss = new LinkedStack<>(); for (String s : "Phasers on stun!".split(" ")) { lss.push(s); } String s; while ((s = lss.pop()) != null) { System.out.println(s); } } } ``` 输出结果: ```java stun! on Phasers ``` 内部类 `Node` 也是一个泛型,它拥有自己的类型参数。 这个例子使用了一个 *末端标识* (end sentinel) 来判断栈何时为空。这个末端标识是在构造 `LinkedStack` 时创建的。然后,每次调用 `push()` 就会创建一个 `Node<T>` 对象,并将其链接到前一个 `Node<T>` 对象。当你调用 `pop()` 方法时,总是返回 `top.item`,然后丢弃当前 `top` 所指向的 `Node<T>`,并将 `top` 指向下一个 `Node<T>`,除非到达末端标识,这时就不能再移动 `top` 了。如果已经到达末端,程序还继续调用 `pop()` 方法,它只能得到 `null`,说明栈已经空了。 ### RandomList 作为容器的另一个例子,假设我们需要一个持有特定类型对象的列表,每次调用它的 `select()` 方法时都随机返回一个元素。如果希望这种列表可以适用于各种类型,就需要使用泛型: ```java // generics/RandomList.java import java.util.*; import java.util.stream.*; public class RandomList<T> extends ArrayList<T> { private Random rand = new Random(47); public T select() { return get(rand.nextInt(size())); } public static void main(String[] args) { RandomList<String> rs = new RandomList<>(); Arrays.stream("The quick brown fox jumped over the lazy brown dog".split(" ")).forEach(rs::add); IntStream.range(0, 11).forEach(i -> System.out.print(rs.select() + " ")); } } ``` 输出结果: ```java brown over fox quick quick dog brown The brown lazy brown ``` `RandomList` 继承了 `ArrayList` 的所有方法。本例中只添加了 `select()` 这个方法。