泛型简介
为什么要使用泛型?
简而言之,泛型使类型(类和接口)能够在定义类、接口和方法时作为参数使用。这类似于方法声明中更常见的形式参数,类型参数让你能够通过不同的输入来复用相同的代码。区别在于:形式参数的输入是值,而类型参数的输入是类型。
使用泛型的代码相比非泛型代码具有诸多优势:
- 更严格的编译时类型检查。Java编译器对泛型代码实施强类型检查,若代码违反类型安全规则则会报错。修复编译时错误比修复运行时错误更容易,后者往往难以定位。
- 消除强制类型转换。以下不使用泛型的代码片段需要强制类型转换:
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);
当重写为使用泛型时,该代码无需强制类型转换:
List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0); // no cast
- 使程序员能够实现泛型算法。通过使用泛型,程序员可以实现适用于不同类型集合的泛型算法,这些算法可定制、类型安全且更易于阅读。
泛型类型
一个简单的Box Class
泛型类型是指通过类型参数化的泛型类或接口。下面将修改 Box 类来演示该概念。
public class Box {
private Object object;
public void set(Object object) { this.object = object; }
public Object get() { return object; }
}
由于其方法接受或返回对象类型,您可自由传入任意类型参数,前提是该类型不属于基本数据类型。编译时无法验证该类的具体使用方式:代码某部分可能向对象容器中存入整数类型数据并期望取出同类型对象,而另一部分代码却可能误传字符串类型参数,最终导致运行时错误。
一个泛型版本的Box Class
泛型类的定义格式如下:
class name<T1, T2, ...,Tn> {/*...*/}
类型参数部分由尖括号(<>)限定,紧跟在类名之后。它指定类型参数(也称为类型变量)T1、T2、…、Tn。
要更新 Box 类以使用泛型,需将代码中的 “public class Box“ 改为 “public class Box<T>“ 以创建泛型类型声明。这引入了类型变量 T,该变量可在类内部任意位置使用。
随着此变更,Box类变更为:
/**
* Generic version of the Box class.
* @param <T> the type of the value being boxed
*/
public class Box<T> {
// T stands for "Type"
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
如您所见,所有出现的 Object 都被替换为 T 。类型变量可以是您指定的任何非基本类型:任何类类型、任何接口类型、任何数组类型,甚至另一个类型变量。
同样的技术也可用于创建通用接口。
类型参数命名约定
按惯例,类型参数名称应为单个大写字母。这与你已知的变量命名惯例形成鲜明对比,且有充分理由:若无此惯例,将难以区分类型变量与普通类或接口名称。
最常用的类型参数名称包括:
- E - Element (被Java集合框架广泛使用)
- K - Key
- N - Number
- T - Type
- V - Value
- S, U, V - 第二、第三、第四类型
- 您将在整个Java SE API以及本节其余部分中看到这些名称的使用。
调用并实例化泛型类型
要在代码中引用泛型 Box 类,必须执行泛型类型调用,将T替换为具体值,例如 Integer:
Box<Integer> integerBox;
你可以将泛型类型的调用视为类似于普通方法调用,但不同之处在于:你不是向方法传递参数,而是向Box类本身传递类型参数——本例中即为Integer。
Type Parameter and Type Argument Terminology:许多开发者将”type parameter” and “type argument” 混用,但这两个术语并不相同。编码时,提供 type arguments 是为了创建参数化类型。因此,Foo<T>中的 T 是Type Parameter,而 Foo
f 中的 String 是type argument。本节在使用这些术语时遵循此定义。
与其他变量声明类似,这段代码并未实际创建新的 Box 对象。它只是声明 integerBox 将持有对”Box of Integer”的引用——这正是 Box
泛型类型的调用通常被称为参数化类型。
要实例化此类,请照常使用 new 关键字,但在类名和括号之间添加
Box<Integer> integerBox = new Box<Integer>();
钻石操作符
在 Java SE 7 及更高版本中,只要编译器能够从上下文中确定或推断出类型参数,就可以用空类型参数集(<>)替换调用泛型类构造函数所需的类型参数。这对尖括号 <> 非正式地称为钻石操作符。例如,你可以使用以下语句创建 Box
Box<Integer> integerBox = new Box<>();
有关钻石符号和类型推断,请看本章的类型推断章节。
多重类型参数
如前所述,泛型类可以拥有多个类型参数。例如,实现泛型 Pair 接口的泛型类 OrderedPair:
public interface Pair<K, V> {
public K getKey();
public V getValue();
}
public class OrderedPair<K, V> implements Pair<K, V> {
private K key;
private V value;
public OrderedPair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
}
以下语句创建了两个 OrderedPair 类的实例:
Pair<String, Integer> p1 = new OrderedPair<String, Integer>("Even", 8);
Pair<String, String> p2 = new OrderedPair<String, String>("hello", "world");
代码 new OrderedPair<String, Integer>() 将 K 实例化为字符串类型,将 V 实例化为整数类型。因此,OrderedPair 构造函数的参数类型分别为 String 和 Integer。由于自动装箱机制,向该类传递字符串和整数参数是有效的。
如钻石操作章节所述,由于Java编译器可从声明 OrderedPair<String, Integer> 推断出 K 和 V 的类型,因此这些语句可通过钻石操作符表示法简化:
OrderedPair<String, Integer> p1 = new OrderedPair<>("Even", 8);
OrderedPair<String, String> p2 = new OrderedPair<>("hello", "world");
创建泛型接口时,请遵循与创建泛型类相同的约定。
参数化类型
您还可以用泛型类型(即 List
OrderedPair<String, Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>(...));
原始类型
原始类型是指不带任何类型参数的泛型类或接口的名称。例如,给定泛型类 Box:
public class Box<T> {
public void set(T t) { /* ... */ }
// ...
}
要创建参数化类型 Box<T>,需为形式类型参数 T 提供实际类型参数:
Box<Integer> intBox = new Box<>();
如果省略实际类型参数,则创建原始类型 Box<T>:
Box rawBox = new Box();
因此,Box 是泛型类型 Box<T> 的原始类型。然而,非泛型类或接口类型并非原始类型。
原始类型出现在遗留代码中,是因为许多API类(如Collections类)在JDK 5.0之前尚未实现泛型支持。使用原始类型时,本质上会获得泛型出现前的行为——Box类返回Object对象。为保持向后兼容性,允许将泛型参数化类型赋值给其原始类型:
Box<String> stringBox = new Box<>();
Box rawBox = stringBox; // OK
但若将原始类型赋值给参数化类型,则会触发警告:
Box rawBox = new Box(); // rawBox is a raw type of Box<T>
Box<Integer> intBox = rawBox; // warning: unchecked conversion
若使用原始类型调用对应泛型类型中定义的泛型方法,系统也会发出警告:
Box<String> stringBox = new Box<>();
Box rawBox = stringBox;
rawBox.set(8); // warning: unchecked invocation to set(T)
该警告表明原始类型会绕过泛型类型检查,将不安全代码的捕获推迟到运行时。因此,应避免使用原始类型。
类型擦除部分提供了更多关于Java编译器如何使用原始类型的信息。
未检查的错误消息
如前所述,在将遗留代码与通用代码混合使用时,您可能会遇到类似以下的警告信息:
Note: Example.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
当使用操作原始类型的旧版 API 时,可能会出现这种情况,如下例所示:
public class WarningDemo {
public static void main(String[] args){
Box<Integer> bi;
bi = createBox();
}
static Box createBox(){
return new Box();
}
}
术语”unchecked”表示编译器缺乏足够的类型信息来执行确保类型安全所需的所有类型检查。默认情况下,”unchecked”警告处于禁用状态,但编译器会给出提示。若要查看所有”unchecked”警告,请使用-Xlint:unchecked 选项重新编译。
使用 -Xlint:unchecked 选项重新编译前面的示例,将显示以下额外信息:
WarningDemo.java:4: warning: [unchecked] unchecked conversion
found : Box
required: Box<java.lang.Integer>
bi = createBox();
^
1 warning
要完全禁用未检查警告,请使用 -Xlint:-unchecked 标志。注解 @SuppressWarnings(“unchecked”) 可抑制未检查警告。若您不熟悉 @SuppressWarnings 语法,请参阅注解章节。
泛型方法
泛型方法是指引入自身类型参数的方法。这类似于声明泛型类型,但类型参数的作用域仅限于其声明的方法内部。允许使用静态和非静态泛型方法,以及泛型类的构造函数。
泛型方法的语法包含一组类型参数,这些参数用尖括号括起,位于方法返回类型之前。对于静态泛型方法,类型参数部分必须出现在方法返回类型之前。
Util 类包含一个泛型方法compare,用于比较两个Pair对象:
public class Util {
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
return p1.getKey().equals(p2.getKey()) &&
p1.getValue().equals(p2.getValue());
}
}
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public void setKey(K key) { this.key = key; }
public void setValue(V value) { this.value = value; }
public K getKey() { return key; }
public V getValue() { return value; }
}
调用此方法的完整语法如下:
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.<Integer, String>compare(p1, p2);
类型已明确提供,如粗体所示。通常情况下,此处可省略类型声明,编译器将自动推断所需类型:
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.compare(p1, p2);
此功能称为类型推断,它允许您像调用普通方法那样调用泛型方法,而无需在尖括号中指定类型。本主题将在下一节“类型推断”中进一步讨论。
约束类型参数
有时您可能需要限制可作为参数化类型类型参数使用的类型。例如,一个操作数字的方法可能只希望接受 Number 类或其子类的实例。这就是约束类型参数的作用。
要声明一个约束类型参数,需列出类型参数的名称,接着是 extends 关键字,然后是其约束(在本例中为 Number)。请注意,在此上下文中,extends 具有泛化含义,既表示类中的 “extends“,也表示接口中的 “implements“。
public class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
public <U extends Number> void inspect(U u){
IO.println("T: " + t.getClass().getName());
IO.println("U: " + u.getClass().getName());
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<Integer>();
integerBox.set(new Integer(10));
integerBox.inspect("some text"); // error: this is still String!
}
}
通过修改通用方法以包含此约束类型参数,编译将失败,因为我们对 inspect 的调用仍包含字符串参数:
Box.java:21: <U>inspect(U) in Box<java.lang.Integer> cannot
be applied to (java.lang.String)
integerBox.inspect("10");
^
1 error
除了限制可用于实例化泛型类型的类型外,约束类型参数还允许你调用约束中定义的方法:
public class NaturalNumber<T extends Integer> {
private T n;
public NaturalNumber(T n) { this.n = n; }
public boolean isEven() {
return n.intValue() % 2 == 0;
}
// ...
}
isEven()方法通过 n 调用Integer类中定义的 intValue()方法。
多重约束
前面的示例展示了具有单个约束的类型参数的使用,但类型参数可以具有多个约束:
<T extends B1 & B2 & B3>
具有多个约束的类型变量是约束中列出的所有类型的子类型。若其中一个约束是类,则必须将其置于首位。例如:
Class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }
class D <T extends A & B & C> { /* ... */ }
如果未首先指定绑定 A,则会引发编译时错误:
class D <T extends B & A & C> { /* ... */ } // compile-time error
泛型方法与约束类型参数
约束类型参数是实现泛型算法的关键。考虑以下方法,该方法统计数组 T[] 中大于指定元素 elem 的元素个数。
public static <T> int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e > elem) // compiler error
++count;
return count;
}
该方法的实现很简单,但无法编译,因为大于运算符(>)仅适用于短整型、整型、双精度型、长整型、浮点型、字节型和字符型等基本类型。无法使用>运算符比较对象。要解决此问题,请使用受Comparable
public interface Comparable<T> {
public int compareTo(T o);
}
生成的代码将为:
public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e.compareTo(elem) > 0)
++count;
return count;
}
泛型、继承与子类型
如您所知,只要类型兼容,就可以将一种类型的对象赋值给另一种类型的对象。例如,您可以将Integer赋值给Object类型,因为Object是Integer的超类型之一:
Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger; // OK
在面向对象术语中,这被称为 “is” 关系。由于 Integer 是 Object 的一种类型,因此允许进行赋值。但 Integer 同时也是 Number 的一种类型,因此以下代码同样有效:
public void someMethod(Number n) { /* ... */ }
someMethod(new Integer(10)); // OK
someMethod(new Double(10.1)); // OK
泛型同样如此。你可以执行泛型类型调用,将Number作为其类型参数传递,只要后续调用的参数与Number兼容,任何后续对add的调用都将被允许:
Box<Number> box = new Box<Number>();
box.add(new Integer(10)); // OK
box.add(new Double(10.1)); // OK
现在考虑以下方法:
public void boxTest(Box<Number> n) { /* ... */ }
它接受什么类型的参数?通过查看其签名,你可以看到它接受单个参数,类型为 Box<Number>。但这意味着什么?你是否可以像预期那样传入Box<Integer>或Box<Double>?答案是否定的,因为Box<Integer>和Box<Double>并非Box<Number>的子类型。
这是泛型编程中常见的误解,但却是需要掌握的重要概念。尽管Integer是Number的子类型,Box<Integer>却并非Box<Number>的子类型。
注:对于两个具体类型 A 和 B(例如 Number) 和 Integer),MyClass<A> 与 MyClass<B> 之间不存在任何关联,无论 A 和 B 是否相关。MyClass<A> 和 MyClass<B> 的共同父类均为 Object。
有关如何在类型参数相关时为两个泛型类创建类似子类的关系的信息,请参阅“通配符与子类型化”章节。
泛型类与子类型化
你可以通过扩展或实现来对泛型类或接口进行子类型化。一个类或接口的类型参数与另一个类或接口的类型参数之间的关系,由extends子句和implements子句决定。
以集合类为例,ArrayList
现在假设我们想要定义自己的列表接口 PayloadList,该接口为每个元素关联一个泛型类型 P 的可选值。其声明可能如下所示:
interface PayloadList<E,P> extends List<E> {
void setPayload(int index, P val);
...
}
以下 PayloadList 的参数化实现是 List<String> 的子类型:
- PayloadList<String,String>
- PayloadList<String,Integer>
- PayloadList<String,Exception>
