当前位置: 首页 > 编程日记 > 正文

Java泛型进阶 - 如何取出泛型类型参数

在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区域的这块泛型信息呢?

  1. Java API提供了Class.getGenericSuperClass()方法,来取出一个Type类型的实例
  2. 如果直接父类的实际类型就是泛型类型的话,那取出的Type类型实例就可以被显示地转换为ParameterizeType

    (Type只是一个标记型接口,它里面仅包含一个方法:getTypeName()。所以取出的实例的实际类型会是ParameterizedTypeImpl,但不应直接暴露实际类型,应一直暴露Type接口)。
  3. 感谢ParameterizedType接口,现在我们可以直接调用ParameterizeType.getActualTypeArguments()取出又一个Type类型实例数组
  4. 父类所有的泛型类型参数都会被包含在这个数组里,并且以被声明的顺序放在数组对应的下标中。
  5. 当数组中的类型参数为非泛型类型时,我们就可以简单地把它显示转换为Class<?>

    为了保持文章的简洁性,我们跳过了GenericArrayType的情况。

clipboard.png

现在我们可以使用以上知识编写一个工具类了:

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];
}public static void testCase1() {Class<?> genericType = findDirectSuperClassParameterType(new MyStringSubClass());System.out.println(genericType);assert genericType == String.class;
}

然而,请注意到

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);

clipboard.png

这又是为什么呢?到目前为止我们都在设想:MyGenericClass的类型参数T的相关信息会存储在它的直接子类中。那么上述的类继承关系就有以下逻辑:

  1. MyStringSubClass.class中存储了MyGenericSubClass<U> --> U = String
  2. 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
clipboard.png
clipboard.png
如果两个泛型变量被分别定义在不同的类中,那么这两个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]);}
}

相关文章:

C++11中override的使用

override是C11中的一个继承控制关键字。override确保在派生类中声明的重载函数跟基类的虚函数有相同的声明。 override明确地表示一个函数是对基类中一个虚函数的重载。更重要的是&#xff0c;它会检查基类虚函数和派生类中重载函数的签名不匹配问题。如果签名不匹配&#xff…

平头哥发布一站式芯片设计平台“无剑”,芯片设计成本降低50%

导读&#xff1a;8 月 29 日&#xff0c;在上海举行的世界人工智能大会上&#xff0c;阿里巴巴旗下半导体公司平头哥发布 SoC 芯片平台“无剑”。无剑是面向 AIoT 时代的一站式芯片设计平台&#xff0c;提供集芯片架构、基础软件、算法与开发工具于一体的整体解决方案&#xff…

Windows XP下,JDK环境变量配置

2019独角兽企业重金招聘Python工程师标准>>> 1.安装JDK&#xff0c;安装过程中可以自定义安装目录等信息&#xff0c;例如我们选择安装目录为D:\java\jdk1.5.0_08&#xff1b; 2.安装完成后&#xff0c;右击“我的电脑”&#xff0c;点击“属性”&#xff1b; 3.选择…

Markdown语法简介

Markdown是一种方便记忆、书写的纯文本标记语言&#xff0c;用户可以使用这些标记符号以最小的输入代价生成极富表现力的文档。它目标是实现易读易写。Markdown的语法全由一些符号所组成。Markdown语法的目标是成为一种适用于网络的书写语言。 Markdown优点&#xff1a;纯文本…

吴恩达:AI未来将呈现四大发展趋势

作者 | 夕颜出品 | AI科技大本营&#xff08;ID:rgznai100&#xff09;导读&#xff1a;8 月 30 日&#xff0c;世界人工智能大会精彩继续。在今天的全球工业智能峰会上&#xff0c;Landing.AI 创始人及首席执行官吴恩达来到现场&#xff0c;做了题为《人工智能是新电力》的演讲…

嵌入式课程安排 嵌入式培训课程大纲参考

嵌入式是一门综合性的学科&#xff0c;现在学习嵌入式开发不是单纯局限于单片机或者Linux&#xff0c;嵌入式课程中包含着非常多的内容。以粤嵌嵌入式课程进行参考&#xff0c;看看我们要学习嵌入式的话&#xff0c;要掌握哪些必备的技能。嵌入式课程安排包含&#xff1a;1、入…

Linux网站架构系列之Apache----进阶篇

本篇博文为Linux网站架构系列之apache的第二篇&#xff0c;我将带大家一起学习apache的编译参数&#xff0c;目录结构和配置文件等方面的知识&#xff0c;实现对apache服务的进一步掌握&#xff0c;并使之能更好的应用到生产实战中去。一、编译参数在上篇的apache部署中&#x…

