促成泛型出现的最主要的动机之一是为了创建集合类,我们先看一个只能持有单个对象的类。这个类可以明确指定其持有的对象的类型:
// generics/Holder1.java
class Automobile {}
public class Holder1 {
private Automobile a;
public Holder1(Automobile a) { this.a = a; }
Automobile get() { return a; }
}
这个类的可复用性不高,它无法持有其他类型的对象。我们可不希望为碰到的每个类型都编写一个新的类。在 Java 5 之前,我们可以让这个类直接持有 Object 类型的对象:
// 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() 取值时,直接就是正确的类型。
这就是 Java 泛型的核心概念:你只需告诉编译器要使用什么类型,剩下的细节交给它来处理。
一个元组类库
有时一个方法需要能返回多个对象。而 return 语句只能返回单个对象,解决方法就是创建一个对象,用它打包想要返回的多个对象。当然,可以在每次需要的时候,专门创建一个类来完成这样的工作。但是有了泛型,我们就可以一劳永逸。同时,还获得了编译时的类型安全。
这个概念称为元组
,它是将一组对象直接打包存储于单一对象中。可以从该对象读取其中的元素,但不允许向其中存储新对象(这个概念也称为 数据传输对象 或 信使 )。
通常,元组可以具有任意长度,元组中的对象可以是不同类型的。不过,我们希望能够为每个对象指明类型,并且从元组中读取出来时,能够得到正确的类型。要处理不同长度的问题,我们需要创建多个不同的元组。下面是一个可以存储两个对象的元组:
// onjava/Tuple2.java
package onjava;
public class Tuple2<A, B> {
public final A a1;
public final B a2;
public Tuple2(A a, B b) { a1 = a; a2 = b; }
public String rep() { return a1 + ", " + a2; }
@Override
public String toString() {
return "(" + rep() + ")";
}
}
构造函数传入要存储的对象。这个元组隐式地保持了其中元素的次序。
初次阅读上面的代码时,你可能认为这违反了 Java 编程的封装原则。a1
和 a2
应该声明为 private,然后提供 getFirst()
和 getSecond()
取值方法才对呀?考虑下这样做能提供的“安全性”是什么:元组的使用程序可以读取 a1
和 a2
然后对它们执行任何操作,但无法对 a1
和 a2
重新赋值。例子中的 final
可以实现同样的效果,并且更为简洁明了。
另一种设计思路是允许元组的用户给 a1
和 a2
重新赋值。然而,采用上例中的形式无疑更加安全,如果用户想存储不同的元素,就会强制他们创建新的 Tuple2
对象。
我们可以利用继承机制实现长度更长的元组。添加更多的类型参数就行了:
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;
}
}
使用元组时,你只需要定义一个长度适合的元组,将其作为返回值即可。注意下面例子中方法的返回类型:
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)
*/
// generics/Amphibian.java
public class Amphibian {}
// generics/Vehicle.java
public class Vehicle {}
到目前为止,我们已经研究了参数化整个类。其实还可以参数化类中的方法。类本身可能是泛型的,也可能不是,不过这与它的方法是否是泛型的并没有什么关系。
泛型方法独立于类而改变方法。作为准则,请“尽可能”使用泛型方法。通常将单个方法泛型化要比将整个类泛型化更清晰易懂。
如果方法是 static 的,则无法访问该类的泛型类型参数,因此,如果使用了泛型类型参数,则它必须是泛型方法。
要定义泛型方法,请将泛型参数列表放置在返回值之前,如下所示:
// 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
*/
尽管可以同时对类及其方法进行参数化,但这里未将 GenericMethods 类参数化。只有方法 f()
具有类型参数,该参数由方法返回类型之前的参数列表指示。
对于泛型类,必须在实例化该类时指定类型参数。使用泛型方法时,通常不需要指定参数类型,因为编译器会找出这些类型。 这称为 类型参数推断。因此,对 f()
的调用看起来像普通的方法调用,并且 f()
看起来像被重载了无数次一样。它甚至会接受 GenericMethods 类型的参数。
如果使用基本类型调用 f()
,自动装箱就开始起作用,自动将基本类型包装在它们对应的包装类型中。
节选自《On Java8》