前言今天原本正在快乐的摸鱼 ing。突然,坐我旁边的阿里大佬估计是发现我很闲,于是说:我看你挺闲啊,小伙子。你泛型熟悉吗?我考你一道题,你要是能实现,那你的泛型可以出师了!可以年薪百万不是梦了!那我听到这话,那男人不能说自己不行啊!于是我果断答应了下来,心想有什么题能难倒我?我 Java 可有一坤年! 应用场景是封装给第三方接口的 SDK
题目
如图所示,我希望执行 execute() 方法之后,不同的调用方能够返回不同的响应。例如:RequestA 调用方法后会返回 ResponseA,RequestB 调用方法后会返回 ResponseB
看到这个题,我就蒙了。这还是我熟悉的泛型吗?泛型还能这样吗?废话不多说,我们先从泛型的基础开始,通过回顾基础知识点来加深我们的理解。
一、泛型概述1. 泛型定义泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参列表,普通方法的形参列表中,每个形参的数据类型是确定的,而变量是一个参数。在调用普通方法时需要传入对应形参数据类型的变量(实参),若传入的实参与形参定义的数据类型不匹配,则会报错
那参数化类型是什么意思呢?以方法的定义为例,在方法定义时,将方法签名中的形参的数据类型也设置为参数(也可称之为类型参数),在调用该方法时再从外部传入一个具体的数据类型和变量
泛型的本质是为了将类型参数化, 也就是说在泛型使用过程中,数据类型被设置为一个参数,在使用时再从外部传入一个数据类型;而一旦传入了具体的数据类型后,如果传入变量(实参)的数据类型和形参的数据类型不匹配,编译器就会直接报错。这种参数化类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法
2. 泛型使用场景我们可以看下面这个使用场景:👇🏻
在 ArrayList 集合中,允许放入所有包装类型的对象,假设现在这个 ArrayList 中只存储了 String 这一种数据类型
上述代码能够正常执行。在遍历 ArrayList 集合时,程序可以通过向下转型自动将 Object 转换为 String 类型对象
但如果在添加 String 对象时,不小心添加了一个 Integer 对象,会发生什么?
上述代码在项目编译时没有报错,但在运行时却抛出了ClassCastException异常。其原因在于 Integer 类型不能强转为 String 类型
输出:
如果我们希望在程序编译期间对于不合规的属性,编译器能直接给我们报错,而不是在程序运行期间才抛出异常。就需要使用到 泛型 了
使用泛型改造后的代码如下:
3. 泛型小结与使用 Object 对象代替一切引用数据类型对象这样简单粗暴方式相比,使用泛型使得具体的数据类型可以像参数一样由外部传递进来。它提供了一种扩展能力,更符合面向对象开发的软件编程宗旨当具体的数据类型确定后,泛型又提供了一种类型安全检测机制,只有数据类型相匹配的变量才能正常的赋值,否则编译器就不通过。所以说,泛型一定程度上提高了软件的安全性,防止出现低级的失误泛型提高了程序代码的可读性。在定义泛型阶段(类、接口、方法)或者对象实例化阶段,由于 <类型参数> 需要在代码中显式地编写,所以程序员能够快速猜测出代码所要操作的数据类型,提高了代码可读性泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法。下面,我将分别讲解这三种使用方式
二、泛型类1. 泛型类的定义当类型参数用于类的定义中,则该类被称为泛型类。通过泛型可以实现对一组类的操作对外开放相同的一套接口
最典型的例子就是各种容器类,如:List、Map、Set 等
定义语法:
代码语言:javascript代码运行次数:0运行复制class 类名称 <泛型参数> {
private 泛型标识 /*或:成员变量类型*/ 变量名;
.....
}
}泛型参数(也称为:泛型标识)是自定义的。在 Java 中,常见的泛型标识极其含义如下:
T:代表一般的任何类E:代表 Element 元素的意思,或者 Exception 异常的意思K:代表 Key 的意思V:代表 Value 的意思,通常与 K 配合一起使用S:代表 Subtype 的意思,本文后面章节会讲解示例:
在泛型类中,类型参数定义的位置有三处(对应上图)。分别为:
非静态的成员属性类型非静态方法的形参类型(包括非静态成员方法和构造器)非静态的成员方法的返回值类型2. 泛型类的注意事项泛型类中的静态方法和静态变量不可使用泛型类所声明的类型参数原因在于,泛型类中的类型参数是在创建泛型类对象时(new)确定的。而静态变量和静态方法在类加载时就已经初始化,此时,泛型类的泛型参数可能还未确定。因此泛型类的类型参数不能再静态成员中使用
静态泛型方法可以使用自身方法签名中定义的类型参数(即泛型方法),但不能使用泛型类中定义的类型参数泛型类可以接受多个类型参数3. 泛型类的使用在实例化泛型类的对象时,必须指定类型参数的具体数据类型
即< >中要传入具体的数据类型。如果< >中什么也不传入,则默认是
使用示例:
定义一个泛型类创建泛型类的实例对象当我们在< >内传入 String 类型时,原泛型类的 T 类型参数就会被自动替换成我们传入的 String
三、泛型接口1. 泛型接口的定义泛型接口和泛型类的定义差不多
定义语法:
代码语言:javascript代码运行次数:0运行复制public interface 接口名<类型参数> {
...
}示例:
2. 泛型接口的注意事项泛型接口中的类型参数,在该接口被继承或被实现时确定在泛型接口中,静态成员不能使用泛型接口定义的类型参数定义一个类实现泛型接口。如果类上没有指定泛型参数,则默认是 Object可以定义一个泛型类实现泛型接口。泛型类中声明的类型参数必须要和泛型接口中的类型参数相同四、泛型方法1. 泛型方法的定义当一个方法签名中的返回值前面声明了一个< T >时,该方法就被声明为一个泛型方法。< T >表明该方法声明了一个类型参数 T,并且这个类型参数 T 只能在该方法中使用。当然,泛型方法中也可以使用泛型类中定义的泛型参数
定义语法:
代码语言:javascript代码运行次数:0运行复制public <类型参数> 返回类型 方法名(类型参数 变量名) {
...
}只有在方法签名中声明了
Test 是泛型类,testMethod(U u) 是泛型类中的普通方法。其使用的类型参数是泛型类上定义的类型参数testMethod1(T t) 是一个泛型方法,其使用的类型参数是方法签名中定义的类型参数哪怕泛型类中定义的类型参数和泛型方法中定义的类型参数标识都是
当调用泛型方法时,根据外部传入的实际对象的数据类型,编译器就可以判断出类型参数 T 所代表的具体数据类型
当调用泛型方法时,根据传入的实际对象,编译器会判断出类型形参 T 所代表的具体数据类型
4. 泛型方法的类型推断在调用泛型方法的时候,可以显式地指定类型参数,也可以不指定当泛型方法的形参列表中有多个类型参数时,在不指定类型参数的情况下,方法中声明的的类型参数为泛型方法中的几种类型参数的共同父类的最小级,直到 Object在指定了类型参数的时候,传入泛型方法中的实参的数据类型必须为指定数据类型或者其子类五、类型擦除1. 类型擦除的定义泛型的本质是将数据类型参数化,它通过擦除的方式来实现,即编译器会在编译期间擦除代码中的所有泛型语法并相应的做出一些类型转换动作
换而言之,泛型信息只存在于代码编译阶段,在代码编译结束后,与泛型相关的信息会被擦除掉,专业术语叫做 类型擦除。也就是说,成功编译过后的 class 文件中不包含任何泛型信息,泛型信息不会进入到运行时阶段
示例:
一、假如我们给 ArrayList 集合传入两种不同的数据类型,那比较他们的类信息是一样的吗?
代码语言:javascript代码运行次数:0运行复制public class GenericType {
public static void main(String[] args) {
ArrayList
ArrayList
System.out.println(arrayString.getClass() == arrayInteger.getClass()); // 输出 True
}
}在这个例子中,我们定义了两个 ArrayList 集合,不过一个是 ArrayList< String>,只能存储字符串。一个是 ArrayList< Integer>,只能存储整型对象。我们通过 arrayString 对象和 arrayInteger 对象的 getClass() 方法获取它们的类信息并比较,发现结果为 true
那明明我们在 < > 中传入了两种不同的数据类型,按照上文所说的,它们的类型参数 T 不是应该被替换成我们传入的数据类型了吗,那为什么它们的类信息还是相同呢?
因为:在编译期间,所有的泛型信息都会被擦除, ArrayList
二、再看一个例子。假设定义一个泛型类如下。观察编译后的泛型参数被擦除成了什么类型
代码语言:javascript代码运行次数:0运行复制public class Caculate
private T num;
}将这个泛型类反编译:
代码语言:javascript代码运行次数:0运行复制public class Caculate {
public Caculate() {} // 默认构造器,不用管
private Object num; // T 被替换为 Object 类型
}可以发现编译器擦除了 Caculate 类后面的泛型标识
是不是所有的类型参数被擦除后都以 Object 类进行替换呢?
答案是否定的,大部分情况下,类型参数 T 被擦除后都会以 Object 类进行替换
而有一种情况则不是,那就是使用到了 extends 和 super 语法的有界类型参数(即泛型通配符,后面我们会详细解释)
2. 泛型擦除的原理假如我们定义了一个 ArrayList
不是说泛型信息在编译的时候就会被擦除掉吗?那既然泛型信息被擦除了,如何保证我们在集合中只添加指定的数据类型的对象呢?换而言之,我们虽然定义了 ArrayList
其实在创建一个泛型类的对象时, Java 编译器是先检查代码中传入
可以把泛型的类型安全检查机制和类型擦除想象成演唱会的验票机制
以 ArrayList< Integer> 泛型集合为例:
当我们在创建一个 ArrayList< Integer > 泛型集合的时候,ArrayList 可以看作是演唱会场馆,而< T >就是场馆的验票系统,Integer 是验票系统设置的门票类型当验票系统设置好为< Integer >后,只有持有 Integer 门票的人才可以通过验票系统,进入演唱会场馆(集合)中;若是未持有 Integer 门票的人想进场,则验票系统会发出警告(编译器报错)在通过验票系统时,门票会被收掉(类型擦除),但场馆后台(JVM)会记录下观众信息(泛型信息)进场后的观众变成了没有门票的普通人(原始数据类型)。但是,在需要查看观众的信息时(操作对象),场馆后台可以找到记录的观众信息(编译器会自动将对象进行类型转换)擦除 ArrayList
对原始方法 get() 的调用,返回的是 Object 类型将返回的 Object 类型强制转换为 Integer 类型3. 泛型擦除小结泛型信息(包括泛型类、接口、方法)只在代码编译阶段存在,在代码成功编译后,其内的所有泛型信息都会被擦除,并且类型参数 T 会被统一替换为其原始类型(默认是 Object 类,若有 extends 或者 super 则另外分析)在泛型信息被擦除后,若还需要使用到对象相关的泛型信息,编译器底层会自动进行类型转换(从原始类型转换为未擦除前的数据类型)六、泛型通配符1. 泛型的继承在介绍泛型通配符之前,我们先回顾下 Java 中多态的相关知识
在 Java 中,我们可以将一个子类对象赋值给其父类的引用,这种叫做 向上转型
代码语言:javascript代码运行次数:0运行复制public class GenericType {
public static void main(String[] args) {
List list = new ArrayList(); // 向上转型,体现了多态特性
}
}我们知道 ArrayList 底层实现了 List 接口,源码如下
public class ArrayList
我们发现,ArrayList
代码语言:javascript代码运行次数:0运行复制public class GenericType {
public static void main(String[] args) {
List
}
}我们发现可以成功进行转换。但两者的泛型数据类型必须相同
那既然 Java 中对象可以向上转型。泛型可以向上转型吗?我们知道 Number 是 Integer 的父类。我们能将 ArrayList
代码语言:javascript代码运行次数:0运行复制public class GenericType {
public static void main(String[] args) {
List
ArrayList
}
}不难发现,上述代码会报错。这说明了:在一般的泛型中,泛型类型不能向上转型
我们不妨试试,如果 Java 允许泛型类型可以向上转型会发生什么情况呢?
代码语言:javascript代码运行次数:0运行复制public class GenericType {
public static void main(String[] args) {
// 创建一个 ArrayList
ArrayList
// 添加一个 Integer 对象
integerList.add(new Integer(123));
// “向上转型”为 ArrayList
ArrayList
// 添加一个 Float 对象,Float 也是 Number 的子类,编译器不报错
numberList.add(new Float(12.34));
// 从 ArrayList
Integer n = integerList.get(1); // ClassCastException,运行出错
}
}我们发现,当我们对 ArrayList
正因如此,编译器为了避免这种错误。不允许泛型类型的向上转型
2. 泛型通配符的三种类型在现实编码中,确实有这样的需求,希望泛型能够处理 某一类型范围内 的类型参数,比如某个泛型类和它的子类,为此 Java 引入了 泛型通配符 这个概念
泛型通配符有 3 种形式:
>:被称作无限定的通配符 extends T>:被称作有上界的通配符 super T>:被称作有下界的通配符在引入泛型通配符之后,我们便得到了一个在逻辑上可以表示为某一类型参数范围的父类引用类型
七、进阶用法公司架构大佬给我出了一道题:我希望执行 execute() 方法之后,不同的调用方能够返回不同的响应结果,并且 execute()方法内部是没有任何实现的。例如:RequestA 调用方法后会返回 ResponseA,RequestB 调用方法后会返回 ResponseB
那具体应该如何实现呢?我们首先想到的肯定是需要通过泛型来建立 Request 和 Response 之间的联系,那这个泛型类型应该如何写呢?
代码:
定义 execute 泛型方法代码语言:javascript代码运行次数:0运行复制public class Actuator {
// 泛型方法
public static
return null;
}
}我们定义了一个泛型方法 execute,这个方法巧妙的地方在于形参接收一个 Request,返回值是 Request 的泛型参数
🤔 思考:这里不用泛型方法可以吗?
那肯定是不可以的,原因有两点:
Actuator 不是泛型类,这种使用场景下肯定是不希望把 Actuator 声明成泛型类的,不然执行一次 execute 方法就要 new 一个对应类型的 Actuator 实例execute 方法是一个静态方法,就算 Actuator 是泛型类,静态方法也是不可以使用泛型类中的泛型参数的定义 Request 抽象类代码语言:javascript代码运行次数:0运行复制/**
* @Description TODO
* @Author Mr.Zhang
* @Date 2025/4/24 22:10
* @Version 1.0
*/
public abstract class Request
}因为将来不同的 Request 实现类都会调用同一个 execute 方法,所以为了代码的复用性,抽取一个 Request 抽象类来当作 execute 方法的入参
定义 RequestA、RequestB 实现类代码语言:javascript代码运行次数:0运行复制/**
* @Author: ZhangGongMing
* @CreateTime: 2025/4/25 09:15
* @Description:
* @Version: 1.0
*/
public class RequestA extends Request
}
/**
* @Author: ZhangGongMing
* @CreateTime: 2025/4/25 09:16
* @Description:
* @Version: 1.0
*/
public class RequestB extends Request
}不同的实现类指定不同的泛型参数
定义 ResponseA、ResponseB代码语言:javascript代码运行次数:0运行复制@Data
public class ResponseA {
}
@Data
public class ResponseB {
}测试是否成功代码语言:javascript代码运行次数:0运行复制public static void main(String[] args) {
RequestA requestA = new RequestA();
RequestB requestB = new RequestB();
ResponseA responseA = execute(requestA);
ResponseB responseB = execute(requestB);
}我们发现,泛型成功转换了!
至此,泛型相关知识总结完毕~