泛型擦除
- Java泛型擦除后,能够添加不是泛型类型的元素吗?------答案是可以的,通过反射
- 泛型擦除后是如何获取泛型信息?-----匿名内部类
1 泛型擦除后能否添加非泛型类型的元素?
下面看一段代码:
public static void main(String[] args) throws Exception {
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2);//true
ArrayList<String> strings = new ArrayList<>();
strings.add("aa");
// strings.add(1);//此处会报错,因为泛型检查此处需要String类型
}
打印输出为true,验证了泛型擦除机制,两个泛型不一致的集合,比较class对象的时候,是一样的。
然后声明了一个泛型为String的ArrayList集合,在调用string.add(1)的时候,编译出错,提示我们需要一个String类型的元素,但是这里却传入了一个int类型的。
那么我们有没有办法向ArrayList中添加一个非String类型的元素呢?下面看一段代码演示:
public static void main(String[] args) throws Exception {
ArrayList<String> strings = new ArrayList<>();
strings.add("aa");
// strings.add(1);//此处会报错,因为泛型检查此处需要String类型
Method methodAdd = strings.getClass().getDeclaredMethod("add", Object.class);
methodAdd.setAccessible(true);
methodAdd.invoke(strings, 2);
System.out.println(strings.size());
for (Object o : strings) {
System.out.println(o);
}
}
我们知道泛型是一种编译时类型安全检查机制,因为要兼容之前的JDK1.6之前的版本,所以泛型在编译为class字节码文件时候,泛型就被擦除了。
那么说到这里,我们就应该知道,可以通过反射来非泛型元素,上面的代码也正是这个思路实现的。往集合strings里添加了 数字 2.
但是反射本来就是一个非常规的操作,明明知道不能添加非泛型类型的元素,却非要通过反射添加的话,就有类型转换异常的风险,需要自己承担。
2 泛型擦除后是如何保存泛型信息的?
我们通过如下两段代码来发现一些端倪:
public class Sign {
public static void test(List list) {
System.out.println("test");
}
}
public class Sign {
public static void test(List<String> list) {
System.out.println("test");
}
}
上面两段代码唯一的区别是下面比上面的加了泛型为String的泛型信息。我们通过javac Sign.java后再通过javap -v Sign.class对比下面两段反编译后的class字节码。
public static void test(java.util.List);
descriptor: (Ljava/util/List;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String test
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 13: 0
line 14: 8
public static void test(java.util.List<java.lang.String>);
descriptor: (Ljava/util/List;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String test
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 11: 0
line 12: 8
Signature: #14 // (Ljava/util/List<Ljava/lang/String;>;)V
我们能看到加了泛型的反编译后多了一个最后一行的签名文件Signature,保存到就是泛型的信息。而字节码层面没有什么差别。
泛型类型只会在类,字段,以及方法形参保存泛型信息,正是因为有了Signature对泛型信息的保存,我们才能获取他们,下面通过一段代码演示获取字段和方法泛型信息:
public class Sign {
private List<String> list;
public static void test(List<String> list) {
System.out.println("test");
}
public static void main(String[] args) throws Exception {
Field list = Sign.class.getDeclaredField("list");
list.setAccessible(true);
Type genericType = list.getGenericType();
System.out.println(genericType);//java.util.List<java.lang.String>
ParameterizedType parameterizedType = (ParameterizedType) genericType;
System.out.println(parameterizedType.getActualTypeArguments()[0]);//class java.lang.String
System.out.println("------------------");
Method methodTest = Sign.class.getDeclaredMethod("test", List.class);
methodTest.setAccessible(true);
System.out.println(methodTest.getGenericParameterTypes()[0]);
ParameterizedType parameterizedType1 = (ParameterizedType) methodTest.getGenericParameterTypes()[0];//java.util.List<java.lang.String>
System.out.println(parameterizedType1.getActualTypeArguments()[0]);//class java.lang.String
}
}
但是上面的泛型获取有一个问题,就是不能获取到实参的泛型信息,比如我们要进行序列化和反序列化的话,获取不到泛型信息的话,就很难进行序列化和反序列化了,这个时候我们可以参考Gson的解决方案,使用TypeToken匿名内部类的方式来保存泛型信息。我们模拟写出了一个MyTypeToken类,如下:
实例代码如下:
public class Deserialize {
static class Response<T> {
T data;
int code;
String message;
@Override
public String toString() {
return "Response{" +
"data=" + data +
", code=" + code +
", message='" + message + '\'' +
'}';
}
public Response(T data, int code, String message) {
this.data = data;
this.code = code;
this.message = message;
}
}
static class Data {
String result;
public Data(String result) {
this.result = result;
}
@Override
public String toString() {
return "Data{" +
"result=" + result +
'}';
}
}
public static void main(String[] args) {
Response<Data> dataResponse = new Response(new Data("数据"), 1, "成功");
Gson gson = new Gson();
String json = gson.toJson(dataResponse);
System.out.println(json);
System.out.println("---------------");
/**
* 有花括号: 代表是匿名内部类,创建一个匿名内部类的实例对象
* 没花括号:创建实例对象
*/
Type type1 = new TypeToken<Response<Data>>() {
}.getType();
Type type = new MyTypeToken<Response<Data>>() {
}.getType();
System.out.println(type1);
System.out.println(type);
Response<Data> response = gson.fromJson(json, type);
System.out.println(response);
System.out.println(response.data);
System.out.println(response.data.getClass());
}
}
//运行结果
{"data":{"result":"数据"},"code":1,"message":"成功"}
---------------
com.oman.lib.gson.Deserialize$Response<com.oman.lib.gson.Deserialize$Data>
com.oman.lib.gson.Deserialize.com.oman.lib.gson.Deserialize$Response<com.oman.lib.gson.Deserialize$Data>
Response{data=Data{result=数据}, code=1, message='成功'}
Data{result=数据}
class com.oman.lib.gson.Deserialize$Data
上面使用匿名内部类满足了我们的需求,我们可以反编译一下看看加{}和不加括号的区别:
public static void main(String[] args) {
Type type = new MyTypeToken<Response<Data>>().getType();
}
public static void main(String[] args) {
Type type = new MyTypeToken<Response<Data>>(){
}.getType();
}
他们的区别在于加了花括号的时候,反编译的时候多了下面一行,从而保存了实参的泛型信息。
Signature: #15 // Lcom/oman/lib/gson/Deserialize$MyTypeToken<Lcom/oman/lib/gson/Deserialize$Response<Lcom/oman/lib/gson/Deserialize$Data;>;>;
3 总结
- Java泛型擦除后可以通过反射添加元素,不过最好不要这么做,会有类型转换异常的风险。
- 泛型擦除后,类,字段和方法的形参泛型信息是会保存到Signature中的,另外可以通过匿名内部类的方式获取泛型的实参类型,比如Gson中的使用。
转载 #面试题:你知道泛型擦除后是如何获取泛型信息的吗?_Zhou Jiang的博客-CSDN博客_java泛型擦除后保存在哪里