Java 中泛型的实现原理
泛型是 Java 开发中常用的技术,了解泛型的几种形式和实现泛型的基本原理,有助于写出更优质的代码。本文总结了 Java 泛型的三种形式以及泛型实现原理。
泛型
泛型的本质是对类型进行参数化,在代码逻辑不关注具体的数据类型时使用。例如:实现一个通用的排序算法,此时关注的是算法本身,而非排序的对象的类型。
泛型方法
如下定义了一个泛型方法, 声明了一个类型变量,它可以应用于参数,返回值,和方法内的代码逻辑。
class GenericMethod{
public <T> T[] sort(T[] elements){
return elements;
}
}
泛型类
与泛型方法类似,泛型类也需要声明类型变量,只不过位置放在了类名后面,作用的范围包括了当前中的成员变量类型,方法参数类型,方法返回类型,以及方法内的代码中。
子类继承泛型类时或者实例化泛型类的对象时,需要指定具体的参数类型或者声明一个参数变量。如下,SubGenericClass 继承了泛型类 GenericClass,其中类型变量 ID 的值为 Integer,同时子类声明了另一个类型变量 E,并将E 填入了父类声明的 T 中。
class GenericClass<ID, T>{
}
class SubGenericClass<T> extends GenericClass<Integer, T>{
}
泛型接口
泛型接口与泛型类类似,也需要在接口名后面声明类型变量,作用于接口中的抽象方法返回类型和参数类型。子类在实现泛型接口时需要填入具体的数据类型或者填入子类声明的类型变量。
interface GenericInterface<T> {
T append(T seg);
}
泛型的基本原理
泛型本质是将数据类型参数化,它通过擦除的方式来实现。声明了泛型的 .java 源代码,在编译生成 .class 文件之后,泛型相关的信息就消失了。可以认为,源代码中泛型相关的信息,就是提供给编译器用的。泛型信息对 Java 编译器可以见,对 Java 虚拟机不可见。
Java 编译器通过如下方式实现擦除:
- 用 Object 或者界定类型替代泛型,产生的字节码中只包含了原始的类,接口和方法;
- 在恰当的位置插入强制转换代码来确保类型安全;
- 在继承了泛型类或接口的类中插入桥接方法来保留多态性。
Java 官方文档原文
- Replace all type parameters in generic types with their bounds or Object if the type parameters are unbounded. The produced bytecode, therefore, contains only ordinary classes, interfaces, and methods.
- Insert type casts if necessary to preserve type safety.
- Generate bridge methods to preserve polymorphism in extended generic types.
下面通过具体代码来说明 Java 中的类型擦除。
实验原理:先用 javac 将 .java 文件编译成 .class 文件,再使用反编译工具 jad 将 .class 文件反编成回 Java 代码,反编译出来的 Java 代码内容反映的即为 .class 文件中的信息。
如下源代码,定义 User 类,实现了 Comparable 接口,类型参数填入 User,实现 compareTo 方法。
class User implements Comparable<User> {
String name;
public int compareTo(User other){
return this.name.compareTo(other.name);
}
}
JDK 中 Comparable 接口源码内容如下:
package java.lang;
public interface Comparable<T>{
int compareTo(T o);
}
我们首先反编译它的接口,Comparable 接口的字节码文件,可以在 $JRE_HOME/lib/rt.jar 中找到,将它复制到某个目录。使用 jad.exe(需要另外安装)反编译这个 Comparable.class 文件。
$ jad Comparable.class
反编译出来的内容放在 Comparable.jad 文件中,文件内容如下:
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: Comparable.java
package java.lang;
// Referenced classes of package java.lang:
// Object
public interface Comparable
{
public abstract int compareTo(Object obj);
}
对比源代码 Comparable.java 和反编译代码 Comparable.jad 的内容不难发现,反编译之后的内容中已经没有了类型变量 T 。compareTo 方法中的参数类型 T 也被替换成了 Object。这就符合上面提到的第 1 条擦除原则。这里演示的是用 Object 替换类型参数,使用界定类型替换类型参数的例子可以反编译一下 Collections.class 试试,里面使用了大量的泛型。
使用 javac.exe 将 User.java 编译成 .class 文件,然后使用 jad 将 .class 文件反编译成 Java 代码。
$ javac User.java
$ jad User.class
User.jad 文件内容如下:
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: User.java
class User
implements Comparable
{
User()
{
}
public int compareTo(User user)
{
return name.compareTo(user.name);
}
// 桥接方法
public volatile int compareTo(Object obj)
{
return compareTo((User)obj);
}
String name;
}
对比编辑的源代码 User.java 和反编译出来的代码 User.jad,容易发现:类型参数没有了,多了一个无参构造方法,多了一个 compareTo(Object obj) 方法,这个就是桥接方法,还可以发现参数 obj 被强转成 User 再传入 compareTo(User user) 方法。通过这些内容可以看到擦除规则 2 和规则 3 的实现方式。
强转规则比较好理解,因为泛型被替换成了 Object,要调用具体类型的方法或者成员变量,当然需要先强转成具体类型才能使用。那么插入的桥接方法该如何理解呢?
如果我们只按照下面方式去使用 User 类,这样确实不需要参数类型为 Object 的桥接方法。
User user = new User();
User other = new User();
user.comparetTo(other);
但是,Java 中的多态特性允许我们使用一个父类或者接口的引用指向一个子类对象。
Comparable<User> user = new User();
而按照 Object 替换泛型参数原则,Comparable 接口中只有 compareTo(Object) 方法,假设没有桥接方法,显然如下代码是不能运行的。所以 Java 编译器需要为子类(泛型类的子类或泛型接口的实现类)中使用了泛型的方法额外生成一个桥接方法,通过这个方法来保证 Java 中的多态特性。
Comparable<User> user = new User();
Object other = new User();
user.compareTo(other);
而普通类中的泛型方法在进行类型擦除时不会产生桥接方法。例如:
class Dog{
<T> void eat(T[] food){
}
}
类型擦除之后变成了:
class Dog
{
Dog()
{
}
void eat(Object aobj[])
{
}
}
小结
Java 中的泛型有 3 种形式,泛型方法,泛型类,泛型接口。Java 通过在编译时类型擦除的方式来实现泛型。擦除时使用 Object 或者界定类型替代泛型,同时在要调用具体类型方法或者成员变量的时候插入强转代码,为了保证多态特性,Java 编译器还会为泛型类的子类生成桥接方法。类型信息在编译阶段被擦除之后,程序在运行期间无法获取类型参数所对应的具体类型。
参考
https://docs.oracle.com/javase/tutorial/java/generics/index.html
https://stackoverflow.com/questions/25040837/generics-bridge-method-on-polymorphism
Java 中泛型的实现原理的更多相关文章
- Java中泛型 类型擦除
转自:Java中泛型是类型擦除的 Java 泛型(Generic)的引入加强了参数类型的安全性,减少了类型的转换,但有一点需要注意:Java 的泛型在编译器有效,在运行期被删除,也就是说所有泛型参数类 ...
- 从虚拟机指令执行的角度分析JAVA中多态的实现原理
从虚拟机指令执行的角度分析JAVA中多态的实现原理 前几天突然被一个"家伙"问了几个问题,其中一个是:JAVA中的多态的实现原理是什么? 我一想,这肯定不是从语法的角度来阐释多态吧 ...
- Java中泛型使用
Java中泛型使用 泛型作用: 泛型:集合类添加对象不用强转 反射机制:将泛型固定的类的所有方法和成员全部显示出来 核心代码: ArrayList<Ls> ff=new ArrayList ...
- Java中泛型区别以及泛型擦除详解
一.引言 复习javac的编译过程中的解语法糖的时候看见了泛型擦除中的举例,网上的资料大多比较散各针对性不一,在此做出自己的一些详细且易懂的总结. 二.泛型简介 泛型是JDK 1.5的一项新特性,一种 ...
- Java中泛型在集合框架中的应用
泛型是Java中的一个重要概念,上一篇文章我们说过,当元素存入集合时,集合会将元素转换为Object类型存储,当取出时也是按照Object取出的,所以用get方法取出时,我们会进行强制类型转换,并且通 ...
- java中jvm的工作原理
首先我们安装了jdk和jre,但是jdk是为java软件开发工程师而使用的开发工具,我们运行java项目只要含有jre文件即可.对于jvm是内存分配的一块区域,我们知道,当我们开始使用java命令时, ...
- Java 中泛型的全面解析(转)
Java泛型(generics) 是JDK 5中引入的一个新特性,允许在定义类和接口的时候使用类型参数(type parameter).声明的类型参数在使用时用具体的类型来替换.泛型最主要的应用是在J ...
- Java中泛型的理解
Java中的泛型,本质上来说,就是是参数化类型,就是说所操作的数据类型被指定为一个参数,而不是确定的某种类型.这种数据类型可以用在类.接口和方法创建中.即泛型类.泛型接口.泛型方法.这样说可能不够生动 ...
- Java中泛型数组创建总结
在java中,可以声明一个泛型数组,不能通过直接通过T[] tarr=new T[10]的方式来创建数组,最简单的方式便是通过Array.newInstance(Classtype,int size) ...
随机推荐
- 用STM32定时器测量信号频率——测频法和测周法[原创cnblogs.com/helesheng]
工业测试与控制系统中,经常需要对未知信号的频率进行测试.对于10MHz以下的信号,用单片机(MCU)定时器完成这项任务显然是最常见和最佳的选择.目前性价比最高的单片机STM32拥有功能强大且数量众多的 ...
- flex:align-items和align-content的区别
属性值 align-items的属性值有:baseline.center.flex-end.flex-start.stretch.inherit.initial.unset align-content ...
- web移动端点击穿透问题
在移动端开发的时候,我们有时候会遇到这样一个bug:点击关闭遮罩层的时候,遮罩层下面的带有点击的元素也会被触发,给人一种击穿了页面的感觉,这是为什么呢?主要是因为用户touch事件关闭按钮的时候,触发 ...
- CSP-S 初赛最后的复习
2020CSP-S 模拟赛1 3.一个圆形水池中等概率随机分布着四只鸭子,那么存在一条直径,使得鸭子全在直径一侧的概率是(). A.\(\frac 1{16}\) B.\(\frac 1{8}\) C ...
- AcWing 195. 骑士精神
双向BFS (广搜) \(O(8 ^ 7)\) 看到没有双向BFS的题解我就过来了 这道题也可以用双向\(BFS\)来做,时间复杂度与\(IDA*\)不相上下. 双向\(BFS\)的实现有多种: 把初 ...
- rsync全网备份low方法
要求: 1.基本备份要求已知3 台服务器主机名分别为web01.backup .nfs01,主机信息见下表:服务器说明外网IP(NAT) 内网IP(NAT) 主机名称nginx web 服务器10.0 ...
- 一个最简单的typescript工程
初级: 1.新建一个文件夹~/a/ 2.cd ~/a/ 3.npm init -y 生成package.json 4.新建index.ts,内容:console.log(" ...
- 2020.12.16 模拟赛x+1
A. 接力比赛 跑两遍背包,再进行一些玄学的剪枝 代码 #include<cstdio> #include<algorithm> #define rg register inl ...
- 图的建立以及应用(BFS,DFS,Prim)
关于带权无向图的一些操作 题目:根据图来建立它的邻接矩阵,通过邻接矩阵转化为邻接表,对邻接表进行深度优先访问和广度优先访问,最后用邻接矩阵生成它的最小生成树: 1.输入一个带权无向图(如下面图1和图2 ...
- UWP 自定义RadioButton实现Tab底部导航
先看效果: 参照Android的实现方式用RadioButton来实现,但是Uwp的RadioButton并没有安卓的Selector选择器 下面是一个比较简单的实现,如果有同学有更好的实现,欢迎留言 ...