文章已收录至https://lichong.work,转载请注明原文链接。
ps:欢迎关注公众号“Fun肆编程”或添加我的私人微信交流经验🤝
背景
在JDK5引入了泛型特性之后,迅速地成为Java编程中不可或缺的元素。然而,就跟泛型乍一看似乎非常容易一样,许多开发者也非常容易就迷失在这项特性里。
多数Java开发者都会注意到Java编译器的类型擦除实现方式,Type Erasure会导致关于某个Class的所有泛型信息都会在源代码编译时消失掉。在一个Java应用中,可以认为所有的泛型实现类,都共享同一个基础类(注意与继承区分开来)。这是为了兼容JDK5之前的所有JDK版本,就是人们经常说的向后兼容性。
向后兼容性
译者注:原文较为琐碎,大致意思是。在JVM整个内存空间中,只会存在一个ArrayList.class
。
为了能够区分ArrayList<String>
和ArrayList<Integer>
,现在假想的实现方式是在Class文件信息表(函数表+字段表)里添加额外的泛型信息。这个时候JVM的内存空间中就会存在(假设)ArrayList
&String.class
和(假设)ArrayList
&Integer.class
文件。顺着这种情况延续下去的话,就必须要修改JDK5之前所有版本的JVM对Class文件的识别逻辑,因为它破坏了JVM内部一个Class只对应唯一一个.class这条规则。这也是人们常说的: 破坏了向后兼容性。
注:参考Python3舍弃掉Python2的例子,也是放弃了对2的兼容,Python3才能发展并构造更多的新特性。
那应该怎么做?
既然Java开发团队选择了兼容JDK5之前的版本,那就不能在JVM里做手脚了。但Java编译器的代码似乎还是可以修改的。于是,Java编译器在编译时就会把泛型信息都擦除,所以以下的比较在JVM运行时会永远为真。
assert new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass();
对JVM运行时来说,上述代码等同于
assert new ArrayList.class == ArrayList.class
到目前为止,上述内容都是大家所熟知的事情。然而,与普遍印象相反的是,某些情况下在运行时获取到泛型类型信息也是可行的。举个栗子:
class MyGenericClass<T> { }
class MyStringSubClass extends MyGenericClass<String> { }
MyStringSubClass
相当于对MyGenericClass<T>
做了类型参数赋值T = String
。于是,Java编译器可以把这部分泛型信息(父类MyGenericClass
的泛型参数是String
),存储在它的子类MyStringSubClass的字节码区域中。
而且因为这部分泛型信息在被编译后,仅仅被存储在被老版JVM所忽略的字节码区域中,所以这种方式并没有破坏向后兼容性。与此同时,因为T
已经被赋值为String
,所有的MyStringSubClass
类的对象实例仍然共享同一个MyStringSubClass.class
。
如何获取泛型信息?
应该如何获取到被存储在byte code
区域的这块泛型信息呢?
Java API提供了Class.getGenericSuperClass()
方法,来取出一个Type
类型的实例。
如果直接父类的实际类型就是泛型类型的话,那取出的Type
类型实例就可以被显示地转换为ParameterizeType
。
(Type
只是一个标记型接口,它里面仅包含一个方法:getTypeName()
。所以取出的实例的实际类型会是ParameterizedTypeImpl
,但不应直接暴露实际类型,应一直暴露Type
接口)。
感谢ParameterizedType
接口,现在我们可以直接调用ParameterizeType.getActualTypeArguments()
取出又一个Type类型实例数组。
父类所有的泛型类型参数都会被包含在这个数组里,并且以被声明的顺序放在数组对应的下标中。
当数组中的类型参数为非泛型类型时,我们就可以简单地把它显示转换为Class<?>
。
为了保持文章的简洁性,我们跳过了GenericArrayType
的情况。
现在我们可以使用以上知识编写一个工具类了:
public static Class<?> findSuperClassParameterType(Object instance, Class<?> clazzOfInterest, int parameterIndex) {
Class<?> subClass = instance.getClass();
while (subClass.getSuperclass() != clazzOfInterest) {
subClass = subClass.getSuperclass();
if (subClass == null) throw new IllegalArgumentException();
}
ParameterizedType pt = (ParameterizedType) (subClass.getGenericSuperclass());
return (Class<?>) pt.getActualTypeArguments()[parameterIndex];
}
然而,请注意到
findSuperClassParamerterType(new MyGenericClass<String>(), MyGenericClass.class, 0)
这样调用会抛出IllegalArgumentException
异常。之前说过:泛型信息只有在子类的帮助下才能被取出。然而,MyGenericClass<String>
只是一个拥有泛型参数的类,并不是MyGenericClass.class
的子类。没有显式的子类,就没有地方存储String
类型参数。因此上述调用不可避免地会被Java编译器进行类型擦除。如果你已预见到你的项目中会出现这种情况,也想要避免它,一种良好的编程实践是将MyGenericClass
声明为abstract。
然而,我们还没有解决问题,毕竟我们目前为止还有许多坑没有填。
链式泛型
class MyGenericClass<T> {}
class MyGenericSubClass<U> extends MyGenericClass<U> {}
class MyStringSubSubClass extends MyGenericSubClass<String> {}
如下调用,仍然会抛出异常。
findSuperClassParameterType(new MyStringSubClass(), MyGenericClass.class, 0);
这又是为什么呢?到目前为止我们都在设想:MyGenericClass
的类型参数T
的相关信息会存储在它的直接子类中。那么上述的类继承关系就有以下逻辑:
MyStringSubClass.class
中存储了MyGenericSubClass<U> --> U = String
。
MyGenericSubClass.class
中仅存储了MyGenericClass<T> --> T = U
但U
并不是一个Class
类型,而是TypeVariable
类型的类型变量,如果我们想要解析这种继承关系,就必须解析它们之间所有的依赖关系。代码如下:
public static Class<?> findSubClassParameterType(Object instance, Class<?> classOfInterest, int parameterIndex) {
Map<Type, Type> typeMap = new HashMap<>();
Class<?> instanceClass = instance.getClass();
while (instanceClass.getSuperclass() != classOfInterest) {
extractTypeArguments(typeMap, instanceClass);
instanceClass = instanceClass.getSuperclass();
if (instanceClass == null) throw new IllegalArgumentException();
}
// System.out.println(typeMap);
ParameterizedType pt = (ParameterizedType) instanceClass.getGenericSuperclass();
Type actualType = pt.getActualTypeArguments()[parameterIndex];
if (typeMap.containsKey(actualType)) {
actualType = typeMap.get(actualType);
}
if (actualType instanceof Class) {
return (Class<?>) actualType;
} else {
throw new IllegalArgumentException();
}
}
private static void extractTypeArguments(Map<Type, Type> typeMap, Class<?> clazz) {
Type genericSuperclass = clazz.getGenericSuperclass();
if (!(genericSuperclass instanceof ParameterizedType)) {
return ;
}
ParameterizedType pt = (ParameterizedType) genericSuperclass;
Type[] typeParameters = ((Class<?>) pt.getRawType()).getTypeParameters();
Type[] actualTypeArguments = pt.getActualTypeArguments();
for (int i = 0; i < typeParameters.length; i++) {
if (typeMap.containsKey(actualTypeArguments[i])) {
actualTypeArguments[i] = typeMap.get(actualTypeArguments[i]);
}
typeMap.put(typeParameters[i], actualTypeArguments[i]);
}
}
代码中通过一个map
可以解析所有链式泛型类型的定义。不过仍然不够完美,毕竟MyClass<A, B> extends MyOtherClass<B, A>
也是一种完全合法的子类定义。
嵌套类
好了好了,仍然没有结束:
class MyGenericOuterClass<U> {
public class MyGenericInnerClass<U> { }
}
class MyStringOuterSubClass extends MyGenericOuterClass<String> { }
MyStringOuterSubClass.MyGenericInnerClass inner = new MyStringOuterSubClass().new MyGenericInnerClass();
下面这样调用仍然会失败。
findSuperClassParameterType(inner, MyGenericInnerClass.class, 0);
这种失败几乎是可预见的,我们正试图在MyGenericInnerClass
的对象实例里面寻找MyGenericInnerClass
的泛型信息。就像之前所说,因为MyGenericInnerClass
并没有子类,所以从MyGenericInnerClass.class
中寻找泛型信息是不可能的,毕竟MyGenericInnerClass.class
里面根本就不存在泛型信息。不过在这个例子中,我们检查的是MyStringOuterSubClass
中的非static
内部类: MyGenericInnerClass
的对象实例。那么,MyStringOuterSubClass
是知道它的父类MyGennericOuterClass<U> --> U = String
。当使用反射取出MyGenericInnerClass
中的类型参数时,就必须把这点纳入考量。
现在这件事就变得相当棘手了。
- 为了取出
MyGenericOuterClass
的泛型信息 - 就必须先得到
MyGenericOuterClass.class
这依然可以通过反射取得,Java编译器会在内部类MyGenericInnerClass
中生成一个synthetic-field: this$0
,这个字段可以通过Class.getDeclaredField("this$0")
获取到。
javap -p -v MyGenericOuterClass$MyGenericInnerClass.class
…
…
final cn.local.test.MyGenericOuterClass this$0;
descriptor: Lcn/local/test/MyGenericOuterClass;
flags: ACC_FINAL, ACC_SYNTHETIC
…
既然已经有办法可以获取到MyGenericOuterClass.class
了,那接下来我们似乎可以直接复用之前的扫描逻辑了。
这里需要注意,MyGenericOuterClass<U>
的U
并不等同于<MyGenericInnerClass<U>
的U
。
我们可以做以下推理,MyGenericInnerClass
是可以声明为static
的,这就意味着static
情况下,MyGenericInnerClass
拥有它自己独享的泛型type
命名空间。所以,Java API中所有的TypeVariable
接口实现类,都拥有一个属性叫genericDeclaration
。
如果两个泛型变量被分别定义在不同的类中,那么这两个TypeVariable
类型变量,从genericDeclaration
的定义上来说就是不相等的。
获取嵌套类的泛型的代码
private static Class<?> browseNestedTypes(Object instance, TypeVariable<?> actualType) {
Class<?> instanceClass = instance.getClass();
List<Class<?>> nestedOuterTypes = new LinkedList<Class<?>>();
for (
Class<?> enclosingClass = instanceClass.getEnclosingClass();
enclosingClass != null;
enclosingClass = enclosingClass.getEnclosingClass() ) {
try {
Field this$0 = instanceClass.getDeclaredField("this$0");
Object outerInstance = this$0.get(instance);
Class<?> outerClass = outerInstance.getClass();
nestedOuterTypes.add(outerClass);
Map<Type, Type> outerTypeMap = new HashMap<>();
extractTypeArguments(outerTypeMap, outerClass);
for (Map.Entry<Type, Type> entry : outerTypeMap.entrySet()) {
if (!(entry.getKey() instanceof TypeVariable)) {
continue;
}
TypeVariable<?> foundType = (TypeVariable<?>) entry.getKey();
if (foundType.getName().equals(actualType.getName())
&& isInnerClass(foundType.getGenericDeclaration(), actualType.getGenericDeclaration())) {
if (entry.getValue() instanceof Class) {
return (Class<?>) entry.getValue();
}
actualType = (TypeVariable<?>) entry.getValue();
}
}
} catch (NoSuchFieldException e) {
/* however, this should never happen. */
} catch (IllegalAccessException e) {
/* this might happen */
}
}
throw new IllegalArgumentException();
}
private static boolean isInnerClass(GenericDeclaration outerDeclaration, GenericDeclaration innerDeclaration) {
if (!(outerDeclaration instanceof Class) || !(innerDeclaration instanceof Class)) {
throw new IllegalArgumentException();
}
Class<?> outerClass = (Class<?>) outerDeclaration;
Class<?> innerClass = (Class<?>) innerDeclaration;
while ((innerClass = innerClass.getEnclosingClass()) != null) {
if (innerClass == outerClass) {
return true;
}
}
return false;
}
private static void extractTypeArguments(Map<Type, Type> typeMap, Class<?> clazz) {
Type genericSuperclass = clazz.getGenericSuperclass();
if (!(genericSuperclass instanceof ParameterizedType)) {
return;
}
ParameterizedType pt = (ParameterizedType) genericSuperclass;
Type[] typeParameters = ((Class<?>) pt.getRawType()).getTypeParameters();
Type[] actualTypeArguments = pt.getActualTypeArguments();
for (int i = 0; i < typeParameters.length; i++) {
if (typeMap.containsKey(actualTypeArguments[i])) {
actualTypeArguments[i] = typeMap.get(actualTypeArguments[i]);
}
typeMap.put(typeParameters[i], actualTypeArguments[i]);
}
}
文章已收录至https://lichong.work,转载请注明原文链接。
ps:欢迎关注公众号“Fun肆编程”或添加我的私人微信交流经验🤝
【Docker】入门教程-基本概念解读
【前端-开发环境】使用NVM实现不同nodejs版本的自由切换(NVM完整安装使用手册)
【前端-NPM私服】内网使用verdaccio搭建私有npm服务器
【前端-IE兼容】Win10和Win11使用Edge调试前端兼容IE6、IE7、IE8、IE9、IE10、IE11问题
【前端-工程化】React项目工程化记录-内置项目活文档(老项目升级优化-集成Hosky/ESLint/Prettier-升级Webpack/Babel/NodeSass/React)
【工具-TWRP-frp-Termux】旧手机暴改成免费云服务器-MIUI刷TWRP安装magisk获取root
【工具-Shell脚本】java程序产品包模板-linux和windows通用shell启动停止脚本(无需系统安装Java运行环境)
【工具-Nginx】从入门安装到高可用集群搭建
【工具-Nginx】Nginx高性能通用配置文件-注释版-支持防刷限流、可控高并发、HTTP2、防XSS、Gzip、OCSP Stapling、负载、SSL
【工具-WireShark】网络HTTP抓包使用教程
【后端-maven打包】通过profile标签解决同时打jar包 war包需求
【架构-DDD】使用领域驱动设计-互联网未来架构设计之道(一)
【后端-SpringCache】基于Spring Cache封装一个能够批量操作的Redis缓存记录下踩坑历程(pipeline或mget封装)
【后端-SkyWalking】SkyWalking前后端开发环境搭建详细教程步骤-6.x/7.x/8.x版本通用-插件二次开发利器(一)
【后端-Quartz】Springboot整合Quartz支持集群环境-设计业务与框架分离及实现定时任务调度
✨欢迎为耿直少年点赞、关注、收藏!!!
👇👇👇