Java的泛型详解(一)
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
Java的泛型详解(⼀)
Java的泛型详解
泛型的好处
编写的代码可以被不同类型的对象所重⽤。
因为上⾯的⼀个优点,泛型也可以减少代码的编写。
泛型的使⽤
简单泛型类
public class Pair<T> {
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<T, U> {....}
注: 类型变量的定义需要⼀定的规范:
(1) 类型变量使⽤⼤写形式,并且要⽐较短;
(2)常见的类型变量特别代表⼀些意义:变量E 表⽰集合类型,K和V表⽰关键字和值的类型;T、U、S表⽰任意类型;
类定义的类型变量可以作为⽅法的返回类型或者局部变量的类型;
例如: private T first;
⽤具体的类型替换类型变量就可以实例化泛型类型;
例如: Pair<String> 代表将上述所有的T 都替换成了String
由此可见泛型类是可以看作普通类的⼯⼚
泛型⽅法
我们应该如何定义⼀个泛型⽅法呢?
泛型的⽅法可以定义在泛型类,也可以定义在普通类,那如果定义在普通类需要有⼀个尖括号加类型来指定这个泛型⽅法具体的类型;
public class TestUtils {
public static <T> T getMiddle(T... a){
return a[a.length / 2];
}
}
类型变量放在修饰符(static)和返回类型的中间;
当你调⽤上⾯的⽅法的时候只需要在⽅法名前⾯的尖括号放⼊具体的类型即可;
String middle = TestUtils.<String>getMiddle("a", "b", "c");
如果上图这种情况其实可以省略,因为编译器能够推断出调⽤的⽅法⼀定是String,所以下⾯这种调⽤也是可以的;
String middle = TestUtils.getMiddle("a", "b", "c");
但是如果是以下调⽤可能会有问题:
如图:可以看到变意思没有办法确定这⾥的类型,因为此时我们⼊参传递了⼀个Double3.14 两个Integer1729 和0 编译器认为这三个不属于同⼀个类型;此时有⼀种解决办法就是把整型写成Double类型
类型变量的限定
有时候我们不能⽆限制的让使⽤者传递任意的类型,我们需要对我们泛型的⽅法进⾏限定传递变量,⽐如如下例⼦
计算数组中最下的元素
这个时候是⽆法编译通过的,且编译器会报错
因为我们的编译器不能确定你这个T 类型是否有compareTo这个函数,所以这么能让编译器相信我们这个T是⼀定会有compareTo呢?
我们可以这么写<T extends Comparable> 这⾥的意思是T⼀定是继承Comparable的类
因为Comparable是⼀定有compareTo这个⽅法,所以T⼀定有compareTo⽅法,于是编译器就不会报错了
因为加了限定那么min这个⽅法也只有继承了Comparable的类才可以调⽤;
如果要限定⽅法的泛型继承多个类可以加extends 关键字并⽤&分割如:T extends Comparable & Serializable
限定类型是⽤&分割的,逗号来分割多个类型变量<T extends Comparable & Serializable , U extends Comparable>
类型擦除
不论什么时候定义⼀个泛型类型,虚拟机都会提供⼀个相应的原始类型(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<T extends Comparable & Serializable> implements Serializable{
private T lower;
private T upper;
}
原始类型如下所⽰:
public class Interval implements Serializable{
private Comparable lower;
private Comparable upper;
}
翻译泛型表达式
上⾯说到泛型擦除类型变量后对于⽆限定变量后会以Object来替换泛型类型变量;
但是我们使⽤的时候并不需要进⾏强制类型转换;
原因是编译器已经强制插⼊类型转换;
例如:
Pair<Employee> 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 extends Comparable> T min(T[] a)
类型擦除后
public static Comparable min(Comparable[] a)
此时可以看到类型参数T已经被擦除了,只剩下限定类型Comparable;
⽅法的类型擦除带来了两个复杂的问题,看下⾯的⽰例:
public class DateInterval extends Pair<LocalDate> {
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<LocalDate> 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
这⾥有个重要的概念就是桥⽅法
为了解决此问题并在类型擦除后保留通⽤类型的多态性,
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(ng.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的字节码中的确会有⼀个桥⽅法,同时验证了上⾯的问题;
总结
虚拟机中没有泛型,只有普通的类和⽅法
所有的类型参数都⽤他们的限定类型替换
桥⽅法被合成来保持多态
为保持类型安全性,必要时插⼊强制类型转换。