Java 泛型详解

Java 泛型详解

前言今天原本正在快乐的摸鱼 ing。突然,坐我旁边的阿里大佬估计是发现我很闲,于是说:我看你挺闲啊,小伙子。你泛型熟悉吗?我考你一道题,你要是能实现,那你的泛型可以出师了!可以年薪百万不是梦了!那我听到这话,那男人不能说自己不行啊!于是我果断答应了下来,心想有什么题能难倒我?我 Java 可有一坤年! 应用场景是封装给第三方接口的 SDK

题目

如图所示,我希望执行 execute() 方法之后,不同的调用方能够返回不同的响应。例如:RequestA 调用方法后会返回 ResponseA,RequestB 调用方法后会返回 ResponseB

看到这个题,我就蒙了。这还是我熟悉的泛型吗?泛型还能这样吗?废话不多说,我们先从泛型的基础开始,通过回顾基础知识点来加深我们的理解。

一、泛型概述1. 泛型定义泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参列表,普通方法的形参列表中,每个形参的数据类型是确定的,而变量是一个参数。在调用普通方法时需要传入对应形参数据类型的变量(实参),若传入的实参与形参定义的数据类型不匹配,则会报错

那参数化类型是什么意思呢?以方法的定义为例,在方法定义时,将方法签名中的形参的数据类型也设置为参数(也可称之为类型参数),在调用该方法时再从外部传入一个具体的数据类型和变量

泛型的本质是为了将类型参数化, 也就是说在泛型使用过程中,数据类型被设置为一个参数,在使用时再从外部传入一个数据类型;而一旦传入了具体的数据类型后,如果传入变量(实参)的数据类型和形参的数据类型不匹配,编译器就会直接报错。这种参数化类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法

2. 泛型使用场景我们可以看下面这个使用场景:👇🏻

在 ArrayList 集合中,允许放入所有包装类型的对象,假设现在这个 ArrayList 中只存储了 String 这一种数据类型

上述代码能够正常执行。在遍历 ArrayList 集合时,程序可以通过向下转型自动将 Object 转换为 String 类型对象

但如果在添加 String 对象时,不小心添加了一个 Integer 对象,会发生什么?

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

输出:

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

使用泛型改造后的代码如下:

就是一个泛型。其限制了集合中存放对象的数据类型只能是 String 类型,当添加一个非 String 对象时,编译器会直接报错。这样,我们便解决了上面产生的ClassCastException异常的问题(这样体现了泛型的类型安全检测机制)

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) 是一个泛型方法,其使用的类型参数是方法签名中定义的类型参数哪怕泛型类中定义的类型参数和泛型方法中定义的类型参数标识都是 。但他们彼此之前是独立的。也就是说,泛型方法始终以自己声明的类型参数为准2. 泛型方法的注意事项表明该方法声明了一个类型参数 T,并且这个类型参数 T 只能在该方法中使用为了避免混淆,如果在一个泛型类中存在泛型方法,那么两者的类型参数最好不要同名与泛型类的类型参数定义一样,泛型方法中的 T 可以写为`任意标识`,常见的如 T、E、K、V 等形式的参数常用于表示泛型在泛型类中,静态成员不能使用泛型类定义的类型参数。但我们可以把静态成员方法定义成一个泛型方法3. 泛型方法的使用泛型类,在创建类的对象的时候确定类型参数的具体类型泛型方法,在调用方法的时候再确定类型参数的具体类型泛型方法签名中声明的类型参数只能在该方法里使用,而泛型接口、泛型类中声明的类型参数则可以在整个接口、类中使用

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

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

4. 泛型方法的类型推断在调用泛型方法的时候,可以显式地指定类型参数,也可以不指定当泛型方法的形参列表中有多个类型参数时,在不指定类型参数的情况下,方法中声明的的类型参数为泛型方法中的几种类型参数的共同父类的最小级,直到 Object在指定了类型参数的时候,传入泛型方法中的实参的数据类型必须为指定数据类型或者其子类五、类型擦除1. 类型擦除的定义泛型的本质是将数据类型参数化,它通过擦除的方式来实现,即编译器会在编译期间擦除代码中的所有泛型语法并相应的做出一些类型转换动作

