Java泛型
必要性
在程序日益复杂庞大的今天,编写泛用性代码的价值愈发变得巨大。
而要做到这一点,其诀窍仅只两字而已————解耦。
最简单的解耦,无疑是使用基类替代子类。然而由于 Java 仅支持单继承,这种解耦方法所带来的局限性未免过大,有种“只准投胎一次”的感觉。
使用接口替代具体类算是更近了一步,算是多给了一条命吧,但限制依旧存在。要是我们所写的代码本身就是为了应用于“某种不确定的类型”呢?
这时候就轮到泛型登场了。
简单泛型
虽然理想远大。但 Java 引入泛型的初衷,也许只是为了创建容器类也说不定。
站在类库设计者的角度,我们不妨走上一遭。
得益于单根继承结构,我们可以这样来设计一个持有单个对象的容器:
public class Holder1 {
private Object a;
public Holder1(Object a) {
this.a = a;
}
Object get() {
return a;
}
}
这个容器确实能持有多种类型的对象,但通常而言我们只会用它来存储一种对象。也就是说虽然设计时希望它能存储任意类型,但使用时却只能存储我们想要的确定类型。
泛型可以达到这一目的,与此同时,这也能使编译器为我们提供编译器检查。
class Automobile {}
public class Holder2<T> {
private T a;
public Holder2(T a) {
this.a = a;
}
public void set(T a) {
this.a = a;
}
public T get() {
return a;
}
public static void main(String[] args) {
Holder2<Automobile> h2 =
new Holder2<Automobile>(new Automobile());
Automobile a = h2.get(); // No cast needed
// h2.set("Not an Automobile"); // Error
// h2.set(1); // Error
}
}
如你所见,使用方法即在类名后添加尖括号,然后填写 类型参数 T
。使用时用明确的类型参数替换掉 T
,即为该容器指定了其存储的 确定类型。
泛型方法
泛型可以应用于方法,只需要将泛型参数列表放在方法返回值之前即可。
下面这个例子中,f()的效果看起来像是重载过一样:
//: 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()
时传入了基本数据类型,自动打包机制 将会被触发,将基本数据类型包装为对应的对象。
擦除
Java 泛型是使用 擦除 来实现的,这意味着在泛型代码内部,无法获取关于类型参数的信息。
谨记,泛型类型参数将擦除到它的 第一个边界,默认边界为 Object
,对于 <T extends Bound>
来说,第一个边界为 Bound
,即像是在类的声明中使用 Bound
替换掉 T
一样。
以下例子说明了这一问题:
//: 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
来进行处理;用类字面常量来进行说明应该会更为直观:c1 与 c2 的值为 ArrayList.class
,而不是 ArrayList<String>.class
与 ArrayList<Integer>.class
知道了这一点后,你或许能猜测出容器类的一些具体实现细节了。
打开 ArrayList
的源码,会发现在其内部,用来存储数据的数组都是这样定义的:
/**
* The elements in this list, followed by nulls.
*/
transient Object[] array;
而 get()
方法则是这样:
@SuppressWarnings("unchecked") @Override public E get(int index) {
if (index >= size) {
throwIndexOutOfBoundsException(index, size);
}
return (E) array[index];
}
注:当 E
的第一个边界为 Object
时,那么这个方法实际上就根本没有进行转型(从 Object 到 Object)。
知道这一点后,你大概会对以下代码为何能符合预期的运行感到疑惑:
//: generics/GenericHolder.java
public class GenericHolder<T> {
private T obj;
public void set(T obj) { this.obj = obj; }
public T get() { return obj; }
public static void main(String[] args) {
GenericHolder<String> holder =
new GenericHolder<String>();
holder.set("Item");
String s = holder.get(); // Why it works?
}
} ///:~
使用 javap -c
反编译,我们可以找到答案:
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 GenericHolder
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 --------Watch this line--------
21: astore_2
22: return
奥秘就是,编译器在编译期为我们执行类型检查,然后插入了转型代码。
再看下面这个例子:
//: 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>(String.class);
String[] stringArray = stringMaker.create(9);
System.out.println(Arrays.toString(stringArray));
}
} /* Output:
[null, null, null, null, null, null, null, null, null]
*///:~
因为擦除的关系, kind
只是被存储为 Class
,使用 Array.newInstance()
; 创建数组也就只能得到非具体的结果,实际使用中我们需要对其进行向下转型,但是并没有足够的类型信息用以进行类型检查,所以编译器报错,只能采用注解 @SuppressWarnings("unchecked")
强行将其消去。
通配符
有些时候你需要限定条件,使用通配符可以满足这一特性。
这是指定上界的情况:
//: generics/GenericsAndCovariance.java
import java.util.*;
public class GenericsAndCovariance {
public static void main(String[] args) {
// Wildcards allow covariance:
List<? extends Fruit> flist = new ArrayList<Apple>();
// 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 that it returns at least Fruit:
Fruit f = flist.get(0);
}
} ///:~
flist
的类型为 List<? extends Fruit>
,读作 任何从 Fruit 继承而来的类型构成的列表。但这并不意味着这个 List
将持有任何类型的 Fruit
,通配符引用的其实时 明确类型,这个例子中它意味着 某种指定了上界为 Fruit
的具体类型。
造成 flist
的 add()
完全不可用的原因是,在这种情况下 add()
的参数也变成了 ? extends Fruit
。下面这个例子可以帮助你进行理解:
//: 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; }
public boolean equals(Object obj) {
return value.equals(obj);
}
public static void main(String[] args) {
Holder<Apple> Apple = new Holder<Apple>(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(); // Returns ‘Object’
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: (Sample)
java.lang.ClassCastException: Apple cannot be cast to Orange
true
*///:~
同样的道理,对于上例中的 flist
来说,其 set()
方法的参数变成了 ? extends Fruit
,这意味着其接受的参数可以时任意类型,只需满足上界为 Fruit
即可,而编译器无法验证 任意类型 的类型安全性。
反过来看看指定下界的效果:
//: generics/SuperTypeWildcards.java
import java.util.*;
class Jonathan extends Apple {}
public class SuperTypeWildcards {
static void writeTo(List<? super Apple> apples) {
apples.add(new Apple());
apples.add(new Jonathan());
// apples.add(new Fruit()); // Error
}
} ///:~
可以看到,写入操作变得合法。显然,Apple
类型满足下界需求,执行写入操作没有安全性问题,而 Jonathan
是 Apple
的子类,经过 向上转型,也可以符合要求,而 Apple
的基类 Fruit
则仍然由于类型不定而被拒绝。
基本类型不能作为类型参数
不能创建 List<int>
之类,而需使用 List<Integer>
,但因为自动包装机制的存在,所以写入数据时可以使用基本数据类型。
实现参数化接口
一个类不能实现同一个泛型接口的两种变体,因为擦除会让它们变成相同的接口:
//: generics/MultipleInterfaceVariants.java
// {CompileTimeError} (Won’t compile)
interface Payable<T> {}
class Employee implements Payable<Employee> {}
class Hourly extends Employee
implements Payable<Hourly> {} ///:~
Hourly
不能编译。但是,如果从 Payable
的两种用法中移除掉泛型参数(就像编译器在擦除阶段做的那样),这段代码将能够编译。
重载
以下代码无法编译,因为擦除会让两个方法产生相同的签名:
//: generics/UseList.java
// {CompileTimeError} (Won’t compile)
import java.util.*;
public class UseList<W,T> {
void f(List<T> v) {}
void f(List<W> v) {}
} ///:~