Java 泛型一直被编程办所诟病,但是其实它也有优点,这促使我们使用泛型。
假如有一个持有对象的容器类,我们想让它能够持有任何类型的对象,如下
public class Holder {
private Object object;
public void set(Object obj) {
object = obj;
}
public Object get() {
return object;
}
}
Holder 类使用 Object 类的对象来保存任何类型的对象,这看起来很完美。
现在我们用这个 Holder 类来保存和获取对象
Holder h = new Holder();
// 保存String对象
h.set("hello");
// 获取String对象
String s = (String) h.get();
首先创建了一个 Holder 对象,然后用这个对象保存了一个 String 类型对象,最后获取保存的这个对象,由于我们知道保存的对象是String类型,因此这里可以强制转换,并且不会有任何问题。
然而如果我们把一个Holder对象传入一个方法,如下
public void test(Holder h) {
// 具体的类型是什么呢?
Object obj = h.get();
}
通过get()
方法获取的对象是什么类型呢?你可以会认为是String类型,因为你刚才保存的就是这个类型,但是谁又能保存没有其它程序员,把这个Holder对象保存了其它类型对象呢? 如果存在这个情况,那么这里强制转换为String类型,就是出现异常。
然而,如果使用泛型,可以解决这个问题。我们通过泛型改写 Holder 类
public class Holder<T> {
private T item;
public void set(T t) {
item = t;
}
public T get() {
return item;
}
public static void main(String[] args) {
Holder<String> h = new Holder<>();
h.set("Hello");
// 编译报错
// h.set(1);
// 不用执行强制类型转换
String s = h.get();
}
}
Holder<String>
保证了添加的元素只能是String类型,这个是由编译器保证的。
同时获取对象时,不用再手动执行强制类型转换,因为编译器会自动生成类型转换代码。
如果你查看字节码 Holder.class
,就会发现一些端倪
public class Holder<T> {
private T item;
public Holder() {
}
public void set(T t) {
this.item = t;
}
public T get() {
return this.item;
}
public static void main(String[] args) {
Holder<String> h = new Holder();
h.set("Hello");
String s = (String)h.get();
}
}
首先创建Holder对象时,使用的是new Holder()
,而不是new Holder<>()
,这里可以看出泛型被擦除了。
其次,调用的 get()
方法,编译器自动帮我们加上了类型转换的代码。
其实,当调用有泛型参数的方法时,例如set(T t)
,编译器会检测传入参数的类型。当调用返回与泛型相关的类型时,例如T get()
,编译器会自动添加类型转换代码。这两个动作被称之为编译器对泛型的边界动作,即对泛型参数的检测以及对泛型返回值的转型。