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
这里有个重要的概念就是桥方法
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的泛型详解(一)的更多相关文章
- Java基础 - 泛型详解
2022-03-24 09:55:06 @GhostFace 泛型 什么是泛型? 来自博客 Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了&quo ...
- java 泛型详解(普通泛型、 通配符、 泛型接口)
java 泛型详解(普通泛型. 通配符. 泛型接口) JDK1.5 令我们期待很久,可是当他发布的时候却更换版本号为5.0.这说明Java已经有大幅度的变化.本文将讲解JDK5.0支持的新功能---- ...
- java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一
对java的泛型特性的了解仅限于表面的浅浅一层,直到在学习设计模式时发现有不了解的用法,才想起详细的记录一下. 本文参考java 泛型详解.Java中的泛型方法. java泛型详解 1. 概述 泛型在 ...
- Java泛型详解(转)
文章转自 importNew:Java 泛型详解 引言 泛型是Java中一个非常重要的知识点,在Java集合类框架中泛型被广泛应用.本文我们将从零开始来看一下Java泛型的设计,将会涉及到通配符处理 ...
- 【转】java 泛型详解
java 泛型详解 对java的泛型特性的了解仅限于表面的浅浅一层,直到在学习设计模式时发现有不了解的用法,才想起详细的记录一下. 本文参考java 泛型详解.Java中的泛型方法. java泛型详解 ...
- 【转载】Java泛型详解
[转载]http://www.importnew.com/24029.html 对java的泛型特性的了解仅限于表面的浅浅一层,直到在学习设计模式时发现有不了解的用法,才想起详细的记录一下. 本文参考 ...
- [转载]Java迭代器(iterator详解以及和for循环的区别)
Java迭代器(iterator详解以及和for循环的区别) 觉得有用的话,欢迎一起讨论相互学习~[Follow] 转载自 https://blog.csdn.net/Jae_Wang/article ...
- Java 字符串格式化详解
Java 字符串格式化详解 版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 文中如有纰漏,欢迎大家留言指出. 在 Java 的 String 类中,可以使用 format() 方法 ...
- Java 序列化Serializable详解
Java 序列化Serializable详解(附详细例子) Java 序列化Serializable详解(附详细例子) 1.什么是序列化和反序列化Serialization(序列化)是一种将对象以一连 ...
随机推荐
- (转)如何学好C语言
原文:http://coolshell.cn/articles/4102.html 作者:陈皓 有人在酷壳的留言版上询问下面的问题 keep_walker : 今天晚上我看到这篇文章. http ...
- 在okhttp的callback回调中加Toast出现Cant create handler inside hread that has not called Looper.prepare()...
2019独角兽企业重金招聘Python工程师标准>>> 分析:callback中回调的response方法中还是在子线程中运行的,所以要调取Toast必须回到主线程中更新ui 解决方 ...
- 自定义fastjson对枚举类型的序列化及反序列化过程
通常,fastjson在序列化及反序列化枚举时,一般以下几种策略: 1).根据枚举的name值序列化及反序列化(默认) 2).根据枚举的ordinal序列化及反序列化 3).根据枚举的toString ...
- Pig设计模式概要以及与SQL的设计模式的对比
2019独角兽企业重金招聘Python工程师标准>>> 1概要模式 概要模式其实就是数据的全貌信息的获取,主要分为3种: 1.1数值概要 #HSQL SELECT MIN(num), ...
- 详解如何使用gulp实现项目在浏览器中的自动刷新
情况描述: 我们很容易遇到这样一种情况: 我们并不是一开始就规划好了整个项目,比如可能接手别人的项目或者工程已经手动创建好了,现在要想利用gulp来实现浏览器自动刷新,那么如何做呢? 其实非常简单,本 ...
- JS异步与同步
这里展示一个操作场景:需要对数据进行异步处理,但这次操作可能会失败,所以需要定期对数据进行再次处理,直至处理成功. 实现:手动触发的处理以及定期触发的处理,是相同的,即可以抽取出来成一个公共函数,定期 ...
- JavaSpring中级联查询
一对一级联查询映射文件PersonMapper.xml代码: <?xml version="1.0" encoding="UTF-8"?> < ...
- C# 基础知识系列- 14 IO篇 流的使用
0. 前言 继续之前的C# IO流,在前几篇小短片中我们大概看了下C# 的基础IO也对文件.目录和路径的操作有了一定的了解.这一篇开始,给大家演示一下流的各种操作.以文件流为例,一起来看看如何操作吧. ...
- 题目分享E 二代目
题意:一棵点数为n的树,每个节点有点权,要求在树中中找到一个最小的x,使得存在一个点满足max(该点点权,该点相邻的点的点权+1,其他点的点权+2)=x 分析:首先要能把题目转化为上述题意 首先题目让 ...
- Spring Cloud Alibaba系列(一)nacos作为服务注册中心
Spring Cloud Alibaba各组件版本关系 Spring Cloud Alibaba Version Sentinel Version Nacos Version RocketMQ Ver ...