泛型简介
为什么要使用泛型?
简而言之,泛型允许类型(类和接口)在定义类、接口和方法时作为参数。就像方法声明中更熟悉的形式参数一样,类型参数提供了一种方法,使您能够使用不同的输入重复使用相同的代码。区别在于形式参数的输入是值,而类型参数的输入是类型。
使用泛型的代码比非泛型代码具有许多优点
在编译时进行更强的类型检查。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 类
泛型类型是泛型类或接口,它在类型上参数化。以下 Box
类将被修改以演示此概念。
public class Box {
private Object object;
public void set(Object object) { this.object = object; }
public Object get() { return object; }
}
由于其方法接受或返回 Object
,因此您可以自由地传入任何您想要的内容,前提是它不是基本类型之一。在编译时,无法验证类是如何使用的。代码的一部分可能将 Integer
放入框中并期望从中获取类型为 Integer
的对象,而代码的另一部分可能会错误地传入 String
,从而导致运行时错误。
Box 类的泛型版本
泛型类使用以下格式定义
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 - 元素(Java 集合框架广泛使用)
K - 键
N - 数字
T - 类型
V - 值
S、U、V 等 - 第 2、第 3、第 4 个类型
您将在整个 Java SE API 和本节的其余部分看到这些名称的使用。
调用和实例化泛型类型
要在代码中引用泛型 Box
类,您必须执行泛型类型调用,它将 T
替换为一些具体值,例如 Integer
Box<Integer> integerBox;
您可以将泛型类型调用视为类似于普通方法调用,但您不是将参数传递给方法,而是将类型参数传递给 Box
类本身——在本例中为 Integer
。
类型参数和类型参数术语:许多开发人员互换使用“类型参数”和“类型参数”这两个术语,但这两个术语并不相同。在编码时,人们提供类型参数以创建参数化类型。因此,
Foo<T>
中的T
是一个类型参数,而Foo<String> f
中的String
是一个类型参数。本节在使用这些术语时会遵守此定义。
与任何其他变量声明一样,此代码实际上并没有创建一个新的 Box
对象。它只是声明 integerBox
将保存对“Integer 的 Box”的引用,这就是 Box<Integer>
的读取方式。
泛型类型的调用通常称为参数化类型。
要实例化此类,请照常使用 new
关键字,但在类名和括号之间放置 <Integer>
Box<Integer> integerBox = new Box<Integer>();
菱形
在 Java SE 7 及更高版本中,您可以用一组空的类型参数 (<>
) 替换调用泛型类构造函数所需的类型参数,只要编译器可以从上下文中确定或推断类型参数即可。这对尖括号 <>
非正式地称为菱形。例如,您可以使用以下语句创建 Box<Integer>
的实例
Box<Integer> integerBox = new Box<>();
有关菱形表示法和类型推断的更多信息,请参阅本教程的类型推断部分。
多个类型参数
如前所述,泛型类可以具有多个类型参数。例如,泛型 OrderedPair
类,它实现了泛型 Pair
接口
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
实例化为 String
,并将 V
实例化为 Integer
。因此,OrderedPair
构造函数的参数类型分别为 String
和 Integer
。由于自动装箱,将 String
和 int
传递给类是有效的。
如菱形部分所述,因为 Java 编译器可以从声明 OrderedPair<String, Integer>
中推断出 K
和 V
类型,所以可以使用菱形表示法缩短这些语句
OrderedPair<String, Integer> p1 = new OrderedPair<>("Even", 8);
OrderedPair<String, String> p2 = new OrderedPair<>("hello", "world");
要创建泛型接口,请遵循与创建泛型类相同的约定。
参数化类型
您还可以用参数化类型替换类型参数(即 K
或 V
),即 List<String>
。例如,使用 OrderedPair<K, V>
示例
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>
的原始类型。但是,非泛型类或接口类型不是原始类型。
原始类型出现在遗留代码中,因为在 JDK 5.0 之前,许多 API 类(如集合类)不是泛型的。使用原始类型时,您基本上获得了泛型之前的行为——Box 会为您提供 Objects。为了向后兼容,允许将参数化类型分配给其原始类型
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();
}
}
术语“未经检查”意味着编译器没有足够的类型信息来执行所有必要的类型检查以确保类型安全。默认情况下,“未经检查”警告被禁用,但编译器会给出提示。要查看所有“未经检查”警告,请使用-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){
System.out.println("T: " + t.getClass().getName());
System.out.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 的调用仍然包含 String
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;
}
该方法的实现很简单,但它无法编译,因为大于运算符 (>
) 仅适用于原始类型,例如short
、int
、double
、long
、float
、byte
和char
。您不能使用>
运算符来比较对象。要解决此问题,请使用以 Comparable<T>
接口为边界的类型参数。
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 a”关系。由于 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>
的子类型。
这是在使用泛型进行编程时常见的误解,但这是一个重要的概念。Box<Integer>
不是Box<Number>
的子类型,即使 Integer
是 Number
的子类型。
注意:给定两个具体类型
A
和B
,例如Number
和Integer
,MyClass<A>
与MyClass<B>
无关,无论A
和B
是否相关。MyClass<A>
和MyClass<B>
的共同父类是Object
。
有关如何在类型参数相关时在两个泛型类之间创建子类型关系的信息,请参阅 通配符和子类型化 部分。
泛型类和子类型化
您可以通过扩展或实现泛型类或接口来对其进行子类型化。一个类或接口的类型参数与另一个的类型参数之间的关系由 extends 和 implements 子句确定。
以 Collections 类为例,ArrayList<E>
实现 List<E>
,而 List<E>
扩展 Collection<E>
。因此,ArrayList<String>
是 List<String>
的子类型,后者是 Collection<String>
的子类型。只要您不改变类型参数,子类型关系就会在类型之间保留。
现在假设我们要定义自己的列表接口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>
上次更新: 2021 年 9 月 14 日