从 Java 程序设计语言 1.0 版本发布以来,变化最大的部分就是泛型,致使Java SE 5.0 中增加泛型机制的主要原因是为了满足 1999 年制定的最早的 Java 规范需求之一 (JSR 14)。使用泛型机制编写程序代码要比那些杂乱地使用 Object 变量,然后再进行强制类型转换的代码具有更好的安全性和可读性。[1]
为什么要使用泛型
泛型程序设计(Generic programming)意味着编写的代码可以被很多不同类型的对象所重用,例如 ArrayList 类 可以存放 String 类型对象,也可以存放 Integer 类型对象
类型参数的好处
在 Java 增加泛型类之前,泛型程序设计是用继承实现的,ArrayList 类只维护一个 Object 引用的数组。
public class ArrayList { // before generic classes
private Object[] elementData;
...
public Object get(int i) {}
public void add(Object 0) {}
}
这种方法存在两个问题:
- 当获取一个值是必须进行强制类型转换:
String filename = (String) files.get(0);
- 没有错误检查,可以向数组列表中添加任意类型的对象:
files.add(new File("..."));
,对于这个调用,编译和运行都不会报错,然而在其他地方,如果将 get 的结果强制类型转换为 String 类型,就会产生一个错误。
泛型提供了更好的解决方案:类型参数(type parameters): ArrayList<String> files = new ArrayList<String>();
Java SE7 之后,构造函数中可以省略泛型类型:
ArrayList<String> files = new ArrayList<>();
编译器可以很好地使用类型参数, 当调用 get 时,不用进行强制类型转换,编译器知道返回类型为 String,而且编译器知道有类型为 String 的 add 方法,会检查插入参数的类型是否一致,这些使程序具有更好的可读性和安全性。
泛型类
一个泛型类(generic class)就是具有一个或者多个类型变量的类,参考 corejava 的示例代码,我们只关注泛型,而不会为数据存储的细节烦恼。
public class Pair<T> {
private T first; // use the type variable
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 first) {
this.first = first;
}
public void setSecond(T second) {
this.second = second;
}
}
类定义中的类型变量(type parameters,示例代码中为T)制定方法的返回类型以及域和局部变量的类型,泛型类可以看成普通类的工厂。
// 自定义泛型类的简单使用
public class PairTest1 {
public static void main(String[] args) {
String[] words = {"Mary", "had", "little", "lamb"};
Pair<String> mm = ArrayAlg.minmax(words);
System.out.println("min = " + mm.getFirst()); // min = Mary
System.out.println("max = " + mm.getSecond()); // max = lamb
}
}
class ArrayAlg {
public static Pair<String> minmax(String[] a) {
if (a == null || a.length == 0) return null;
String min = a[0];
String max = a[0];
for (int i = 0; i < a.length; i++) {
if (min.compareTo(a[i]) > 0) min = a[i];
if (min.compareTo(a[i]) < 0) max = a[i];
}
return new Pair<>(min, max);
}
}
泛型方法
泛型方法可以定义在普通类中,也可以 定义在泛型类中,其中类型变量放在修饰符后面,返回类型前面。
class ArrayAlg {
public static <T> T getMiddle(T... a) {
return a[a.length / 2];
}
}
String middle = ArrayAlg.<String>getMiddle(words);
String middle = ArrayAlg.getMiddle(words); // 编译器可以 自动判断出T的类型。
类型变量的限定
有时需要对类或方法对泛型参数进行限定,此时可以通过使用通配符来解决。
public class PairTest2 {
public static void main(String[] args) {
LocalDate[] birthdays = {
LocalDate.of(1906, 12, 9),
LocalDate.of(1985, 3, 5),
LocalDate.of(1406, 2, 4),
LocalDate.of(1996, 6, 7),
};
Pair<LocalDate> mm = ArrayAlg.minmax(birthdays);
System.out.println("min = " + mm.getFirst());
System.out.println("max = " + mm.getSecond());
}
}
class ArrayAlg {
public static <T extends Comparable> Pair<T> minmax(T[] a) {
if (a == null || a.length == 0) return null;
T min = a[0];
T max = a[0];
for (int i = 0; i < a.length; i++) {
if (min.compareTo(a[i]) > 0) min = a[i]; // min = 1406-02-04
if (min.compareTo(a[i]) < 0) max = a[i]; // max = 1996-06-07
}
return new Pair<>(min, max);
}
}
<T extends BoundingType>
表示 T 应该是绑定类型的子类型, T 和绑定类型可以是类,也可以是接口,选择关键字 extends 的原因是更接近子类的概念。一个变量或通配符可以有多个限定,如 T, U extends Comparable. & Serializable
。在 Java 继承中,可以选择多个接口超类型,但限定中至多有一个类,如果用一个类作为限定,它必须是限定列表 中的第一个。
泛型代码和虚拟机
虚拟机没有泛型类型对象——所有对象都属于普通类。
类型擦除
无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type),原始类型的名字就说是删去类型参数后的反响类型名。擦除(erased)类型变量,并替换为限定类型(无限定的变量用Object),如之前在继承与多态中用的示例代码,在擦除后的原始类型如下:
public class Pair {
private Object first;
private Object second;
public Pair2(Object first, Object second) {
this.first = first;
this.second = second;
}
public Object getFirst() {
return first;
}
public Object getSecond() {
return second;
}
public void setFirst(Object first) {
this.first = first;
}
public void setSecond(Object second) {
this.second = second;
}
}
因为 T 是一个无限定变量,所以直接用 Object 替换。在程序中可以包含不同类型的 Pair,如 Pair<String>
和 Pair<LocalDate>
, 而擦除类型之后就变成原始的 Pair 类型。
泛型擦除的规则为:原始类型用第一个限定的类型变量来替换,如果没有给定限定就用 Object 来替换。我们看下面的例子。
public class Interval<T extends Comparable & Serializable> implements Serializable {
private T lower;
private T upper;
public Interval(T lower, T upper) {
if (lower.compareTo(upper) > 0) {
this.lower = lower;
this.upper = upper;
} else {
this.upper = lower;
this.lower = upper;
}
}
}
在泛型擦除之后,原始类型如下:
public class Interval implements Serializable {
private Comparable lower;
private Comparable upper;
public Interval(Comparable lower, Comparable upper) {
if (lower.compareTo(upper) > 0) {
this.lower = lower;
this.upper = upper;
} else {
this.upper = lower;
this.lower = upper;
}
}
}
如果写出 class Interval<T extends Serializable & Comparable>
,那么原始类型会用 Serializable
来代替 T,而编译器在必要时要向 Comparable 插入强制类型转换。所以为了提高效率,应该将标签(tagging)接口(即没有方向的接口)放在边界列表的末尾。
翻译泛型表达式
当程序调用泛型方法时,如果擦除返回类型,编译器会插入强制类型转换,例如我们的Pair<T>
Pair<Employee> buddies = ...
Employee buddy = buddies.getFirst();
调用 getFirst 方法时编译器把这个方法调用翻译为两条虚拟机指令:
- 对原始方法 Pair.getFirst 的调用
- 将返回的 Object 类型强制转换为 Employee 类型
同样的情况也会出现在存取一个泛型域时,如
Employee buddy = buddies.first;
翻译泛型方法
类型擦除也会出现在泛型方法中,我们看以下泛型方法的定义:
public static <T extends Comparable> T min(T[] a)
泛型擦除后:
public static Comparable min(Comparable[] a)
我们可以看到参数 T 已经被擦除了,只留下了限定类型 Comparable,方法的擦除会带来两个巨大的问题。这里使用 oracle tutorial 中的代码来讲解[2]。
// before erasure
public class Node<T> {
public T data;
public Node(T data) {
this.data = data;
}
public void setData(T data) {
System.out.println("Node.setData");
this.data = data;
}
}
public class MyNode extends Node<Integer> {
public MyNode(Integer data) {
super(data);
}
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
// after erasure
public class Node {
public Object data;
public Node(Object data) { this.data = data; }
public void setData(Object data) {
System.out.println("Node.setData");
this.data = data;
}
}
public class MyNode extends Node {
public MyNode(Integer data) { super(data); }
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
MyNode 是 Node 的子类,并且它复写了父类的 setData 方法。当我们尝试调用以下代码。
// after erasure
MyNode mn = new MyNode(1);
Node n = mn;
n.setData("Hello");
Integer x = mn.data;
// before erasure
MyNode mn = new MyNode(1);
Node n = (MyNode) mn; // A raw type - compiler throws an unchecked warning
n.setData("Hello"); // Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
Integer x = (String) mn.data;
看到上述代码时,第一反应是为什么n.setData("Hello");
不会报错,按照多态的原理,此时会调用MyNode.setData(Integer data)
方法,放入一个 String 为什么编译器会没报错?
先引用官方的解释
After type erasure, the method signatures do not match. The
Node
method becomessetData(Object)
and theMyNode
method becomessetData(Integer)
. Therefore, theMyNode
setData
method does not override theNode
setData
method.
因为存在泛型擦除,在擦除后 Node 中的方法变为 Node.setData(Object data)
, 很明显 Object 与 Integer 不是同一个类型,所以 MyNode 中同时存在从 Node 中继承过来的 Node.setData(Object data)
方法,n.setData("Hello");
实际上调用的 Node.setData(Object data)
, 所以在编译时期能检查通过,这行代码运行时发生如下操作:
- 调用 MyNode 类中的 setData(Object) (这个方法会被编译器自动改写为桥方法)方法(因为 MyNode 从 Node 中继承了 setData(Object)方法)
- n 引用的对象中的 data 域 被赋值为 "Hello"
- mn 引用与 n 同一个对象,但是这里的 data 域被期望为 Integer 类型,因为 mn 是
MyNode extends Node<Integer>
的对象,此时因为桥方法中的强制类型转换而抛出ClassCastException,程序结束。
桥方法(Birdge Method)
根据多态的设计初衷,n.setData("Hello");
应该调用 MyNode.setData(Integer data)
,但是最终它调用的却是 Node.setData(Object data)
,我们可以看出类型擦除(type erasure) 与多态(polymorphism) 之间存在冲突,为了保证多态的可用性,Java 编译器会自动生成桥方法来解决这个问题, 如 MyNode 将会变成如下代码。
class MyNode extends Node {
// Bridge method generated by the compiler
//
public void setData(Object data) {
setData((Integer) data);
}
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
// ...
}
正是因为有桥方法的存在,才能保证多态功能的正常使用。
桥方法还可以用在其他地方,比如一个方法覆盖另一方法是可以指定一个 更严格的返回类型。
public class Employee implements Cloneable{
public Employee clone() throws CloneNotSupportedException {
Employee clone = (Employee) super.clone();
return clone;
}
}
这里其实 Employee 有两个 clone 方法
Employee clone();
Object clone();
此时也需要编译器合成桥方法,合成的桥方法调用了新定义的方法。
总结
- 虚拟机中没有泛型,只有普通的类和方法
- 所有的类型参数都用他们的限定类型替换
- 桥方法被用来合成保持多态
- 为保持类型安全性,必要时插入强制类型转换。
约束与局限性
使用泛型时也有一些约束与局限性,大部分的约束都是由类型擦除引起的。
不能用基本类型实例化类型参数:如不能
Pair<double>
,只能Pair<Double>
,原因很简单,当类型擦除之后,只剩下 Object 类型的域, 而 Object 不能存储 double 的值,这样做与 Java 语言中基本类型的独立状态相一致。-
运行时类型查询只适用于原始类型:所有的类型查询只产生原始类型,如
if (a instanceof Pair<String>) // Error if (a instanceof Pair<T>) // Error Pair<String> stringPair = new Pair<>(); Pair<Employee> employeePair = new Pair<>(); stringPair.getClass() == employeePair.getClass // true getClass方法总返回原始类型
不能创建参数化类型的数组:
Node<String>[] node = new Node<String>[10]; // Error
,因为类型擦除后 node 的类型变成Node[]
,可以把它转化为Object[] objArray = node;
,数组会记住它的元素类型,如果试图存储其他的类型,如objArray[0] = "hello";
,就会抛出ArrayStoreException
异常。Varargs警告:
public static <T> void addAll(Collection<T> coll, T... ts)
这个方法定义会抛出警告,因为其中的一个参数为可变参数,本质上是泛型数组,这就违反了上一条规则,Java SE 7后可以使用@SafeVarargs
进行消除警告。-
不能实例化类型变量:
不能使用
new T(...), new T[...], T.class
这些表达式,也不能使用如下构造器:public Pair<T> { first = new T(); second = new T(); }
比较好的解决方法为使用构造器表达式,如
public static <T> Pair<T> makePair(Supplier<T> constr) { return new Pair<>(constr.get(), constr.get()); } // 调用 Pair<String> p = Pari.makePair(String::new);
比较传统的方法是通过反射调用 Class.newInstance 方法来构造泛型对象
first = T.class.newInstance(); // Error,因为存在泛型擦除, T.class会被擦除为 Object.class public static <T> Pair<T> makePair(Class<T> c1) { try { return new Pair<>(c1.newInstance(), c1.newInstance()); } catch(Exception ex) { return null; } } // 调用 Pair<String> p = Pari.makePair(String.class);
泛型类的静态上下文中类型变量无效:即不能在静态域或方法中引用类型变量。
不能抛出和捕获泛型类的实例
可以消除对受查异常的检查
注意擦除后的冲突
泛型类的继承
若 Manager 是 Employee 的子类,那么 Pair<Manager>
不是 Pair<Employee>
的子类,这一限制主要是出于类型安全的考虑,考虑一下代码:
Pair<Manager> manager = new Pair<>(cto, cfo);
Pair<Employee> employee = manager;
employee.setFirst(staff); // 将普通员工与管理者放在一起明显破坏了程序设计的意图
永远可以将一个参数化类型转化为一个原始类型,例如 Pair<Employee>
是原始类型的子类型,并且泛型类可以扩展或实现其他的泛型类,如 ArrayList<T>
实现 List<T>
接口,这意味着一个 ArrayList<Manager>
可以转换为List<Manager>
, 但是一个 ArrayList<Manager>
与 ArrayList<Employee>
或 List<Employee>
之间没有关系。
通配符类型
public static void test(Pair<? extends Employee> test) // 表示任何泛型 Pair 类型,它的类型参数是 Employee 的子类 以及其本身。
public static void test(Pair<? super Employee> test) // 表示任何泛型 Pair 类型,它的类型参数是 Employee 的父类 以及其本身。
直观得讲,带有超类型限定的通配符可以向泛型对象写入(可以用构造器方法),带有子类型限定的通配符对象可以从泛型对象读取。[1]
Pair<?> 无限定通配符
本文章与github上同步,欢迎来玩,提交issue。