Java的泛型详解(一)

Wesley13
• 阅读 809

Java的泛型详解(一)

编写的代码可以被不同类型的对象所重用。
因为上面的一个优点,泛型也可以减少代码的编写。
1|2泛型的使用
简单泛型类
public class Pair { private T first; private T second; public Pair() { first = null; second = null; } public Pair(T first, T second){ this.first = first; this.second = second; } public T getFirst(){ return first; } public T getSecond(){ return second; } public void setFirst(T first) { this.first = first; } public void setSecond(T second) { this.second = second; } }
上面例子可以看出泛型变量为T;
用尖括号(<>)括起来,并放在类名后面;
泛型还可以定义多个类型变量比如上面的例子 first和second不同的类型:
public class Pair {....}
注: 类型变量的定义需要一定的规范:
(1) 类型变量使用大写形式,并且要比较短;
(2)常见的类型变量特别代表一些意义:变量E 表示集合类型,K和V表示关键字和值的类型;T、U、S表示任意类型;

类定义的类型变量可以作为方法的返回类型或者局部变量的类型;
例如: private T first;

用具体的类型替换类型变量就可以实例化泛型类型;
例如: Pair 代表将上述所有的T 都替换成了String
由此可见泛型类是可以看作普通类的工厂
泛型方法
我们应该如何定义一个泛型方法呢?
泛型的方法可以定义在泛型类,也可以定义在普通类,那如果定义在普通类需要有一个尖括号加类型来指定这个泛型方法具体的类型;
public class TestUtils { public static T getMiddle(T... a){ return a[a.length / 2]; } }
类型变量放在修饰符(static)和返回类型的中间;
当你调用上面的方法的时候只需要在方法名前面的尖括号放入具体的类型即可;
String middle = TestUtils.getMiddle("a", "b", "c");
如果上图这种情况其实可以省略,因为编译器能够推断出调用的方法一定是String,所以下面这种调用也是可以的;

String middle = TestUtils.getMiddle("a", "b", "c");
但是如果是以下调用可能会有问题:

如图:可以看到变意思没有办法确定这里的类型,因为此时我们入参传递了一个Double3.14 两个Integer1729 和0 编译器认为这三个不属于同一个类型;
此时有一种解决办法就是把整型写成Double类型

类型变量的限定
有时候我们不能无限制的让使用者传递任意的类型,我们需要对我们泛型的方法进行限定传递变量,比如如下例子
计算数组中最下的元素

这个时候是无法编译通过的,且编译器会报错
因为我们的编译器不能确定你这个T 类型是否有compareTo这个函数,所以这么能让编译器相信我们这个T是一定会有compareTo呢?
我们可以这么写 这里的意思是T一定是继承Comparable的类
因为Comparable是一定有compareTo这个方法,所以T一定有compareTo方法,于是编译器就不会报错了
因为加了限定那么min这个方法也只有继承了Comparable的类才可以调用;
如果要限定方法的泛型继承多个类可以加extends 关键字并用&分割如:T extends Comparable & Serializable
限定类型是用&分割的,逗号来分割多个类型变量
1|3类型擦除
不论什么时候定义一个泛型类型,虚拟机都会提供一个相应的原始类型(raw type)。原始类型的名字就是删掉类型参数后的泛型类型。擦除类型变量,并替换限定类型(没有限定类型的变量用Object)

列如: Pair 的原始类型如下所示

public class Pair { private Object first; private Object second; public Pair() { first = null; second = null; } public Pair(Object first, Object second){ this.first = first; this.second = second; } public Object getFirst(){ return first; } public Object getSecond(){ return second; } public void setFirst(Object first) { this.first = first; } public void setSecond(Object second) { this.second = second; } }
因为上面的T是没有限定变量,于是用Object代替了;
如果有限定变量则会以第一个限定变量替换为原始类型如:
public class Interval implements Serializable{ private T lower; private T upper; }
原始类型如下所示:
public class Interval implements Serializable{ private Comparable lower; private Comparable upper; }
翻译泛型表达式
上面说到泛型擦除类型变量后对于无限定变量后会以Object来替换泛型类型变量;
但是我们使用的时候并不需要进行强制类型转换;
原因是编译器已经强制插入类型转换;
例如:

Pair buddies = ...; Employee buddy = buddies.getFirst();
擦除getFirst的返回类型后将返回Object类型,但是编译器自动插入Employee的强制类型转换,编译器会把这个方法调用翻译为两条虚拟机指令;
对原始方法Pair.getFirst的调用
将返回的Object类型强制转换为Employee类型;
我们可以反编译验证一下

关键的字节码有以下两条
9: invokevirtual #4 // Method com/canglang/Pair.getFirst:()Ljava/lang/Object;
12: checkcast #5 // class com/canglang/model/Employee

虚拟机指令含义如下:

invokevirtual:虚函数调用,调用对象的实例方法,根据对象的实际类型进行派发,支持多态;
checkcast:用于检查类型强制转换是否可以进行。如果可以进行,checkcast指令不会改变操作数栈,否则它会抛出ClassCastException异常;
由此我们可以验证了上述的结论,在反编译后的字节码中看到,当对泛型表达式调用时,虚拟机操作如下:

对于对象的实际类型进行替换泛型;
检查类型是否可以强制转换,如果可以将对返回的类型进行强制转换;
翻译泛型方法
类型擦除也会出现在泛型方法里面

public static T min(T[] a)
类型擦除后

public static Comparable min(Comparable[] a)
此时可以看到类型参数T已经被擦除了,只剩下限定类型Comparable;
方法的类型擦除带来了两个复杂的问题,看下面的示例:

