先讲结果:
- super 是指定泛型的『下界』;
- extend 是指定泛型的『上界』;
- ?是通配符;
看个例子
public class Main {
public static void main(String[] args) {
Class clz0 = new ArrayList<String>().getClass();
Class clz1 = new ArrayList<Integer>().getClass();
System.out.println("clz0 == clz1 ?= " + (clz0 == clz1));
}
}
打印结果如下:
clz0 == clz1 ?= true
明明 clz0 是 String 的,clz1 是 Integer 的,为什么打印出他们是同一种类型呢?
Java的泛型就是使用擦除来实现的(JDK最开始是不支持的,JDK1.5 版本引进的概念,为了兼容以前,在编译成 class 后类型被擦除)。因此,当你在使用泛型的时候,任何信息都被擦除,你所知道的就是你在使用一个对象。所以 List< Integer> 和 List< String> 在运行时,会被擦除成他们的原生类型List。
泛型信息只存在于代码编译阶段,编译器编译完带有泛形的java程序后,生成的class文件中与泛型相关的信息会被擦除掉,以此使程序运行效率不受到影响,这个过程叫做类型擦除,也就是说泛型类和普通类在 java 虚拟机内是一样的。
再来看个例子

我们看到,IDE直接提示错误,编译报错。
因为泛型的擦除,main 中传入的泛型在 Test 中变成了 Object,因此无法与 Printer 这个类绑定,也就无法调用 print 方法!
那如何才能调用 print 方法呢?
这就是我们本篇内容要讲的了:。
我们可以将 Test<T> 改为 Test<T extend Printer>,通过指定泛型的上边界,告诉编译器泛型T必需具有类型Printer 或者从 Printer 导出的类型。
擦除带来的问题
泛型不能用于显性地引用运行时类型的操作之中,例如 转型,instanceof 和 new 操作(包括 new一个对象,new一个数组),因为所有关于参数的类型信息都在运行时丢失了,所以任何在运行时需要获取类型信息的操作都无法进行工作。
例如:
if(obj instanceof T);
T t = new T();
T[] ts = new T[10];
解决擦除带来的问题
1. 解决instanceof
使用 instanceof 会失败,是因为类型信息已经被擦除,因此我们可以引入类型标签Class< T>,就可以转用动态的 isInstance()。
class A{}
class B extends A {}
public class Main<T> {
private Class<T> clz;
public Main(Class<T> clz) {
this.clz = clz;
}
public boolean compare(Object o) {
return clz.isInstance(o);
}
public static void main(String[] args) {
Main<A> a = new Main<>(A.class);
System.out.println(a.compare(new A()));
System.out.println(a.compare(new B()));
}
}
打印结果:
true
true
解决创建类型实例
解决办法是使用工厂。
interface Factory<T>{
T create();
}
class Product<T> {
public <P extends Factory<T>> Product(P factory) {
factory.create();
}
}
class ProductFactory implements Factory<Integer> {
@Override
public Integer create() {
Integer num = 1;
System.out.println(num);
return num;
}
}
public class Main {
public static void main(String[] args) {
new Product<>(new ProductFactory());
}
}
打印结果:
1
解决创建泛型数组
不能创建泛型数组的情况下,一般的解决方案是使用 ArrayList 代替泛型数组。因为ArrayList 内部就是使用数组,因此使用 ArrayList 能够获取数组的行为,和由泛型提供的编译器的类型安全。
但是假如,某种特定的场合,你仍然要使用泛型数组,推荐的方式是使用 类型标签+Array.newInstance 来实现,并用注解 @SuppressWarnings(“unchecked”) 抑制住警告:
public class Main<T> {
private Class<T> clz;
public Main(Class<T> clz) {
this.clz = clz;
}
@SuppressWarnings("unchecked")
T[] create(int size) {
return (T[]) Array.newInstance(clz, size);
}
public static void main(String[] args) {
Main<Integer> main = new Main<>(Integer.class);
Integer[] as = main.create(5);
System.out.println(as.length);
}
}
打印结果:
5
边界
正是因为有了擦除,把类型信息擦除了,所以,用无界泛型参数调用的方法只是那些可以用object调用的方法。但是,如果给定边界,将这个参数限制为某个类型的子集,就可以使用这些类型子集来调用方法。
- 指定上界
interface A { void a(); }
public class Main<T extends A> {
T t;
public Main(T t) {
this.t = t;
}
public void test() {
t.a();
}
}
可见,类型T已经可以调用 A 的 a 方法了。
- 指定多个上界
interface A { void a(); }
interface B { void b(); }
interface C { void c(); }
public class Main<T extends A & B & C> {
T t;
public Main(T t) {
this.t = t;
}
public void test() {
t.a();
t.b();
t.c();
}
}
这里需要注意的是,extends 后面跟的第一个边界,可以为类或接口,之后的均为接口。
通配符和泛型上界和下界
为什么要用通配符和边界?
class Fruit {}
class Apple extends Fruit {}
有一个最简单的容器:Plate类。表示盘子里可以放一个泛型的『东西』。我们可以对这个东西做最简单的『放』和『取』的动作:set() / get()
class Plate<T> {
private T item;
public Plate(T item) { this.item = item; }
public void set(T item) { this.item = item; }
public T get() { return this.item; }
}
现在我定义一个『水果盘子』,逻辑上水果盘子可以装苹果:
Plate<Fruit> p = new Plate<Apple>(new Apple());
很不幸的是:Java编译器不允许这个操作,会报错:『装苹果的盘子』无法转换成『装水果的盘子』
error: incompatible types: Plate<Apple> can not be converted to Plate<Fruit>
实际上编译器脑袋里认定的逻辑是:
- 苹果 IS-A 水果
- 装苹果的盘子 NOT-IS-A 装水果的盘子
所以,就算容器里装的东西之间有继承关系,但容器之间是没有继承关系的。所以,我们也就不能将 Plate<Apple>的引用传递给 Plate<Fruit>。
因此,为了让泛型用起来稍微舒服点,SUN的大脑们就想出了 <? extends T> 和 <? super T>的办法,以此让『水果盘子』和『苹果盘子』之间发生点关系。
上界<? extends Class>


