1. Why ——引入泛型机制的原因
假如我们想要实现一个 String 数组,并且要求它可以动态改变大小,这时我们都会想到用 ArrayList 来聚合 String 对象。然而,过了一阵,我们想要实现一个大小可以改变的 Date 对象数组,这时我们当然希望能够重用之前写过的那个针对 String 对象的 ArrayList 实现。
在 Java 5 之前,ArrayList 的实现大致如下:
- 1 public class ArrayList {
- 2 public Object get(int i) {...
- }
- 3 public void add(Object o) {...
- }
- 4...5 private Object[] elementData;
- 6
- }
从以上代码我们可以看到,用于向 ArrayList 中添加元素的 add 函数接收一个 Object 型的参数,从 ArrayList 获取指定元素的 get 方法也返回一个 Object 类型的对象,Object 对象数组 elementData 存放这 ArrayList 中的对象, 也就是说,无论你向 ArrayList 中放入什么类型的类型,到了它的内部,都是一个 Object 对象。
基于继承的泛型实现会带来两个问题:第一个问题是有关 get 方法的,我们每次调用 get 方法都会返回一个 Object 对象,每一次都要强制类型转换为我们需要的类型,这样会显得很麻烦;第二个问题是有关 add 方法的,假如我们往聚合了 String 对象的 ArrayList 中加入一个 File 对象,编译器不会产生任何错误提示,而这不是我们想要的。
所以,从 Java 5 开始,ArrayList 在使用时可以加上一个类型参数(type parameter),这个类型参数用来指明 ArrayList 中的元素类型。类型参数的引入解决了以上提到的两个问题,如以下代码所示:
- ArrayList s = new ArrayList();
- s.add("abc");
- String s = s.get(0); //无需进行强制转换
- s.add(123); //编译错误,只能向其中添加String对象
- ...
在以上代码中,编译器 "获知"ArrayList 的类型参数 String 后,便会替我们完成强制类型转换以及类型检查的工作。
2. 泛型类
所谓泛型类(generic class)就是具有一个或多个类型参数的类。例如:
- public class Pair {
- private T first;
- private U second;
- public Pair(T first, U second) {
- this.first = first;
- this.second = second;
- }
- public T getFirst() {
- return first;
- }
- public U getSecond() {
- return second;
- }
- public void setFirst(T newValue) {
- first = newValue;
- }
- public void setSecond(U newValue) {
- second = newValue;
- }
- }
上面的代码中我们可以看到,泛型类 Pair 的类型参数为 T、U,放在类名后的尖括号中。这里的 T 即 Type 的首字母,代表类型的意思,常用的还有 E(element)、K(key)、V(value)等。当然不用这些字母指代类型参数也完全可以。
实例化泛型类的时候,我们只需要把类型参数换成具体的类型即可,比如实例化一个 Pair<T, U> 类我们可以这样:
- Pair pair = new Pair();
3. 泛型方法
所谓泛型方法,就是带有类型参数的方法,它既可以定义在泛型类中,也可以定义在普通类中。例如:
- public class ArrayAlg {
- public static T getMiddle(T[] a) {
- return a[a.length / 2];
- }
- }
以上代码中的 getMiddle 方法即为一个泛型方法,定义的格式是类型变量放在修饰符的后面、返回类型的前面。我们可以看到,以上泛型方法可以针对各种类型的数组调用,在这些数组的类型已知切有限时,虽然也可以用过重载实现,不过编码效率要低得多。调用以上泛型方法的示例代码如下:
- String[] strings = {
- "aa",
- "bb",
- "cc"
- };
- String middle = ArrayAlg.getMiddle(names);
4. 类型变量的限定
在有些情况下,泛型类或者泛型方法想要对自己的类型参数进一步加一些限制。比如,我们想要限定类型参数只能为某个类的子类或者只能为实现了某个接口的类。相关的语法如下:
<T extends BoundingType>(BoundingType 是一个类或者接口)。其中的 BoundingType 可以多于 1 个,用 "&" 连接即可。
5. 深入理解泛型的实现
实际上,从虚拟机的角度看,不存在 "泛型" 概念。比如上面我们定义的泛型类 Pair,在虚拟机看来(即编译为字节码后),它长的是这样的:
- public class Pair {
- private Object first;
- private Object second;
- public Pair(Object first, Object second) {
- this.first = first;
- this.second = second;
- }
- public Object getFirst() {
- return first;
- }
- public Object getSecond() {
- return second;
- }
- public void setFirst(Object newValue) {
- first = newValue;
- }
- public void setSecond(Object newValue) {
- second = newValue;
- }
- }
上面的类是通过类型擦除得到的,是 Pair 泛型类对应的原始类型(raw type)。类型擦除就是把所有类型参数替换为 BoundingType(若未加限定就替换为 Object)。
我们可以简单地验证下,编译 Pair.java 后,键入 "javap -c -s Pair" 可得到:
上图中带 "descriptor" 的行即为相应方法的签名,比如从第四行我们可以看到 Pair 构造方法的两个形参经过类型擦除后均已变为了 Object。
由于在虚拟机中泛型类 Pair 变为它的 raw type,因而 getFirst 方法返回的是一个 Object 对象,而从编译器的角度看,这个方法返回的是我们实例化类时指定的类型参数的对象。实际上, 是编译器帮我们完成了强制类型转换的工作。也就是说编译器会把对 Pair 泛型类中 getFirst 方法的调用转化为两条虚拟机指令:
第一条是对 raw type 方法 getFirst 的调用,这个方法返回一个 Object 对象;第二条指令把返回的 Object 对象强制类型转换为当初我们指定的类型参数类型。
我们通过以下的代码来直观的感受下:
- 1 public class Pair {
- 2 //请见上面贴出的代码
- 3 4 public static void main(String[] args) {
- 5 String first = "first",
- second = "second";
- 6 Pair p = new Pair(first, second);
- 7 String result = p.getFirst();
- 8
- }
- 9 10
- }
编译后我们通过 javap 查看下生成的字节码:
我们重点关注下上面标着 "17:" 的那行,根据后面的注释,我们知道这是对 getFirst 方法的调用,可以看到他的返回类型的确是 Object。
我们再看下标着 "20:" 的那行,是一个 checkcast 指令,字面上我们就可以知道这条指令的含义是检查类型转换是否成功,再看后面的注释,我们这里确实存在一个到 String 的强制类型转换。
类型擦除也会发生于泛型方法中,如以下泛型方法:
- public static extends Comparable> T min(T[] a)
编译后经过类型擦除会变成下面这样:
- public static Comparable min(Comparable[] a)
方法的类型擦除会带来一些问题,考虑以下的代码:
- 1 public class DateInterval extends Pair {
- 2 public DateInterval(Date first, Date second) {
- 3 super(first, second);
- 4
- }
- 5 6 7 public void setSecond(Date second) {
- 8
- if (second.compareTo(getFirst()) >= 0) {
- 9 super.setSecond(second);
- 10
- }
- 11
- }
- 12 13
- }
以上代码经过类型擦除后,变为:
- 1 public class DateInterval extends Pair {
- 2 3...4 public void setSecond(Date second) {
- 5
- if (second.compareTo(getFirst()) >= 0) {
- 6 super.setSecond(second);
- 7
- }
- 8
- }
- 9 10
- }
而在 DateInterval 类还存在一个从 Pair 类继承而来的 setSecond 的方法(经过类型擦除后)如下:
- public void setSecond(Object second)
现在我们可以看到,这个方法与 DateInterval 重写的 setSecond 方法具有不同的方法签名(形参不同),所以是两个不同的方法,然而这两个方法之前却是 override 的关系。考虑以下的代码:
- DateInterval interval = new DateInterval(...);
- Pair pair = interval;
- Date aDate = new Date(...);
- pair.setSecond(aDate);
由以上代码可知,pair 实际引用的是 DateInterval 对象,因此应该调用 DateInterval 的 setSecond 方法,这里的问题是类型擦除与多态发生了冲突。
我们来梳理下为什么会发生这个问题:pair 在之前被声明为类型 Pair<Date, Date>,该类在虚拟机看来只有一个 "setSecond(Object)" 方法。因此在运行时,虚拟机发现 pair 实际引用的是 DateInterval 对象后,会去调用 DateInterval 的 "setSecond(Object)",然而 DateInterval 类中却只有 "setSecond(Date)" 方法。
解决这个问题的方法是由编译器在 DateInterval 中生成一个桥方法:
- public void setSecond(Object second) {
- setSecond((Date) second);
- }
我们再来通过 javap 来感受下:
我们可以看到,在 DateInterval 类中存在两个 setSecond 方法,第一个 setSecond 方法(即我们定义的 setSecond 方法)的形参为 Date,第二个 setSecond 方法的形参是 Object,第二个方法就是编译器为我们生成的桥方法。我们可以看到第二个方法中存在到 Date 的强制类型转换,而且调用了第一个 setSecond 方法。
综合以上,我们知道了泛型机制的实现实际上是编译器帮我们分担了一些麻烦的工作。一方面通过使用类型参数,可以告诉编译器在编译时进行类型检查;另一方面,原本需要我们做的强制类型转换的工作也由编译器为我们代劳了。
6. 注意事项
(1) 不能用基本类型实例化类型参数
也就是说,以下语句是非法的:
- Pair < int,
- int > pair = new Pair < int,
- int > ();
不过我们可以用相应的包装类型来代替。
(2)不能抛出也不能捕获泛型类实例
泛型类扩展 Throwable 即为不合法,因此无法抛出或捕获泛型类实例。但在异常声明中使用类型参数是合法的:
- public static extends Throwable> void doWork(T t) throws T {
- try {
- ...
- } catch (Throwable realCause) {
- t.initCause(realCause);
- throw t;
- }
- }
(3)参数化类型的数组不合法
在 Java 中,Object[] 数组可以是任何数组的父类(因为任何一个数组都可以向上转型为它在定义时指定元素类型的父类的数组)。考虑以下代码:
- String[] strs = new String[10];
- Object[] objs = strs;
- obj[0] = new Date(...);
在上述代码中,我们将数组元素赋值为满足父类(Object)类型,但不同于原始类型(Pair)的对象,在编译时能够通过,而在运行时会抛出 ArrayStoreException 异常。
基于以上原因,假设 Java 允许我们通过以下语句声明并初始化一个泛型数组:
- Pair[] pairs = new Pair[10];
那么在虚拟机进行类型擦除后,实际上 pairs 成为了 Pair[] 数组,我们可以将它向上转型为 Object[] 数组。这时我们若往其中添加 Pair<Date, Date> 对象,便能通过编译时检查和运行时检查,而我们的本意是只想让这个数组存储 Pair<String, String> 对象,这会产生难以定位的错误。因此,Java 不允许我们通过以上的语句形式声明并初始化一个泛型数组。
可用如下语句声明并初始化一个泛型数组:
- Pair[] pairs = (Pair[]) new Pair[10];
(4)不能实例化类型变量
不能以诸如 "new T(...)", "new T[...]", "T.class" 的形式使用类型变量。Java 禁止我们这样做的原因很简单,因为存在类型擦除,所以类似于 "new T(...)" 这样的语句就会变为 "new Object(...)", 而这通常不是我们的本意。我们可以用如下语句代替对 "new T[...]" 的调用:
- arrays = (T[]) new Object[N];
(5)泛型类的静态上下文中不能使用类型变量
注意,这里我们强调了泛型类。因为普通类中可以定义静态泛型方法,如上面我们提到的 ArrayAlg 类中的 getMiddle 方法。关于为什么有这样的规定,请考虑下面的代码:
- public class People {
- public static T name;
- public static T getName() {
- ...
- }
- }
我们知道,在同一时刻,内存中可能存在不只一个 People<T> 类实例。假设现在内存中存在着一个 People<String> 对象和 People<Integer> 对象,而类的静态变量与静态方法是所有类实例共享的。那么问题来了,name 究竟是 String 类型还是 Integer 类型呢?基于这个原因,Java 中不允许在泛型类的静态上下文中使用类型变量。
7. 类型通配符
介绍类型通配符前,首先介绍两点:
(1)假设 Student 是 People 的子类,Pair<Student, Student> 却不是 Pair<People, People> 的子类,它们之间不存在 "is-a" 关系。
(2)Pair<T, T> 与它的原始类型 Pair 之间存在 "is-a" 关系,Pair<T, T> 在任何情况下都可以转换为 Pair 类型。
现在考虑这样一个方法:
- public static void printName(Pair p) {
- People p1 = p.getFirst();
- System.out.println(p1.getName()); //假设People类定义了getName实例方法
- }
在以上的方法中,我们想要同时能够传入 Pair<Student, Student> 和 Pair<People, People> 类型的参数,然而二者之间并不存在 "is-a" 关系。在这种情况下,Java 提供给我们这样一种解决方案:使用 Pair<? extends People> 作为形参的类型。也就是说,Pair<Student, Student> 和 Pair<People, People> 都可以看作是 Pair<? extends People> 的子类。
形如 "BoundingType>" 的代码叫做通配符的子类型限定。与之对应的还有通配符的超类型限定,格式是这样的:BoundingType>。
现在我们考虑下面这段代码:
- Pair students = new Pair(student1, student2);
- Pairextends People> wildchards = students;
- wildchards.setFirst(people1);
以上代码的第三行会报错,因为 wildchards 是一个 Pair<? extends People> 对象,它的 setFirst 方法和 getFirst 方法是这样的:
- void setFirst(? extends People)
- ? extends People getFirst()
对于 setFirst 方法来说,会使得编译器不知道形参究竟是什么类型(只知道是 People 的子类),而我们试图传入一个 People 对象,编译器无法判定 People 和形参类型是否是 "is-a" 的关系,所以调用 setFirst 方法会报错。而调用 wildchards 的 getFirst 方法是合法的,因为我们知道它会返回一个 People 的子类,而 People 的子类 "always is a People"。(总是可以把子类对象转换为父类对象)
而对于通配符的超类型限定的情况下,调用 getter 方法是非法的,而调用 setter 方法是合法的。
除了子类型限定和超类型限定,还有一种通配符叫做无限定的通配符,它是这样的:。这个东西我们什么时候会用到呢?考虑一下这个场景,我们调用一个会返回一个 getPairs 方法,这个方法会返回一组 Pair
- Pair<?>[] pairs = getPairs(...);
对于无限定的通配符,调用 getter 方法和 setter 方法都是非法的。
来源: