「 Java 基础 - 泛型 」一文说清 Java 泛型中的通配符 T、E、K、V、N、?和 Object 的区别和含义
前言
当我们在阅读源码的时候通常会看到如下所示代码中存在“E”、“T”或“?”,那么,这些大写字母到底有着怎样的含义呢?接下来我们具体讨论一下。
一、什么是泛型?
Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许开发者在编译时检测到非法的类型。
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
泛型(Generic type 或者 generics)是对 Java 语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类。
可以把类型参数看作是使用参数化类型时指定的类型的一个占位符,就像方法的形式参数是运行时传递的值的占位符一样。
1.1 泛型的语法规则
a、所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前;
eg 1: 比如说这是一个用来打印数组的泛型方法:
b、同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符;
eg 2: 比如下面这个方法:
c、类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符;
d、泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(int double char 等);
e、泛型的参数类型可以使用 extends 语句,例如
<T extends superclass>
。
1.2 泛型的通配符
其实,Java 中的 **T,E,K,V,?**本质上都是通配符,常用于泛型定义的时候,E、T、K、V、N 等这些字母之间没什么区别,使用 T 的地方完全可以换成 U、S、Z 等任意字母。当然,一般我们会使用一些常用的字母,这些字符一般是一些类型的缩写,约定的定义如下:
1、T (type) 表⽰具体的⼀个 java 类型;
2、K、V (key value) 分别代表 java 键值中的 Key 和 Value;
3、E (element) 代表 Element;
4、N Number 代表数值类型;
5、**?**表示不确定的 java 类型;
1.3 泛型类
泛型类的定义格式:
格式:
修饰符 class 类名<类型>{}
范例:
public class Generic<T>{}
此处 T 可以随便写为任意标识,常见的如 T、E、K、V 等形式的参数常用于表示泛型.
1.4 泛型方法
格式:
修饰符<类型> 返回值类型方法名(类型 变量名){}
范例:
public <T> void show(T t){}
1.5 泛型接口
格式:
修饰符 interface 接口名<类型>{ }
范例:
public interface Generic<T>{ }
定义接口:
定义接口实现类:
1.6 可变参数
1.6.1 定义格式
格式:
修饰符 返回值类型 方法名(数据类型... a){ }
范例:
public static int sum(int... a){ }
1.6.2 示例
若可变参数与不可变参数在同一个方法定义中出现,则不可变参数要放置在第一个参数位置,例如:
1.6.3 可变参数在实际中的应用
Arrays 工具类中有一个静态方法:
public static List asList(T… a):返回由指定数组支持的固定大小的列表
注意
:返回的集合不能做增删操作,可以做修改操作
List 接口中有一个静态方法:
public static List of(E… element):返回包含任意数量元素的不可变列表
注意
:返回的集合不能做增删改操作
Set 接口中有一个静态方法:
public static Set of(E… elements):返回一个包含任意数量元素的不可变集合
注意
:a、在给元素的时候,不能给重复的元素;
b、返回的集合不能做增删操作,没有修改的方法;
二、泛型的好处
**1,类型安全
泛型的主要目标是提高 Java 程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在一个高得多的程度上验证类型假设。没有泛型,这些假设就只存在于程序员的头脑中(或者如果幸运的话,还存在于代码注释中)。
2,消除强制类型转换
泛型的一个附带好处是,消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会。
3,潜在的性能收益
泛型为较大的优化带来可能。在泛型的初始实现中,编译器将强制类型转换(没有泛型的话,程序员会指定这些强制类型转换)插入生成的字节码中。但是更多类型信息可用于编译器这一事实,为未来版本的 JVM 的优化带来可能。由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改。所有工作都在编译器中完成,编译器生成类似于没有泛型(和强制类型转换)时所写的代码,只是更能确保类型安全而已。
三、通配符
3.1 最常用的 T,E,K,V 和 ?
T,E,K,V,?是最常用的通配符,也是最容易理解的,它们是这样约定的:
? 表示不确定的 java 类型
T (type) 表示具体的一个 java 类型
K V (key value) 分别代表 java 键值中的 Key Value
E (element) 代表 Element
3.2 "?" 无界通配符
有一个父类 Animal 和几个子类,如狗、猫等,现在我需要一个动物的列表,第一个想法是像这样的:
但是老板的想法确实这样的:
为什么要使用通配符而不是简单的泛型呢?通配符其实在声明局部变量时是没有什么意义的,但是当你为一个方法声明一个参数时,它是非常重要的。
所以总结如下:
对于不确定或者不关心实际要操作的类型,可以使用无限制通配符(尖括号里一个问号,即 <?> ),表示可以持有任何类型。像 countLegs1 方法中,限定了上界,但是不关心具体类型是什么,所以对于传入的 Animal 的所有子类都可以支持,并且不会报错。而 countLegs2 就不行,当调用 countLegs2 时,程序会报
类型转换异常
。
3.3 ?extends T 上界通配符
上界通配符:用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。
在类型参数中使用 extends 表示这个泛型中的参数必须是 E 或者 E 的子类,这样有两个好处:
如果传入的类型不是 E 或者 E 的子类,编译不成功
泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用
类型参数列表中如果有多个类型参数上限,用逗号分开
3.4 ? super T 下界通配符
下界通配符: 用 super 进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object
在类型参数中使用 super 表示这个泛型中的参数必须是 E 或者 E 的父类。
a 类型 “大于等于” b 的类型,这里的“大于等于”是指 a 表示的范围比 b 要大,因此装得下 a 的容器也就能装 b。
3.5 T 和 ?的区别
3.5.1 通过 T 来确保泛型参数的一致性
像下面的代码中,约定的 T 是 Number 的子类才可以,但是申明时是用的 String ,所以就会飘红报错。
不能保证两个 List 具有相同的元素类型的情况
上面的代码在编译器并不会报错,但是当进入到testNon
方法内部操作时(比如赋值),对于a
和b
而言,就还是需要进行类型转换。
3.5.2 类型参数可以多重限定而通配符不行
使用 & 符号设定多重边界(Multi Bounds),指定泛型类型 T 必须是 DemoInterfaceA 和 DemoInterfaceB 的共有子类型,此时变量 t 就具有了所有限定的方法和属性。对于通配符来说,因为它不是一个确定的类型,所以不能进行多重限定。
3.5.3 通配符可以使用超类限定而类型参数不行
类型参数 T 只具有一种类型限定方式:
但是通配符 ? 可以进行两种限定:
3.5.4 注意事项
?和 T 都表示不确定的类型,区别在于我们可以对 T 进行操作,但是对 ?不行,比如如下这种 :
简单总结下:
T 是一个确定的类型,通常用于泛型类和泛型方法的定义,?是一个不确定的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。
3.6 T 和 Object 的区别
Object 是 Java 的超类(所有对象的父类),在编码过程中就难免出现类型转化问题,且在编译阶段不会报错,到了运行阶段才暴露问题,大大降低了程序的安全性和健壮性!
3.6.1 转型的分类
向上转型 -> 用父类声明一个子类对象
实例:
向下转型 -> 将父类对象强转为其子类
实例:
3.6.2 类型转换的问题
当我们使用 Object 作为泛型来使用时,不仅写起来麻烦,还要不停的进行类型转化,还很容易出现问题,如下实例:
程序运行起来就会报类型转换异常
3.7 Class<T> 和 Class<?> 区别
Class<T>
和 Class<?>
最常见的是在反射场景下的使用,这里以用一段发射的代码来说明下。
对于上述代码,在运行期,如果反射的类型不是DemoA
类,那么一定会报 java.lang.ClassCastException
错误。
对于这种情况,则可以使用下面的代码来代替,使得在在编译期就能直接 检查到类型的问题:
Class<T>
在实例化的时候,T
要替换成具体类。Class<?>
它是个通配泛型,? 可以代表任何类型,所以主要用于声明时的限制情况。比如,我们可以这样做申明:
所以总结如下:
当不知道定声明什么类型的
Class
的时候可以定义一 个Class<?>
,如果已经明确要反射的类型,必须让当前的类也指定T
四、泛型的 PECS 原则
**PECS **是 Producer Extends,**Consumer Super **的缩写.
4.1 Producer Extends
现在要扩展 Stack 的功能,增加一个 pushAll 方法,我们第一版的实现可能是这样的。
上面的写法在使用时可能会报函数要求的参数类型与提供的不一致
问题,如下图所示。
为了解决这个问题,我们需要做出如下修改,也就是把参数类型改成Iterable<? extends E>
,这是一种有限制的通配符类型(bounded wildcard type
),意思是"E 的某个子类型的Iterable
接口"。pushAll
的source
参数产生 E 实例供 Stack 使用,也就是source
是生产者,因此 source 的类型是Iterable<? extends E>
。
4.2 Consumer Super
现在要扩展 Stack 的功能,增加一个与 pushAll 对应的 popAll 方法,我们第一版的实现可能是这样的。
上面的写法在使用时可能会报函数要求的参数类型与提供的不一致
问题,如下代码所示:
为了解决这个问题,我们需要做出如下修改,也就是把参数类型改成Collection<? super E>
,意思是"E 的某种超类
集合”。popAll 的 destination 参数通过 Stack 消费 E 实例,也就是destination
是消费者,因此destination
的类型是Collection<? super E>
,如下所示。
4.3 PECS 原则总结
从上述 4.1 和 4.2 两方面的分析,总结 PECS 原则如下:
如果要从集合中读取类型 T 的数据,并且不能写入,可以使用 ? extends 通配符;(Producer Extends)
如果要从集合中写入类型 T 的数据,并且不需要读取,可以使用 ? super 通配符;(Consumer Super)
如果既要存又要取,那么就不要使用任何通配符。
总结
这篇文章我们从 java 源码中的泛型通配符出发,介绍了什么是泛型、使用泛型的好处、泛型的通配符区别及含义,以及泛型的 PECS 原则,如有疏漏及错误,烦请留言补充。
参考
1、Java泛型T、E、K、V、N、和Object区别和含义
感谢前人的经验、分享和付出,让我们可以有机会站在巨人的肩膀上眺望星辰大海!
版权声明: 本文为 InfoQ 作者【小刘学编程】的原创文章。
原文链接:【http://xie.infoq.cn/article/b7a8020b60593e81037075053】。文章转载请联系作者。
评论