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 两个Integer17290 编译器认为这三个不属于同一个类型;

此时有一种解决办法就是把整型写成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

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

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的字节码中的确会有一个桥方法,同时验证了上面的问题;

总结

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

Java的泛型详解(一)的更多相关文章

  1. Java基础 - 泛型详解

    2022-03-24 09:55:06 @GhostFace 泛型 什么是泛型? 来自博客 Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了&quo ...

  2. java 泛型详解(普通泛型、 通配符、 泛型接口)

    java 泛型详解(普通泛型. 通配符. 泛型接口) JDK1.5 令我们期待很久,可是当他发布的时候却更换版本号为5.0.这说明Java已经有大幅度的变化.本文将讲解JDK5.0支持的新功能---- ...

  3. java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一

    对java的泛型特性的了解仅限于表面的浅浅一层,直到在学习设计模式时发现有不了解的用法,才想起详细的记录一下. 本文参考java 泛型详解.Java中的泛型方法. java泛型详解 1. 概述 泛型在 ...

  4. Java泛型详解(转)

    文章转自  importNew:Java 泛型详解 引言 泛型是Java中一个非常重要的知识点,在Java集合类框架中泛型被广泛应用.本文我们将从零开始来看一下Java泛型的设计,将会涉及到通配符处理 ...

  5. 【转】java 泛型详解

    java 泛型详解 对java的泛型特性的了解仅限于表面的浅浅一层,直到在学习设计模式时发现有不了解的用法,才想起详细的记录一下. 本文参考java 泛型详解.Java中的泛型方法. java泛型详解 ...

  6. 【转载】Java泛型详解

    [转载]http://www.importnew.com/24029.html 对java的泛型特性的了解仅限于表面的浅浅一层,直到在学习设计模式时发现有不了解的用法,才想起详细的记录一下. 本文参考 ...

  7. [转载]Java迭代器(iterator详解以及和for循环的区别)

    Java迭代器(iterator详解以及和for循环的区别) 觉得有用的话,欢迎一起讨论相互学习~[Follow] 转载自 https://blog.csdn.net/Jae_Wang/article ...

  8. Java 字符串格式化详解

    Java 字符串格式化详解 版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 文中如有纰漏,欢迎大家留言指出. 在 Java 的 String 类中,可以使用 format() 方法 ...

  9. Java 序列化Serializable详解

    Java 序列化Serializable详解(附详细例子) Java 序列化Serializable详解(附详细例子) 1.什么是序列化和反序列化Serialization(序列化)是一种将对象以一连 ...

随机推荐

  1. 2019-2020-1 20199303《Linux内核原理与分析》第六周作业

    系统调用的三层机制 首先是为系统增加新的命令 运行脚本自动生成文件系统 其中有一个显示时间的功能 编辑test.c文件,增加一个hello函数用来显示学号,再次使用make roofts自动编译,调用 ...

  2. Hawkeye部署Github监控系统

    2019独角兽企业重金招聘Python工程师标准>>> step1:python环境安装 #pwd /usr/local/soft #wget https://www.python. ...

  3. #if 和#ifdef的区别

    转自:https://blog.csdn.net/zhangchiytu/article/details/7563329 先看个例子:#define TARGET_LITTLE_ENDINA 1#de ...

  4. Codeforces Round #590

    题目链接:Round #590 题目答案:官方Editorial.My Solution A. Equalize Prices Again 签到题还WA了一发,向上取整有点问题: //my wrong ...

  5. CF思维联系--CodeForces - 218C E - Ice Skating (并查集)

    题目地址:24道CF的DIv2 CD题有兴趣可以做一下. ACM思维题训练集合 Bajtek is learning to skate on ice. He's a beginner, so his ...

  6. 图论--Dijkstra算法总结

    Key word: ①BFS转换Dijkstra ②其他关系转化为最短路 ③反向建边及反向Dijkstra ④稠密图.稀疏图 ⑤链式前向星 ⑥Vector建图 ⑦超级源点&汇点 详解: 1.B ...

  7. Nginx模块开发(3)————使用upstream访问第三方服务

    该模块可以完成如下的功能,当我们输入http://你的ip/lcwupstream时,会使用upstream方式访问淘宝搜索,打开淘宝搜索的主页面,代码如下: //start from the ver ...

  8. java并发之线程安全问题

    并发(concurrency)一个并不陌生的词,简单来说,就是cpu在同一时刻执行多个任务. 而Java并发则由多线程实现的. 在jvm的世界里,线程就像不相干的平行空间,串行在虚拟机中.(当然这是比 ...

  9. Java 函数式接口

    目录 Java 函数式接口 1. 函数式接口 1.1 概念 1.2 格式 1.3 函数式接口的使用 2. 函数式编程 2.1 Lambda的延迟执行 性能浪费的日志案例 使用Lambda表达式的优化 ...

  10. 循环结构(for、while)

    3.4用for语句实现循环结构 什么是循环结构 for语句 1.什么是循环结构? 循环结构又称为重复结构,是利用计算机运算速度快以及能进行逻辑控制的特点来重复执行某些操作.重复执行的部分称为循环体. ...