仅用10天设计的JavaScript,凭什么成为程序员最受欢迎的编程语言?

导语&#xff1a;在这个世纪之交诞生的 JavaScript&#xff0c;没人想到会发展为当今世界上最流行的语言之一。它不够成熟&#xff0c;不够严肃&#xff0c;甚至连名字都是模仿的 Java。那么&#xff0c;JavaScript 的成功是依靠运气和完美时机的侥幸吗&#xff1f;其实不然——…

C++11中= delete;的使用

C11中&#xff0c;对于deleted函数&#xff0c;编译器会对其禁用&#xff0c;从而避免某些非法的函数调用或者类型转换&#xff0c;从而提高代码的安全性。 对于 C 的类&#xff0c;如果程序员没有为其定义特殊成员函数&#xff0c;那么在需要用到某个特殊成员函数的时候&…

vue 使用scss

使用vue-cli模板创建的项目中&#xff0c;使用scss步骤 1. cmd命令&#xff1a; cnpm install sass-loader --save-devcnpm install node-sass --sava-dev2.查看package.json文件中是否已自动添加以下信息 3. 转载于:https://www.cnblogs.com/duanzhenzhen/p/10453495.html

EBS form日历可选范围设置(calendar.setup )介绍

Calendar是Template提供给我们的standard object.可以使我们方便的为日期型字段提供日期的选择列表.form中设置日历方法:1. 为日期型字段指定LOV(ENABLE_LIST_LAMP)2. 在字段的KEY–LISTVAL事件中编写代码:Calendar.showCalendar Package包含如下几个Procedure:1. Calendar.sho…

人工智能对地球环境科学的推进

一项德国耶拿[1]和汉堡[2]科学家在《自然》杂志发起的研究表明&#xff0c;人工智能可以有效地推进我们对于地球气候系统的理解。特别是在当前深度学习的潜力还未被完全开发的情况下。在人工智能的帮助下一些复杂的动态环境&#xff0c;如飓风&#xff0c;森林火灾&#xff0c;…

从概念到应用,终于有人把数据挖掘讲明白了

作者&#xff1a;陈封能&#xff08;Pang-Ning Tan&#xff09;、迈克尔斯坦巴赫&#xff08;Michael Steinbach&#xff09;等来源 | 大数据&#xff08;ID&#xff1a; hzdashuju&#xff09;【导语】数据采集和存储技术的迅速发展&#xff0c;加之数据生成与传播的便捷性&am…

C++11中default的使用

在C11中&#xff0c;对于defaulted函数&#xff0c;编译器会为其自动生成默认的函数定义体&#xff0c;从而获得更高的代码执行效率&#xff0c;也可免除程序员手动定义该函数的工作量。 C的类有四类特殊成员函数&#xff0c;它们分别是&#xff1a;默认构造函数、析构函数、拷…

Android开发:setAlpha()方法和常用RGB颜色表----颜色, r g b分量数值(int), 16进制表示 一一对应...

杂家前文Android颜色对照表只有颜色和十六进制&#xff0c;有时候需要设置r g b分量的int值&#xff0c;如paint.setARGB(255, 127, 255, 212);就需要自己计算下分量的各个值。这里提供一个带有r g b分量的int型的颜色表。注意paint.setAlpha()及paint.setARGB&#xff08;&…

【redis】c/c++操作redis(对于hiredis的封装)

前言 最近一直在学习redis&#xff0c;通过c/cpp来执行redis命令&#xff0c;使用的是hiredis客户端来实现的。 先简单贴一下代码 头文件 #include <vector> #include <string> #include <hiredis/hiredis.h> typedef enum en_redisResultType {redis_reply_…

OpenCV代码提取:transpose函数的实现

OpenCV中的transpose函数实现图像转置&#xff0c;公式为&#xff1a;目前fbc_cv库中也实现了transpose函数&#xff0c;支持多通道&#xff0c;uchar和float两种数据类型&#xff0c;经测试&#xff0c;与OpenCV3.1结果完全一致。实现代码transpose.hpp&#xff1a;// fbc_cv …

只给测试集不给训练集,要怎么做自己的物体检测器?

9 月5 日&#xff0c;下周四&#xff0c;大家期待已久的由《动手学深度学习》作者&#xff0c;亚马逊首席科学家亲自带领的「深度学习实训营」就要在北京开营了。今天&#xff0c;李沐已经把这次深度学习实训营白天的教学内容和代码上传到 Gituhub 和 D2L.ai 网站了&#xff0c…

MYSQL忘记登录密码

