Java之泛型进阶——泛型代码转化为普通代码
简介
本篇是java中泛型深入一点的东西,主要是泛型代码如何转换成普通的Java代码,类型擦除以及一个通过反射绕开编译器类型检测的例子来证明类型在运行时是真正被擦除的。
泛型的转化
用泛型编写的Java程序和普通的Java程序基本相同,只是多了一些参数化的类型同时少了一些类型转换。实际上泛型程序也是首先被转化成一般的不带泛型的 Java 程序后再进行处理的,编译器自动完成了从 Generic Java 到普通 Java 的翻译。转化主要是两个关键点,擦除和桥方法。
擦除的概念
将泛型代码转换成普通的Java代码的过程是由编译器(如javac)来完成的,虚拟机并不负责这一任务。当编译器对带有泛型的java代码进行编译时,它会去执行类型检查和类型推断,然后生成普通的不带泛型的字节码,这种普通的字节码可以被一般的Java虚拟机接收病执行,这中技术就叫做擦除(erasure)。
可见,编译器可以在对源程序(带有泛型的 Java 代码)进行编译时使用泛型类型信息保证类型安全,对大量如果没有泛型就不会去验证的类型安全约束进行验证,同时在生成的字节码当中,将这些类型信息清除掉。
简单示例
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
System.out.println(strings.getClass() == integers.getClass());//true
上面代码输出结果并不是预期的false,而是true。其原因就是泛型的擦除。
泛型转化大致步骤
具体的转化过程大致分为以下几个部分:
- 将参数化类型中的类型参数“擦除”掉;
//此处的String就是类型参数
List<String> list = new ArrayList<>();
//转换为
ist list = new ArrayList();
- 将类型变量用“上限(upper bound)”取代,通常情况类型变量的上限是Object,但是也有一些情况是限定类型参数的边界的。这里的类型变量是实例域、本地方法域方法参数以及方法返回值中用来标记类型信息的“变量”,如我们在集合中常见的“E”。
void add(int index, E element);
//转换为
void add(int index, Object element);
class<T> {private T t;}
//转换为
class {private Object t;}
- 添加类型转换并插入”桥方法”(bridge method),以便Java方法的重写(overridden)可以正常的工作,当一个类实现了一个参数化的接口或是继承了一个参数化的类时,需要引入桥方法。。
源码:
public class Person implements Comparable<Person>{
protected int age;
private Person person;
public Person(int age) {
this.age = age;
}
public Person getPerson() {
return person;
}
@Override
public boolean compareTo(Person that) {
return this.age > that.age;
}
}
interface Comparable<T> {
boolean compareTo(T that);
}
编译器编译之后的代码:
public class Person implements Comparable<Person>{
...
/**
* 这个方法是重写了接口Comparable中的compareTo方法,但是我们注意到接口中compareTo方法的参数是一个类型变量,在编译器编译的时候会
* 转换成 compareTo(Object that)这种,也就是说当实现类实现此方法时,方法名称相同并且方法参数也必须是Object类型的。但很明显下面方
* 法中的参数不是,其原因是编译器在编译的时候加入了桥方法。
*/
@Override
public boolean compareTo(Person that) {
return this.age > that.age;
}
/**
* 编译器加入的桥方法
*
public boolean compareTo(Object that) {
return this.compareTo((Person) that);
}
*/
...
}
在某些情况下,擦除技术需要引入类型转换(cast),这些情况主要包括:
情况 1. 方法的返回类型是类型参数;
E elementData(int index) {
return (E) elementData[index];
}
情况 2. 在访问数据域时,域的类型是一个类型参数(也就是上面使用桥方法的情况)。
具体擦除示例
源码:
package org.andy.items.thkinjava.generics.generic2016.brige;
/**
* Author: oscar
* Create Data: 10/03/16
*/
public class Person implements Comparable<Person>{
protected int age;
public Person(int age) { this.age = age; }
@Override
public boolean compareTo(Person that) { return this.age > that.age; }
}
interface Comparable<T> { boolean compareTo(T that); }
编译后:
public class Person implements Comparable{
protected int age;
public Person(int age) { this.age = age; }
@Override
public boolean compareTo(Person that) { return this.age > that.age; }
public boolean compareTo(Object that) {
return this.compareTo((Person) that);
}
}
interface Comparable{ boolean compareTo(Object that); }
使用反射来避开泛型的验证
public class HowToAvoidGenericRestrict {
/**
* 简单演示一下如何使用反射向一个拥有泛型的List中添加泛型指定类型之外的元素
* @param args ignore
*/
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
//声明一个只能存放String类型的list
List<String> list = new ArrayList<>();
//向上面一个list中加入一个Integer类型的元素
//list.add(1); //compile error
System.out.println("Before add, the size of list is : " + list.size());
Class<?> clazz = list.getClass();
Method method = clazz.getMethod("add", Object.class);
method.invoke(list, 1);
System.out.println("After add, the size of list is : " + list.size());
System.out.println(list.get(0));
}
}
输出:
Before add, the size of list is : 0
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
After add, the size of list is : 1
at org.andy.items.thkinjava.generics.generic2016.avoid.generic.restrict.HowToAvoidGenericRestrict.main(HowToAvoidGenericRestrict.java:29)