public class DateInterval extends Pair { public void setSecond(LocalDate second){ System.out.println("DateInterval: 进来这里了!"); } }
此时有个问题,从Pair继承的setSecond方法类型擦除后为

public void setSecond(Object second)
这个和DateInterval的setSecond明显是两个不同的方法,因为他们有不同的类型的参数,一个是Object,一个LocalDate;
那么看下面一个列子

public class Test { public static void main(String[] args) { DateInterval interval = new DateInterval(); Pair pair = interval; pair.setSecond(LocalDate.of(2020, 5, 20)); } }
Pair引用了DateInterval对象,所以应该调用DateInterval.setSecond。
我们看一下运行结果

但是看了反编译的字节码可能发现一个问题:
17: invokestatic #4 // Method java/time/LocalDate.of:(III)Ljava/time/LocalDate;
20: invokevirtual #5 // Method com/canglang/Pair.setSecond:(Ljava/lang/Object;)V
这里可以看到此处字节码调用的是Pair.setSecond

这里有个重要的概念就是桥方法

引用Oracle中对于这个现象的解释
为了解决此问题并在类型擦除后保留通用类型的 多态性,
Java编译器生成了一个桥接方法,以确保子类型能够按预期工作。
对于DateInterval类,编译器为setSecond生成以下桥接方法:

public class DateInterval extends Pair { // Bridge method generated by the compiler // public void setSecond(Object second) { setSecond((LocalDate)second); } public void setSecond(LocalDate second){ System.out.println("DateInterval: 进来这里了!"); } }
那么我们如何验证是否生成这个桥方法呢?我们可以反编译一下DateInterval.java看一下字节码;

public void setSecond(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: checkcast #5 // class java/time/LocalDate
5: invokevirtual #6 // Method setSecond:(Ljava/time/LocalDate;)V
8: return
我截取了部分发现在 DateInterval的字节码中的确会有一个桥方法,同时验证了上面的问题;

总结:

虚拟机中没有泛型,只有普通的类和方法
所有的类型参数都用他们的限定类型替换
桥方法被合成来保持多态
为保持类型安全性,必要时插入强制类型转换

EOF

本文作者:白羽流光
本文链接:https://www.cnblogs.com/yantt/p/12924906.html

点赞
收藏
评论区
推荐文章
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
浪人 浪人
3年前
死磕Java泛型(一篇就够)
Java泛型,算是一个比较容易产生误解的知识点,因为Java的泛型基于擦除实现,在使用Java泛型时,往往会受到泛型实现机制的限制,如果不能深入全面的掌握泛型知识,就不能较好的驾驭使用泛型,同时在阅读开源项目时也会处处碰壁,这一篇就带大家全面深入的死磕Java泛型。泛型擦除初探相信泛型大家都使用过,所以一些基础的知识点就不废话了,以免显得啰嗦。
Wesley13 Wesley13
3年前
java 泛型详解
对java的泛型特性的了解仅限于表面的浅浅一层,直到在学习设计模式时发现有不了解的用法,才想起详细的记录一下。本文参考java泛型详解、Java中的泛型方法、java泛型详解1\.概述泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。什么是泛型?为什么要使用泛型?泛型,即“参数化类型”。一提到参数,最熟
浪人 浪人
3年前
java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一
java泛型详解绝对是对泛型方法讲解最详细的,没有之一对java的泛型特性的了解仅限于表面的浅浅一层,直到在学习设计模式时发现有不了解的用法,才想起详细的记录一下。本文参考、、1、概述泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。什么是泛型?
Stella981 Stella981
3年前
Gson通过借助TypeToken获取泛型参数的类型的方法
最近在使用Google的Gson包进行Json和Java对象之间的转化,对于包含泛型的类的序列化和反序列化Gson也提供了很好的支持,感觉有点意思,就花时间研究了一下。由于Java泛型的实现机制,使用了泛型的代码在运行期间相关的泛型参数的类型会被擦除,我们无法在运行期间获知泛型参数的具体类型(所有的泛型类型在运行时都是Object类型)。但是有的时候
Wesley13 Wesley13
3年前
Java泛型详解
引言Java泛型是jdk1.5中引入的一个新特性,泛型提供了编译时的类型检测机制,该机制允许程序员在编译时检测到非法的类型。泛型是Java中一个非常重要的知识点,在Java集合类框架中泛型被广泛应用。本文我们将从零开始来看一下Java泛型的设计,将会涉及到通配符处理,以及让人苦恼的类型擦除。泛型基础
Easter79 Easter79
3年前
Thinking in java Chapter15 泛型
1与C比较2简单泛型泛型类3泛型接口4泛型方法5匿名内部类6构建复杂模型78910“泛型”意思就是:适用于许多许多的类型<h2id"1"1与C比较</h2C
Stella981 Stella981
3年前
20175209 《Java程序设计》第八周学习总结
20175209《Java程序设计》第八周学习总结一、教材知识点总结1.泛型1.泛型类声明:格式classPeople<EPeople是泛型类名称E是泛型列表,可以是任何对象或接口,但不能是基本类型数据
Wesley13 Wesley13
3年前
Java泛型一览笔录
1、什么是泛型?泛型(Generics)是把类型参数化,运用于类、接口、方法中,可以通过执行泛型类型调用分配一个类型,将用分配的具体类型替换泛型类型。然后,所分配的类型将用于限制容器内使用的值,这样就无需进行类型转换,还可以在编译时提供更强的类型检查。2、泛型有什么用?泛型主要有两个好处:(1)消除显
可莉 可莉
3年前
20175209 《Java程序设计》第八周学习总结
20175209《Java程序设计》第八周学习总结一、教材知识点总结1.泛型1.泛型类声明:格式classPeople<EPeople是泛型类名称E是泛型列表,可以是任何对象或接口,但不能是基本类型数据