可见,指定了下边界,却不能add任何类型,甚至Object都不行,除了 null,因为 null 代表任何类型。List< ? extends Fruit> 可以解读为,“具有任何从Fruit继承的类型”,但实际上,它意味着,它没有指定具体类型。对于编译器来说,当你指定了一个 List< ? extends Fruit>,add 的参数也变成了“? extends Fruit”。因此编译器并不能了解这里到底需要哪种 Fruit 的子类型,因此他不会接受任何类型的 Fruit。
然而,contain 和 indexof 却能执行,这是因为,这两个方法的参数是 Object,不涉及任何的通配符,所以编译器允许它调用。
list.get(0) 能够执行是因为,当item在此list存在时,编译器能够确定他是Apple的子类,所以能够安全获得。
下界<? super Class>


这里可以看到,list.add(new Fruit()) 这句不能编译成功,这是因为 List< ? super Apple> 表示“具有Apple的父类的列表”。但是为什么 add(new Fruit())不能成功 呢?正是因为『?代表Apple的父类』,但是编译器不知道你要添加哪种Apple的父类,因此不能安全地添加。
对于 super,get 返回的是 Object,因为编译器不能确定列表中的是Apple的哪个子类,所以只能返回 Object。
PECS原则
- 如果要从集合中读取类型T的数据,并且不能写入,可以使用 ? extends 通配符;(Producer Extends)
- 如果要从集合中写入类型T的数据,并且不需要读取,可以使用 ? super 通配符;(Consumer Super)
- 如果既要存又要取,那么就不要使用任何通配符。
网友评论