泛型
Java 中的泛型,就是在设计类、接口或方法时,不事先确定操作对象的具体类型,而是将其指定为参数,称为类型参数(type parameters),等到使用这些类、接口或方法时,才将具体类型传递。 使用泛型的好处在于:
-
可重用性:实现代码的模板化
-
可读性:无需进行强制类型转换
-
安全性:在编译阶段发现错误
一、定义泛型
定义在类上
具有一个或多个类型变量的类就属于泛型类(generic class),用一个简单的 Pair 类作为例子:
public class Pair<T> { private T first; private T second; public Pair() { first = null; second = null; } public Pair(T first, T second) { this.first = first; this.second = second; } public T getFirst() { return first; } public T getSecond() { return second; } public void setFirst(T value) { first = value; } public void setSecond(T value) { second = value; } }
类型变量要用<>括起来,置于类名之后。可以有多个类型变量,为不同的域指定不同的类型,比如:
public class Pair<T, U> { private T first; private U second; // ... }
使用时,将类型变量替换成具体的类型就可以:
Pair<Integer> pair = new Pair<>;
从 Java SE 7 开始,构造函数中可以省略泛型类型(又叫“菱形语法”)。
定义在方法上
定义一个带有类型参数的方法:
class ArrayAlg { public static <T> T getMiddle(T... a) { return a[a.length / 2]; } }
String middle = ArrayAlg.<String>getMiddle("hello", "here", "hero"); // String middle = ArrayAlg.getMiddle("hello", "here", "hero");
限定类型变量
有时,一个类或方法并不适用于任意的类型,比如一个计算最小(大)值的方法,必然要求传入的类型要实现 Comparable 接口,于是便要对类型变量加以约束。
class ArrayAlg { public static <T extends Comparable> T getMin(T[] a) { if (a == null || a.length == 0) return null; T min = a[0]; for (int i = 1; i < a.length; i++) { min = min.compareTo(a[i]) > 0 ? a[i] : min; } return min; } }
这种记法:
<T extends BoundingType>
表示 T 应该是限定类型的子类型,T 和限定类型可以是类,也可以是接口。另外,限定类型可以有多个,用 & 分隔:
<T extends Comparable & Serializable>
与继承的限制一样,限定类型中至多有一个类,并且(如果有的话)必须是限定列表中的第一个。
先来看一段简单的代码:
Pair<String> stringPair = new Pair(str1, str2); Pair<Person> personPair = new Pair(person1, person2); System.out.println(stringPair.getClass == personPair.getClass); // always true
为什么会出现这种情况?
编译器会检测泛型使用是否正确,但是,在编译阶段就会对泛型代码进行类型擦除(erased),在必要的地方再进行强制类型转换,就跟未引入泛型时的做法一样。从这个角度来说,Java 其实是“伪泛型”,基本是在编译器的层次中实现的,字节码文件中已不包含关于泛型的信息。在虚拟机的层次中,不存在泛型类型对象,所有对象都属于普通类。正因为这样,理解类型擦除成为弄懂 Java 泛型机制的首要前提。
类型擦除
public class Pair { // not Pair<T> private Object first; private Object second; public Pair(Object first, Object second) {...} public Object getFirst() {...} public Object getSecond() {...} public void setFirst(Object value) {...} public void setSecond(Object value) {...} }
与 C++ 模板的区别
C++ 模板的每一个实例化都会产生不同的类型,导致代码膨胀,而 Java 泛型机制不存在这个问题。
对于泛型方法,通常认为是一个完整的方法族:
public static <T extends Comparable> T min(T[] a)
而擦除类型后,就只剩下一个方法:
public static Comparable min(Comparable[] a)
翻译类型擦除后的泛型表达式
Pair 中的 get 方法返回泛型类型:
Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();
编译器对 getFirst 方法进行类型擦除,将返回类型改为 Object 后,会自动插入 Employee 的强制类型转换,也就是说会翻译成两条虚拟机指令:
-
将返回的 Object 类型或限定类型强制转换为语境需要的类型
类型擦除与多态的冲突
class DateInterval extends Pair<LocalDate> { public void setSecond(LocalDate second) { if (second.compareTo(getFirst()) >= 0) // ensure second >= first super.setSecond(second); } }
class DateInterval extends Pair { public void setSecond(LocalDate second) { ... } }
这个时候存在另一个从 Pair 继承过来的 setSecond 方法:
public void setSecond(Object second)
这样一来,在 DateInterval 中定义的 setSecond 方法还是重写(override)吗?不,因为参数列表不同,所以变成了重载(overload),看看下面的方法调用:
DateInterval interval = new DateInterval(...); Pair<LocalDate> pair = interval; pair.setSecond(date);
pair 引用了子类对象 interval,多态性使得 interval.setSecond(Object) 被调用,而不是我们希望的 setSecond(LocalDate),现在,类型擦除与多态发生了冲突。
对于上述问题,编译器会在 DateInterval 中生成一个桥方法(bridge method),顾名思义,就是起到一个联结的作用:
public void setSecond(Object second) { setSecond((LocalDate) second); }
这个方法对继承自 Pair(类型已擦除)的 setSecond 进行了重写,其内部实现只是调用重载的 setSecond(LocalDate),建立联结,解决了冲突。
对于重写的 getSecond 方法,会稍微有一点不同。比如说是这样:
class DateInterval extends Pair<LocalDate> { ... public LocalDate getSecond() { return (LocalDate) super.getSecond().clone(); } }
编译器合成桥方法后,DateInterval 中会有两个 getSecond 方法:
LocalDate getSecond()
Object getSecond()
为什么编译器可以产生只有返回类型不同的方法?
这是因为在虚拟机中,通过参数类型和返回类型共同确定一个方法。
三、对泛型的限制
-
-
运行时的类型查询(instanceof / getClass)只适用于原始类型
-
不能实例化(但是可以声明)参数化类型的数组,比如 new Pair<String>[10]
-
不能实例化类型变量,比如 new T(...)
-
不能将类型参数用于静态域和静态方法
-
不能抛出和捕获泛型类的实例
四、通配符
最后分享一篇关于泛型的好文章(点击此处访问,作者 frank909),其中一段话让我觉得很有意思:
我在文章开头将泛型比作是一个守门人,原因就是他本意是好的,守护我们的代码安全,然后在门牌上写着出入的各项规定,及“xxx 禁止出入”的提醒。但是同我们日常所遇到的那些门卫一般,他们古怪偏执,死板守旧,我们可以利用反射基于类型擦除的认识,来绕过泛型中某些限制,现实生活中,也总会有调皮捣蛋者能够基于对门卫们生活作息的规律,选择性地绕开他们的监视,另辟蹊径溜进或者溜出大门,然后扬长而去,剩下守卫者一个孤独的身影。