1. 前言
Java源码中有大量泛型的使用, 但是, 日常开发中, 对泛型特性的了解却仅限于表面的浅浅一层, 所以这里记录泛型相关的一些知识.
2. 泛型概述
2.1 什么是泛型
泛型, 即”参数化类型”. 一提到参数, 最熟悉的就是定义方法时有形参, 然后调用此方法时传递实参. 那么参数化类型怎么理解呢? 顾名思义, 就是将类型由原来的具体的类型参数化, 类似于方法中的变量参数, 此时类型也定义成参数形式(可以称之为类型形参), 然后在使用/调用时传入具体的类型(类型实参). 泛型的本质是为了参数化类型(在不创建新的类型的情况下, 通过泛型指定的不同类型来控制形参具体限制的类型). 也就是说在泛型使用过程中, 操作的数据类型被指定为一个参数, 这种参数类型可以用在类, 接口和方法中, 分别被称为泛型类, 泛型接口, 泛型方法.
2.2 为什么要有泛型
代码示例:
1 | public class Main { |
运行结果:
1 | 泛型测试, item = aaaa |
ArrayList可以存放任意类型, 例子中添加了一个String类型, 添加了一个Integer类型, 在使用时都以String的方式使用, 因此程序崩溃了. 为了解决类似这样的问题(在编译阶段就可以解决), 泛型应运而生.
我们将第一行声明初始化list的代码更改一下, 编译器会在编译阶段就能够帮我们发现类似这样的问题.
1 | List<String> arrayList = new ArrayList<String>(); |
2.3 特性
泛型只在编译阶段有效.看下面的代码
1 | public class Main { |
结果:
1 | class java.util.ArrayList |
通过上面的例子可以证明, 在编译之后程序会采取去泛型化的措施. 也就是说Java中的泛型, 只在编译阶段有效. 在编译过程中, 正确检验泛型结果后, 会将泛型的相关信息擦除, 并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法. 也就是说, 泛型信息不会进入到运行时阶段.
3. 泛型的使用
泛型有三种使用方式, 分别为: 泛型类, 泛型接口, 泛型方法
3.1 泛型类
泛型类型用于类的定义中, 被称为泛型类. 通过泛型可以完成对一组类的操作对外开放相同的接口. 最典型的就是各种容器类, 如: List, Set, Map.
泛型类的最基本写法(这么看可能会有点晕, 会在下面的例子中详解):
1 | class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{ |
一个最普通的泛型类:
1 | //此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型 |
1 | public class Main { |
运行结果:
1 | 泛型测试, key is 123456 |
定义的泛型类, 就一定要传入泛型类型实参么? 并不是这样, 在使用泛型的时候如果传入泛型实参, 则会根据传入的泛型实参做相应的限制, 此时泛型才会起到本应起到的限制作用. 如果不传入泛型类型实参的话, 在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型.
1 | public class Main { |
运行结果:
1 | 泛型测试, key is 111111 |
3.2 泛型接口
泛型接口与泛型类的定义及使用基本相同. 泛型接口常被用在各种类的生产器中, 可以看一个例子:
1 | //定义一个泛型接口 |
当实现泛型接口的类, 未传入泛型实参时:
1 | /** |
当实现泛型接口的类, 传入泛型实参时:
1 | /** |
3.3 泛型通配符
我们知道Integer是Number的一个子类, 同时在特性章节中我们也验证过Generic
为了弄清楚这个问题, 我们使用Generic
1 | public class Main { |
1 | //此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型 |
通过提示信息我们可以看到Generic
回到上面的例子, 如何解决上面的问题? 总不能为了定义一个新的方法来处理Generic
我们可以将上面的方法改一下:
1 | public void showKeyValue(Generic<?> obj){ |
类型通配符一般是使用? 代替具体的类型实参, 注意了, 此处 ? 是类型实参, 而不是类型形参. 再直白点的意思就是, 此处的?和Number, String, Integer一样都是一种实际的类型, 可以把?看成所有类型的父类. 是一种真实的类型.
可以解决当具体类型不确定的时候, 这个通配符就是?; 当操作类型时, 不需要使用类型的具体功能时, 只使用Object类中的功能. 那么可以用? 通配符来表未知类型.
3.4 泛型方法
泛型类, 是在实例化类的时候指明泛型的具体类型, 泛型方法, 是在调用方法的时候指明泛型的具体类型.
1 | /** |
1 | Object obj = genericMethod(Class.forName("Test")); |
3.4.1 泛型方法的基本用法
光看上面的例子有的同学可能依然会非常迷糊, 我们再通过一个例子, 把我泛型方法再总结一下.
1 | public class GenericTest { |
3.4.2 类中的泛型方法
当然这并不是泛型方法的全部, 泛型方法可以出现杂任何地方和任何场景中使用. 但是有一种情况是非常特殊的, 当泛型方法出现在泛型类中时, 我们再通过一个例子看一下
1 | public class GenericFruit { |
3.4.3 泛型方法与可变参数
再看一个泛型方法和可变参数的例子:
1 | public class Main{ |
3.4.4 静态方法与泛型
静态方法有一种情况需要注意一下, 那就是在类中的静态方法使用泛型: 静态方法无法访问类上定义的泛型; 如果静态方法操作的引用数据类型不确定的时候, 必须要将泛型定义在方法上.
即: 如果静态方法要使用泛型的话, 必须将静态方法也定义成泛型方法.
1 | public class StaticGenerator<T> { |
3.4.5 泛型方法总结
泛型方法能使方法独立于类而产生变化, 以下是一个基本的指导原则:
- 无论何时, 如果你能做到, 你就该尽量使用泛型方法. 也就是说, 如果使用泛型方法将整个类泛型化, 那么就应该使用泛型方法. 另外对于一个static的方法而言, 无法访问泛型类型的参数. 所以如果static方法要使用泛型能力, 就必须使其成为泛型方法.
3.5 泛型上下边界
在使用泛型的时候, 我们还可以为传入的泛型类型实参进行上下边界的限制, 如: 类型实参只准传入某种类型的父类或某种类型的子类.
为泛型添加上边界, 即传入的类型实参必须是指定类型的子类型.
1 | public class Main{ |
如果我们把泛型类的定义也改一下:
1 | public class Generic<T extends Number>{ |
1 | //这一行代码也会报错,因为String不是Number的子类 |
再来一个泛型方法的例子:
1 | //在泛型方法中添加上下边界限制的时候,必须在权限声明与返回值之间的<T>上添加上下边界,即在泛型声明的时候添加 |
通过上面的两个例子可以看出: 泛型的上下边界添加, 必须与泛型的声明在一起.
3.6 泛型数组
在Java中 “是不能创建一个确切的泛型类型的数组” 的.
也就是说下面的这个例子是不可以的:
1 | List<String>[] ls = new ArrayList<String>[10]; |
而使用通配符创建泛型数组是可以的, 如下面这个例子:
1 | List<?>[] ls = new ArrayList<?>[10]; |
这样也是可以的:
1 | List<String>[] ls = new ArrayList[10]; |
下面使用Sun的一篇文档的一个例子来说明这个问题:
1 | List<String>[] lsa = new List<String>[10]; // Not really allowed. |
- 这种情况下, 由于JVM泛型的擦除机制, 在运行时JVM是不知道泛型信息的, 所以可以给oa[1]赋上一个ArrayList而不会出现异常, 但是在取出数据的时候却要做一次类型转换, 所以就会出现ClassCastException, 如果可以进行泛型数组的声明, 上面说的这种情况在编译期将不会出现任何的警告和错误, 只有在运行时才会出错.
这个部分写不下去了, 记住一点就好. 不要使用泛型数组!!!