换而言之,泛型信息只存在于代码编译阶段,在代码编译结束后,与泛型相关的信息会被擦除掉,专业术语叫做 类型擦除。也就是说,成功编译过后的 class 文件中不包含任何泛型信息,泛型信息不会进入到运行时阶段

示例:

一、假如我们给 ArrayList 集合传入两种不同的数据类型,那比较他们的类信息是一样的吗?

代码语言:javascript代码运行次数:0运行复制public class GenericType {

public static void main(String[] args) {

ArrayList arrayString = new ArrayList();

ArrayList arrayInteger = new ArrayList();

System.out.println(arrayString.getClass() == arrayInteger.getClass()); // 输出 True

}

}在这个例子中,我们定义了两个 ArrayList 集合,不过一个是 ArrayList< String>,只能存储字符串。一个是 ArrayList< Integer>,只能存储整型对象。我们通过 arrayString 对象和 arrayInteger 对象的 getClass() 方法获取它们的类信息并比较,发现结果为 true

那明明我们在 < > 中传入了两种不同的数据类型,按照上文所说的,它们的类型参数 T 不是应该被替换成我们传入的数据类型了吗,那为什么它们的类信息还是相同呢?

因为:在编译期间,所有的泛型信息都会被擦除, ArrayList 和 ArrayList 类型,在编译后都会变成 ArrayList 类型

二、再看一个例子。假设定义一个泛型类如下。观察编译后的泛型参数被擦除成了什么类型

代码语言:javascript代码运行次数:0运行复制public class Caculate {

private T num;

}将这个泛型类反编译:

代码语言:javascript代码运行次数:0运行复制public class Caculate {

public Caculate() {} // 默认构造器,不用管

private Object num; // T 被替换为 Object 类型

}可以发现编译器擦除了 Caculate 类后面的泛型标识 ,并且将 num 属性的数据类型替换为 Object 类型,替换了泛型类型 T 的数据类型我们称之为 原始数据类型

是不是所有的类型参数被擦除后都以 Object 类进行替换呢?

答案是否定的,大部分情况下,类型参数 T 被擦除后都会以 Object 类进行替换

而有一种情况则不是,那就是使用到了 extends 和 super 语法的有界类型参数(即泛型通配符,后面我们会详细解释)

2. 泛型擦除的原理假如我们定义了一个 ArrayList 泛型集合,若向该集合中插入 String 类型的对象,不需要运行程序,编译器就会直接报错。这里可能有小伙伴就产生了疑问:

不是说泛型信息在编译的时候就会被擦除掉吗?那既然泛型信息被擦除了,如何保证我们在集合中只添加指定的数据类型的对象呢?换而言之,我们虽然定义了 ArrayList 泛型集合,但其泛型信息最终被擦除后就变成了 ArrayList 集合,那为什么不允许向其中插入 String 对象呢?那 Java 是如何解决这个问题的呢?

其实在创建一个泛型类的对象时, Java 编译器是先检查代码中传入 的数据类型,并记录下来,然后再对代码进行编译,编译的同时进行类型擦除;如果需要对被擦除了泛型信息的对象进行操作,编译器会自动将对象进行类型转换

可以把泛型的类型安全检查机制和类型擦除想象成演唱会的验票机制

以 ArrayList< Integer> 泛型集合为例:

当我们在创建一个 ArrayList< Integer > 泛型集合的时候,ArrayList 可以看作是演唱会场馆,而< T >就是场馆的验票系统,Integer 是验票系统设置的门票类型当验票系统设置好为< Integer >后,只有持有 Integer 门票的人才可以通过验票系统,进入演唱会场馆(集合)中;若是未持有 Integer 门票的人想进场,则验票系统会发出警告(编译器报错)在通过验票系统时,门票会被收掉(类型擦除),但场馆后台(JVM)会记录下观众信息(泛型信息)进场后的观众变成了没有门票的普通人(原始数据类型)。但是,在需要查看观众的信息时(操作对象),场馆后台可以找到记录的观众信息(编译器会自动将对象进行类型转换)擦除 ArrayList 的泛型信息后,get() 方法的返回值将返回 Object 类型,但编译器会自动插入 Integer 的强制类型转换。也就是说,编译器把 get() 方法调用翻译为两条字节码指令:

对原始方法 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 implements List {...}

我们发现,ArrayList 是一个泛型类,它的父类 List也是一个泛型接口。那如果我们把 T 泛型参数都传相同的类型,是否还可以将子类的对象赋值给父类的引用呢?

代码语言:javascript代码运行次数:0运行复制public class GenericType {

public static void main(String[] args) {

List list = new ArrayList(); // 可以赋值

}

}我们发现可以成功进行转换。但两者的泛型数据类型必须相同

那既然 Java 中对象可以向上转型。泛型可以向上转型吗?我们知道 Number 是 Integer 的父类。我们能将 ArrayList 对象赋值给 List 父类引用吗?

代码语言:javascript代码运行次数:0运行复制public class GenericType {

public static void main(String[] args) {

List list01 = new ArrayList(); // 编译错误

ArrayList list02 = new ArrayList(); // 编译错误

}

}不难发现,上述代码会报错。这说明了:在一般的泛型中,泛型类型不能向上转型

我们不妨试试,如果 Java 允许泛型类型可以向上转型会发生什么情况呢?

代码语言:javascript代码运行次数:0运行复制public class GenericType {

public static void main(String[] args) {

// 创建一个 ArrayList 集合

ArrayList integerList = new ArrayList<>();

// 添加一个 Integer 对象

integerList.add(new Integer(123));

// “向上转型”为 ArrayList

ArrayList numberList = integerList;

// 添加一个 Float 对象,Float 也是 Number 的子类,编译器不报错

numberList.add(new Float(12.34));

// 从 ArrayList 集合中获取索引为 1 的元素(即添加的 Float 对象):

Integer n = integerList.get(1); // ClassCastException,运行出错

}

}我们发现,当我们对 ArrayList 向上转型为 ArrayList 类型后,这个 ArrayList 集合就可以接收 Float 对象了,因为 Float 类是 Number 类的子类。这显然是有问题的。当通过 get()方法获取元素时,编译器会自动将 Float 对象强转为 Integer 对象,而这会产生 ClassCastException 异常

正因如此,编译器为了避免这种错误。不允许泛型类型的向上转型

2. 泛型通配符的三种类型在现实编码中,确实有这样的需求,希望泛型能够处理 某一类型范围内 的类型参数,比如某个泛型类和它的子类,为此 Java 引入了 泛型通配符 这个概念

泛型通配符有 3 种形式:

:被称作无限定的通配符:被称作有上界的通配符:被称作有下界的通配符在引入泛型通配符之后,我们便得到了一个在逻辑上可以表示为某一类型参数范围的父类引用类型

七、进阶用法公司架构大佬给我出了一道题:我希望执行 execute() 方法之后,不同的调用方能够返回不同的响应结果,并且 execute()方法内部是没有任何实现的。例如:RequestA 调用方法后会返回 ResponseA,RequestB 调用方法后会返回 ResponseB

那具体应该如何实现呢?我们首先想到的肯定是需要通过泛型来建立 Request 和 Response 之间的联系,那这个泛型类型应该如何写呢?

代码:

定义 execute 泛型方法代码语言:javascript代码运行次数:0运行复制public class Actuator {

// 泛型方法

public static T execute(Request req) {

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);

}我们发现,泛型成功转换了!

至此,泛型相关知识总结完毕~

黄金推荐

川大石碧院士&廖学品教授团队Small:通过原位生成铈–钨纳米粒子制备高性能再生胶原纤维基辐射防护材料
iPhoneX“刘海”深度解析:刷脸全靠它
365完美体育app

iPhoneX“刘海”深度解析:刷脸全靠它

🕒 10-29 💰 7011
历史上的王昭君是怎么死的?死因藏在了她与汉元帝的最后一封信中