1、关闭Mysql&#xff1a; 如果 MySQL 正在运行&#xff0c;首先杀之 killall -TERM mysqld 2、另外的方法启动 MySQL &#xff1a;bin/safe_mysqld --skip-grant-tables 3、可以不需要密码就进入 MySQL 了。 然后就是 >use mysql>update user set passwordpassword(&qu…

OpenCV代码提取:flip函数的实现

OpenCV中实现图像翻转的函数flip&#xff0c;公式为&#xff1a;目前fbc_cv库中也实现了flip函数&#xff0c;支持多通道&#xff0c;uchar和float两种数据类型&#xff0c;经测试&#xff0c;与OpenCV3.1结果完全一致。实现代码flip.hpp&#xff1a;// fbc_cv is free softwar…

NLP这两年:15个预训练模型对比分析与剖析

作者 | JayLou来源 | 知乎前言在之前写过的《NLP的游戏规则从此改写&#xff1f;从word2vec, ELMo到BERT》一文中&#xff0c;介绍了从word2vec到ELMo再到BERT的发展路径。而在BERT出现之后的这大半年的时间里&#xff0c;模型预训练的方法又被Google、Facebook、微软、百度、O…

大三下学期第一周总结

本周以是开学第一周了&#xff0c;在生活方面&#xff0c;生活琐事确实变多了起来。每天上课&#xff0c;看着老师熟悉的面庞&#xff0c;如履春风。感觉学习没有那么多的陌生恐惧。学习是一方面&#xff0c;身体锻炼不能落下。一周至少保证三小时及其以上的运动。身体是革命的…

AD rodc扩展报错

AD rodc扩展报错AD RODC抢夺FSMO五大角色后&#xff0c;架构扩展报错&#xff0c;解决办法参考链接&#xff1a;http://support.microsoft.com/kb/949257/en-us

Dropout、梯度消失/爆炸、Adam优化算法,神经网络优化算法看这一篇就够了

作者 | mantch来源 | 知乎1. 训练误差和泛化误差对于机器学习模型在训练数据集和测试数据集上的表现。如果你改变过实验中的模型结构或者超参数&#xff0c;你也许发现了&#xff1a;当模型在训练数据集上更准确时&#xff0c;它在测试数据集上却不⼀定更准确。这是为什么呢&am…

入行AI,你需要一本Python机器学习入门

​目前机器学习红遍全球。男女老少都在学机器学习模型&#xff0c;分类器&#xff0c;神经网络和吴恩达。你也想成为一份子&#xff0c;但你该如何开始&#xff1f;今天小编推荐这本《Python机器学习》教你快速入门。​01什么是机器学习&#xff1f;从出生的那天起&#xff0c;…

通过transpose和flip实现图像旋转90/180/270度

在fbc_cv库中&#xff0c;提供了对图像进行任意角度旋转的函数rotate&#xff0c;其实内部也是调用了仿射变换函数warpAffine。如果图像仅是进行90度倍数的旋转&#xff0c;是没有必要用warpAffine函数的。这里通过transpose和flip函数实现对图像进行顺时针90度、180度、270度的…

DIY强大的虚拟化环境-技术可行性部分

【技术可行性部分】大体的cpu支不支持呀&#xff0c;实际效果使用呀&#xff0c;截图效果截图嵌套虚拟化[esxi&#xff0c;xenserver&#xff0c;Hyper-V]嵌套虚拟化&#xff1a;经过各种查资料&#xff0c;和测试验证[只测过intel的&#xff0c;amd的有类似的文章请去下面的资…

C++11中rvalue references的使用

Rvalue references are a feature of C that was added with the C11 standard. The syntax of an rvalue reference is to add && after a type.为了支持移动操作&#xff0c;C11引入了一种新的引用类型----右值引用(rvalue reference)。所谓右值引用就是必须绑定到右…

AIの幕后人:探秘“硬核英雄”的超级武器

作者 | 云计算的阿晶 出品 | AI科技大本营&#xff08;ID:rgznai100&#xff09; 掐指一算八年之前&#xff0c;那时正是国内互联网卯足劲头起飞的一年&#xff0c;各行各业表现都很突出&#xff0c;尤其是与人们生活密切相关的手机&#xff0c;正大踏步地从功能机向智能手机转…

PAT乙级1003

1003 我要通过&#xff01; &#xff08;20 point(s)&#xff09;“答案正确”是自动判题系统给出的最令人欢喜的回复。本题属于 PAT 的“答案正确”大派送 —— 只要读入的字符串满足下列条件&#xff0c;系统就输出“答案正确”&#xff0c;否则输出“答案错误”。 得到“答案…