Java 泛型知多少
你可能遇到过以下困惑,为什么在 java 中我们不能 new 一个泛型,而 C++ 却可以,如下这样
此种方法在 java 中直接通不过编译,而如果我们要实现同样的功能,只能通过显示的传入 class,然后通过 RTTI 方式来在运行时动态创建
public class AnimalPlayground<T> {
public T create(Class<T> t) {
try {
return t.newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
这个原因是因为 Java 的泛型其实是假泛型,他只在编译时进行了泛型校验,生成的字节码其实不存在泛型的概念,也就是所谓的泛型擦除。比如如下两段代码:
public class AnimalPlayground<T> {
private T obj;
public void create(T t) {
this.obj = t;
}
}
public class AnimalPlayground {
private Object obj;
public void create(Object t) {
this.obj = t;
}
}
通过 javac 然后 javap 看他们的字节码,发现生成的字节码其实是一样的,没有包含特殊的泛型信息。
public void create(T);
descriptor: (Ljava/lang/Object;)V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #7 // Field obj:Ljava/lang/Object;
5: return
这一点解释了为什么我们无法对泛型进行 new 的操作,因为泛型进行了擦除,所以我们无法验证这个泛型是否有默认的构造函数,所以 Java 编译器干脆进行了编译时报错处理。因为泛型擦除的原因,所以以下等式可以成立
List<Integer> list1 = new ArrayList<>();
List<String> list2 = new ArrayList<>();
System.out.println(list1.getClass() == list2.getClass()); // true
而且我们也留意到,我们可以声明 List.class
而无法使用 List<Integer>.class
,因为第二种本身无意义。那如何得到包含泛型的列表的类型呢,此时我们可以使用 ParameterizedType。此篇文章对 ParameterizedType 的用法讲解的比较详细。
协变和逆变
个人理解泛型的本质是在尽可能编程灵活的情况下,做到逻辑自洽,此时一门语言是否好用就取决于它的编译器的聪明程度。
比如我们声明一个 Animal 类,然后再声明一个 Dog 类继承自 Animal。此时按照我们直觉的推断,如果一个 Dog 是 Animal 的子类,那么一个 Dog 的列表是否是一个 Animal 列表的子类呢?很可惜,如果我们直接这样做直接编译器就会不通过:
此时我们就需要引入协变的概念,字面意思就是可以协同实际类型来变化。在 Java 中 List 是支持协变的,可以使用以下写法:
ArrayList<? extends Animal> animals = new ArrayList<Dog>();
任何 Animal 子类的列表都可以赋值给:ArrayList<? extends Animal>
。但要注意在 Java 中协变的 List 是没法使用 add
去增加元素的。这是为啥呢?
因为协变类型可以被任何子类数组赋值,而由于 Java 的泛型擦除机制,我们是没办法在编译时及时发现这个列表被传入了其他子类,比如上面的 animals 如果可以使用 add,那么我们执行 animals.add(new Cat())
是没法在编译时发现问题的,那就有违 Java 是一门类型安全语言的设定,所以 Java 中直接去掉了协变增加元素的功能。
协变 我们知道是跟随实际类型的父子关系而来,那逆变呢?按照字面意思理解就是和实际类型的父子关系反过来,比如同样的 ArrayList 逆变则可以使用如下表达
ArrayList<? super Animal> animals = new ArrayList<Animal>();
ArrayList<? super Animal>
这种表达是 Animal 子类的一个列表,只能接收一个 Animal 的列表。此种方法我们知道列表一定是一个 Animal 类的列表,所以我们可以随意的向其中增加元素
ArrayList<? super Animal> animals = new ArrayList<>();
animals.add(new Cat());
animals.add(new Dog());
animals.add(new Animal());
写在最后
C++ 中使用泛型是通过 template 来实现,你可以理解它只是一个模板,是无法单独编译。C++ 在编译的过程中遇到 template 则会使用真实的类型来替换模板中的泛型,然后新生成一段机器码。而 Java 的泛型类是可以单独编译的,编译完成后的字节码就是一个普通的类,在运行层面的使用就是把它当作一个普通类来使用的。理解这一点,应该能帮助你理解 Java 中泛型种种限制的原因。
作者:CPPAlien
链接:https://juejin.cn/post/7146080877552861220
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。