20-Generics
第二十章 泛型
普通的类和方法只能使用特定的类型:基本数据类型或类类型。如果编写的代码需要应用于多种类型,这种严苛的限制对代码的束缚就会很大。
多态是一种面向对象思想的泛化机制。你可以将方法的参数类型设为基类,这样的方法就可以接受任何派生类作为参数,包括暂时还不存在的类。这样的方法更通用,应用范围更广。在类内部也是如此,在任何使用特定类型的地方,基类意味着更大的灵活性。除了 final
类(或只提供私有构造函数的类)任何类型都可被扩展,所以大部分时候这种灵活性是自带的。
拘泥于单一的继承体系太过局限,因为只有继承体系中的对象才能适用基类作为参数的方法中。如果方法以接口而不是类作为参数,限制就宽松多了,只要实现了接口就可以。这给予调用方一种选项,通过调整现有的类来实现接口,满足方法参数要求。接口可以突破继承体系的限制。
即便是接口也还是有诸多限制。一旦指定了接口,它就要求你的代码必须使用特定的接口。而我们希望编写更通用的代码,能够适用“非特定的类型”,而不是一个具体的接口或类。
这就是泛型的概念,是
如果你从未接触过参数化类型机制,你会发现泛型对
然而,如果你了解其他语言(例如
这并不是说
与C++ 的比较
但是,
第二个原因是,在
因此,本章中会介绍少量
简单泛型
促成泛型出现的最主要的动机之一是为了创建集合类,参见 集合 章节。集合用于存放要使用到的对象。数组也是如此,不过集合比数组更加灵活,功能更丰富。几乎所有程序在运行过程中都会涉及到一组对象,因此集合是可复用性最高的类库之一。
我们先看一个只能持有单个对象的类。这个类可以明确指定其持有的对象的类型:
// generics/Holder1.java
class Automobile {}
public class Holder1 {
private Automobile a;
public Holder1(Automobile a) { this.a = a; }
Automobile get() { return a; }
}
这个类的可复用性不高,它无法持有其他类型的对象。我们可不希望为碰到的每个类型都编写一个新的类。
在Object
类型的对象:
// 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
就是类型参数:
// 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()
取值时,直接就是正确的类型。
这就是
你可能注意到 h3
的定义非常繁复。在 =
左边有GenericHolder<Automobile>
// 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
的定义处,=
右边的尖括号是空的(称为“钻石语法”
一般来说,你可以认为泛型和其他类型差不多,只不过它们碰巧有类型参数罢了。在使用泛型时,你只需要指定它们的名称和类型参数列表即可。
一个元组类库
有时一个方法需要能返回多个对象。而
这个概念称为元组,它是将一组对象直接打包存储于单一对象中。可以从该对象读取其中的元素,但不允许向其中存储新对象(这个概念也称为 数据传输对象 或 信使
通常,元组可以具有任意长度,元组中的对象可以是不同类型的。不过,我们希望能够为每个对象指明类型,并且从元组中读取出来时,能够得到正确的类型。要处理不同长度的问题,我们需要创建多个不同的元组。下面是一个可以存储两个对象的元组:
// 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() + ")";
}
}
构造函数传入要存储的对象。这个元组隐式地保持了其中元素的次序。
初次阅读上面的代码时,你可能认为这违反了a1
和 a2
应该声明为getFirst()
和 getSecond()
取值方法才对呀?考虑下这样做能提供的“安全性”是什么:元组的使用程序可以读取 a1
和 a2
然后对它们执行任何操作,但无法对 a1
和 a2
重新赋值。例子中的 final
可以实现同样的效果,并且更为简洁明了。
另一种设计思路是允许元组的用户给 a1
和 a2
重新赋值。然而,采用上例中的形式无疑更加安全,如果用户想存储不同的元素,就会强制他们创建新的 Tuple2
对象。
我们可以利用继承机制实现长度更长的元组。添加更多的类型参数就行了:
// 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;
}
}
演示需要,再定义两个类:
// generics/Amphibian.java
public class Amphibian {}
// generics/Vehicle.java
public class Vehicle {}
使用元组时,你只需要定义一个长度适合的元组,将其作为返回值即可。注意下面例子中方法的返回类型:
// 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"
语句的报错,我们可以看出,
在上面的程序中,new
表达式有些啰嗦。本章稍后会介绍,如何利用 泛型方法 简化它们。
一个堆栈类
接下来我们看一个稍微复杂一点的例子:堆栈。在 集合 一章中,我们用 LinkedList
实现了 onjava.Stack
类。在那个例子中,LinkedList
本身已经具备了创建堆栈所需的方法。Stack
是通过两个泛型类 Stack<T>
和 LinkedList<T>
的组合来创建。我们可以看出,泛型只不过是一种类型罢了(稍后我们会看到一些例外的情况
这次我们不用 LinkedList
来实现自己的内部链式存储机制。
// 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);
}
}
}
输出结果:
stun!
on
Phasers
内部类 Node
也是一个泛型,它拥有自己的类型参数。
这个例子使用了一个 末端标识LinkedStack
时创建的。然后,每次调用 push()
就会创建一个 Node<T>
对象,并将其链接到前一个 Node<T>
对象。当你调用 pop()
方法时,总是返回 top.item
,然后丢弃当前 top
所指向的 Node<T>
,并将 top
指向下一个 Node<T>
,除非到达末端标识,这时就不能再移动 top
了。如果已经到达末端,程序还继续调用 pop()
方法,它只能得到 null
,说明栈已经空了。
RandomList
作为容器的另一个例子,假设我们需要一个持有特定类型对象的列表,每次调用它的 select()
方法时都随机返回一个元素。如果希望这种列表可以适用于各种类型,就需要使用泛型:
// 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() + " "));
}
}
输出结果:
brown over fox quick quick dog brown The brown lazy brown
RandomList
继承了 ArrayList
的所有方法。本例中只添加了 select()
这个方法。
泛型接口
泛型也可以应用于接口。例如 生成器,这是一种专门负责创建对象的类。实际上,这是 工厂方法 设计模式的一种应用。不过,当使用生成器创建新的对象时,它不需要任何参数,而工厂方法一般需要参数。生成器无需额外的信息就知道如何创建新对象。
一般而言,一个生成器只定义一个方法,用于创建对象。例如 java.util.function
类库中的 Supplier
就是一个生成器,调用其 get()
获取对象。get()
是泛型方法,返回值为类型参数 T
。
为了演示 Supplier
,我们需要定义几个类。下面是个咖啡相关的继承体系:
// generics/coffee/Coffee.java
package generics.coffee;
public class Coffee {
private static long counter = 0;
private final long id = counter++;
@Override
public String toString() {
return getClass().getSimpleName() + " " + id;
}
}
// generics/coffee/Latte.java
package generics.coffee;
public class Latte extends Coffee {}
// generics/coffee/Mocha.java
package generics.coffee;
public class Mocha extends Coffee {}
// generics/coffee/Cappuccino.java
package generics.coffee;
public class Cappuccino extends Coffee {}
// generics/coffee/Americano.java
package generics.coffee;
public class Americano extends Coffee {}
// generics/coffee/Breve.java
package generics.coffee;
public class Breve extends Coffee {}
现在,我们可以编写一个类,实现 Supplier<Coffee>
接口,它能够随机生成不同类型的 Coffee
对象:
// generics/coffee/CoffeeSupplier.java
// {java generics.coffee.CoffeeSupplier}
package generics.coffee;
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
public class CoffeeSupplier
implements Supplier<Coffee>, Iterable<Coffee> {
private Class<?>[] types = { Latte.class, Mocha.class,
Cappuccino.class, Americano.class, Breve.class };
private static Random rand = new Random(47);
public CoffeeSupplier() {}
// For iteration:
private int size = 0;
public CoffeeSupplier(int sz) { size = sz; }
@Override
public Coffee get() {
try {
return (Coffee) types[rand.nextInt(types.length)].newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
class CoffeeIterator implements Iterator<Coffee> {
int count = size;
@Override
public boolean hasNext() { return count > 0; }
@Override
public Coffee next() {
count--;
return CoffeeSupplier.this.get();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
@Override
public Iterator<Coffee> iterator() {
return new CoffeeIterator();
}
public static void main(String[] args) {
Stream.generate(new CoffeeSupplier())
.limit(5)
.forEach(System.out::println);
for (Coffee c : new CoffeeSupplier(5)) {
System.out.println(c);
}
}
}
输出结果:
Americano 0
Latte 1
Americano 2
Mocha 3
Mocha 4
Breve 5
Americano 6
Latte 7
Cappuccino 8
Cappuccino 9
参数化的 Supplier
接口确保 get()
返回值是参数的类型。CoffeeSupplier
同时还实现了 Iterable
接口,所以能用于
下面是另一个实现 Supplier<T>
接口的例子,它负责生成
// generics/Fibonacci.java
// Generate a Fibonacci sequence
import java.util.function.*;
import java.util.stream.*;
public class Fibonacci implements Supplier<Integer> {
private int count = 0;
@Override
public Integer get() { return fib(count++); }
private int fib(int n) {
if(n < 2) return 1;
return fib(n-2) + fib(n-1);
}
public static void main(String[] args) {
Stream.generate(new Fibonacci())
.limit(18)
.map(n -> n + " ")
.forEach(System.out::print);
}
}
输出结果:
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584
虽然我们在 Fibonacci
类的里里外外使用的都是 int
类型,但是其参数类型却是 Integer
。这个例子引出了Fibonacci
类对 int
的使用,我们已经看到了这种效果。
如果还想更进一步,编写一个实现了 Iterable
的 Fibnoacci
生成器。我们的一个选择是重写这个类,令其实现 Iterable
接口。不过,你并不是总能拥有源代码的控制权,并且,除非必须这么做,否则,我们也不愿意重写一个类。而且我们还有另一种选择,就是创建一个 适配器
有多种方法可以实现适配器。例如,可以通过继承来创建适配器类:
// generics/IterableFibonacci.java
// Adapt the Fibonacci class to make it Iterable
import java.util.*;
public class IterableFibonacci
extends Fibonacci implements Iterable<Integer> {
private int n;
public IterableFibonacci(int count) { n = count; }
@Override
public Iterator<Integer> iterator() {
return new Iterator<Integer>() {
@Override
public boolean hasNext() { return n > 0; }
@Override
public Integer next() {
n--;
return IterableFibonacci.this.get();
}
@Override
public void remove() { // Not implemented
throw new UnsupportedOperationException();
}
};
}
public static void main(String[] args) {
for(int i : new IterableFibonacci(18))
System.out.print(i + " ");
}
}
输出结果:
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584
在IterableFibonacci
,必须在构造函数中提供一个边界值,这样 hasNext()
才知道何时返回
泛型方法
到目前为止,我们已经研究了参数化整个类。其实还可以参数化类中的方法。类本身可能是泛型的,也可能不是,不过这与它的方法是否是泛型的并没有什么关系。
泛型方法独立于类而改变方法。作为准则,请“尽可能”使用泛型方法。通常将单个方法泛型化要比将整个类泛型化更清晰易懂。
如果方法是
要定义泛型方法,请将泛型参数列表放置在返回值之前,如下所示:
// generics/GenericMethods.java
public class GenericMethods {
public <T> void f(T x) {
System.out.println(x.getClass().getName());
}
public static void main(String[] args) {
GenericMethods gm = new GenericMethods();
gm.f("");
gm.f(1);
gm.f(1.0);
gm.f(1.0F);
gm.f('c');
gm.f(gm);
}
}
/* Output:
java.lang.String
java.lang.Integer
java.lang.Double
java.lang.Float
java.lang.Character
GenericMethods
*/
尽管可以同时对类及其方法进行参数化,但这里未将f()
具有类型参数,该参数由方法返回类型之前的参数列表指示。
对于泛型类,必须在实例化该类时指定类型参数。使用泛型方法时,通常不需要指定参数类型,因为编译器会找出这些类型。 这称为 类型参数推断。因此,对 f()
的调用看起来像普通的方法调用,并且 f()
看起来像被重载了无数次一样。它甚至会接受
如果使用基本类型调用 f()
,自动装箱就开始起作用,自动将基本类型包装在它们对应的包装类型中。
变长参数和泛型方法
泛型方法和变长参数列表可以很好地共存:
// generics/GenericVarargs.java
import java.util.ArrayList;
import java.util.List;
public class GenericVarargs {
@SafeVarargs
public static <T> List<T> makeList(T... args) {
List<T> result = new ArrayList<>();
for (T item : args)
result.add(item);
return result;
}
public static void main(String[] args) {
List<String> ls = makeList("A");
System.out.println(ls);
ls = makeList("A", "B", "C");
System.out.println(ls);
ls = makeList(
"ABCDEFFHIJKLMNOPQRSTUVWXYZ".split(""));
System.out.println(ls);
}
}
/* Output:
[A]
[A, B, C]
[A, B, C, D, E, F, F, H, I, J, K, L, M, N, O, P, Q, R,
S, T, U, V, W, X, Y, Z]
*/
此处显示的 makeList()
方法产生的功能与标准库的 java.util.Arrays.asList()
方法相同。
@SafeVarargs
注解保证我们不会对变长参数列表进行任何修改,这是正确的,因为我们只从中读取。如果没有此注解,编译器将无法知道这些并会发出警告。
一个泛型的Supplier
这是一个为任意具有无参构造方法的类生成
// onjava/BasicSupplier.java
// Supplier from a class with a no-arg constructor
package onjava;
import java.util.function.Supplier;
public class BasicSupplier<T> implements Supplier<T> {
private Class<T> type;
public BasicSupplier(Class<T> type) {
this.type = type;
}
@Override
public T get() {
try {
// Assumes type is a public class:
return type.newInstance();
} catch (InstantiationException |
IllegalAccessException e) {
throw new RuntimeException(e);
}
}
// Produce a default Supplier from a type token:
public static <T> Supplier<T> create(Class<T> type) {
return new BasicSupplier<>(type);
}
}
此类提供了产生以下对象的基本实现:
-
是
public 的。 因为 BasicSupplier 在单独的包中,所以相关的类必须具有 public 权限,而不仅仅是包级访问权限。 -
具有无参构造方法。要创建一个这样的
BasicSupplier 对象,请调用 create()
方法,并将要生成类型的类型令牌传递给它。通用的create()
方法提供了BasicSupplier.create(MyType.class)
这种较简洁的语法来代替较笨拙的new BasicSupplier <MyType>(MyType.class)
。
例如,这是一个具有无参构造方法的简单类:
// generics/CountedObject.java
public class CountedObject {
private static long counter = 0;
private final long id = counter++;
public long id() {
return id;
}
@Override
public String toString() {
return "CountedObject " + id;
}
}
toString()
报告这些实例的数量。
// generics/BasicSupplierDemo.java
import onjava.BasicSupplier;
import java.util.stream.Stream;
public class BasicSupplierDemo {
public static void main(String[] args) {
Stream.generate(
BasicSupplier.create(CountedObject.class))
.limit(5)
.forEach(System.out::println);
}
}
/* Output:
CountedObject 0
CountedObject 1
CountedObject 2
CountedObject 3
CountedObject 4
*/
泛型方法减少了产生create()
方法中将其用于类型推断。
简化元组的使用
使用类型参数推断和静态导入,我们将把早期的元组重写为更通用的库。在这里,我们使用重载的静态方法创建元组:
// onjava/Tuple.java
// Tuple library using type argument inference
package onjava;
public class Tuple {
public static <A, B> Tuple2<A, B> tuple(A a, B b) {
return new Tuple2<>(a, b);
}
public static <A, B, C> Tuple3<A, B, C>
tuple(A a, B b, C c) {
return new Tuple3<>(a, b, c);
}
public static <A, B, C, D> Tuple4<A, B, C, D>
tuple(A a, B b, C c, D d) {
return new Tuple4<>(a, b, c, d);
}
public static <A, B, C, D, E>
Tuple5<A, B, C, D, E> tuple(A a, B b, C c, D d, E e) {
return new Tuple5<>(a, b, c, d, e);
}
}
我们修改
// generics/TupleTest2.java
import onjava.Tuple2;
import onjava.Tuple3;
import onjava.Tuple4;
import onjava.Tuple5;
import static onjava.Tuple.tuple;
public class TupleTest2 {
static Tuple2<String, Integer> f() {
return tuple("hi", 47);
}
static Tuple2 f2() {
return tuple("hi", 47);
}
static Tuple3<Amphibian, String, Integer> g() {
return tuple(new Amphibian(), "hi", 47);
}
static Tuple4<Vehicle, Amphibian, String, Integer> h() {
return tuple(
new Vehicle(), new Amphibian(), "hi", 47);
}
static Tuple5<Vehicle, Amphibian,
String, Integer, Double> k() {
return tuple(new Vehicle(), new Amphibian(),
"hi", 47, 11.1);
}
public static void main(String[] args) {
Tuple2<String, Integer> ttsi = f();
System.out.println(ttsi);
System.out.println(f2());
System.out.println(g());
System.out.println(h());
System.out.println(k());
}
}
/* Output:
(hi, 47)
(hi, 47)
(Amphibian@14ae5a5, hi, 47)
(Vehicle@135fbaa4, Amphibian@45ee12a7, hi, 47)
(Vehicle@4b67cf4d, Amphibian@7ea987ac, hi, 47, 11.1)
*/
请注意,f()
返回一个参数化的f2()
返回一个未参数化的f2()
,因为返回值未以参数化方式使用。从某种意义上说,它被“向上转型”为一个未参数化的f2()
的结果放入到参数化的
一个Set 工具
对于泛型方法的另一个示例,请考虑由
// onjava/Sets.java
package onjava;
import java.util.HashSet;
import java.util.Set;
public class Sets {
public static <T> Set<T> union(Set<T> a, Set<T> b) {
Set<T> result = new HashSet<>(a);
result.addAll(b);
return result;
}
public static <T>
Set<T> intersection(Set<T> a, Set<T> b) {
Set<T> result = new HashSet<>(a);
result.retainAll(b);
return result;
}
// Subtract subset from superset:
public static <T> Set<T>
difference(Set<T> superset, Set<T> subset) {
Set<T> result = new HashSet<>(superset);
result.removeAll(subset);
return result;
}
// Reflexive--everything not in the intersection:
public static <T> Set<T> complement(Set<T> a, Set<T> b) {
return difference(union(a, b), intersection(a, b));
}
}
前三个方法通过将第一个参数的引用复制到新的
这四种方法代表数学集合操作: union()
返回一个包含两个参数并集的intersection()
返回一个包含两个参数集合交集的difference()
从complement()
返回所有不在交集中的元素的
// generics/watercolors/Watercolors.java
package watercolors;
public enum Watercolors {
ZINC, LEMON_YELLOW, MEDIUM_YELLOW, DEEP_YELLOW,
ORANGE, BRILLIANT_RED, CRIMSON, MAGENTA,
ROSE_MADDER, VIOLET, CERULEAN_BLUE_HUE,
PHTHALO_BLUE, ULTRAMARINE, COBALT_BLUE_HUE,
PERMANENT_GREEN, VIRIDIAN_HUE, SAP_GREEN,
YELLOW_OCHRE, BURNT_SIENNA, RAW_UMBER,
BURNT_UMBER, PAYNES_GRAY, IVORY_BLACK
}
为了方便起见(不必全限定所有名称EnumSet.range()
要求提供所要在结果
// generics/WatercolorSets.java
import watercolors.*;
import java.util.EnumSet;
import java.util.Set;
import static watercolors.Watercolors.*;
import static onjava.Sets.*;
public class WatercolorSets {
public static void main(String[] args) {
Set<Watercolors> set1 =
EnumSet.range(BRILLIANT_RED, VIRIDIAN_HUE);
Set<Watercolors> set2 =
EnumSet.range(CERULEAN_BLUE_HUE, BURNT_UMBER);
System.out.println("set1: " + set1);
System.out.println("set2: " + set2);
System.out.println(
"union(set1, set2): " + union(set1, set2));
Set<Watercolors> subset = intersection(set1, set2);
System.out.println(
"intersection(set1, set2): " + subset);
System.out.println("difference(set1, subset): " +
difference(set1, subset));
System.out.println("difference(set2, subset): " +
difference(set2, subset));
System.out.println("complement(set1, set2): " +
complement(set1, set2));
}
}
/* Output:
set1: [BRILLIANT_RED, CRIMSON, MAGENTA, ROSE_MADDER,
VIOLET, CERULEAN_BLUE_HUE, PHTHALO_BLUE, ULTRAMARINE,
COBALT_BLUE_HUE, PERMANENT_GREEN, VIRIDIAN_HUE]
set2: [CERULEAN_BLUE_HUE, PHTHALO_BLUE, ULTRAMARINE,
COBALT_BLUE_HUE, PERMANENT_GREEN, VIRIDIAN_HUE,
SAP_GREEN, YELLOW_OCHRE, BURNT_SIENNA, RAW_UMBER,
BURNT_UMBER]
union(set1, set2): [BURNT_SIENNA, BRILLIANT_RED,
YELLOW_OCHRE, MAGENTA, SAP_GREEN, CERULEAN_BLUE_HUE,
ULTRAMARINE, VIRIDIAN_HUE, VIOLET, RAW_UMBER,
ROSE_MADDER, PERMANENT_GREEN, BURNT_UMBER,
PHTHALO_BLUE, CRIMSON, COBALT_BLUE_HUE]
intersection(set1, set2): [PERMANENT_GREEN,
CERULEAN_BLUE_HUE, ULTRAMARINE, VIRIDIAN_HUE,
PHTHALO_BLUE, COBALT_BLUE_HUE]
difference(set1, subset): [BRILLIANT_RED, MAGENTA,
VIOLET, CRIMSON, ROSE_MADDER]
difference(set2, subset): [BURNT_SIENNA, YELLOW_OCHRE,
BURNT_UMBER, SAP_GREEN, RAW_UMBER]
complement(set1, set2): [BURNT_SIENNA, BRILLIANT_RED,
YELLOW_OCHRE, MAGENTA, SAP_GREEN, VIOLET, RAW_UMBER,
ROSE_MADDER, BURNT_UMBER, CRIMSON]
*/
接下来的例子使用 Sets.difference()
方法来展示
// onjava/CollectionMethodDifferences.java
// {java onjava.CollectionMethodDifferences}
package onjava;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Collectors;
public class CollectionMethodDifferences {
static Set<String> methodSet(Class<?> type) {
return Arrays.stream(type.getMethods())
.map(Method::getName)
.collect(Collectors.toCollection(TreeSet::new));
}
static void interfaces(Class<?> type) {
System.out.print("Interfaces in " +
type.getSimpleName() + ": ");
System.out.println(
Arrays.stream(type.getInterfaces())
.map(Class::getSimpleName)
.collect(Collectors.toList()));
}
static Set<String> object = methodSet(Object.class);
static {
object.add("clone");
}
static void
difference(Class<?> superset, Class<?> subset) {
System.out.print(superset.getSimpleName() +
" extends " + subset.getSimpleName() +
", adds: ");
Set<String> comp = Sets.difference(
methodSet(superset), methodSet(subset));
comp.removeAll(object); // Ignore 'Object' methods
System.out.println(comp);
interfaces(superset);
}
public static void main(String[] args) {
System.out.println("Collection: " +
methodSet(Collection.class));
interfaces(Collection.class);
difference(Set.class, Collection.class);
difference(HashSet.class, Set.class);
difference(LinkedHashSet.class, HashSet.class);
difference(TreeSet.class, Set.class);
difference(List.class, Collection.class);
difference(ArrayList.class, List.class);
difference(LinkedList.class, List.class);
difference(Queue.class, Collection.class);
difference(PriorityQueue.class, Queue.class);
System.out.println("Map: " + methodSet(Map.class));
difference(HashMap.class, Map.class);
difference(LinkedHashMap.class, HashMap.class);
difference(SortedMap.class, Map.class);
difference(TreeMap.class, Map.class);
}
}
/* Output:
Collection: [add, addAll, clear, contains, containsAll,
equals, forEach, hashCode, isEmpty, iterator,
parallelStream, remove, removeAll, removeIf, retainAll,
size, spliterator, stream, toArray]
Interfaces in Collection: [Iterable]
Set extends Collection, adds: []
Interfaces in Set: [Collection]
HashSet extends Set, adds: []
Interfaces in HashSet: [Set, Cloneable, Serializable]
LinkedHashSet extends HashSet, adds: []
Interfaces in LinkedHashSet: [Set, Cloneable,
Serializable]
TreeSet extends Set, adds: [headSet,
descendingIterator, descendingSet, pollLast, subSet,
floor, tailSet, ceiling, last, lower, comparator,
pollFirst, first, higher]
Interfaces in TreeSet: [NavigableSet, Cloneable,
Serializable]
List extends Collection, adds: [replaceAll, get,
indexOf, subList, set, sort, lastIndexOf, listIterator]
Interfaces in List: [Collection]
ArrayList extends List, adds: [trimToSize,
ensureCapacity]
Interfaces in ArrayList: [List, RandomAccess,
Cloneable, Serializable]
LinkedList extends List, adds: [offerFirst, poll,
getLast, offer, getFirst, removeFirst, element,
removeLastOccurrence, peekFirst, peekLast, push,
pollFirst, removeFirstOccurrence, descendingIterator,
pollLast, removeLast, pop, addLast, peek, offerLast,
addFirst]
Interfaces in LinkedList: [List, Deque, Cloneable,
Serializable]
Queue extends Collection, adds: [poll, peek, offer,
element]
Interfaces in Queue: [Collection]
PriorityQueue extends Queue, adds: [comparator]
Interfaces in PriorityQueue: [Serializable]
Map: [clear, compute, computeIfAbsent,
computeIfPresent, containsKey, containsValue, entrySet,
equals, forEach, get, getOrDefault, hashCode, isEmpty,
keySet, merge, put, putAll, putIfAbsent, remove,
replace, replaceAll, size, values]
HashMap extends Map, adds: []
Interfaces in HashMap: [Map, Cloneable, Serializable]
LinkedHashMap extends HashMap, adds: []
Interfaces in LinkedHashMap: [Map]
SortedMap extends Map, adds: [lastKey, subMap,
comparator, firstKey, headMap, tailMap]
Interfaces in SortedMap: [Map]
TreeMap extends Map, adds: [descendingKeySet,
navigableKeySet, higherEntry, higherKey, floorKey,
subMap, ceilingKey, pollLastEntry, firstKey, lowerKey,
headMap, tailMap, lowerEntry, ceilingEntry,
descendingMap, pollFirstEntry, lastKey, firstEntry,
floorEntry, comparator, lastEntry]
Interfaces in TreeMap: [NavigableMap, Cloneable,
Serializable]
*/
在第十二章 集合的本章小结 部分将会用到这里的输出结果。
构建复杂模型
泛型的一个重要好处是能够简单安全地创建复杂模型。例如,我们可以轻松地创建一个元组列表:
// generics/TupleList.java
// Combining generic types to make complex generic types
import onjava.Tuple4;
import java.util.ArrayList;
public class TupleList<A, B, C, D>
extends ArrayList<Tuple4<A, B, C, D>> {
public static void main(String[] args) {
TupleList<Vehicle, Amphibian, String, Integer> tl =
new TupleList<>();
tl.add(TupleTest2.h());
tl.add(TupleTest2.h());
tl.forEach(System.out::println);
}
}
/* Output:
(Vehicle@7cca494b, Amphibian@7ba4f24f, hi, 47)
(Vehicle@3b9a45b3, Amphibian@7699a589, hi, 47)
*/
这将产生一个功能强大的数据结构,而无需太多代码。
下面是第二个例子。每个类都是组成块,总体包含很多个块。在这里,该模型是一个具有过道,货架和产品的零售商店:
// generics/Store.java
// Building a complex model using generic collections
import onjava.Suppliers;
import java.util.ArrayList;
import java.util.Random;
import java.util.function.Supplier;
class Product {
private final int id;
private String description;
private double price;
Product(int idNumber, String descr, double price) {
id = idNumber;
description = descr;
this.price = price;
System.out.println(toString());
}
@Override
public String toString() {
return id + ": " + description +
", price: $" + price;
}
public void priceChange(double change) {
price += change;
}
public static Supplier<Product> generator =
new Supplier<Product>() {
private Random rand = new Random(47);
@Override
public Product get() {
return new Product(rand.nextInt(1000), "Test",
Math.round(
rand.nextDouble() * 1000.0) + 0.99);
}
};
}
class Shelf extends ArrayList<Product> {
Shelf(int nProducts) {
Suppliers.fill(this, Product.generator, nProducts);
}
}
class Aisle extends ArrayList<Shelf> {
Aisle(int nShelves, int nProducts) {
for (int i = 0; i < nShelves; i++)
add(new Shelf(nProducts));
}
}
class CheckoutStand {
}
class Office {
}
public class Store extends ArrayList<Aisle> {
private ArrayList<CheckoutStand> checkouts =
new ArrayList<>();
private Office office = new Office();
public Store(
int nAisles, int nShelves, int nProducts) {
for (int i = 0; i < nAisles; i++)
add(new Aisle(nShelves, nProducts));
}
@Override
public String toString() {
StringBuilder result = new StringBuilder();
for (Aisle a : this)
for (Shelf s : a)
for (Product p : s) {
result.append(p);
result.append("\n");
}
return result.toString();
}
public static void main(String[] args) {
System.out.println(new Store(5, 4, 3));
}
}
/* Output: (First 8 Lines)
258: Test, price: $400.99
861: Test, price: $160.99
868: Test, price: $417.99
207: Test, price: $268.99
551: Test, price: $114.99
278: Test, price: $804.99
520: Test, price: $554.99
140: Test, price: $530.99
...
*/
Store.toString()
显示了结果:尽管有复杂的层次结构,但多层的集合仍然是类型安全的和可管理的。令人印象深刻的是,组装这样的模型并不需要耗费过多精力。
Suppliers.fill()
这个实用程序,该实用程序接受
泛型擦除
当你开始更深入地钻研泛型时,会发现有大量的东西初看起来是没有意义的。例如,尽管可以说 ArrayList.class
,但不能说成 ArrayList<Integer>.class
。考虑下面的情况:
// generics/ErasedTypeEquivalence.java
import java.util.*;
public class ErasedTypeEquivalence {
public static void main(String[] args) {
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2);
}
}
/* Output:
true
*/
ArrayList<String>
和 ArrayList<Integer>
应该是不同的类型。不同的类型会有不同的行为。例如,如果尝试向 ArrayList<String>
中放入一个 Integer
,所得到的行为(失败)和向 ArrayList<Integer>
中放入一个 Integer
所得到的行为(成功)完全不同。然而上面的程序认为它们是相同的类型。
下面的例子是对该谜题的补充:
// generics/LostInformation.java
import java.util.*;
class Frob {}
class Fnorkle {}
class Quark<Q> {}
class Particle<POSITION, MOMENTUM> {}
public class LostInformation {
public static void main(String[] args) {
List<Frob> list = new ArrayList<>();
Map<Frob, Fnorkle> map = new HashMap<>();
Quark<Fnorkle> quark = new Quark<>();
Particle<Long, Double> 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]
*/
根据
残酷的现实是:
在泛型代码内部,无法获取任何有关泛型参数类型的信息。
因此,你可以知道如类型参数标识符和泛型边界这些信息,但无法得知实际的类型参数从而用来创建特定的实例。如果你曾是
List<String>
和 List<Integer>
在运行时实际上是相同的类型。它们都被擦除成原生类型 List
。
理解擦除并知道如何处理它,是你在学习
C++ 的方式
下面是使用模版的
// generics/Templates.cpp
#include <iostream>
using namespace std;
template<class T> 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<HasF> manipulator(hf);
manipulator.manipulate();
}
/* Output:
HasF::f()
*/
manipulate()
方法会调用f()
方法。它是如何知道类型参数f()
方法的呢?Manipulator<HasF>
实例化的那一刻,它看到f()
。如果情况并非如此,你就会得到一个编译期错误,保持类型安全。
用
// generics/HasF.java
public class HasF {
public void f() {
System.out.println("HasF.f()");
}
}
如果我们将示例的其余代码用
// generics/Manipulation.java
// {WillNotCompile}
class Manipulator<T> {
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<HasF> manipulator = new Manipulator<>(hf);
manipulator.manipulate();
}
}
因为擦除,manipulate()
方法必须能调用f()
方法这一需求映射到f()
方法这个事实上。为了调用 f()
,我们必须协助泛型类,给定泛型类一个边界,以此告诉编译器只能接受遵循这个边界的类型。这里重用了
public class Manipulator2<T extends HasF> {
private T obj;
Manipulator2(T x) {
obj = x;
}
public void manipulate() {
obj.f();
}
}
边界 <T extends HasF>
声明f()
方法。
我们说泛型类型参数会擦除到它的第一个边界(可能有多个边界,稍后你将看到
你可能正确地观察到了泛型在
// generics/Manipulator3.java
class Manipulator3 {
private HasF obj;
Manipulator3(HasF x) {
obj = x;
}
public void manipulate() {
obj.f();
}
}
这提出了很重要的一点:泛型只有在类型参数比某个具体类型(以及其子类)更加“泛化”——代码能跨多个类工作时才有用。因此,类型参数和它们在有用的泛型代码中的应用,通常比简单的类替换更加复杂。但是,不能因此认为使用 <T extends HasF>
形式就是有缺陷的。例如,如果某个类有一个返回
// generics/ReturnGenericType.java
public class ReturnGenericType<T extends HasF> {
private T obj;
ReturnGenericType(T x) {
obj = x;
}
public T get() {
return obj;
}
}
你必须查看所有的代码,从而确定代码是否复杂到必须使用泛型的程度。
我们将在本章稍后看到有关边界的更多细节。
迁移兼容性
为了减少潜在的关于擦除的困惑,你必须清楚地认识到这不是一个语言特性。它是
如果
在基于擦除的实现中,泛型类型被当作第二类类型处理,即不能在某些重要的上下文使用泛型类型。泛型类型只有在静态类型检测期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换为它们的非泛型上界。例如, List<T>
这样的类型注解会被擦除为
擦除的核心动机是你可以在泛化的客户端上使用非泛型的类库,反之亦然。这经常被称为“迁移兼容性”。在理想情况下,所有事物将在指定的某天被泛化。在现实中,即使程序员只编写泛型代码,他们也必须处理
因此
例如,假设一个应用使用了两个类库
如果没有某种类型的迁移途径,所有已经构建了很长时间的类库就需要与希望迁移到
擦除的问题
因此,擦除主要的正当理由是从非泛化代码到泛化代码的转变过程,以及在不破坏现有类库的情况下将泛型融入到语言中。擦除允许你继续使用现有的非泛型客户端代码,直至客户端准备好用泛型重写这些代码。这是一个崇高的动机,因为它不会骤然破坏所有现有的代码。
擦除的代价是显著的。泛型不能用于显式地引用运行时类型的操作中,例如转型、
考虑如下的代码段:
class Foo<T> {
T var;
}
看上去当你创建一个
Foo<Cat> f = new Foo<>();
另外,擦除和迁移兼容性意味着,使用泛型并不是强制的,尽管你可能希望这样:
// generics/ErasureAndInheritance.java
class GenericBase<T> {
private T element;
public void set(T arg) {
element = arg;
}
public T get() {
return element;
}
}
class Derived1<T> extends GenericBase<T> {}
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!
}
}
set()
方法时才出现警告。
为了关闭警告,
@SuppressWarnings("unchecked")
这个注解放置在产生警告的方法上,而不是整个类上。当你要关闭警告时,最好尽可能地“聚焦”,这样就不会因为过于宽泛地关闭警告,而导致意外地遮蔽掉真正的问题。
可以推断,
当你希望将类型参数不仅仅当作
边界处的动作
因为擦除,我发现了泛型最令人困惑的方面是可以表示没有任何意义的事物。例如:
// generics/ArrayMaker.java
import java.lang.reflect.*;
import java.util.*;
public class ArrayMaker<T> {
private Class<T> kind;
public ArrayMaker(Class<T> kind) {
this.kind = kind;
}
@SuppressWarnings("unchecked")
T[] create(int size) {
return (T[]) Array.newInstance(kind, size);
}
public static void main(String[] args) {
ArrayMaker<String> 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]
*/
即使Class<T>
,擦除也意味着它实际被存储为没有任何参数的Array.newInstance()
实际上并未拥有
注意,对于在泛型中创建数组,使用 Array.newInstance()
是推荐的方式。
如果我们创建一个集合而不是数组,情况就不同了:
// generics/ListMaker.java
import java.util.*;
public class ListMaker<T> {
List<T> create() {
return new ArrayList<>();
}
public static void main(String[] args) {
ListMaker<String> stringMaker = new ListMaker<>();
List<String> stringList = stringMaker.create();
}
}
编译器不会给出任何警告,尽管我们知道(从擦除中)在 create()
内部的 new ArrayList<>()
中的 <T>
被移除了——在运行时,类内部没有任何 <T>
,因此这看起来毫无意义。但是如果你遵从这种思路,并将这个表达式改为 new ArrayList()
,编译器就会发出警告。
本例中这么做真的毫无意义吗?如果在创建
// generics/FilledList.java
import java.util.*;
import java.util.function.*;
import onjava.*;
public class FilledList<T> extends ArrayList<T> {
FilledList(Supplier<T> 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<String> list = new FilledList<>("Hello", 4);
System.out.println(list);
// Supplier version:
List<Integer> ilist = new FilledList<>(() -> 47, 4);
System.out.println(ilist);
}
}
/* Output:
[Hello,Hello,Hello,Hello]
[47,47,47,47]
*/
即使编译器无法得知 add()
中的
因为擦除移除了方法体中的类型信息,所以在运行时的问题就是边界:即对象进入和离开方法的地点。这些正是编译器在编译期执行类型检查并插入转型代码的地点。
考虑如下这段非泛型示例:
// 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();
}
}
如果用
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 "<init>":()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()
时接受检查。
现在将泛型融入上例代码中:
// generics/GenericHolder2.java
public class GenericHolder2<T> {
private T obj;
public void set(T obj) {
this.obj = obj;
}
public T get() {
return obj;
}
public static void main(String[] args) {
GenericHolder2<String> holder = new GenericHolder2<>();
holder.set("Item");
String s = holder.get();
}
}
从 get()
返回后的转型消失了,但是我们还知道传递给 set()
的值在编译期会被检查。下面是相关的字节码:
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 "<init>":()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()
产生了相同的字节码,这就告诉我们泛型的所有动作都发生在边界处——对入参的编译器检查和对返回值的转型。这有助于澄清对擦除的困惑,记住
补偿擦除
因为擦除,我们将失去执行泛型代码中某些操作的能力。无法在运行时知道确切类型:
// generics/Erased.java
// {WillNotCompile}
public class Erased<T> {
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];
}
}
有时,我们可以对这些问题进行编程,但是有时必须通过引入类型标签来补偿擦除。这意味着为所需的类型显式传递一个
例如,由于擦除了类型信息,因此在上一个程序中尝试使用isInstance()
:
// generics/ClassTypeCapture.java
class Building {
}
class House extends Building {
}
public class ClassTypeCapture<T> {
Class<T> kind;
public ClassTypeCapture(Class<T> kind) {
this.kind = kind;
}
public boolean f(Object arg) {
return kind.isInstance(arg);
}
public static void main(String[] args) {
ClassTypeCapture<Building> ctt1 =
new ClassTypeCapture<>(Building.class);
System.out.println(ctt1.f(new Building()));
System.out.println(ctt1.f(new House()));
ClassTypeCapture<House> ctt2 =
new ClassTypeCapture<>(House.class);
System.out.println(ctt2.f(new Building()));
System.out.println(ctt2.f(new House()));
}
}
/* Output:
true
true
false
true
*/
编译器来保证类型标签与泛型参数相匹配。
创建类型的实例
试图在new T()
是行不通的,部分原因是由于擦除,部分原因是编译器无法验证
// generics/InstantiateGenericType.cpp
// C++, not Java!
template<class T> class Foo {
T x; // Create a field of type T
T* y; // Pointer to T
public:
// Initialize the pointer:
Foo() { y = new T(); }
};
class Bar {};
int main() {
Foo<Bar> fb;
Foo<int> fi; // ... and it works with primitives
}
newInstance()
创建该类型的新对象:
// generics/InstantiateGenericType.java
import java.util.function.Supplier;
class ClassAsFactory<T> implements Supplier<T> {
Class<T> kind;
ClassAsFactory(Class<T> kind) {
this.kind = kind;
}
@Override
public T get() {
try {
return kind.newInstance();
} catch (InstantiationException |
IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
class Employee {
@Override
public String toString() {
return "Employee";
}
}
public class InstantiateGenericType {
public static void main(String[] args) {
ClassAsFactory<Employee> fe =
new ClassAsFactory<>(Employee.class);
System.out.println(fe.get());
ClassAsFactory<Integer> fi =
new ClassAsFactory<>(Integer.class);
try {
System.out.println(fi.get());
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
/* Output:
Employee
java.lang.InstantiationException: java.lang.Integer
*/
这样可以编译,但对于 ClassAsFactory<Integer>
会失败,这是因为
// generics/FactoryConstraint.java
import onjava.Suppliers;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
class IntegerFactory implements Supplier<Integer> {
private int i = 0;
@Override
public Integer get() {
return ++i;
}
}
class Widget {
private int id;
Widget(int n) {
id = n;
}
@Override
public String toString() {
return "Widget " + id;
}
public static
class Factory implements Supplier<Widget> {
private int i = 0;
@Override
public Widget get() {
return new Widget(++i);
}
}
}
class Fudge {
private static int count = 1;
private int n = count++;
@Override
public String toString() {
return "Fudge " + n;
}
}
class Foo2<T> {
private List<T> x = new ArrayList<>();
Foo2(Supplier<T> factory) {
Suppliers.fill(x, factory, 5);
}
@Override
public String toString() {
return x.toString();
}
}
public class FactoryConstraint {
public static void main(String[] args) {
System.out.println(
new Foo2<>(new IntegerFactory()));
System.out.println(
new Foo2<>(new Widget.Factory()));
System.out.println(
new Foo2<>(Fudge::new));
}
}
/* Output:
[1, 2, 3, 4, 5]
[Widget 1, Widget 2, Widget 3, Widget 4, Widget 5]
[Fudge 1, Fudge 2, Fudge 3, Fudge 4, Fudge 5]
*/
Supplier<Integer>
的工厂。 Fudge::new
仍然会产生工厂行为,因为编译器将对函数方法 ::new
的调用转换为对 get()
的调用。
另一种方法是模板方法设计模式。在以下示例中,create()
是模板方法,在子类中被重写以生成该类型的对象:
// generics/CreatorGeneric.java
abstract class GenericWithCreate<T> {
final T element;
GenericWithCreate() {
element = create();
}
abstract T create();
}
class X {
}
class XCreator extends GenericWithCreate<X> {
@Override
X create() {
return new X();
}
void f() {
System.out.println(
element.getClass().getSimpleName());
}
}
public class CreatorGeneric {
public static void main(String[] args) {
XCreator xc = new XCreator();
xc.f();
}
}
/* Output:
X
*/
element
字段,并通过无参构造函数强制其初始化,该构造函数又调用抽象的 create()
方法。这种创建方式可以在子类中定义,同时建立
泛型数组
正如在
// generics/ListOfGenerics.java
import java.util.ArrayList;
import java.util.List;
public class ListOfGenerics<T> {
private List<T> array = new ArrayList<>();
public void add(T item) {
array.add(item);
}
public T get(int index) {
return array.get(index);
}
}
这样做可以获得数组的行为,并且还具有泛型提供的编译时类型安全性。
有时,仍然会创建泛型类型的数组(例如,
// generics/ArrayOfGenericReference.java
class Generic<T> {
}
public class ArrayOfGenericReference {
static Generic<Integer>[] gia;
}
编译器接受此操作而不产生警告。但是我们永远无法创建具有该确切类型(包括类型参数)的数组,因此有点令人困惑。由于所有数组,无论它们持有什么类型,都具有相同的结构(每个数组插槽的大小和数组布局
// generics/ArrayOfGeneric.java
public class ArrayOfGeneric {
static final int SIZE = 100;
static Generic<Integer>[] gia;
@SuppressWarnings("unchecked")
public static void main(String[] args) {
try {
gia = (Generic<Integer>[]) new Object[SIZE];
} catch (ClassCastException e) {
System.out.println(e.getMessage());
}
// Runtime type is the raw (erased) type:
gia = (Generic<Integer>[]) new Generic[SIZE];
System.out.println(gia.getClass().getSimpleName());
gia[0] = new Generic<>();
//- gia[1] = new Object(); // Compile-time error
// Discovers type mismatch at compile time:
//- gia[2] = new Generic<Double>();
}
}
/* Output:
[Ljava.lang.Object; cannot be cast to [LGeneric;
Generic[]
*/
问题在于数组会跟踪其实际类型,而该类型是在创建数组时建立的。因此,即使 gia
被强制转换为 Generic<Integer>[]
,该信息也仅在编译时存在(并且没有
让我们看一个更复杂的示例。考虑一个包装数组的简单泛型包装器:
// generics/GenericArray.java
public class GenericArray<T> {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArray(int sz) {
array = (T[]) new Object[sz];
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) {
return array[index];
}
// Method that exposes the underlying representation:
public T[] rep() {
return array;
}
public static void main(String[] args) {
GenericArray<Integer> gai = new GenericArray<>(10);
try {
Integer[] ia = gai.rep();
} catch (ClassCastException e) {
System.out.println(e.getMessage());
}
// This is OK:
Object[] oa = gai.rep();
}
}
/* Output:
[Ljava.lang.Object; cannot be cast to
[Ljava.lang.Integer;
*/
和以前一样,我们不能说 T[] array = new T[sz]
,所以我们创建了一个
rep()
方法返回一个 T[]
,在主方法中它应该是 gai
的 Integer[]
,但是如果调用它并尝试将结果转换为 Integer[]
引用,则会得到Object[]
。
如果再注释掉
GenericArray.java uses unchecked or unsafe operations.
Recompile with -Xlint:unchecked for details.
在这里,我们收到了一个警告,我们认为这是有关强制转换的。
但是要真正确定,请使用 -Xlint:unchecked
进行编译:
GenericArray.java:7: warning: [unchecked] unchecked cast array = (T[])new Object[sz]; ^ required: T[] found: Object[] where T is a type-variable: T extends Object declared in class GenericArray 1 warning
确实是在抱怨那个强制转换。由于警告会变成噪音,因此,一旦我们确认预期会出现特定警告,我们可以做的最好的办法就是使用
由于擦除,数组的运行时类型只能是 Object[]
。 如果我们立即将其转换为 T[]
,则在编译时会丢失数组的实际类型,并且编译器可能会错过一些潜在的错误检查。因此,最好在集合中使用 Object[]
,并在使用数组元素时向
// generics/GenericArray2.java
public class GenericArray2<T> {
private Object[] array;
public GenericArray2(int sz) {
array = new Object[sz];
}
public void put(int index, T item) {
array[index] = item;
}
@SuppressWarnings("unchecked")
public T get(int index) {
return (T) array[index];
}
@SuppressWarnings("unchecked")
public T[] rep() {
return (T[]) array; // Unchecked cast
}
public static void main(String[] args) {
GenericArray2<Integer> gai =
new GenericArray2<>(10);
for (int i = 0; i < 10; i++)
gai.put(i, i);
for (int i = 0; i < 10; i++)
System.out.print(gai.get(i) + " ");
System.out.println();
try {
Integer[] ia = gai.rep();
} catch (Exception e) {
System.out.println(e);
}
}
}
/* Output:
0 1 2 3 4 5 6 7 8 9
java.lang.ClassCastException: [Ljava.lang.Object;
cannot be cast to [Ljava.lang.Integer;
*/
最初,看起来并没有太大不同,只是转换的位置移动了。没有Object[]
而不是 T[]
。 调用 get()
时,它将对象强制转换为rep()
,它将再次尝试将 Object[]
强制转换为 T[]
,但仍然不正确,并在编译时生成警告,并在运行时生成异常。因此,无法破坏基础数组的类型,该基础数组只能是 Object[]
。在内部将数组视为 Object[]
而不是 T[]
的优点是,我们不太可能会忘记数组的运行时类型并意外地引入了
对于新代码,请传入类型标记。在这种情况下,
// generics/GenericArrayWithTypeToken.java
import java.lang.reflect.Array;
public class GenericArrayWithTypeToken<T> {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArrayWithTypeToken(Class<T> type, int sz) {
array = (T[]) Array.newInstance(type, sz);
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) {
return array[index];
}
// Expose the underlying representation:
public T[] rep() {
return array;
}
public static void main(String[] args) {
GenericArrayWithTypeToken<Integer> gai =
new GenericArrayWithTypeToken<>(
Integer.class, 10);
// This now works:
Integer[] ia = gai.rep();
}
}
类型标记T[]
。
不幸的是,如果查看
public ArrayList(Collection c) {
size = c.size();
elementData = (E[])new Object[size];
c.toArray(elementData);
}
如果你浏览
Note: ArrayList.java uses unchecked or unsafe operations
Note: Recompile with -Xlint:unchecked for details.
果然,标准库会产生很多警告。如果你使用过
Neal Gafter(
请注意,在
边界
边界(bounds)在本章的前面进行了简要介绍。边界允许我们对泛型使用的参数类型施加约束。尽管这可以强制执行有关应用了泛型类型的规则,但潜在的更重要的效果是我们可以在绑定的类型中调用方法。
由于擦除会删除类型信息,因此唯一可用于无限制泛型参数的方法是那些extends
关键字。
重要的是要理解,当用于限定泛型类型时,extends
的含义与通常的意义截然不同。此示例展示边界的基础应用:
// generics/BasicBounds.java
interface HasColor {
java.awt.Color getColor();
}
class WithColor<T extends HasColor> {
T item;
WithColor(T item) {
this.item = item;
}
T getItem() {
return item;
}
// The bound allows you to call a method:
java.awt.Color color() {
return item.getColor();
}
}
class Coord {
public int x, y, z;
}
// This fails. Class must be first, then interfaces:
// class WithColorCoord<T extends HasColor & Coord> {
// Multiple bounds:
class WithColorCoord<T extends Coord & HasColor> {
T item;
WithColorCoord(T item) {
this.item = item;
}
T getItem() {
return item;
}
java.awt.Color color() {
return item.getColor();
}
int getX() {
return item.x;
}
int getY() {
return item.y;
}
int getZ() {
return item.z;
}
}
interface Weight {
int weight();
}
// As with inheritance, you can have only one
// concrete class but multiple interfaces:
class Solid<T extends Coord & HasColor & Weight> {
T item;
Solid(T item) {
this.item = item;
}
T getItem() {
return item;
}
java.awt.Color color() {
return item.getColor();
}
int getX() {
return item.x;
}
int getY() {
return item.y;
}
int getZ() {
return item.z;
}
int weight() {
return item.weight();
}
}
class Bounded
extends Coord implements HasColor, Weight {
@Override
public java.awt.Color getColor() {
return null;
}
@Override
public int weight() {
return 0;
}
}
public class BasicBounds {
public static void main(String[] args) {
Solid<Bounded> solid =
new Solid<>(new Bounded());
solid.color();
solid.getY();
solid.weight();
}
}
你可能会观察到
// generics/InheritBounds.java
class HoldItem<T> {
T item;
HoldItem(T item) {
this.item = item;
}
T getItem() {
return item;
}
}
class WithColor2<T extends HasColor>
extends HoldItem<T> {
WithColor2(T item) {
super(item);
}
java.awt.Color color() {
return item.getColor();
}
}
class WithColorCoord2<T extends Coord & HasColor>
extends WithColor2<T> {
WithColorCoord2(T item) {
super(item);
}
int getX() {
return item.x;
}
int getY() {
return item.y;
}
int getZ() {
return item.z;
}
}
class Solid2<T extends Coord & HasColor & Weight>
extends WithColorCoord2<T> {
Solid2(T item) {
super(item);
}
int weight() {
return item.weight();
}
}
public class InheritBounds {
public static void main(String[] args) {
Solid2<Bounded> solid2 =
new Solid2<>(new Bounded());
solid2.color();
solid2.getY();
solid2.weight();
}
}
这是一个具有更多层次的示例:
// generics/EpicBattle.java
// Bounds in Java generics
import java.util.List;
interface SuperPower {
}
interface XRayVision extends SuperPower {
void seeThroughWalls();
}
interface SuperHearing extends SuperPower {
void hearSubtleNoises();
}
interface SuperSmell extends SuperPower {
void trackBySmell();
}
class SuperHero<POWER extends SuperPower> {
POWER power;
SuperHero(POWER power) {
this.power = power;
}
POWER getPower() {
return power;
}
}
class SuperSleuth<POWER extends XRayVision>
extends SuperHero<POWER> {
SuperSleuth(POWER power) {
super(power);
}
void see() {
power.seeThroughWalls();
}
}
class
CanineHero<POWER extends SuperHearing & SuperSmell>
extends SuperHero<POWER> {
CanineHero(POWER power) {
super(power);
}
void hear() {
power.hearSubtleNoises();
}
void smell() {
power.trackBySmell();
}
}
class SuperHearSmell
implements SuperHearing, SuperSmell {
@Override
public void hearSubtleNoises() {
}
@Override
public void trackBySmell() {
}
}
class DogPerson extends CanineHero<SuperHearSmell> {
DogPerson() {
super(new SuperHearSmell());
}
}
public class EpicBattle {
// Bounds in generic methods:
static <POWER extends SuperHearing>
void useSuperHearing(SuperHero<POWER> hero) {
hero.getPower().hearSubtleNoises();
}
static <POWER extends SuperHearing & SuperSmell>
void superFind(SuperHero<POWER> hero) {
hero.getPower().hearSubtleNoises();
hero.getPower().trackBySmell();
}
public static void main(String[] args) {
DogPerson dogPerson = new DogPerson();
useSuperHearing(dogPerson);
superFind(dogPerson);
// You can do this:
List<? extends SuperHearing> audioPeople;
// But you can't do this:
// List<? extends SuperHearing & SuperSmell> dogPs;
}
}
接下来将要研究的通配符将会把范围限制在单个类型。
通配符
你已经在 集合 章节中看到了一些简单示例使用了通配符——在泛型参数表达式中的问号,在 类型信息 一章中这种示例更多。本节将更深入地探讨这个特性。
我们的起始示例要展示数组的一种特殊行为:你可以将派生类的数组赋值给基类的引用:
// generics/CovariantArrays.java
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}
public class CovariantArrays {
public static void main(String[] args) {
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple(); // OK
fruit[1] = new Jonathan(); // OK
// Runtime type is Apple[], not Fruit[] or Orange[]:
try {
// Compiler allows you to add Fruit:
fruit[0] = new Fruit(); // ArrayStoreException
} catch (Exception e) {
System.out.println(e);
}
try {
// Compiler allows you to add Oranges:
fruit[0] = new Orange(); // ArrayStoreException
} catch (Exception e) {
System.out.println(e);
}
}
}
/* Output:
java.lang.ArrayStoreException: Fruit
java.lang.ArrayStoreException: Orange
main()
中的第一行创建了
但是,如果实际的数组类型是
向上转型用在这里不合适。你真正在做的是将一个数组赋值给另一个数组。数组的行为是持有其他对象,这里只是因为我们能够向上转型而已,所以很明显,数组对象可以保留有关它们包含的对象类型的规则。看起来就像数组对它们持有的对象是有意识的,因此在编译期检查和运行时检查之间,你不能滥用它们。
数组的这种赋值并不是那么可怕,因为在运行时你可以发现插入了错误的类型。但是泛型的主要目标之一是将这种错误检测移到编译期。所以当我们试图使用泛型集合代替数组时,会发生什么呢?
// generics/NonCovariantGenerics.java
// {WillNotCompile}
import java.util.*;
public class NonCovariantGenerics {
// Compile Error: incompatible types:
List<Fruit> flist = new ArrayList<Apple>();
}
尽管你在首次阅读这段代码时会认为“不能将一个
真正的问题是我们在讨论的集合类型,而不是集合持有对象的类型。与数组不同,泛型没有内建的协变类型。这是因为数组是完全在语言中定义的,因此可以具有编译期和运行时的内建检查,但是在使用泛型时,编译器和运行时系统不知道你想用类型做什么,以及应该采用什么规则。
但是,有时你想在两个类型间建立某种向上转型关系。通配符可以产生这种关系。
// generics/GenericsAndCovariance.java
import java.util.*;
public class GenericsAndCovariance {
public static void main(String[] args) {
// Wildcards allow covariance:
List<? extends Fruit> flist = new ArrayList<>();
// Compile Error: can't add any type of object:
// flist.add(new Apple());
// flist.add(new Fruit());
// flist.add(new Object());
flist.add(null); // Legal but uninteresting
// We know it returns at least Fruit:
Fruit f = flist.get(0);
}
}
List<? extends Fruit>
,你可以读作“一个具有任何从
你可能认为事情开始变得有点走极端了,因为现在你甚至不能向刚刚声明过将持有List<? extends Fruit>
可能合法地指向一个 List<Orange>
。一旦执行这种类型的向上转型,你就丢失了向其中传递任何对象的能力,甚至传递
另一方面,如果你调用了一个返回
编译器有多聪明
现在你可能会猜想自己不能去调用任何接受参数的方法,但是考虑下面的代码:
// generics/CompilerIntelligence.java
import java.util.*;
public class CompilerIntelligence {
public static void main(String[] args) {
List<? extends Fruit> flist = Arrays.asList(new Apple());
Apple a = (Apple) flist.get(0); // No warning
flist.contains(new Apple()); // Argument is 'Object'
flist.indexOf(new Apple()); // Argument is 'Object'
}
}
这里对 contains()
和 indexOf()
的调用接受
通过查看add()
接受一个泛型参数类型的参数,但 contains()
和 indexOf()
接受的参数类型是ArrayList<? extends Fruit>
时,add()
的参数就变成了add()
这样参数列表中涉及通配符的方法。
contains()
和 indexOf()
的参数类型是
下面展示一个简单的
// generics/Holder.java
public class Holder<T> {
private T value;
public Holder() {}
public Holder(T val) {
value = val;
}
public void set(T val) {
value = val;
}
public T get() {
return value;
}
@Override
public boolean equals(Object o) {
return o instanceof Holder && Objects.equals(value, ((Holder) o).value);
}
@Override
public int hashCode() {
return Objects.hashCode(value);
}
public static void main(String[] args) {
Holder<Apple> apple = new Holder<>(new Apple());
Apple d = apple.get();
apple.set(d);
// Holder<Fruit> fruit = apple; // Cannot upcast
Holder<? extends Fruit> fruit = apple; // OK
Fruit p = fruit.get();
d = (Apple) fruit.get();
try {
Orange c = (Orange) fruit.get(); // No warning
} catch (Exception e) {
System.out.println(e);
}
// fruit.set(new Apple()); // Cannot call set()
// fruit.set(new Fruit()); // Cannot call set()
System.out.println(fruit.equals(d)); // OK
}
}
/* Output
java.lang.ClassCastException: Apple cannot be cast to Orange
false
*/
set()
方法,一个返回get()
方法和一个接受equals()
方法。正如你所见,如果创建了一个 Holder<Apple>
,就不能将其向上转型为 Holder<Fruit>
,但是可以向上转型为 Holder<? extends Fruit>
。如果调用 get()
,只能返回一个set()
方法不能工作在set()
的参数也是
但是,equals()
方法可以正常工作,因为它接受的参数是
equals()
和 hashCode()
方法变得更加容易,当然还有很多其他功能。equals()
方法的标准形式参考 附录:理解
逆变
还可以走另外一条路,即使用超类型通配符。这里,可以声明通配符是由某个特定类的任何基类来界定的,方法是指定 <?super MyClass>
,或者甚至使用类型参数: <?super T>
(尽管你不能对泛型参数给出一个超类型边界;即不能声明 <T super MyClass>
// generics/SuperTypeWildcards.java
import java.util.*;
public class SuperTypeWildcards {
static void writeTo(List<? super Apple> apples) {
apples.add(new Apple());
apples.add(new Jonathan());
// apples.add(new Fruit()); // Error
}
}
参数
// generics/GenericReading.java
import java.util.*;
public class GenericReading {
static List<Apple> apples = Arrays.asList(new Apple());
static List<Fruit> fruit = Arrays.asList(new Fruit());
static <T> T readExact(List<T> list) {
return list.get(0);
}
// A static method adapts to each call:
static void f1() {
Apple a = readExact(apples);
Fruit f = readExact(fruit);
f = readExact(apples);
}
// A class type is established
// when the class is instantiated:
static class Reader<T> {
T readExact(List<T> list) {
return list.get(0);
}
}
static void f2() {
Reader<Fruit> fruitReader = new Reader<>();
Fruit f = fruitReader.readExact(fruit);
//- Fruit a = fruitReader.readExact(apples);
// error: incompatible types: List<Apple>
// cannot be converted to List<Fruit>
}
static class CovariantReader<T> {
T readCovariant(List<? extends T> list) {
return list.get(0);
}
}
static void f3() {
CovariantReader<Fruit> fruitReader = new CovariantReader<>();
Fruit f = fruitReader.readCovariant(fruit);
Fruit a = fruitReader.readCovariant(apples);
}
public static void main(String[] args) {
f1();
f2();
f3();
}
}
readExact()
方法使用了精确的类型。如果使用这个没有任何通配符的精确类型,就可以向readExact()
可以有效地“适应”每个方法调用,并能够从 List<Apple>
中返回一个List<Fruit>
中返回一个f1()
中看到的那样。因此,如果可以摆脱静态泛型方法,那么在读取时就不需要协变类型了。
然而对于泛型类来说,当你创建这个类的实例时,就要为这个类确定参数。就像在 f2()
中看到的,List<Fruit>
中读取一个List<Apple>
也应该产生CovariantReader.readCovariant()
方法将接受 List<?extends T>
,因此,从这个列表中读取一个f3()
中,你可以看到现在可以从 List<Apple>
中读取
无界通配符
无界通配符 <?>
看起来意味着“任何事物”,因此使用无界通配符好像等价于使用原生类型。事实上,编译器初看起来是支持这种判断的:
// generics/UnboundedWildcards1.java
import java.util.*;
public class UnboundedWildcards1 {
static List list1;
static List<?> list2;
static List<? extends Object> list3;
static void assign1(List list) {
list1 = list;
list2 = list;
//- list3 = list;
// warning: [unchecked] unchecked conversion
// list3 = list;
// ^
// required: List<? extends Object>
// found: List
}
static void assign2(List<?> list) {
list1 = list;
list2 = list;
list3 = list;
}
static void assign3(List<? extends Object> list) {
list1 = list;
list2 = list;
list3 = list;
}
public static void main(String[] args) {
assign1(new ArrayList());
assign2(new ArrayList());
//- assign3(new ArrayList());
// warning: [unchecked] unchecked method invocation:
// method assign3 in class UnboundedWildcards1
// is applied to given types
// assign3(new ArrayList());
// ^
// required: List<? extends Object>
// found: ArrayList
// warning: [unchecked] unchecked conversion
// assign3(new ArrayList());
// ^
// required: List<? extends Object>
// found: ArrayList
// 2 warnings
assign1(new ArrayList<>());
assign2(new ArrayList<>());
assign3(new ArrayList<>());
// Both forms are acceptable as List<?>:
List<?> wildList = new ArrayList();
wildList = new ArrayList<>();
assign1(wildList);
assign2(wildList);
assign3(wildList);
}
}
有很多情况都和你在这里看到的情况类似,即编译器很少关心使用的是原生类型还是 <?>
。在这些情况中,<?>
可以被认为是一种装饰,但是它仍旧是很有价值的,因为,实际上它是在声明
// generics/UnboundedWildcards2.java
import java.util.*;
public class UnboundedWildcards2 {
static Map map1;
static Map<?,?> map2;
static Map<String,?> map3;
static void assign1(Map map) {
map1 = map;
}
static void assign2(Map<?,?> map) {
map2 = map;
}
static void assign3(Map<String,?> map) {
map3 = map;
}
public static void main(String[] args) {
assign1(new HashMap());
assign2(new HashMap());
//- assign3(new HashMap());
// warning: [unchecked] unchecked method invocation:
// method assign3 in class UnboundedWildcards2
// is applied to given types
// assign3(new HashMap());
// ^
// required: Map<String,?>
// found: HashMap
// warning: [unchecked] unchecked conversion
// assign3(new HashMap());
// ^
// required: Map<String,?>
// found: HashMap
// 2 warnings
assign1(new HashMap<>());
assign2(new HashMap<>());
assign3(new HashMap<>());
}
}
但是,当你拥有的全都是无界通配符时,就像在 Map<?,?>
中看到的那样,编译器看起来就无法将其与原生List<?>
和 List<? extends Object>
是不同的。
令人困惑的是,编译器并非总是关注像 List
和 List<?>
之间的这种差异,因此它们看起来就像是相同的事物。事实上,因为泛型参数擦除到它的第一个边界,因此 List<?>
看起来等价于 List<Object>
,而List<Object>
——除非这些语句都不为真。List<?>
表示“具有某种特定类型的非原生Holder<T>
类,它包含接受
// generics/Wildcards.java
// Exploring the meaning of wildcards
public class Wildcards {
// Raw argument:
static void rawArgs(Holder holder, Object arg) {
//- holder.set(arg);
// warning: [unchecked] unchecked call to set(T)
// as a member of the raw type Holder
// holder.set(arg);
// ^
// where T is a type-variable:
// T extends Object declared in class Holder
// 1 warning
// Can't do this; don't have any 'T':
// T t = holder.get();
// OK, but type information is lost:
Object obj = holder.get();
}
// Like rawArgs(), but errors instead of warnings:
static void unboundedArg(Holder<?> holder, Object arg) {
//- holder.set(arg);
// error: method set in class Holder<T>
// cannot be applied to given types;
// holder.set(arg);
// ^
// required: CAP#1
// found: Object
// reason: argument mismatch;
// Object cannot be converted to CAP#1
// where T is a type-variable:
// T extends Object declared in class Holder
// where CAP#1 is a fresh type-variable:
// CAP#1 extends Object from capture of ?
// 1 error
// Can't do this; don't have any 'T':
// T t = holder.get();
// OK, but type information is lost:
Object obj = holder.get();
}
static <T> T exact1(Holder<T> holder) {
return holder.get();
}
static <T> T exact2(Holder<T> holder, T arg) {
holder.set(arg);
return holder.get();
}
static <T> T wildSubtype(Holder<? extends T> holder, T arg) {
//- holder.set(arg);
// error: method set in class Holder<T#2>
// cannot be applied to given types;
// holder.set(arg);
// ^
// required: CAP#1
// found: T#1
// reason: argument mismatch;
// T#1 cannot be converted to CAP#1
// where T#1,T#2 are type-variables:
// T#1 extends Object declared in method
// <T#1>wildSubtype(Holder<? extends T#1>,T#1)
// T#2 extends Object declared in class Holder
// where CAP#1 is a fresh type-variable:
// CAP#1 extends T#1 from
// capture of ? extends T#1
// 1 error
return holder.get();
}
static <T> void wildSupertype(Holder<? super T> holder, T arg) {
holder.set(arg);
//- T t = holder.get();
// error: incompatible types:
// CAP#1 cannot be converted to T
// T t = holder.get();
// ^
// where T is a type-variable:
// T extends Object declared in method
// <T>wildSupertype(Holder<? super T>,T)
// where CAP#1 is a fresh type-variable:
// CAP#1 extends Object super:
// T from capture of ? super T
// 1 error
// OK, but type information is lost:
Object obj = holder.get();
}
public static void main(String[] args) {
Holder raw = new Holder<>();
// Or:
raw = new Holder();
Holder<Long> qualified = new Holder<>();
Holder<?> unbounded = new Holder<>();
Holder<? extends Long> bounded = new Holder<>();
Long lng = 1L;
rawArgs(raw, lng);
rawArgs(qualified, lng);
rawArgs(unbounded, lng);
rawArgs(bounded, lng);
unboundedArg(raw, lng);
unboundedArg(qualified, lng);
unboundedArg(unbounded, lng);
unboundedArg(bounded, lng);
//- Object r1 = exact1(raw);
// warning: [unchecked] unchecked method invocation:
// method exact1 in class Wildcards is applied
// to given types
// Object r1 = exact1(raw);
// ^
// required: Holder<T>
// found: Holder
// where T is a type-variable:
// T extends Object declared in
// method <T>exact1(Holder<T>)
// warning: [unchecked] unchecked conversion
// Object r1 = exact1(raw);
// ^
// required: Holder<T>
// found: Holder
// where T is a type-variable:
// T extends Object declared in
// method <T>exact1(Holder<T>)
// 2 warnings
Long r2 = exact1(qualified);
Object r3 = exact1(unbounded); // Must return Object
Long r4 = exact1(bounded);
//- Long r5 = exact2(raw, lng);
// warning: [unchecked] unchecked method invocation:
// method exact2 in class Wildcards is
// applied to given types
// Long r5 = exact2(raw, lng);
// ^
// required: Holder<T>,T
// found: Holder,Long
// where T is a type-variable:
// T extends Object declared in
// method <T>exact2(Holder<T>,T)
// warning: [unchecked] unchecked conversion
// Long r5 = exact2(raw, lng);
// ^
// required: Holder<T>
// found: Holder
// where T is a type-variable:
// T extends Object declared in
// method <T>exact2(Holder<T>,T)
// 2 warnings
Long r6 = exact2(qualified, lng);
//- Long r7 = exact2(unbounded, lng);
// error: method exact2 in class Wildcards
// cannot be applied to given types;
// Long r7 = exact2(unbounded, lng);
// ^
// required: Holder<T>,T
// found: Holder<CAP#1>,Long
// reason: inference variable T has
// incompatible bounds
// equality constraints: CAP#1
// lower bounds: Long
// where T is a type-variable:
// T extends Object declared in
// method <T>exact2(Holder<T>,T)
// where CAP#1 is a fresh type-variable:
// CAP#1 extends Object from capture of ?
// 1 error
//- Long r8 = exact2(bounded, lng);
// error: method exact2 in class Wildcards
// cannot be applied to given types;
// Long r8 = exact2(bounded, lng);
// ^
// required: Holder<T>,T
// found: Holder<CAP#1>,Long
// reason: inference variable T
// has incompatible bounds
// equality constraints: CAP#1
// lower bounds: Long
// where T is a type-variable:
// T extends Object declared in
// method <T>exact2(Holder<T>,T)
// where CAP#1 is a fresh type-variable:
// CAP#1 extends Long from
// capture of ? extends Long
// 1 error
//- Long r9 = wildSubtype(raw, lng);
// warning: [unchecked] unchecked method invocation:
// method wildSubtype in class Wildcards
// is applied to given types
// Long r9 = wildSubtype(raw, lng);
// ^
// required: Holder<? extends T>,T
// found: Holder,Long
// where T is a type-variable:
// T extends Object declared in
// method <T>wildSubtype(Holder<? extends T>,T)
// warning: [unchecked] unchecked conversion
// Long r9 = wildSubtype(raw, lng);
// ^
// required: Holder<? extends T>
// found: Holder
// where T is a type-variable:
// T extends Object declared in
// method <T>wildSubtype(Holder<? extends T>,T)
// 2 warnings
Long r10 = wildSubtype(qualified, lng);
// OK, but can only return Object:
Object r11 = wildSubtype(unbounded, lng);
Long r12 = wildSubtype(bounded, lng);
//- wildSupertype(raw, lng);
// warning: [unchecked] unchecked method invocation:
// method wildSupertype in class Wildcards
// is applied to given types
// wildSupertype(raw, lng);
// ^
// required: Holder<? super T>,T
// found: Holder,Long
// where T is a type-variable:
// T extends Object declared in
// method <T>wildSupertype(Holder<? super T>,T)
// warning: [unchecked] unchecked conversion
// wildSupertype(raw, lng);
// ^
// required: Holder<? super T>
// found: Holder
// where T is a type-variable:
// T extends Object declared in
// method <T>wildSupertype(Holder<? super T>,T)
// 2 warnings
wildSupertype(qualified, lng);
//- wildSupertype(unbounded, lng);
// error: method wildSupertype in class Wildcards
// cannot be applied to given types;
// wildSupertype(unbounded, lng);
// ^
// required: Holder<? super T>,T
// found: Holder<CAP#1>,Long
// reason: cannot infer type-variable(s) T
// (argument mismatch; Holder<CAP#1>
// cannot be converted to Holder<? super T>)
// where T is a type-variable:
// T extends Object declared in
// method <T>wildSupertype(Holder<? super T>,T)
// where CAP#1 is a fresh type-variable:
// CAP#1 extends Object from capture of ?
// 1 error
//- wildSupertype(bounded, lng);
// error: method wildSupertype in class Wildcards
// cannot be applied to given types;
// wildSupertype(bounded, lng);
// ^
// required: Holder<? super T>,T
// found: Holder<CAP#1>,Long
// reason: cannot infer type-variable(s) T
// (argument mismatch; Holder<CAP#1>
// cannot be converted to Holder<? super T>)
// where T is a type-variable:
// T extends Object declared in
// method <T>wildSupertype(Holder<? super T>,T)
// where CAP#1 is a fresh type-variable:
// CAP#1 extends Long from capture of
// ? extends Long
// 1 error
}
}
在 rawArgs()
中,编译器知道 Holder
是一个泛型类型,因此即使它在这里被表示成一个原生类型,编译器仍旧知道向 set()
传递一个set()
,而这个对象将被向上转型为get()
的调用说明了相同的问题:没有任何Holder
与 Holder<?>
是大致相同的事物。但是 unboundedArg()
强调它们是不同的——它揭示了相同的问题,但是它将这些问题作为错误而不是警告报告,因为原生Holder<?>
将持有具有某种具体类型的同构集合,因此不能只是向其中传递exact1()
和 exact2()
中,你可以看到使用了确切的泛型参数——没有任何通配符。你将看到,exact2()
与 exact1()
具有不同的限制,因为它有额外的参数。
在 wildSubtype()
中,在holder
可以是 Holder<Apple>
,这是合法的。为了防止将Holder<Apple>
中,对 set()
的调用(或者对任何接受这个类型参数为参数的方法的调用)都是不允许的。但是,你仍旧知道任何来自 Holder<?extends Fruit>
的对象至少是get()
(或者任何将产生具有这个类型参数的返回值的方法)都是允许的。
wildSupertype()
展示了超类型通配符,这个方法展示了与 wildSubtype()
相反的行为:holder
可以是持有任何set()
可以接受get()
是没有用的,因为由 holder
持有的类型可以是任何超类型,因此唯一安全的类型就是unbounded()
中使用无界通配符能够做什么不能做什么所做出的限制:因为你没有set()
或 get()
作用于
在 main()
方法中你看到了某些方法在接受某些类型的参数时没有报错和警告。为了迁移兼容性,rawArgs()
将接受所有unboundedArg()
方法也可以接受相同的所有类型,尽管如前所述,它在方法体内部处理这些类型的方式并不相同。
如果向接受“确切”泛型类型(没有通配符)的方法传递一个原生exact1()
传递一个无界引用,就不会有任何可以确定返回类型的类型信息。
可以看到,exact2()
具有最多的限制,因为它希望精确地得到一个 Holder<T>
,以及一个具有类型wildSubtype()
中看到的那样wildSupertype()
中看到的那样
捕获转换
有一种特殊情况需要使用 <?>
而不是原生类型。如果向一个使用 <?>
的方法传递原生类型,那么对编译器来说,可能会推断出实际的类型参数,使得这个方法可以回转并调用另一个使用这个确切类型的方法。下面的示例演示了这种技术,它被称为捕获转换,因为未指定的通配符类型被捕获,并被转换为确切类型。这里,有关警告的注释只有在 @SuppressWarnings
注解被移除之后才能起作用:
// generics/CaptureConversion.java
public class CaptureConversion {
static <T> void f1(Holder<T> holder) {
T t = holder.get();
System.out.println(t.getClass().getSimpleName());
}
static void f2(Holder<?> holder) {
f1(holder); // Call with captured type
}
@SuppressWarnings("unchecked")
public static void main(String[] args) {
Holder raw = new Holder<>(1);
f1(raw);
// warning: [unchecked] unchecked method invocation:
// method f1 in class CaptureConversion
// is applied to given types
// f1(raw);
// ^
// required: Holder<T>
// found: Holder
// where T is a type-variable:
// T extends Object declared in
// method <T>f1(Holder<T>)
// warning: [unchecked] unchecked conversion
// f1(raw);
// ^
// required: Holder<T>
// found: Holder
// where T is a type-variable:
// T extends Object declared in
// method <T>f1(Holder<T>)
// 2 warnings
f2(raw); // No warnings
Holder rawBasic = new Holder();
rawBasic.set(new Object());
// warning: [unchecked] unchecked call to set(T)
// as a member of the raw type Holder
// rawBasic.set(new Object());
// ^
// where T is a type-variable:
// T extends Object declared in class Holder
// 1 warning
f2(rawBasic); // No warnings
// Upcast to Holder<?>, still figures it out:
Holder<?> wildcarded = new Holder<>(1.0);
f2(wildcarded);
}
}
/* Output:
Integer
Integer
Object
Double
*/
f1()
中的类型参数都是确切的,没有通配符或边界。在 f2()
中,f2()
中调用了 f1()
,而 f1()
需要一个已知参数。这里所发生的是:在调用 f2()
的过程中捕获了参数类型,并在调用 f1()
时使用了这种类型。
你可能想知道这项技术是否可以用于写入,但是这要求在传递 Holder<?>
时同时传递一个具体类型。捕获转换只有在这样的情况下可以工作:即在方法内部,你需要使用确切的类型。注意,不能从 f2()
中返回f2()
来说是未知的。捕获转换十分有趣,但是非常受限。
问题
本节将阐述在使用
任何基本类型都不能作为类型参数
正如本章早先提到的,ArrayList<int>
之类的东西。
解决方法是使用基本类型的包装器类以及自动装箱机制。如果创建一个 ArrayList<Integer>
,并将基本类型ArrayList<int>
一样:
// generics/ListOfInt.java
// Autoboxing compensates for the inability
// to use primitives in generics
import java.util.*;
import java.util.stream.*;
public class ListOfInt {
public static void main(String[] args) {
List<Integer> li = IntStream.range(38, 48)
.boxed() // Converts ints to Integers
.collect(Collectors.toList());
System.out.println(li);
}
}
/* Output:
[38, 39, 40, 41, 42, 43, 44, 45, 46, 47]
*/
通常,这种解决方案工作得很好——能够成功地存储和读取
// generics/ByteSet.java
import java.util.*;
public class ByteSet {
Byte[] possibles = { 1,2,3,4,5,6,7,8,9 };
Set<Byte> mySet = new HashSet<>(Arrays.asList(possibles));
// But you can't do this:
// Set<Byte> mySet2 = new HashSet<>(
// Arrays.<Byte>asList(1,2,3,4,5,6,7,8,9));
}
自动装箱机制解决了一些问题,但并没有解决所有问题。
在下面的示例中,main()
中,可以看到 FillArray.fill()
使用对象填充了数组:
// generics/PrimitiveGenericTest.java
import onjava.*;
import java.util.*;
import java.util.function.*;
// Fill an array using a generator:
interface FillArray {
static <T> T[] fill(T[] a, Supplier<T> gen) {
Arrays.setAll(a, n -> gen.get());
return a;
}
static int[] fill(int[] a, IntSupplier gen) {
Arrays.setAll(a, n -> gen.getAsInt());
return a;
}
static long[] fill(long[] a, LongSupplier gen) {
Arrays.setAll(a, n -> gen.getAsLong());
return a;
}
static double[] fill(double[] a, DoubleSupplier gen) {
Arrays.setAll(a, n -> gen.getAsDouble());
return a;
}
}
public class PrimitiveGenericTest {
public static void main(String[] args) {
String[] strings = FillArray.fill(
new String[5], new Rand.String(9));
System.out.println(Arrays.toString(strings));
int[] integers = FillArray.fill(
new int[9], new Rand.Pint());
System.out.println(Arrays.toString(integers));
}
}
/* Output:
[btpenpccu, xszgvgmei, nneeloztd, vewcippcy, gpoalkljl]
[635, 8737, 3941, 4720, 6177, 8479, 6656, 3768, 4948]
*/
自动装箱不适用于数组,因此我们必须创建 FillArray.fill()
的重载版本,或创建产生java.util.Arrays.setAll()
有用一点,因为它返回填充的数组。
实现参数化接口
一个类不能实现同一个泛型接口的两种变体,由于擦除的原因,这两个变体会成为相同的接口。下面是产生这种冲突的情况:
// generics/MultipleInterfaceVariants.java
// {WillNotCompile}
package generics;
interface Payable<T> {}
class Employee implements Payable<Employee> {}
class Hourly extends Employee implements Payable<Hourly> {}
Payable<Employe>
和 Payable<Hourly>
简化为相同的类
在使用某些更基本的Comparable<T>
时,这个问题可能会变得十分令人恼火,就像你在本节稍后看到的那样。
转型和警告
使用带有泛型类型参数的转型或
// generics/GenericCast.java
import java.util.*;
import java.util.stream.*;
class FixedSizeStack<T> {
private final int size;
private Object[] storage;
private int index = 0;
FixedSizeStack(int size) {
this.size = size;
storage = new Object[size];
}
public void push(T item) {
if(index < size)
storage[index++] = item;
}
@SuppressWarnings("unchecked")
public T pop() {
return index == 0 ? null : (T)storage[--index];
}
@SuppressWarnings("unchecked")
Stream<T> stream() {
return (Stream<T>)Arrays.stream(storage);
}
}
public class GenericCast {
static String[] letters = "ABCDEFGHIJKLMNOPQRS".split("");
public static void main(String[] args) {
FixedSizeStack<String> strings =
new FixedSizeStack<>(letters.length);
Arrays.stream("ABCDEFGHIJKLMNOPQRS".split(""))
.forEach(strings::push);
System.out.println(strings.pop());
strings.stream()
.map(s -> s + " ")
.forEach(System.out::print);
}
}
/* Output:
S
A B C D E F G H I J K L M N O P Q R S
*/
如果没有pop()
产生 “unchecked cast” 警告。由于擦除的原因,编译器无法知道这个转型是否是安全的,并且 pop()
方法实际上并没有执行任何转型。
这是因为,pop()
实际上只是将
// generics/NeedCasting.java
import java.io.*;
import java.util.*;
public class NeedCasting {
@SuppressWarnings("unchecked")
public void f(String[] args) throws Exception {
ObjectInputStream in = new ObjectInputStream(
new FileInputStream(args[0]));
List<Widget> shapes = (List<Widget>)in.readObject();
}
}
正如你将在 附录:对象序列化 中学到的那样,readObject()
无法知道它正在读取的是什么,因此它返回的是必须转型的对象。但是当注释掉
NeedCasting.java uses unchecked or unsafe operations.
Recompile with -Xlint:unchecked for details.
And if you follow the instructions and recompile with -
Xlint:unchecked :(如果遵循这条指示,使用-Xlint:unchecked来重新编译:)
NeedCasting.java:10: warning: [unchecked] unchecked cast
List<Widget> shapes = (List<Widget>)in.readObject();
required: List<Widget>
found: Object
1 warning
你会被强制要求转型,但是又被告知不应该转型。为了解决这个问题,必须使用
// generics/ClassCasting.java
import java.io.*;
import java.util.*;
public class ClassCasting {
@SuppressWarnings("unchecked")
public void f(String[] args) throws Exception {
ObjectInputStream in = new ObjectInputStream(
new FileInputStream(args[0]));
// Won't Compile:
// List<Widget> lw1 =
// List<>.class.cast(in.readObject());
List<Widget> lw2 = List.class.cast(in.readObject());
}
}
但是,不能转型到实际类型( List<Widget>
List<Widget>.class.cast(in.readobject())
甚至当你添加一个像下面这样的另一个转型时:
(List<Widget>)List.class.cast(in.readobject())
仍旧会得到一个警告。
重载
下面的程序是不能编译的,即使它看起来是合理的:
// generics/UseList.java
// {WillNotCompile}
import java.util.*;
public class UseList<W, T> {
void f(List<T> v) {}
void f(List<W> v) {}
}
因为擦除,所以重载方法产生了相同的类型签名。
因而,当擦除后的参数不能产生唯一的参数列表时,你必须提供不同的方法名:
// generics/UseList2.java
import java.util.*;
public class UseList2<W, T> {
void f1(List<T> v) {}
void f2(List<W> v) {}
}
幸运的是,编译器可以检测到这类问题。
基类劫持接口
假设你有一个实现了
// generics/ComparablePet.java
public class ComparablePet implements Comparable<ComparablePet> {
@Override
public int compareTo(ComparablePet o) {
return 0;
}
}
尝试缩小
// generics/HijackedInterface.java
// {WillNotCompile}
class Cat extends ComparablePet implements Comparable<Cat> {
// error: Comparable cannot be inherited with
// different arguments: <Cat> and <ComparablePet>
// class Cat
// ^
// 1 error
public int compareTo(Cat arg) {
return 0;
}
}
不幸的是,这不能工作。一旦
// generics/RestrictedComparablePets.java
public class Hamster extends ComparablePet implements Comparable<ComparablePet> {
@Override
public int compareTo(ComparablePet arg) {
return 0;
}
}
// Or just:
class Gecko extends ComparablePet {
public int compareTo(ComparablePet arg) {
return 0;
}
}
自限定的类型
在
class SelfBounded<T extends SelfBounded<T>> { // ...
这就像两面镜子彼此照向对方所引起的目眩效果一样,是一种无限反射。
当你首次看到它时,很难去解析它,它强调的是当
古怪的循环泛型
为了理解自限定类型的含义,我们从这个惯用法的一个简单版本入手,它没有自限定的边界。
不能直接继承一个泛型参数,但是,可以继承在其自己的定义中使用这个泛型参数的类。也就是说,可以声明:
// generics/CuriouslyRecurringGeneric.java
class GenericType<T> {}
public class CuriouslyRecurringGeneric
extends GenericType<CuriouslyRecurringGeneric> {}
这可以按照
// generics/BasicHolder.java
public class BasicHolder<T> {
T element;
void set(T arg) { element = arg; }
T get() { return element; }
void f() {
System.out.println(element.getClass().getSimpleName());
}
}
这是一个普通的泛型类型,它的一些方法将接受和产生具有其参数类型的对象,还有一个方法在其存储的域上执行操作(尽管只是在这个域上执行
// generics/CRGWithBasicHolder.java
class Subtype extends BasicHolder<Subtype> {}
public class CRGWithBasicHolder {
public static void main(String[] args) {
Subtype st1 = new Subtype(), st2 = new Subtype();
st1.set(st2);
Subtype st3 = st1.get();
st1.f();
}
}
/* Output:
Subtype
*/
注意,这里有些东西很重要:新类set()
的参数和从 get()
返回的类型都是确切的
自限定
// generics/Unconstrained.java
// (c)2017 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
class Other {}
class BasicOther extends BasicHolder<Other> {}
public class Unconstrained {
public static void main(String[] args) {
BasicOther b = new BasicOther();
BasicOther b2 = new BasicOther();
b.set(new Other());
Other other = b.get();
b.f();
}
}
/* Output:
Other
*/
限定将采取额外的步骤,强制泛型当作其自身的边界参数来使用。观察所产生的类可以如何使用以及不可以如何使用:
// generics/SelfBounding.java
class SelfBounded<T extends SelfBounded<T>> {
T element;
SelfBounded<T> set(T arg) {
element = arg;
return this;
}
T get() { return element; }
}
class A extends SelfBounded<A> {}
class B extends SelfBounded<A> {} // Also OK
class C extends SelfBounded<C> {
C setAndGet(C arg) {
set(arg);
return get();
}
}
class D {}
// Can't do this:
// class E extends SelfBounded<D> {}
// Compile error:
// Type parameter D is not within its bound
// Alas, you can do this, so you cannot force the idiom:
class F extends SelfBounded {}
public class SelfBounding {
public static void main(String[] args) {
A a = new A();
a.set(new A());
a = a.set(new A()).get();
a = a.get();
C c = new C();
c = c.setAndGet(new C());
}
}
自限定所做的,就是要求在继承关系中,像下面这样使用这个类:
class A extends SelfBounded<A>{}
这会强制要求将正在定义的类当作参数传递给基类。
自限定的参数有何意义呢?它可以保证类型参数必须与正在被定义的类相同。正如你在
// generics/NotSelfBounded.java
public class NotSelfBounded<T> {
T element;
NotSelfBounded<T> set(T arg) {
element = arg;
return this;
}
T get() { return element; }
}
class A2 extends NotSelfBounded<A2> {}
class B2 extends NotSelfBounded<A2> {}
class C2 extends NotSelfBounded<C2> {
C2 setAndGet(C2 arg) {
set(arg);
return get();
}
}
class D2 {}
// Now this is OK:
class E2 extends NotSelfBounded<D2> {}
因此很明显,自限定限制只能强制作用于继承关系。如果使用自限定,就应该了解这个类所用的类型参数将与使用这个参数的类具有相同的基类型。这会强制要求使用这个类的每个人都要遵循这种形式。 还可以将自限定用于泛型方法:
// generics/SelfBoundingMethods.java
// (c)2017 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
public class SelfBoundingMethods {
static <T extends SelfBounded<T>> T f(T arg) {
return arg.set(arg).get();
}
public static void main(String[] args) {
A a = f(new A());
}
}
这可以防止这个方法被应用于除上述形式的自限定参数之外的任何事物上。
参数协变
自限定类型的价值在于它们可以产生协变参数类型——方法参数类型会随子类而变化。
尽管自限定类型还可以产生与子类类型相同的返回类型,但是这并不十分重要,因为协变返回类型是在
// generics/CovariantReturnTypes.java
class Base {}
class Derived extends Base {}
interface OrdinaryGetter {
Base get();
}
interface DerivedGetter extends OrdinaryGetter {
// Overridden method return type can vary:
@Override
Derived get();
}
public class CovariantReturnTypes {
void test(DerivedGetter d) {
Derived d2 = d.get();
}
}
get()
方法覆盖了get()
,并返回了一个从 OrdinaryGetter.get()
的返回类型中导出的类型。尽管这是完全合乎逻辑的事情(导出类方法应该能够返回比它覆盖的基类方法更具体的类型)但是这在早先的
自限定泛型事实上将产生确切的导出类型作为其返回值,就像在 get()
中所看到的一样:
// generics/GenericsAndReturnTypes.java
interface GenericGetter<T extends GenericGetter<T>> {
T get();
}
interface Getter extends GenericGetter<Getter> {}
public class GenericsAndReturnTypes {
void test(Getter g) {
Getter result = g.get();
GenericGetter gg = g.get(); // Also the base type
}
}
注意,这段代码不能编译,除非是使用囊括了协变返回类型的
然而,在非泛型代码中,参数类型不能随子类型发生变化:
// generics/OrdinaryArguments.java
class OrdinarySetter {
void set(Base base) {
System.out.println("OrdinarySetter.set(Base)");
}
}
class DerivedSetter extends OrdinarySetter {
void set(Derived derived) {
System.out.println("DerivedSetter.set(Derived)");
}
}
public class OrdinaryArguments {
public static void main(String[] args) {
Base base = new Base();
Derived derived = new Derived();
DerivedSetter ds = new DerivedSetter();
ds.set(derived);
// Compiles--overloaded, not overridden!:
ds.set(base);
}
}
/* Output:
DerivedSetter.set(Derived)
OrdinarySetter.set(Base)
*/
set(derived)
和 set(base)
都是合法的,因此 DerivedSetter.set()
没有覆盖 OrdinarySetter.set()
,而是重载了这个方法。从输出中可以看到,在
// generics/SelfBoundingAndCovariantArguments.java
interface SelfBoundSetter<T extends SelfBoundSetter<T>> {
void set(T arg);
}
interface Setter extends SelfBoundSetter<Setter> {}
public class SelfBoundingAndCovariantArguments {
void
testA(Setter s1, Setter s2, SelfBoundSetter sbs) {
s1.set(s2);
//- s1.set(sbs);
// error: method set in interface SelfBoundSetter<T>
// cannot be applied to given types;
// s1.set(sbs);
// ^
// required: Setter
// found: SelfBoundSetter
// reason: argument mismatch;
// SelfBoundSetter cannot be converted to Setter
// where T is a type-variable:
// T extends SelfBoundSetter<T> declared in
// interface SelfBoundSetter
// 1 error
}
}
编译器不能识别将基类型当作参数传递给 set()
的尝试,因为没有任何方法具有这样的签名。实际上,这个参数已经被覆盖。
如果不使用自限定类型,普通的继承机制就会介入,而你将能够重载,就像在非泛型的情况下一样:
// generics/PlainGenericInheritance.java
class GenericSetter<T> { // Not self-bounded
void set(T arg) {
System.out.println("GenericSetter.set(Base)");
}
}
class DerivedGS extends GenericSetter<Base> {
void set(Derived derived) {
System.out.println("DerivedGS.set(Derived)");
}
}
public class PlainGenericInheritance {
public static void main(String[] args) {
Base base = new Base();
Derived derived = new Derived();
DerivedGS dgs = new DerivedGS();
dgs.set(derived);
dgs.set(base); // Overloaded, not overridden!
}
}
/* Output:
DerivedGS.set(Derived)
GenericSetter.set(Base)
*/
这段代码在模仿set(Base)
的set(Base)
的 GenericSetter<Base>
。就像set()
的重载版本。如果不使用自限定,将重载参数类型。如果使用了自限定,只能获得方法的一个版本,它将接受确切的参数类型。
动态类型安全
因为可以向checkedCollection()
、checkedList()
、 checkedMap()
、 checkedSet()
、checkedSortedMap()
和 checkedSortedSet()
。这些方法每一个都会将你希望动态检查的集合当作第一个参数接受,并将你希望强制要求的类型作为第二个参数接受。
受检查的集合在你试图插入类型不正确的对象时抛出oldStyleMethod()
表示遗留代码,因为它接受的是原生的
// generics/CheckedList.java
// Using Collection.checkedList()
import typeinfo.pets.*;
import java.util.*;
public class CheckedList {
@SuppressWarnings("unchecked")
static void oldStyleMethod(List probablyDogs) {
probablyDogs.add(new Cat());
}
public static void main(String[] args) {
List<Dog> dogs1 = new ArrayList<>();
oldStyleMethod(dogs1); // Quietly accepts a Cat
List<Dog> dogs2 = Collections.checkedList(
new ArrayList<>(), Dog.class);
try {
oldStyleMethod(dogs2); // Throws an exception
} catch(Exception e) {
System.out.println("Expected: " + e);
}
// Derived types work fine:
List<Pet> pets = Collections.checkedList(
new ArrayList<>(), Pet.class);
pets.add(new Dog());
pets.add(new Cat());
}
}
/* Output:
Expected: java.lang.ClassCastException: Attempt to
insert class typeinfo.pets.Cat element into collection
with element type class typeinfo.pets.Dog
*/
运行这个程序时,你会发现插入一个
泛型异常
由于擦除的原因,
// generics/ThrowGenericException.java
import java.util.*;
interface Processor<T, E extends Exception> {
void process(List<T> resultCollector) throws E;
}
class ProcessRunner<T, E extends Exception>
extends ArrayList<Processor<T, E>> {
List<T> processAll() throws E {
List<T> resultCollector = new ArrayList<>();
for(Processor<T, E> processor : this)
processor.process(resultCollector);
return resultCollector;
}
}
class Failure1 extends Exception {}
class Processor1
implements Processor<String, Failure1> {
static int count = 3;
@Override
public void process(List<String> resultCollector)
throws Failure1 {
if(count-- > 1)
resultCollector.add("Hep!");
else
resultCollector.add("Ho!");
if(count < 0)
throw new Failure1();
}
}
class Failure2 extends Exception {}
class Processor2
implements Processor<Integer, Failure2> {
static int count = 2;
@Override
public void process(List<Integer> resultCollector)
throws Failure2 {
if(count-- == 0)
resultCollector.add(47);
else {
resultCollector.add(11);
}
if(count < 0)
throw new Failure2();
}
}
public class ThrowGenericException {
public static void main(String[] args) {
ProcessRunner<String, Failure1> runner =
new ProcessRunner<>();
for(int i = 0; i < 3; i++)
runner.add(new Processor1());
try {
System.out.println(runner.processAll());
} catch(Failure1 e) {
System.out.println(e);
}
ProcessRunner<Integer, Failure2> runner2 =
new ProcessRunner<>();
for(int i = 0; i < 3; i++)
runner2.add(new Processor2());
try {
System.out.println(runner2.processAll());
} catch(Failure2 e) {
System.out.println(e);
}
}
}
/* Output:
[Hep!, Hep!, Ho!]
Failure2
*/
process()
方法,并且可能会抛出具有类型process()
的结果存储在 List<T>resultCollector
中(这被称为收集参数processAll()
方法,它会在所持有的每个
混型
术语混型随时间的推移好像拥有了无数的含义,但是其最基本的概念是混合多个类的能力,以产生一个可以表示混型中所有类型的类。这往往是你最后的手段,它将使组装多个类变得简单易行。 混型的价值之一是它们可以将特性和行为一致地应用于多个类之上。如果想在混型类中修改某些东西,作为一种意外的好处,这些修改将会应用于混型所应用的所有类型之上。正由于此,混型有一点面向切面编程 (AOP) 的味道,而切面经常被建议用来解决混型问题。
C++ 中的混型
在
// generics/Mixins.cpp
#include <string>
#include <ctime>
#include <iostream>
using namespace std;
template<class T> class TimeStamped : public T {
long timeStamp;
public:
TimeStamped() { timeStamp = time(0); }
long getStamp() { return timeStamp; }
};
template<class T> class SerialNumbered : public T {
long serialNumber;
static long counter;
public:
SerialNumbered() { serialNumber = counter++; }
long getSerialNumber() { return serialNumber; }
};
// Define and initialize the static storage:
template<class T> long SerialNumbered<T>::counter = 1;
class Basic {
string value;
public:
void set(string val) { value = val; }
string get() { return value; }
};
int main() {
TimeStamped<SerialNumbered<Basic>> mixin1, mixin2;
mixin1.set("test string 1");
mixin2.set("test string 2");
cout << mixin1.get() << " " << mixin1.getStamp() <<
" " << mixin1.getSerialNumber() << endl;
cout << mixin2.get() << " " << mixin2.getStamp() <<
" " << mixin2.getSerialNumber() << endl;
}
/* Output:
test string 1 1452987605 1
test string 2 1452987605 2
*/
在 main()
中,
TimeStamped<SerialNumbered<Basic>> mixin1,mixin2;
遗憾的是,
泛型类不能直接继承自一个泛型参数
这突显了许多我在
与接口混合
一种更常见的推荐解决方案是使用接口来产生混型效果,就像下面这样:
// generics/Mixins.java
import java.util.*;
interface TimeStamped { long getStamp(); }
class TimeStampedImp implements TimeStamped {
private final long timeStamp;
TimeStampedImp() {
timeStamp = new Date().getTime();
}
@Override
public long getStamp() { return timeStamp; }
}
interface SerialNumbered { long getSerialNumber(); }
class SerialNumberedImp implements SerialNumbered {
private static long counter = 1;
private final long serialNumber = counter++;
@Override
public long getSerialNumber() { return serialNumber; }
}
interface Basic {
void set(String val);
String get();
}
class BasicImp implements Basic {
private String value;
@Override
public void set(String val) { value = val; }
@Override
public String get() { return value; }
}
class Mixin extends BasicImp
implements TimeStamped, SerialNumbered {
private TimeStamped timeStamp = new TimeStampedImp();
private SerialNumbered serialNumber =
new SerialNumberedImp();
@Override
public long getStamp() {
return timeStamp.getStamp();
}
@Override
public long getSerialNumber() {
return serialNumber.getSerialNumber();
}
}
public class Mixins {
public static void main(String[] args) {
Mixin mixin1 = new Mixin(), mixin2 = new Mixin();
mixin1.set("test string 1");
mixin2.set("test string 2");
System.out.println(mixin1.get() + " " +
mixin1.getStamp() + " " + mixin1.getSerialNumber());
System.out.println(mixin2.get() + " " +
mixin2.getStamp() + " " + mixin2.getSerialNumber());
}
}
/* Output:
test string 1 1494331663026 1
test string 2 1494331663027 2
*/
使用装饰器模式
当你观察混型的使用方式时,就会发现混型概念好像与装饰器设计模式关系很近。装饰器经常用于满足各种可能的组合,而直接子类化会产生过多的类,因此是不实际的。
装饰器模式使用分层对象来动态透明地向单个对象中添加责任。装饰器指定包装在最初的对象周围的所有对象都具有相同的基本接口。某些事物是可装饰的,可以通过将其他类包装在这个可装饰对象的四周,来将功能分层。这使得对装饰器的使用是透明的——无论对象是否被装饰,你都拥有一个可以向对象发送的公共消息集。装饰类也可以添加新方法,但是正如你所见,这将是受限的。
装饰器是通过使用组合和形式化结构(可装饰物
// generics/decorator/Decoration.java
// {java generics.decorator.Decoration}
package generics.decorator;
import java.util.*;
class Basic {
private String value;
public void set(String val) { value = val; }
public String get() { return value; }
}
class Decorator extends Basic {
protected Basic basic;
Decorator(Basic basic) { this.basic = basic; }
@Override
public void set(String val) { basic.set(val); }
@Override
public String get() { return basic.get(); }
}
class TimeStamped extends Decorator {
private final long timeStamp;
TimeStamped(Basic basic) {
super(basic);
timeStamp = new Date().getTime();
}
public long getStamp() { return timeStamp; }
}
class SerialNumbered extends Decorator {
private static long counter = 1;
private final long serialNumber = counter++;
SerialNumbered(Basic basic) { super(basic); }
public long getSerialNumber() { return serialNumber; }
}
public class Decoration {
public static void main(String[] args) {
TimeStamped t = new TimeStamped(new Basic());
TimeStamped t2 = new TimeStamped(
new SerialNumbered(new Basic()));
//- t2.getSerialNumber(); // Not available
SerialNumbered s = new SerialNumbered(new Basic());
SerialNumbered s2 = new SerialNumbered(
new TimeStamped(new Basic()));
//- s2.getStamp(); // Not available
}
}
产生自泛型的类包含所有感兴趣的方法,但是由使用装饰器所产生的对象类型是最后被装饰的类型。也就是说,尽管可以添加多个层,但是最后一层才是实际的类型,因此只有最后一层的方法是可视的,而混型的类型是所有被混合到一起的类型。因此对于装饰器来说,其明显的缺陷是它只能有效地工作于装饰中的一层(最后一层
与动态代理混合
可以使用动态代理来创建一种比装饰器更贴近混型模型的机制(查看 类型信息 一章中关于
// generics/DynamicProxyMixin.java
import java.lang.reflect.*;
import java.util.*;
import onjava.*;
import static onjava.Tuple.*;
class MixinProxy implements InvocationHandler {
Map<String, Object> delegatesByMethod;
@SuppressWarnings("unchecked")
MixinProxy(Tuple2<Object, Class<?>>... pairs) {
delegatesByMethod = new HashMap<>();
for(Tuple2<Object, Class<?>> pair : pairs) {
for(Method method : pair.a2.getMethods()) {
String methodName = method.getName();
// The first interface in the map
// implements the method.
if(!delegatesByMethod.containsKey(methodName))
delegatesByMethod.put(methodName, pair.a1);
}
}
}
@Override
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
String methodName = method.getName();
Object delegate = delegatesByMethod.get(methodName);
return method.invoke(delegate, args);
}
@SuppressWarnings("unchecked")
public static Object newInstance(Tuple2... pairs) {
Class[] interfaces = new Class[pairs.length];
for(int i = 0; i < pairs.length; i++) {
interfaces[i] = (Class)pairs[i].a2;
}
ClassLoader cl = pairs[0].a1.getClass().getClassLoader();
return Proxy.newProxyInstance(cl, interfaces, new MixinProxy(pairs));
}
}
public class DynamicProxyMixin {
public static void main(String[] args) {
Object mixin = MixinProxy.newInstance(
tuple(new BasicImp(), Basic.class),
tuple(new TimeStampedImp(), TimeStamped.class),
tuple(new SerialNumberedImp(), SerialNumbered.class));
Basic b = (Basic)mixin;
TimeStamped t = (TimeStamped)mixin;
SerialNumbered s = (SerialNumbered)mixin;
b.set("Hello");
System.out.println(b.get());
System.out.println(t.getStamp());
System.out.println(s.getSerialNumber());
}
}
/* Output:
Hello
1494331653339
1
*/
因为只有动态类型而不是静态类型才包含所有的混入类型,因此这仍旧不如
潜在类型机制
在本章的开头介绍过这样的思想,即要编写能够尽可能广泛地应用的代码。为了实现这一点,我们需要各种途径来放松对我们的代码将要作用的类型所作的限制,同时不丢失静态类型检查的好处。然后,我们就可以编写出无需修改就可以应用于更多情况的代码,即更加“泛化”的代码。
还是正如你所见到的,当要在泛型类型上执行操作(即调用
某些编程语言提供的一种解决方案称为潜在类型机制或结构化类型机制,而更古怪的术语称为鸭子类型机制,即“如果它走起来像鸭子,并且叫起来也像鸭子,那么你就可以将它当作鸭子对待
泛型代码典型地只能在泛型类型上调用少量方法,而具有潜在类型机制的语言只要求实现某个方法子集,而不是某个特定类或接口,从而放松了这种限制(并且可以产生更加泛化的代码speak()
和 sit()
即可
潜在类型机制是一种代码组织和复用机制。有了它,编写出的代码相对于没有它编写出的代码,能够更容易地复用。代码组织和复用是所有计算机编程的基本手段:编写一次,多次使用,并在一个位置保存代码。因为我并未被要求去命名我的代码要操作于其上的确切接口,所以,有了潜在类型机制,我就可以编写更少的代码,并更容易地将其应用于多个地方。
支持潜在类型机制的语言包括
pyhton 中的潜在类型
如果我们将上面的描述用
# generics/DogsAndRobots.py
class Dog:
def speak(self):
print("Arf!")
def sit(self):
print("Sitting")
def reproduce(self):
pass
class Robot:
def speak(self):
print("Click!")
def sit(self):
print("Clank!")
def oilChange(self):
pass
def perform(anything):
anything.speak()
anything.sit()
a = Dog()
b = Robot()
perform(a)
perform(b)
output = """
Arf!
Sitting
Click!
Clank!
"""
perform()
所表明的那样。注意,在 perform(anything)
中,没有任何针对perform()
期望它执行的操作,因此这里隐含着一个接口。但是你从来都不必显式地写出这个接口——它是潜在的。perform()
不关心其参数的类型,因此我可以向它传递任何对象,只要该对象支持 speak()
和 sit()
方法。如果传递给 perform()
的对象不支持这些操作,那么将会得到运行时异常。
输出规定使用三重引号创建带有内嵌换行符的字符串。
C++ 中的潜在类型
我们可以用
// generics/DogsAndRobots.cpp
#include <iostream>
using namespace std;
class Dog {
public:
void speak() { cout << "Arf!" << endl; }
void sit() { cout << "Sitting" << endl; }
void reproduce() {}
};
class Robot {
public:
void speak() { cout << "Click!" << endl; }
void sit() { cout << "Clank!" << endl; }
void oilChange() {}
};
template<class T> void perform(T anything) {
anything.speak();
anything.sit();
}
int main() {
Dog d;
Robot r;
perform(d);
perform(r);
}
/* Output:
Arf!
Sitting
Click!
Clank!
*/
在perform()
不关心其参数的具体类型,并且潜在类型机制允许它接受这两种类型的对象。
Go 中的潜在类型
这里用
// generics/dogsandrobots.go
package main
import "fmt"
type Dog struct {}
func (this Dog) speak() { fmt.Printf("Arf!\n")}
func (this Dog) sit() { fmt.Printf("Sitting\n")}
func (this Dog) reproduce() {}
type Robot struct {}
func (this Robot) speak() { fmt.Printf("Click!\n") }
func (this Robot) sit() { fmt.Printf("Clank!\n") }
func (this Robot) oilChange() {}
func perform(speaker interface { speak(); sit() }) {
speaker.speak();
speaker.sit();
}
func main() {
perform(Dog{})
perform(Robot{})
}
/* Output:
Arf!
Sitting
Click!
Clank!
*/
perform()
函数使用潜在类型:参数的确切类型并不重要,只要它包含了 speak()
和 sit()
方法即可。 该接口在此处匿名定义,内联,如 perform()
的参数列表所示。
main()
证明 perform()
确实对其参数的确切类型不在乎,只要可以在该参数上调用 talk()
和 sit()
即可。 但是,就像
语法
java 中的直接潜在类型
因为泛型是在这场竞赛的后期才添加到
// generics/Performs.java
public interface Performs {
void speak();
void sit();
}
// generics/DogsAndRobots.java
// No (direct) latent typing in Java
import typeinfo.pets.*;
class PerformingDog extends Dog implements Performs {
@Override
public void speak() { System.out.println("Woof!"); }
@Override
public void sit() { System.out.println("Sitting"); }
public void reproduce() {}
}
class Robot implements Performs {
public void speak() { System.out.println("Click!"); }
public void sit() { System.out.println("Clank!"); }
public void oilChange() {}
}
class Communicate {
public static <T extends Performs>
void perform(T performer) {
performer.speak();
performer.sit();
}
}
public class DogsAndRobots {
public static void main(String[] args) {
Communicate.perform(new PerformingDog());
Communicate.perform(new Robot());
}
}
/* Output:
Woof!
Sitting
Click!
Clank!
*/
但是要注意,perform()
不需要使用泛型来工作,它可以被简单地指定为接受一个
// generics/SimpleDogsAndRobots.java
// Removing the generic; code still works
class CommunicateSimply {
static void perform(Performs performer) {
performer.speak();
performer.sit();
}
}
public class SimpleDogsAndRobots {
public static void main(String[] args) {
CommunicateSimply.perform(new PerformingDog());
CommunicateSimply.perform(new Robot());
}
}
/* Output:
Woof!
Sitting
Click!
Clank!
*/
在本例中,泛型不是必需的,因为这些类已经被强制要求实现
对缺乏潜在类型机制的补偿
尽管
反射
可以使用的一种方式是反射,下面的 perform()
方法就是用了潜在类型机制:
// generics/LatentReflection.java
// Using reflection for latent typing
import java.lang.reflect.*;
// Does not implement Performs:
class Mime {
public void walkAgainstTheWind() {}
public void sit() {
System.out.println("Pretending to sit");
}
public void pushInvisibleWalls() {}
@Override
public String toString() { return "Mime"; }
}
// Does not implement Performs:
class SmartDog {
public void speak() { System.out.println("Woof!"); }
public void sit() { System.out.println("Sitting"); }
public void reproduce() {}
}
class CommunicateReflectively {
public static void perform(Object speaker) {
Class<?> spkr = speaker.getClass();
try {
try {
Method speak = spkr.getMethod("speak");
speak.invoke(speaker);
} catch(NoSuchMethodException e) {
System.out.println(speaker + " cannot speak");
}
try {
Method sit = spkr.getMethod("sit");
sit.invoke(speaker);
} catch(NoSuchMethodException e) {
System.out.println(speaker + " cannot sit");
}
} catch(SecurityException |
IllegalAccessException |
IllegalArgumentException |
InvocationTargetException e) {
throw new RuntimeException(speaker.toString(), e);
}
}
}
public class LatentReflection {
public static void main(String[] args) {
CommunicateReflectively.perform(new SmartDog());
CommunicateReflectively.perform(new Robot());
CommunicateReflectively.perform(new Mime());
}
}
/* Output:
Woof!
Sitting
Click!
Clank!
Mime cannot speak
Pretending to sit
*/
上例中,这些类完全是彼此分离的,没有任何公共基类(除了CommunicateReflectively.perform()
将一个方法应用于序列
反射提供了一些有用的可能性,但是它将所有的类型检查都转移到了运行时,因此在许多情况下并不是我们所希望的。如果能够实现编译期类型检查,这通常会更符合要求。但是有可能实现编译期类型检查和潜在类型机制吗?
让我们看一个说明这个问题的示例。假设想要创建一个 apply()
方法,它能够将任何方法应用于某个序列中的所有对象。这种情况下使用接口不适合,因为你想要将任何方法应用于一个对象集合,而接口不可能描述任何方法。如何用
最初,我们可以用反射来解决这个问题,由于有了
// generics/Apply.java
import java.lang.reflect.*;
import java.util.*;
public class Apply {
public static <T, S extends Iterable<T>>
void apply(S seq, Method f, Object... args) {
try {
for(T t: seq)
f.invoke(t, args);
} catch(IllegalAccessException |
IllegalArgumentException |
InvocationTargetException e) {
// Failures are programmer errors
throw new RuntimeException(e);
}
}
}
在
为什么我们不只使用invoke()
和 apply()
的优点是它们可以接受任意数量的参数。 在某些情况下,灵活性可能至关重要。
为了测试
// generics/Shape.java
public class Shape {
private static long counter = 0;
private final long id = counter++;
@Override
public String toString() {
return getClass().getSimpleName() + " " + id;
}
public void rotate() {
System.out.println(this + " rotate");
}
public void resize(int newSize) {
System.out.println(this + " resize " + newSize);
}
}
被一个子类
// generics/Square.java
public class Square extends Shape {}
通过这些,我们可以测试
// generics/ApplyTest.java
import java.util.*;
import java.util.function.*;
import onjava.*;
public class ApplyTest {
public static
void main(String[] args) throws Exception {
List<Shape> shapes =
Suppliers.create(ArrayList::new, Shape::new, 3);
Apply.apply(shapes, Shape.class.getMethod("rotate"));
Apply.apply(shapes, Shape.class.getMethod("resize", int.class), 7);
List<Square> squares =
Suppliers.create(ArrayList::new, Square::new, 3);
Apply.apply(squares, Shape.class.getMethod("rotate"));
Apply.apply(squares, Shape.class.getMethod("resize", int.class), 7);
Apply.apply(new FilledList<>(Shape::new, 3),
Shape.class.getMethod("rotate"));
Apply.apply(new FilledList<>(Square::new, 3),
Shape.class.getMethod("rotate"));
SimpleQueue<Shape> shapeQ = Suppliers.fill(
new SimpleQueue<>(), SimpleQueue::add,
Shape::new, 3);
Suppliers.fill(shapeQ, SimpleQueue::add,
Square::new, 3);
Apply.apply(shapeQ, Shape.class.getMethod("rotate"));
}
}
/* Output:
Shape 0 rotate
Shape 1 rotate
Shape 2 rotate
Shape 0 resize 7
Shape 1 resize 7
Shape 2 resize 7
Square 3 rotate
Square 4 rotate
Square 5 rotate
Square 3 resize 7
Square 4 resize 7
Square 5 resize 7
Shape 6 rotate
Shape 7 rotate
Shape 8 rotate
Square 9 rotate
Square 10 rotate
Square 11 rotate
Shape 12 rotate
Shape 13 rotate
Shape 14 rotate
Square 15 rotate
Square 16 rotate
Square 17 rotate
*/
在apply()
方法可以接受任何实现了main()
中使用下面定义的
// generics/SimpleQueue.java
// A different kind of Iterable collection
import java.util.*;
public class SimpleQueue<T> implements Iterable<T> {
private LinkedList<T> storage = new LinkedList<>();
public void add(T t) { storage.offer(t); }
public T get() { return storage.poll(); }
@Override
public Iterator<T> iterator() {
return storage.iterator();
}
}
正如反射解决方案看起来那样优雅,我们必须观察到反射(尽管在
几乎可以肯定,你会首先使用
// generics/ApplyFunctional.java
import java.util.*;
import java.util.stream.*;
import java.util.function.*;
import onjava.*;
public class ApplyFunctional {
public static void main(String[] args) {
Stream.of(
Stream.generate(Shape::new).limit(2),
Stream.generate(Square::new).limit(2))
.flatMap(c -> c) // flatten into one stream
.peek(Shape::rotate)
.forEach(s -> s.resize(7));
new FilledList<>(Shape::new, 2)
.forEach(Shape::rotate);
new FilledList<>(Square::new, 2)
.forEach(Shape::rotate);
SimpleQueue<Shape> shapeQ = Suppliers.fill(
new SimpleQueue<>(), SimpleQueue::add,
Shape::new, 2);
Suppliers.fill(shapeQ, SimpleQueue::add,
Square::new, 2);
shapeQ.forEach(Shape::rotate);
}
}
/* Output:
Shape 0 rotate
Shape 0 resize 7
Shape 1 rotate
Shape 1 resize 7
Square 2 rotate
Square 2 resize 7
Square 3 rotate
Square 3 resize 7
Shape 4 rotate
Shape 5 rotate
Square 6 rotate
Square 7 rotate
Shape 8 rotate
Shape 9 rotate
Square 10 rotate
Square 11 rotate
*/
由于使用Apply.apply()
。
我们首先生成两个flatten()
,但是我们可以使用 flatMap(c-> c)
产生相同的结果,后者使用身份映射将操作简化为“ flatten ”。
我们使用 peek()
当做对 rotate()
的调用,因为 peek()
执行一个操作(此处是出于副作用
注意,使用forEach()
比 Apply.apply()
代码整洁得多。 在代码简单性和可读性方面,结果比以前的方法好得多。 并且,现在也不可能从 main()
引发异常。
Java8 中的辅助潜在类型
先前声明关于
我在其他地方从没遇过这种技术,因此我将其称为辅助潜在类型。
我们将重写
// generics/DogsAndRobotMethodReferences.java
// "Assisted Latent Typing"
import typeinfo.pets.*;
import java.util.function.*;
class PerformingDogA extends Dog {
public void speak() { System.out.println("Woof!"); }
public void sit() { System.out.println("Sitting"); }
public void reproduce() {}
}
class RobotA {
public void speak() { System.out.println("Click!"); }
public void sit() { System.out.println("Clank!"); }
public void oilChange() {}
}
class CommunicateA {
public static <P> void perform(P performer,
Consumer<P> action1, Consumer<P> action2) {
action1.accept(performer);
action2.accept(performer);
}
}
public class DogsAndRobotMethodReferences {
public static void main(String[] args) {
CommunicateA.perform(new PerformingDogA(),
PerformingDogA::speak, PerformingDogA::sit);
CommunicateA.perform(new RobotA(),
RobotA::speak, RobotA::sit);
CommunicateA.perform(new Mime(),
Mime::walkAgainstTheWind,
Mime::pushInvisibleWalls);
}
}
/* Output:
Woof!
Sitting
Click!
Clank!
*/
CommunicateA.perform()
在没有约束的Consumer <P>
,它在这里就可以是任何东西,这些 Consumer<P>
代表不带参数的accept()
方法时,它将方法引用绑定到执行者对象并调用该方法。 由于 函数式编程 一章中描述的“魔术”,我们可以将任何符合签名的未绑定方法引用传递给 CommunicateA.perform()
。
之所以称其为“辅助”,是因为您必须显式地为 perform()
提供要使用的方法引用。 它不能只按名称调用方法。
尽管传递未绑定的方法引用似乎要花很多力气,但潜在类型的最终目标还是可以实现的。 我们创建了一个代码片段 CommunicateA.perform()
,该代码可用于任何具有符合签名的方法引用的类型。 请注意,这与我们看到的其他语言中的潜在类型有所不同,因为这些语言不仅需要签名以符合规范,还需要方法名称。 因此,该技术可以说产生了更多的通用代码。
为了证明这一点,我还从
使用Suppliers 类的通用方法
通过辅助潜在类型,我们可以定义本章其他部分中使用的
// onjava/Suppliers.java
// A utility to use with Suppliers
package onjava;
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
public class Suppliers {
// Create a collection and fill it:
public static <T, C extends Collection<T>> C
create(Supplier<C> factory, Supplier<T> gen, int n) {
return Stream.generate(gen)
.limit(n)
.collect(factory, C::add, C::addAll);
}
// Fill an existing collection:
public static <T, C extends Collection<T>>
C fill(C coll, Supplier<T> gen, int n) {
Stream.generate(gen)
.limit(n)
.forEach(coll::add);
return coll;
}
// Use an unbound method reference to
// produce a more general method:
public static <H, A> H fill(H holder,
BiConsumer<H, A> adder, Supplier<A> gen, int n) {
Stream.generate(gen)
.limit(n)
.forEach(a -> adder.accept(holder, a));
return holder;
}
}
create()
为你创建一个新的fill()
的第一个版本将元素放入
前两种方法一般都受约束,只能与fill()
的第二个版本适用于任何类型的adder. fill()
,使用辅助潜在类型来使其与任何具有添加元素方法的BiConsumer <H,A>
,其中accept()
的调用将使用参数
在一个稍作模拟的测试中对
// generics/BankTeller.java
// A very simple bank teller simulation
import java.util.*;
import onjava.*;
class Customer {
private static long counter = 1;
private final long id = counter++;
@Override
public String toString() {
return "Customer " + id;
}
}
class Teller {
private static long counter = 1;
private final long id = counter++;
@Override
public String toString() {
return "Teller " + id;
}
}
class Bank {
private List<BankTeller> tellers =
new ArrayList<>();
public void put(BankTeller bt) {
tellers.add(bt);
}
}
public class BankTeller {
public static void serve(Teller t, Customer c) {
System.out.println(t + " serves " + c);
}
public static void main(String[] args) {
// Demonstrate create():
RandomList<Teller> tellers =
Suppliers.create(
RandomList::new, Teller::new, 4);
// Demonstrate fill():
List<Customer> customers = Suppliers.fill(
new ArrayList<>(), Customer::new, 12);
customers.forEach(c ->
serve(tellers.select(), c));
// Demonstrate assisted latent typing:
Bank bank = Suppliers.fill(
new Bank(), Bank::put, BankTeller::new, 3);
// Can also use second version of fill():
List<Customer> customers2 = Suppliers.fill(
new ArrayList<>(),
List::add, Customer::new, 12);
}
}
/* Output:
Teller 3 serves Customer 1
Teller 2 serves Customer 2
Teller 3 serves Customer 3
Teller 1 serves Customer 4
Teller 1 serves Customer 5
Teller 3 serves Customer 6
Teller 1 serves Customer 7
Teller 2 serves Customer 8
Teller 3 serves Customer 9
Teller 3 serves Customer 10
Teller 2 serves Customer 11
Teller 4 serves Customer 12
*/
可以看到 create()
生成一个新的fill()
添加到现有fill()
显示,它不仅与无关的新类型fill()
的第一个版本在技术上不是必需的,但在使用
总结:类型转换真的如此之糟吗?
自从
这个论点就是:使用泛型类型机制的最吸引人的地方,就是在使用集合类的地方,这些类包括诸如各种
但是,泛型出现之前的
在本书以前的版本中,我曾经说过:
这不止令人恼火,它还可能会产生难以发现的缺陷。如果这个程序的某个部分(或数个部分)向集合中插入了对象,并且通过异常,你在程序的另一个独立的部分中发现有不良对象被放置到了集合中,那么必须发现这个不良插入到底是在何处发生的。
但是,随着对这个论点的进一步检查,我开始怀疑它了。首先,这会多么频繁地发生呢?我记得这类事情从未发生在我身上,并且当我在会议上询问其他人时,我也从来没有听说过有人碰上过。另一本书使用了一个称为
有一位作者甚至断言,这样的缺陷将“潜伏数年”。但是我不记得有任何大量的相关报告,来说明人们在查找“狗在猫列表中”这类缺陷时困难重重,或者是说明人们会非常频繁地产生这种错误。然而,你将在 多线程编程 章节中看到,在使用线程时,出现那些可能看起来极罕见的缺陷,是很寻常并容易发生的事,而且,对于到底出了什么错,这些缺陷只能给你一个很模糊的概念。因此,对于泛型是添加到
还要注意到,因为泛型是后来添加到containsKey(Object key)
和 get(Object key)
中就包含这类情况。如果这些类是使用在它们之前就存在的泛型设计的,那么这些方法将会使用参数化类型而不是
有一件事很明显:在一种语言已经被广泛应用之后,在其较新的版本中引入任何种类的泛型机制,都会是一项非常非常棘手的任务,并且是一项不付出艰辛就无法完成的任务。在
进阶阅读
泛型的入门文档是 《Generics in the Java Programming Language
你可以从 《Adding Wildcards to the Java Programming Language》中学到更多关于通配符的知识,作者是
-
在编写本章期间,
Angelika Langer 的Java 泛型常见问题解答以及她的其他著作(与Klaus Kreft 一起)是非常宝贵的。 ↩︎ -
http://gafter.blogspot.com/2004/09/puzzling-through-erasureanswer.html ↩︎
-
参见本章章末引文。 ↩︎
-
注意,一些编程环境,如
Eclipse 和IntelliJ IDEA ,将会自动生成委托代码。 ↩︎ -
因为可以使用转型,有效地禁止了类型系统,一些人就认为
C++ 是弱类型,但这太极端了。一种可能更好的说法是C++ 是有一道暗门的强类型语言。 ↩︎ -
我再次从
Brian Goetz 那获得帮助。 ↩︎