前言
在落笔前,想了很久以什么作为切入点,因为泛型并不是一个具体的接口或工具类,而是一种Java提供的简化编码、增强代码重用率的内在机制,同时提供了仅在编译时的类型检查机制。
大年二十九两眼昏花,呕心沥血之作,祝各位新春快乐~
泛型
Java泛型 (Generics) 的本质是参数化类型,也就是说所操作的数据类型被作为一个参数,可以由使用时输入控制。
切入泛型的最佳角度应该是JCF,也就是Java Collections Framework。Java集合被设计成能保存任何引用类型的对象,可能初学者没有发觉,但我们在使用各种集合工具类时已经应用了泛型,举个例子:
1 | // author: SilenceZheng66 |
我们在尖括号 <>
中传入了包装类 Integer
作为参数,于是我们获得了仅可以存放 Integer
类型实例的 ArrayList
实例 list,在之后的代码编写中,一旦我们试图在 list 中加入非 Integer
的元素,编译器就会报告错误。
这里 ArrayList
其实就是一个泛型类,如果你查看Java源码,你还会发现它继承了泛型虚拟类 AbstractList
并实现了泛型接口 List
,它还具有一些泛型方法,例如 toArray()
和 get()
等等。
OK,这又有些令人困惑,我们知道了通过尖括号传入类型参数是在使用泛型,但什么是泛型类、泛型接口、泛型方法?
其实这很容易理解,在定义类时使用泛型,你就声明了一个泛型类,泛型接口和泛型方法也是如此。只有当类是泛型类(如ArrayList)时,或接口是泛型接口(如List)时,我们才能通过尖括号传入类型参数。下面我们来具体看看如何定义它们。
泛型类和泛型接口
泛型类和泛型接口的声明极其相似,就放在一起说了。泛型类的声明就是在原本类的声明中加入一个类型参数声明部分,这个类型参数声明应置于类名之后,由尖括号包裹。
类型参数声明部分可包含一个或多个类型参数,参数间用逗号隔开。类型参数仅能代表引用类型,作为泛型类实例化时得到的实际参数类型的占位符。下面我声明一个最简单的泛型类,它具有一个类型参数:
1 | // author: SilenceZheng66 |
现在我们可以实例化这个泛型类,传入任何我们想要的引用数据类型:
1 | // author: SilenceZheng66 |
这很酷,泛型类能够存入任意引用类型的实例。如果你对Java源代码有兴趣,你会发现Java中的类型参数名(或称为泛型标记符)经常是T、E、K、V、R、U之类的大写字母。如果你对这感到疑惑,其实这只是一种编码规范,使用Type的大写首字母T占位,表示此处希望接收一个类型;或是用K表示此处接收Key的类型,用V表示此处接收Value的类型。事实上你可以用任何合法参数名作为类型参数。
那么能不能不传入类型实参直接初始化泛型类实例呢?其实是可以的:
1 | // author: SilenceZheng66 |
下面我们来研究泛型接口,它的声明与泛型类近似,在原有基础上增加类型参数声明部分:
1 | // author: SilenceZheng66 |
现在我们有了一个含两个类型参数的泛型接口,并且泛型接口中存在接口抽象方法produce
,它接收一个 T1
类型的参数并返回一个 T2
类型的值。现在,让我们尝试使用泛型类实现这个泛型接口:
1 | // author: SilenceZheng66 |
目前为止,我们见到的东西都十分熟悉,我们的泛型类实现了泛型接口的全部抽象方法,好消息是这能用!我们成功了! 但坏消息是,我们的实现并没有完全发挥泛型接口的实力,GenericInterface
的 produce
方法允许我们接收一个类型的参数,返回另一个不同的类型值,但 GenericClass
只能使这两个类型相同,因为它只有一个类型参数E!那么我们能不能通过增加它的类型参数数量来扩大 GenericClass.produce()
的能力呢?
1 | // author: SilenceZheng66 |
现在,似乎我们做到了,我们通过增加泛型类的类型参数数量,发挥了 produce
方法的全部功能。(注意泛型类的类型参数与泛型接口的类型参数间存在映射关系,这容易产生错误。)现在让我们来试试通过实现泛型接口获得新功能的泛型类 GenericClass
:
1 | // author: SilenceZheng66 |
这些测试也有点意思,总之,我们现在学会了定义泛型接口和实现泛型接口。可能有人会想到泛型接口的类型参数表除了传入泛型外,还能传入固定类型吗?答案是可以的,并且此时接口中抽象方法的对应位置也都需要置为相同实参,比如这个“float->double转换器”:
1 | // author: SilenceZheng66 |
泛型方法
将泛型方法放在后面是有原因的,因为它很容易与“泛型类(接口)中的方法”混淆。回忆 GenericInterface
中的抽象方法 produce
,它似乎用到了泛型,但它是泛型方法吗?答案是否定的,它只是一个存在于泛型接口中的普通方法,需要按照泛型接口接收到的类型参数办事。
然而泛型方法是另一个层面的“自由”,它不需要依赖泛型类(接口)。在定义泛型方法时,同样需要增加一个类型参数声明部分,这个部分应置于方法返回类型前。而在调用泛型方法时,也需要指明类型实参。下面我们先来写一个比较“抽象”的泛型方法:
1 | // author: SilenceZheng66 |
是的,这确实是一个泛型方法,但是没什么用,我们的泛型声明没有得到利用,于是我们可以修改它为如下方法:
1 | // author: SilenceZheng66 |
现在这个泛型方法会接收一个T类型的List,打印其中的全部内容并返回首项,让我们来验证一下:
1 | // author: SilenceZheng66 |
可以看到,独立的泛型方法需要从函数参数表中获取类型参数,而无法使用尖括号传入类型参数。因此,泛型函数通常意味着从函数参数中获取类型参数。我们不妨来尝试一个更奇怪些的泛型函数:
1 | // author: SilenceZheng66 |
我们现在有三个类型参数,来实验一下:
1 | // author: SilenceZheng66 |
程序在执行第二行时报错:java.lang.ClassCastException: java.lang.Float cannot be cast to java.lang.Integer
,并且错误仅指向这一行,证明类型参数K和J都处于未定义状态,函数返回了T类型值,即Float类型。也就是说,虽然我们声明了三个类型参数,但最终仅有占位符T接收到了类型实参,发挥了作用。
现在让我们回到泛型方法容易混淆的话题,这通常出现于在泛型类中定义泛型方法的情况,让我们来看一个例子:
1 | // author: SilenceZheng66 |
现在我们有了一个泛型类中的泛型方法,让我们来试试它:
1 | // author: SilenceZheng66 |
这没问题,但是如果我们把泛型方法中的占位符 T 换成 E1 会发生什么?我们省略替换,直接进行测试:
1 | // author: SilenceZheng66 |
结果还是相同的,即便泛型类中接受了 Integer
类型作为 T1 的实参,但我们仍然可以向泛型方法中传入 String
类型的参数。这说明泛型方法与泛型类之间是独立的。
这里还有另一种情况需要考虑,即泛型类中的静态方法应该如何使用泛型?先说结论:静态方法无法访问类上定义的泛型。如果静态方法操作的引用数据类型不确定的时候,必须将静态方法定义为泛型方法。 我们来看一个例子:
1 | // author: SilenceZheng66 |
表面上看这没什么问题,但编译时会报错:java: 无法从静态上下文中引用非静态类型变量 T
。其实这不难理解,静态方法如果要从所在类获取泛型,那么就丧失了静态的意义,于是我们应该通过将静态方法改写为泛型静态方法来实现其功能:
1 | // author: SilenceZheng66 |
现在我们来尝试使用该方法:
1 | // author: SilenceZheng66 |
也就是说,如果静态方法要使用泛型能力,就必须使其成为泛型方法。其实,我们在设计类和方法时,也需要尽量将问题抽象化,能够使用泛型方法解决的问题,就不必将整个类都泛型化。
泛型与可变参数列表
Well,一口气读到这里可能有点乱,但我们还是要继续探索一些新东西,比如将泛型与可变参数列表结合起来:
1 | // author: SilenceZheng66 |
这里T... somethings
就是一个可变参数列表,它能够接收任意个T类型的参数:
1 | // author: SilenceZheng66 |
执行程序,则 genericFunction
会依次输出 1.1, 1.5 和 true,然后返回 somethings
的运行时类型为 Serializable
。
我们也可以对 genericFunction
做如下修改:
1 | public static <T> T genericFunction(String start, T... somethings){ |
这时返回值的类型就会根据输入来决定,再次使用同一测试代码,返回值类型为String
。这就是泛型与可变参数列表的全部。如果还有更多,那可能是@SafeVarargs
注解等等…
泛型限制
在使用泛型的过程中,我们可能希望将泛型限制在一定范围内,而不是引用类型的全集。让我来举一个例子说明这一点:
1 | // author: SilenceZheng66 |
我们声明了一个泛型方法printList
,在方法中遍历L类型变量l
并打印其元素。但这段代码无法通过编译,因为程序不能确定L类型是否可以作为for-each
循环的目标。于是我们需要对L的类型加以限制,将泛型的范围缩小为可被迭代类型的集合:
1 | // author: SilenceZheng66 |
现在,这段代码可以通过编译并正确的发挥作用,因为我们规定了 L所代表的类型是Iterable
的子类,更确切些,是实现了Iterable
接口的类。
需要注意的是,限制泛型的适配范围仅支持两个关键字,分别是 extends
和 super
:
1 | // author: SilenceZheng66 |
无论 T 是接口还是类,一律采用这两个关键字进行范围限制。
类型通配符
到目前为止,我们谈论的内容重心都在于如何定义泛型(类、接口、方法),现在我希望读者改变一下思路,从使用泛型的角度考虑问题。
让我们来关注一个问题,假设我们希望设计一个函数,它能够接收一个 Number
类型列表,并返回一个逐元素增加 number
后的列表:
1 | // author: SilenceZheng66 |
这个函数是可以正确编译的,并支持所有的 List<Number>
对象,以及它的子类(比如 ArrayList<Number>
)。但是,当我们传入 ArrayList<Float>
会发生什么?
答案是报错:java: 不兼容的类型: java.util.ArrayList<java.lang.Float>无法转换为java.util.List<java.lang.Number>
,这是可以理解的(参数表仅允许List
及其子类)。但是,逻辑上 ArrayList<Float>
也需要被方法允许作为传入参数,这时我们需要一种能够代表所有类型的类型实参,通过这个神奇的类型实参,我们可以表示一种更广义的父类。ArrayList<Float>
和 ArrayList<Number>
都可以是这个父类的“逻辑子类”,或者更抽象一些,对于任何形如 L<N>
的实参,只要满足 L是List及其子类 且 N是Number及其子类
的对象都可以被这个表示所接受。
在Java中,这个能够代表所有类型的类型实参被记为 ?
,它也被称为类型通配符。下面我们来尝试使用它:
1 | // author: SilenceZheng66 |
这里我使用了 extends
限定类型实参的范围,因为我需要的仅仅是 Number及其子类
。这也表明了另一件事:extends
和 super
关键字在泛型的定义和使用中都起作用。
现在我们的方法可以接收任意的数字列表:
1 | // author: SilenceZheng66 |
看到这里,我认为读者对于泛型已经有了一个基本的理解。我认为好的教程是点到为止,留给读者自己探索的空间。对新事物最好的认知规律永远不是直线,而是螺旋上升。
仅在编译时
Well,我们在一开始就提到了“泛型提供了仅在编译时的类型检查机制”,并举出了一个例子说明。那我们是否有办法绕过泛型检查机制,向 ArrayList
中添加非法数据呢,其实是可以的。
1 | // author: SilenceZheng66 |
程序输出结果为[illegal content!]
,说明我们通过反射将字符串加入到了整型 ArrayList
中,这段代码在编译中不会被泛型机制检查。
这是因为程序编译之后会采取去泛型化的措施。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦除(Type Erasure),并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段,通常会由 Object
取代泛型占位符。
创造泛型的目的是提供在编译时检查类型安全的机制,并且通过自动且隐式的类型转换简化编码,这一点还需要读者多加体会。
由于去泛型化的存在,我们也不能对泛型类的实例使用 instanceof
操作,如下代码是非法的,它会产生编译时错误:
1 | // author: SilenceZheng66 |
事实上很多时候原本常规的代码,用上泛型后却行不通,比如实例化一个 Integer
我们可以这样做: Integer i = new Integer()
。但实例化泛型却会发生编译错误,例如 T t = new T()
,此时我们需要借助反射来实现。
参考文献
[1] https://www.cnblogs.com/coprince/p/8603492.html
[2] https://www.runoob.com/java/java-generics.html
[3] https://www.cnblogs.com/springmorning/p/10285780.html
[4] https://www.liaoxuefeng.com/wiki/1252599548343744/1265104600263968
后记
首发于 silencezheng.top,转载请注明出处。