引言
在面向对象的世界里,我们如果需要一个容器来盛装对象。举个例子:一个篮子。我们可以用这个篮子装苹果,也可以用这个篮子装香蕉。基于 OOP 的思想,我们不希望为苹果和香蕉分别创建不同的篮子;同时,我们希望放进篮子里的是苹果,拿出来的还是苹果。于是,Java 程序员提出了「泛型」的概念——一种类似于 C++ 模板的技术。
早期程序员使用如下代码创建一个泛型集合:
public class ArrayList{
private Object[] elementData;
...
public Object get(int i);
public void add(Object o);
}
我们可以看出,对与这个集合而言,取出 (get) 和放入时都没有进行类型检查。因此,如果我们不记得放入的顺序,把取出的对象进项强制类型转换,很可能出现 ClassCastException
。因此,真正的泛型是可以在编译时期,对数据类型进行检查,保证安全。如下面代码所示:
ArrayList<String> list = new ArrayList<>();
P.S. <>
里的String
叫做类型参数。
使用泛型,为我们提供了如下优点:
- 更强大的编译时期的类型检查
- 避免不必要的类型转换,如:
List<String> list = new ArrayList<>(3);
String str = list.get(0);
- 让程序能够实现通用的算法
泛型类
泛型中使用名为泛型参数表明可以传入类或方法的对象类型,这种设计实现了类型参数化(可以把同一类型的类作为参数进行传递),如下面的代码所示:
泛型类示例
public class Pair<T>{
private T first;
private T last;
public Pair(){}
public Pair(T first, T last){
this.first = frist;
this.last = last;
}
public T getFirst();
public T getLast();
}
泛型方法示例
public class Util{
// 简单的泛型方法
public static <T> T getMiddle(T...a){
return a[a.length/2];
}
// 带限定符的泛型方法,如果有多个限定符,使用 & 连接多个接口或超类
public static <T extends Comparable> T min(T...a){
// 具体实现
}
}
注意,这里的泛型参数(type parameter)在上述示例中指的是用大写字母 T 表示的值,而泛型实参(type argument)则是指 T a
中的 a
。根据惯例,泛型参数通常如下命名:
- E:表示一个元素,Java 集合框架中使用最多
- K:键
- N:数字
- T:类型
- V:值
- S,U,V:其它类型
原始类型(raw type)
原始类型指的是,不包括泛型参数的类型,如上述泛型类中的 Pair
。我们可以通过原生类型构造对象:
Pair pair = new Pair();
同时,可以通过泛型参数构造对象:
Pair<String> pair = new Pair<>();
但是,如果把一个通过原生类型获取的对象指向一个通过泛型参数生成的参数会报 unchecked warning
,如下面的代码:
Pair pair = new Pair();
Pair<String> pair1 = pair;
继承和子类型
在 Java 中,有继承的概念,简而言之,就是一个类型可以指向它的兼容类型,如:
Object object = new Object();
Integer integer = new Integer(20);
object = integer;
上述代码表示:Integer IS-A
Object。这种概念在泛型中也适用。如下定义:
public class Box<T extends Number>{
public void add(T t);
}
那么一个 Box 的对象可以增加任意 Number 子类的值。但是 Box<Double>
和 Box<Integer>
不是同一个类型。
泛型方法
泛型类中可以定义静态、非静态的泛型方法。泛型方法的语法为:<泛型参数类型列表> + 返回类型 + 泛型参数列表。
- 静态方法
public static <T> void foo(T t){
}
- 非静态方法
public void foo(T t){
}
类型限定
在某种情况下,我们希望方法只接受特定类型的参数,可以使用如下语法实现:
public <U extends Number> void inspect(U u){
// 这里是逻辑处理
}
上述代码中,该泛型方法只接受为 Number 类型的参数。同样,也可以在泛型类上加以限制:
public class Utils<T extends Number>{
// 这里的 T 必须为 Number 类型
private T t;
}
当然,也可以使用多重限制,如下面代码所示:
public class Utils<T extends A & B & C>{
}
P.S. 限制中的类必须放在接口的前面。
类型推断
类型推断是:编译器去推断调用方法的参数的类型的能力。
如,泛型方法中:
public <U> void addBox(Box<U> box){
// 这里是处理代码
}
不必通过 obj.<U>addBox(box)
调用,<U>
可以省略。
构造方法中:
// 类型推断
Map<String,List<String>> map = new HashMap<>();
其中,构造方法中的泛型还可以这样用:
// 定义泛型类
public class Box<X>{
public <X> Box(T t){
}
}
// 实例化一个对象
public class Application{
void method(){
Box<Integer> box = new Box<>(");
}
}
通配符
通配符 ?
表示一个未知的类型,可用于参数的类型、字段以及局部变量中,但不可用于调用泛型方法里的类型参数、泛型对象实例化以及泛型超类里。
// 可以
public void foo(Pair<? extends Number> pair){
// 可以
Pair<? super Integer> foo;
}
// 可以
private Pair<? super Integer> pair;
上界通配符
上界通配符表明需要最高限定的类型,下面的代码用来计算所有类型为数字的集合的总和:
public double sumList(List<? extends Number>){
// 这里做逻辑处理
}
无界限通配符
使用无界限通配符表示不确定的类型,以下两种情况可以使用无界限通配符:
- 当方法的参数可以用 Object 对象替换
- 方法的实现不依赖具体的类型
比如,有一个打印集合对象的方法:
// 定义一个打印集合对象列表的方法
public void printList(List<?> list){
for(Object obj: list){
// 打印list
}
}
// 调用方法
List<Integer> integers = Arrays.asList(1,2,3);
List<String> strings = Arrays.asList("A","B","C");
printList(integers);
printList(strings);
P.S. List<?>
和 List<Object>
不同,List<?>
只能插入 null
而 List<Object>
可以插入任何对象。
下界通配符
使用下界统配符,表明最低限度的类型,如:
public double sumList(List<? super Duble>){
// 这里做逻辑处理
}
通配符和子类型
在本文的继承和子类里,提到过:Box<Double>
不是 Box<Number>
的子类。在 Java 泛型中,继承关系可以通过如下图表示:
可以看出,泛型中的 extends
的确限定了上界(父类);super
的确限定了下界(子类型);?
是所有泛型的超类(类似 Object)。
泛型的继承关系(父子类型关系)可以通过下面的韦恩图解释:
我们不妨用某一泛型所占的面积表示其层次关系,面积大的在继承关系上层次高。由上图很容易看出:<? super Integer>
的继承层次比 <? super Number>
的继承层次高;相应地,<? extends Integer>
的继承层次比 <? extends Number>
的继承层次低。
使用泛型的场景
调用一个方法:foo(src, dest);
把 src
看做入参,dest
看做出参,基于以下规则决定是否使用和如何使用泛型:
- 入参使用上界通配符:
extends
- 出参使用下界通配符:
super
- 入参可以用 Object 代替的,使用无边通配符
- 需要获取入参和出参的变量,不要使用通配符
这种原则也叫做 PECS(Producer Extends Consumer Super) 原则。
类型擦除
类型擦除确保被参数化的类型不会创建新的类,不会产生运行时的开销。
泛型擦除时,编译器做了一点小小的工作:如果该泛型参数有边界限制,替换成它的边界;否则,用 Object 替换。
上述泛型类 Pair<T>
会被替换成下面形式:
class Pair{
Object first;
Object last;
public Object getFirst(){}
public Object getLast(){}
}
P.S. 一般使用第一个限定类型替换变为原始类型,没有限定类型,使用 Object 替换。
桥接方法
当子类继承(或实现)父类(或接口)的泛型方法时,在子类中指明了具体的类型。编译器会自动构建桥接方法(bridge method)。如:
class Node<T>{
private T t;
public Node(T t){
setT(t);
}
public void setT(T t){
this.t = t;
}
}
class MyNode extends Node<Integer>{
public MyNode(Integer i){
super(i);
}
public void setT(Integer i){
super.setT(i);
}
}
在上述代码中,编译时期,由于泛型擦除,Node 中的方法为 setT(Object t)
而 MyNode 中的方法为 setT(Inetger i)
。签名不匹配,不再是重写,因此,编译器为 MyNode 生成如下桥接方法:
// 桥接方法
public void setT(Object i){
setData((Integer)i);
}
public void setT(Integer i){
super.setData(i);
}
非具体化类型
非具体化类型定义
具体类型(Reifiable Type)指的是:原始数据类型、非泛型类型、原生类型和调用不受限的通配等在运行时期,信息不会丢失的类型。
非具体类型(Non-Reifiable Type)在运行时期不能获取其所有的信息,如 JVM 无法区别 List<String>
和 List<Integer>
。因此,这种类型不能使用类似 instanceof
的方法。
堆污染
堆污染指的是:一个参数化类型指向一个非该参数化类型对象的过程。通常是,在程序中进行了一些操作,使编译时期发生未检查(unchecked)警告时发生。如:混用原始类型(Raw Type)和参数化类型。
使用非具体化类型做可变参数的潜在缺陷
当使用可变参数作为泛型输入参数时,会造成堆污染。如:
可以通过如下注解消除编译时期的警告:
- @SafeVarargs
- @SuppressWarnings({"unchecked", "varargs"})
泛型的限制
虽然泛型是如此的便利,但不免有缺点:
- 不能用基本类型实例化类型参数
// 编译出错
List<int, int> array = new ArrayList<>();
- 不能通过类型参数实例化对象
public static <E> void foo(List<E> list){
// 编译出错
E element = new E();
list.add(element);
}
- 不能创建泛型变量类型的静态字段
public class Foo<T>{
// 编译出错
private static T field;
}
- 不能使用
instanceof
来确认参数类型
public static <E> void foo(List<E> list){
// 编译出错
if(list instanceof ArrayList<Integer>){
}
}
- 不能创建参数化类型数组
// 编译出错
List<String>[] strings = new ArrayList<>[2];
- 不能抛出或捕获泛型类实例
// 编译出错
public class FooException<T> extends Exception{
}
- 不能重载擦除后有同样方法签名的方法
public class Example{
// 编译出错
public void print(Set<String> string){
}
public void print(Set<Integer> integer){
}
}
- 运行时类型查询只适用于原始类型
- Varargs 警告
- 泛型类的静态上下文的类型变量无效
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。