今天原本正在快乐的摸鱼 ing。突然,坐我旁边的阿里大佬估计是发现我很闲,于是说:我看你挺闲啊,小伙子。你泛型熟悉吗?我考你一道题,你要是能实现,那你的泛型可以出师了!可以年薪百万不是梦了!那我听到这话,那男人不能说自己不行啊!于是我果断答应了下来,心想有什么题能难倒我?我 Java 可有一坤年! 应用场景是封装给第三方接口的 SDK
题目
如图所示,我希望执行 execute() 方法之后,不同的调用方能够返回不同的响应。例如:RequestA 调用方法后会返回 ResponseA,RequestB 调用方法后会返回 ResponseB

看到这个题,我就蒙了。这还是我熟悉的泛型吗?泛型还能这样吗?废话不多说,我们先从泛型的基础开始,通过回顾基础知识点来加深我们的理解。
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参列表,普通方法的形参列表中,每个形参的数据类型是确定的,而变量是一个参数。在调用普通方法时需要传入对应形参数据类型的变量(实参),若传入的实参与形参定义的数据类型不匹配,则会报错
那参数化类型是什么意思呢?以方法的定义为例,在方法定义时,将方法签名中的形参的数据类型也设置为参数(也可称之为类型参数),在调用该方法时再从外部传入一个具体的数据类型和变量
泛型的本质是为了将类型参数化, 也就是说在泛型使用过程中,数据类型被设置为一个参数,在使用时再从外部传入一个数据类型;而一旦传入了具体的数据类型后,如果传入变量(实参)的数据类型和形参的数据类型不匹配,编译器就会直接报错。这种参数化类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法
我们可以看下面这个使用场景:👇🏻
在 ArrayList 集合中,允许放入所有包装类型的对象,假设现在这个 ArrayList 中只存储了 String 这一种数据类型

上述代码能够正常执行。在遍历 ArrayList 集合时,程序可以通过向下转型自动将 Object 转换为 String 类型对象
但如果在添加 String 对象时,不小心添加了一个 Integer 对象,会发生什么?

上述代码在项目编译时没有报错,但在运行时却抛出了ClassCastException异常。其原因在于 Integer 类型不能强转为 String 类型
输出:

如果我们希望在程序编译期间对于不合规的属性,编译器能直接给我们报错,而不是在程序运行期间才抛出异常。就需要使用到 泛型 了
使用泛型改造后的代码如下:

<String>就是一个泛型。其限制了集合中存放对象的数据类型只能是 String 类型,当添加一个非 String 对象时,编译器会直接报错。这样,我们便解决了上面产生的ClassCastException异常的问题(这样体现了泛型的类型安全检测机制)
泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法。下面,我将分别讲解这三种使用方式
当类型参数用于类的定义中,则该类被称为泛型类。通过泛型可以实现对一组类的操作对外开放相同的一套接口
最典型的例子就是各种容器类,如:List、Map、Set 等
定义语法:
class 类名称 <泛型参数> {
private 泛型标识 /*或:成员变量类型*/ 变量名;
.....
}
}泛型参数(也称为:泛型标识)是自定义的。在 Java 中,常见的泛型标识极其含义如下:
示例:

在泛型类中,类型参数定义的位置有三处(对应上图)。分别为:

原因在于,泛型类中的类型参数是在创建泛型类对象时(new)确定的。而静态变量和静态方法在类加载时就已经初始化,此时,泛型类的泛型参数可能还未确定。因此泛型类的类型参数不能再静态成员中使用


在实例化泛型类的对象时,必须指定类型参数的具体数据类型
即< >中要传入具体的数据类型。如果< >中什么也不传入,则默认是<Object>
使用示例:


当我们在< >内传入 String 类型时,原泛型类的 T 类型参数就会被自动替换成我们传入的 String
泛型接口和泛型类的定义差不多
定义语法:
public interface 接口名<类型参数> {
...
}示例:




当一个方法签名中的返回值前面声明了一个< T >时,该方法就被声明为一个泛型方法。< T >表明该方法声明了一个类型参数 T,并且这个类型参数 T 只能在该方法中使用。当然,泛型方法中也可以使用泛型类中定义的泛型参数
定义语法:
public <类型参数> 返回类型 方法名(类型参数 变量名) {
...
}<T>的方法才是泛型方法!仅使用了泛型类定义的类型参数的方法并不是泛型方法示例:



<T>表明该方法声明了一个类型参数 T,并且这个类型参数 T 只能在该方法中使用
泛型方法签名中声明的类型参数只能在该方法里使用,而泛型接口、泛型类中声明的类型参数则可以在整个接口、类中使用
当调用泛型方法时,根据外部传入的实际对象的数据类型,编译器就可以判断出类型参数 T 所代表的具体数据类型

当调用泛型方法时,根据传入的实际对象,编译器会判断出类型形参 T 所代表的具体数据类型

泛型的本质是将数据类型参数化,它通过擦除的方式来实现,即编译器会在编译期间擦除代码中的所有泛型语法并相应的做出一些类型转换动作
换而言之,泛型信息只存在于代码编译阶段,在代码编译结束后,与泛型相关的信息会被擦除掉,专业术语叫做 类型擦除。也就是说,成功编译过后的 class 文件中不包含任何泛型信息,泛型信息不会进入到运行时阶段
示例:
一、假如我们给 ArrayList 集合传入两种不同的数据类型,那比较他们的类信息是一样的吗?
public class GenericType {
public static void main(String[] args) {
ArrayList<String> arrayString = new ArrayList<String>();
ArrayList<Integer> arrayInteger = new ArrayList<Integer>();
System.out.println(arrayString.getClass() == arrayInteger.getClass()); // 输出 True
}
}在这个例子中,我们定义了两个 ArrayList 集合,不过一个是 ArrayList< String>,只能存储字符串。一个是 ArrayList< Integer>,只能存储整型对象。我们通过 arrayString 对象和 arrayInteger 对象的 getClass() 方法获取它们的类信息并比较,发现结果为 true
那明明我们在 < > 中传入了两种不同的数据类型,按照上文所说的,它们的类型参数 T 不是应该被替换成我们传入的数据类型了吗,那为什么它们的类信息还是相同呢?
因为:在编译期间,所有的泛型信息都会被擦除, ArrayList<Integer> 和 ArrayList<String> 类型,在编译后都会变成 ArrayList<Objec t> 类型
二、再看一个例子。假设定义一个泛型类如下。观察编译后的泛型参数被擦除成了什么类型
public class Caculate<T> {
private T num;
}将这个泛型类反编译:
public class Caculate {
public Caculate() {} // 默认构造器,不用管
private Object num; // T 被替换为 Object 类型
}可以发现编译器擦除了 Caculate 类后面的泛型标识 <T>,并且将 num 属性的数据类型替换为 Object 类型,替换了泛型类型 T 的数据类型我们称之为 原始数据类型
是不是所有的类型参数被擦除后都以 Object 类进行替换呢?
答案是否定的,大部分情况下,类型参数 T 被擦除后都会以 Object 类进行替换
而有一种情况则不是,那就是使用到了 extends 和 super 语法的有界类型参数(即泛型通配符,后面我们会详细解释)
假如我们定义了一个 ArrayList<Integer> 泛型集合,若向该集合中插入 String 类型的对象,不需要运行程序,编译器就会直接报错。这里可能有小伙伴就产生了疑问:
那 Java 是如何解决这个问题的呢?
其实在创建一个泛型类的对象时, Java 编译器是先检查代码中传入 <T> 的数据类型,并记录下来,然后再对代码进行编译,编译的同时进行类型擦除;如果需要对被擦除了泛型信息的对象进行操作,编译器会自动将对象进行类型转换
可以把泛型的类型安全检查机制和类型擦除想象成演唱会的验票机制
以 ArrayList< Integer> 泛型集合为例:

擦除 ArrayList<Integer> 的泛型信息后,get() 方法的返回值将返回 Object 类型,但编译器会自动插入 Integer 的强制类型转换。也就是说,编译器把 get() 方法调用翻译为两条字节码指令:
在介绍泛型通配符之前,我们先回顾下 Java 中多态的相关知识
在 Java 中,我们可以将一个子类对象赋值给其父类的引用,这种叫做 向上转型
public class GenericType {
public static void main(String[] args) {
List list = new ArrayList(); // 向上转型,体现了多态特性
}
}我们知道 ArrayList 底层实现了 List 接口,源码如下
public class ArrayList<T> implements List<T> {...}
我们发现,ArrayList<T> 是一个泛型类,它的父类 List<T>也是一个泛型接口。那如果我们把 T 泛型参数都传相同的类型,是否还可以将子类的对象赋值给父类的引用呢?
public class GenericType {
public static void main(String[] args) {
List<Integer> list = new ArrayList<Integer>(); // 可以赋值
}
}我们发现可以成功进行转换。但两者的泛型数据类型必须相同
那既然 Java 中对象可以向上转型。泛型可以向上转型吗?我们知道 Number 是 Integer 的父类。我们能将 ArrayList<Integer> 对象赋值给 List<Number> 父类引用吗?
public class GenericType {
public static void main(String[] args) {
List<Number> list01 = new ArrayList<Integer>(); // 编译错误
ArrayList<Number> list02 = new ArrayList<Integer>(); // 编译错误
}
}不难发现,上述代码会报错。这说明了:在一般的泛型中,泛型类型不能向上转型
我们不妨试试,如果 Java 允许泛型类型可以向上转型会发生什么情况呢?
public class GenericType {
public static void main(String[] args) {
// 创建一个 ArrayList<Integer> 集合
ArrayList<Integer> integerList = new ArrayList<>();
// 添加一个 Integer 对象
integerList.add(new Integer(123));
// “向上转型”为 ArrayList<Number>
ArrayList<Number> numberList = integerList;
// 添加一个 Float 对象,Float 也是 Number 的子类,编译器不报错
numberList.add(new Float(12.34));
// 从 ArrayList<Integer> 集合中获取索引为 1 的元素(即添加的 Float 对象):
Integer n = integerList.get(1); // ClassCastException,运行出错
}
}我们发现,当我们对 ArrayList<Integer> 向上转型为 ArrayList<Number> 类型后,这个 ArrayList<Number> 集合就可以接收 Float 对象了,因为 Float 类是 Number 类的子类。这显然是有问题的。当通过 get()方法获取元素时,编译器会自动将 Float 对象强转为 Integer 对象,而这会产生 ClassCastException 异常
正因如此,编译器为了避免这种错误。不允许泛型类型的向上转型
在现实编码中,确实有这样的需求,希望泛型能够处理 某一类型范围内 的类型参数,比如某个泛型类和它的子类,为此 Java 引入了 泛型通配符 这个概念
泛型通配符有 3 种形式:
在引入泛型通配符之后,我们便得到了一个在逻辑上可以表示为某一类型参数范围的父类引用类型
公司架构大佬给我出了一道题:我希望执行 execute() 方法之后,不同的调用方能够返回不同的响应结果,并且 execute()方法内部是没有任何实现的。例如:RequestA 调用方法后会返回 ResponseA,RequestB 调用方法后会返回 ResponseB

那具体应该如何实现呢?我们首先想到的肯定是需要通过泛型来建立 Request 和 Response 之间的联系,那这个泛型类型应该如何写呢?
代码:
public class Actuator {
// 泛型方法
public static <T> T execute(Request<T> req) {
return null;
}
}我们定义了一个泛型方法 execute,这个方法巧妙的地方在于形参接收一个 Request,返回值是 Request 的泛型参数
🤔 思考:这里不用泛型方法可以吗?
那肯定是不可以的,原因有两点:
/**
* @Description TODO
* @Author Mr.Zhang
* @Date 2025/4/24 22:10
* @Version 1.0
*/
public abstract class Request<T> {
}因为将来不同的 Request 实现类都会调用同一个 execute 方法,所以为了代码的复用性,抽取一个 Request 抽象类来当作 execute 方法的入参
/**
* @Author: ZhangGongMing
* @CreateTime: 2025/4/25 09:15
* @Description:
* @Version: 1.0
*/
public class RequestA extends Request<ResponseA> {
}
/**
* @Author: ZhangGongMing
* @CreateTime: 2025/4/25 09:16
* @Description:
* @Version: 1.0
*/
public class RequestB extends Request<ResponseB> {
}不同的实现类指定不同的泛型参数
@Data
public class ResponseA {
}
@Data
public class ResponseB {
}public static void main(String[] args) {
RequestA requestA = new RequestA();
RequestB requestB = new RequestB();
ResponseA responseA = execute(requestA);
ResponseB responseB = execute(requestB);
}我们发现,泛型成功转换了!
至此,泛型相关知识总结完毕~