Java泛型是JDK1.5引入的新特性.如果用一句话总结泛型的作用,就是类型参数化.
为什么要引入泛型
在JDK1.5之前,如果你使用集合类,代码大致是这样的
public static void main(String[] args) {
List list = new ArrayList();
list.add("bob");
list.add("jack");
list.add(123);
for (int i = 0; i < list.size(); i++) {
System.out.println((String)list.get(i));
}
}
这段代码编译没有任何错误.但是一执行就会抛ClassCastException.
我们总结一下这段代码存在的问题:
- List中存放的数据无法规范
- 编译期无法检查出此类问题.而到运行期发现再去找bug成本很高
为了解决这个问题.JDK1.5中引入了泛型的概念.
泛型的引入
到了JDK1.5之后,代码就成了这个样子.
public static void main(String[] args) {
List list = new ArrayList();
list.add("bob");
list.add("jack");
list.add(123);
for (int i = 0; i < list.size(); i++) {
System.out.println((list.get(i));
}
}
这段代码在编译期就已经提示我们不能往list里放入123.
通过引入泛型,JDK为我们解决了之前代码存在的问题.
- 我们可以通过泛型规范集合中的元素类型.
- 在编译期间就检查出语法错误.
一切看起来很美好.
泛型的擦除
由于泛型是JDK在1.5才提供的功能.JDK作为一个软件,在升级的过程中,要做向下兼容以保证低版本升级到高版本的成本尽可能的小.
这也就导致Java的泛型是在编译器这个层面来实现的.在生成Java字节码层面是不存在泛型的类型的.
这也就是说.不存在List
类型比较
Code:
public static void main(String[] args) {
List list = new ArrayList();
List list2 = new ArrayList();
System.out.println(list.getClass() == list2.getClass());
}
这段代码的执行结果是true.
看起来似乎可以证明,只有List.class.而没有List
但是还是觉得不够通透.我们继续下一个实验.
反射
如果泛型在运行时并不存在,则List的add方法在运行时的方法签名应该和JDK1.5之前保持一致.
boolean add(Object e);
而如果泛型在运行时存在,则方法签名会类似于:
boolean add(String e);
Java的反射可以在在运行时获取,操作类的方法.所以我们只需要看能否获取到指定签名的Method对象就知道在运行时是否存在该方法.
Code:
public static void main(String[] args) throws NoSuchMethodException {
List list = new ArrayList<>();
Method method = list.getClass().getMethod("add", String.class);
System.out.println(method);
}
Console output:
Exception in thread "main" java.lang.NoSuchMethodException:
java.util.ArrayList.add(java.lang.String)
at java.lang.Class.getMethod(Class.java:1786)
List中没有add(String e)方法.继续测试:
Code:
public static void main(String[] args) throws NoSuchMethodException {
List list = new ArrayList<>();
Method method = list.getClass().getMethod("add", Object.class);
System.out.println(method);
}
Console output:
public boolean java.util.ArrayList.add(java.lang.Object)
代码执行正常.在运行时我们找到了add(Object e)方法.
到了这个层面,基本可以确定在运行时,泛型确实被擦除了.但是这个分析过程看起来有点曲线救国的感觉.我们能不能有一个一针见血的方法来证明Java在运行时是没有泛型的呢.
Java指令代码
既然运行时没泛型.那好.我们去看一下编译后的指令代码不就可以了么.
先写一个方法:
import java.util.ArrayList;
import java.util.List;
public class DemoClass {
public static void main(String[] args) {
List list = new ArrayList<>();
list.add("aaa");
System.out.println(list);
}
}
我们先生成这个类的class文件
javac DemoClass.java
然后通过javap命令生成Java指令代码
javap -verbose DemoClass
然后我们得到了一段代码.为了方便阅读.省略了前面大部分.
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."":()V
7: astore_1
8: aload_1
9: ldc #4 // String aaa
11: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
16: pop
17: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
20: aload_1
21: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
24: return
LineNumberTable:
line 9: 0
line 10: 8
line 11: 17
line 12: 24
}
我们看这个main方法Code部分的11:
11: invokeinterface #5, 2
// InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
看到了吧.调用的方法签名中的参数是Object.不是String.
到这里我们已经完全可以确定Java泛型是编译器层面的解决方法.而不是运行时.
泛型类
泛型除了用在集合中,我们也可以自定义泛型类.
Code:
public class TestClass {
private T data;
public TestClass(T data) {
this.data = data;
}
public void setData(T data) {
this.data = data;
}
public T getData() {
return data;
}
}
我们再写一段测试代码:
Code:
TestClass testClass = new TestClass<>("bob");
String name = testClass.getData();
System.out.println(name);
Console output:
bob
到这里我们有一个问题.泛型在运行时已经被擦除.
String name = testClass.getData();
在运行时返回的应该是Object类型.但是我们却可以直接赋值给String类型.这是为什么.为了搞清楚这个问题.依旧可以去看一下Java的指令码.
我们依旧只看一小段关键部分:
11: invokevirtual #6 // Method getData:()Ljava/lang/Object;
14: checkcast #7 // class java/lang/String
17: astore_2
当我们在执行getData之后,并没有直接进行astore操作.而是有一个checkcast指令.
关于这个指令的描述是:Check whether object is of given type
从这个字面我们可以看出这其实是一个检查类型的指令.但是这个解释并没有说明它的完整功能.
我们可以通过简单测试发现.这个指令是在强制类型转换的时候出现.如果类型可以转则通过.如果类型转换失败.则会抛出ClassCastException.关于这个指令可以自行测试.
到这里我们就可以知道,之所以Object可以直接赋值给String.是JVM帮我们做了强转.
泛型擦除带来的问题
类型丢失
由于泛型在运行时被擦除.所以也就无法在运行时对泛型的类型进行操作.
- 无法对泛型进行类型判断
-
无法根据T生成对象
泛型与多态
直接看代码
ParentClass
public class ParentClass {
public void print(T t) {
System.out.println("parentClass");
System.out.println(t);
}
}
ChildClass
public class ChildClass extends ParentClass {
@Override
public void print(String s) {
System.out.println("childClass");
System.out.println(s);
}
}
先看测试代码:
public static void main(String[] args) {
ParentClass childClass = new ChildClass();
childClass.print("aaa");
}
它的输出是
childClass
aaa
可以看到,符合我们对Java运行时绑定的预期.
但是这里有个问题.由于运行时没有泛型.所以父类的print方法签名应该是
public void print(Object t);
而我们的子类里的print方法签名是
public void print(String s);
根据Java对方法重写的定义,要求的是方法签名完全一致.
而我们的代码里其实并没有跟父类完全一样的方法签名.
所以根据动态绑定的原理,应该是调用父类的print(Object t)方法而不是子类的print(String s)
为了搞清楚这个问题,我们需要去看一下ChildClass的指令码.
我们根据指令码可以看到.ChildClass里有两个print方法.
一个和父类相同,print(Object).而另一个和子类中定义的相同.print(String).
而在print(Object)中调用了print(String).
到这里我们就明白了.实际上在这种涉及泛型的多态中,jvm给我们隐式的生成了一个方法(一般叫做桥方法)来达到动态绑定的目的.
参考资料
Java泛型的学习和使用
Java深度历险(五)——Java泛型
Oracle JVM指令解释