深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上鸿蒙开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
list.add(“aaaa”);
System.out.println(list.get(0)); //list.get(0)得到是 Object,还是 String 呢?
list.add(1);//会报错,为什么呢?
看看字节码一探究竟
注意看图中标注的地方,编译器除了类型擦除,在必要之处,还插入类型转换以保持类型安全(checkCast()
方法)
另外除了类型强转,如果涉及到使用泛型类型作为参数的方法,子类做了重写,编译器还会自动生成桥方法以在扩展时保持多态性(Bridge
关键字)
例如:
//父类
public class DRCommonGeneric {
T t;
public void setT(T t) {
this.t = t;
}
}
//子类继承泛型类,将泛型实例化为String
public class DRString extends DRCommonGeneric {
//同时重写了setT
@Override
public void setT(String s) {
super.setT(s);
}
}
泛型真的全部被擦除了吗?
为啥有如此疑问,是因为泛型实际上是支持反射的!!!
拿上文中的 DRCommonGeneric.setT(T t)
举例,虽然擦除为 object, 但是通过反射还是能拿到 T
这个我们定义的泛型类型,说明 T 这个参数类型也是在的。
try {
Method method = DRCommonGeneric.class.getMethod(“setT”, Object.class);
Type[] types = method.getGenericParameterTypes();
for (Type type : types) {
System.out.println(“参数类型:”+type);
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
输出:
另外如果泛型声明的时候指定了具体的类型,也是可以拿到使用时定义的真实类型。 例如:
// Test.class 中声名了这个变量,T 被明切指定为String
DRCommonGeneric dr = new DRCommonGeneric<>();
//反射获取 Test.class 中的 dr 变量
Field field = Test.class.getDeclaredField(“dr”);
//获取泛型参数真实类型
ParameterizedType pType = (ParameterizedType) field.getGenericType();
System.out.println(pType);
输出:
弱小无助的我,在对天呐喊:不是全部擦除了么?反射那是怎么拿到的!!!反射难道是牛到可以从无到有么?!!!
反射表示这个锅他不背。那咱们还是再仔细看看泛型的字节码实现。
通过搜索,我发现字节码文件中确实有泛型类型 [图片上传失败…(image-70cdd3-1677116394697)]
都是signature
和 declaration
在用。原先看到添加了//
注释,我就给忽略了,看来并非这么简单,再次触及知识盲区,我还是浅薄了啊。
截图中它放入注释中,实际上是 IDE 工具的优化,目的方便集中在 code 上去看了,实际上flags/signature都是属于元数据
。(换个“反汇编”工具发现的这一点…),declaration 没有查询到,应该也是这个IDE的自我发挥,不用管,主要核心还是看 signature 的逻辑。
signature 是什么
在JVM中,所有的字段、方法、类都有属于自己的签名。比如方法签名由方法名、参数、访问修饰符等构成,用于确定唯一的方法。而类的签名主要是记录一些JVM类型系统以外的额外的类型信息,比如泛型的类型信息,JVM不支持泛型,但是提供了class signature来存储类泛型的类型信息。
在代码层面,实际上它是个属性,
JDK的源码中可以找到相关证据
- java.lang.reflect.Field
- java.lang.reflect.Method
- java.lang.reflect.Constructor
其中都有一个成员变量 signature
,注释都写的一摸一样,支持泛型和注解。
// Generics and annotations support
private transient String signature;
可以看到 signature
就是个字符串,所以不同的签名有一定的解析规则,主要的逻辑在 sun.reflect.generics.parser.SignatureParser
类中。
签名的实现有两个,分别是方法签名MethodTypeSignature
和类签名ClassSignature
,变量虽然也有签名,但是相对来说比较简单,没有专门的解析类。
为了方便理解,咱们还用上文例子,进行介绍
方法签名: 方法声明:public void setT(T t) 签名字符串:(TT;)V
解析出来分为四个部分:
- 泛型定义:无 (泛型方法才会有)
- 参数类型: T
- 返回值类型:V 无返回值
- 异常类型:无
类签名: 类声明:public class DRCommonGeneric 签名字符串:<T:Ljava/lang/Object;>Ljava/lang/Object; 解析出来分为三个部分:
- 泛型定义:<T:Ljava/lang/Object;>
- 继承的类:无
- 实现的借口:无
签名的创建和保存?
编译时就会生成签名。 这个就是例子的 class signature :
保存在常量池中
签名的解析时机?
咱们通过反射调用泛型有关的API时,例如 method.getGenericParameterTypes()
就会触发到签名的解析,以拿到正确的参数类型。
例如反射调用setT(T t)
方法, 主要流程就是:
- 通过
hasGenericInformation
判断方法签名中(TT;)V
是否有存在泛型 - 通过
getGenericInfo()
解析方法签名字符串,填充到对应的变量tree
中 。 - 在
computeParameterTypes
方法中,将解析好的方法签名tree
,通过getParameterTypes
获取到方法参数类型数组,也就是 [T
]。 - 此时还不知道
T
的实现类是什么,所以再通过解析类签名<T:Ljava/lang/Object;>Ljava/lang/Object;
,获取T
的实际类型java/lang/Object
, - 放入parameterTypes 数组中返回
以上就是T
这个类型参数的保存和解释,上面的例子中其实还举例了 DRCommonGeneric<String>
反射可以拿到String
而不是 Object
。也是一样的道理,将泛型信息放入了 signature
中,从而得到了保存。
GSON 针对类型擦除的处理设计
著名的json解析工具 Gson,在反序列化的时候,也是使用同样的原理来支持泛型的序列化。
例如泛序列化List<T>
这类泛型集合:
List resultList = gson.fromJson(json, new TypeToken<List>() {
}.getType());
new TypeToken<List<xxx>>() {}.getType()
这行代码,重点是要注意到大括号 {}
,这是说明它不是一个实例化了一个对象,而是创建了一个匿名内部类,而且继承于TypeToken
TypeToken 内部也是使用泛型反射的API,区别在于使用了getClass().getGenericSuperclass()
,先获得带有泛型的父类,然后再去拿泛型。
这里特殊设计的地方是使用了匿名内部类的方式,为什么要这么设计呢?
目的是为了实现完整的泛型声明。
- 使用匿名内部类实现:
TypeToken<List<xxx>> typeToken = new TypeToken的匿名子类();
声明了泛型 - 无匿名内部类实现:
new TypeToken<List<xxx>>
未声明泛型
需要明确的是,编译器只会将声明类型的泛型信息进行保存,放在 signature 属性中。
//这是一个完整的泛型使用方式
List list = new ArrayList<>();
相对比而言,下面就没有声明泛型,用的是原始类型List,编译器不认为这个是个泛型需要特别处理
//左侧没有声明泛型类型
List list = new ArrayList();
字节码验证如下:
是不是又get到了一些小细节~
总结
类型擦除的工作过程为:
- 检查泛型类型,获取目标类型
- 擦除类型变量,并替换为限定类型
- 如果泛型类型的类型变量没有限定(),则用Object作为原始类型
- 如果有限定(),则用XClass作为原始类型
- 如果有多个限定(T extends XClass1&XClass2),则使用第一个边界XClass1作为原始类
- 在必要时,插入类型转换以保持类型安全
- 在必要时,为泛型类的子类,生成桥方法以在扩展时保持多态性
- 编译字节码中新增了Signature属性记录泛型信息,保存在常量池中,来解决泛型参数识别问题,所以通过反射手段可以获取到参数化类型
另外需要注意,编译器只会将声明类型的泛型信息进行保存。开源工具 Gson 处理泛型识别的时候借助匿名内部类的方式解决了这个细节问题。
最后的面经分享
泛型是日常开发中经常接触到的知识,所以算是面试必问题吧。而且有些大厂考的很细(特别是字节)。 一面的时候会通过代码改错的方式,考虑你对泛型的理解,避免你照本宣科背书。
例如:
//编辑报错
Set set = new HashSet()
//应该明确泛型的实现类型
Set set = new HashSet<>()
List strList = new ArrayList<>()
List list= new ArrayList<>()
//说出result的值
boolean result = strList.getClass() == list.getClass()
//实际上类型擦除后,都是List
//就算问反射获取List内部的 elementData ,结果也是一样,因为都被擦除为 java.lang.Object
以上比较经典的面试题,当然还有其他的变种,等我看到值得参考的再补充吧,其实呢,原理都是一样的。咱们只有真正理解了,才能融会贯通,举一反三,任它东南西北风。
与君共勉~
参考资料
如何理解ByteCode、IL、汇编等底层语言与上层语言的对应关系?
java里JSON使用中TypeToken为什么要用匿名内部类创建的真正原因(泛型擦除)
作者:段浅浅儿
链接:https://juejin.cn/post/7202560190750294075
最后
如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。
如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。
深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上鸿蒙开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
[外链图片转存中…(img-lHVBx5Z8-1715295545178)]
[外链图片转存中…(img-6vGJRa2D-1715295545179)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上鸿蒙开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新