在J2SE 5.0中引入的这种对类型系统期待已久的增强允许类型或方法对各种类型的对象进行操作,同时提供编译时类型安全。它为集合框架增加了编译时类型安全性,并消除了强制转换的苦差事。
机器翻译,有疑问参考出处 https://docs.oracle.com/javase/tutorial/extra/generics/index.html
介绍
JDK 5.0为Java编程语言引入了几个新的扩展。其中之一就是引入泛型。
这篇文章是对泛型的介绍。您可能熟悉来自其他语言的类似结构,尤其是c++模板。如果是这样,你会发现它们既有相似之处,也有重要的不同之处。如果您不熟悉来自其他地方的类似结构,那就更好了;你可以重新开始,不用忘记任何错误的想法。
泛型允许对类型进行抽象。最常见的例子是容器类型,比如集合层次结构中的容器类型。
下面是这种用法的典型用法:
List myIntList = new LinkedList(); // 1
myIntList.add(new Integer(0)); // 2
Integer x = (Integer) myIntList.iterator().next(); // 3
第3行上的强制类型转换有点烦人。通常,程序员知道哪种类型的数据被放置到特定的list中。然而,转换至关重要。编译器只能保证迭代器返回一个Object。为了确保对Integer类型变量的赋值是类型安全的,需要进行类型转换。
当然,转换不仅会带来混乱。它还引入了运行时错误的可能性,因为程序员可能会弄错。
如果程序员能够真正表达他们的意图,并将列表标记为限制包含特定数据类型,情况会怎样?这是泛型背后的核心思想。下面是上面使用泛型的程序片段的一个版本:
List<Integer> myIntList = new LinkedList<Integer>(); // 1
myIntList.add(new Integer(0)); // 2
Integer x = myIntList.iterator().next(); // 3
请注意变量myIntList的类型声明。它不仅仅是一个列表,而是一个整数列表,写成List。我们说List是一个带类型参数的泛型接口——在本例中是Integer。我们还在创建list对象时指定类型参数。
还要注意,第3行上的强制类型转换转换已经消失了。
现在,您可能会认为只是做了胡乱的移动。不是在第3行将类型转换为Integer,而是在第1行将Integer作为类型参数。然而,这里有一个很大的区别。编译器现在可以在编译时检查程序的类型正确性。当我们说myIntList声明为List类型时,这告诉我们一些关于myIntList变量的信息,无论何时何地使用它都保持为真,并且编译器将保证它是正确的。相反,强制转换告诉我们程序员认为在代码的某一点上是正确的东西。
最终的效果,特别是在大型程序中,是提高了可读性和鲁棒性。
定义简单泛型
下面是java.util包中接口列表和迭代器定义的一小段摘录:
public interface List <E> {
void add(E x);
Iterator<E> iterator();
}
public interface Iterator<E> {
E next();
boolean hasNext();
}
这些代码应该都很熟悉,除了尖括号中的内容。这些是接口List和Iterator的形式类型形参的声明。
类型参数可以在泛型声明的整个过程中使用,与使用普通类型的情况差不多(尽管有一些重要的限制;见小字部分。
在简介中,我们看到了泛型类型声明列表的调用,例如List<Integer>。在调用(通常称为参数化类型)中,所有出现的正式类型参数(在本例中为E)都被实际类型参数(在本例中为Integer)取代。
你可以想象List<Integer>表示一个列表的版本,其中E被统一替换为整数:
public interface IntegerList {
void add(Integer x);
Iterator<Integer> iterator();
}
这种直觉很有帮助,但也会产生误导。
它很有帮助,因为参数化类型List<Integer>确实有类似于这个扩展的方法。
这是一种误导,因为泛型的声明实际上从来不会以这种方式展开。代码没有多个副本——源代码中没有,二进制文件中没有,磁盘上没有,内存中也没有。如果你是一名c++程序员,你就会明白这与c++模板有很大的不同。
泛型类型声明被永久编译,并转换为单个类文件,就像普通类或接口声明一样。
类型参数类似于方法或构造函数中使用的普通参数。就像方法拥有描述其操作的值类型的形式值参数一样,泛型声明也拥有形式类型参数。在调用方法时,将实际参数替换为形式参数,并计算方法体。当调用泛型声明时,将用实际的类型参数代替正式的类型参数。
关于命名约定的说明。我们建议您对正式类型参数使用简洁(如果可能的话,使用单个字符)而又唤起回忆的名称。最好避免在这些名称中使用小写字符,以便于将正式类型参数与普通类和接口区分开来。许多容器类型使用E作为元素,如上面的例子所示。我们将在后面的例子中看到一些额外的约定。
泛型和子类型
让我们测试一下你对泛型的理解。下面的代码片段合法吗?
List<String> ls = new ArrayList<String>(); // 1
List<Object> lo = ls; // 2
第1行当然是合法的。问题中比较棘手的部分是第2行。这可以归结为一个问题:字符串列表是对象列表吗?大多数人都会本能地回答:“当然可以!”
好吧,看看下面几行:
lo.add(new Object()); // 3
String s = ls.get(0); // 4: Attempts to assign an Object to a String!
首先,第1行创建一个String类型List,第2行将lo指向ls。因此,ls中不再只保存字符串,当我们尝试从它那里获取一些东西时,我们得到了一个突然的惊喜。
Java编译器当然会阻止这种情况的发生。第2行将导致编译时错误。
通常,如果Foo是Bar的子类型(子类或子接口),而G是某个泛型类型声明,直觉认为G<Foo>是G<Bar>的一个子类型。这可能是关于泛型您需要学习的最困难的事情,因为它违背了我们根深蒂固的直觉。
我们不应该假定集合不变。我们的本能可能会让我们认为这些东西是不可改变的。
例如,如果机动车辆部向人口普查局提供一份司机名单,这似乎是合理的。我们认为一个名单司机是一个List,假设Driver是Person的子类型。事实上,传递的是驱动程序注册表的副本。否则,人口普查局可能会在名单上增加新的非司机,破坏DMV的记录。
为了应对这种情况,考虑更灵活的泛型类型是很有用的。到目前为止,我们看到的规则是非常严格的。
通配符
考虑编写一个输出集合中所有元素的例程的问题。以下是你在该语言的旧版本(即5.0之前的版本)中可能会如何编写它:
void printCollection(Collection c) {
Iterator i = c.iterator();
for (k = 0; k < c.size(); k++) {
System.out.println(i.next());
}
}
下面是一个使用泛型(和新的for循环语法)编写它的幼稚尝试:
void printCollection(Collection<Object> c) {
for (Object e : c) {
System.out.println(e);
}
}
问题是这个新版本比旧版本用处小得多。旧代码可以使用任何类型的集合作为参数来调用,而新代码只接受collections <Object>,正如我们刚才所演示的,它不是所有类型的集合的超类型!
那么,各种集合的超类型是什么呢?书面Collection<?>,即元素类型匹配任何元素的集合。它被称为通配符类型的原因很明显。我们可以写:
void printCollection(Collection<?> c) {
for (Object e : c) {
System.out.println(e);
}
}
现在,我们可以用任何类型的集合来调用它。请注意,在printCollection()内部,我们仍然可以从c中读取元素并给它们类型对象。这总是安全的,因为无论集合的实际类型是什么,它都包含对象。然而,向它添加任意对象是不安全的:
Collection<?> c = new ArrayList<String>();
c.add(new Object()); // Compile time error
因为我们不知道c的元素类型代表什么,所以我们不能向它添加对象。add()方法接受E类型的参数,即集合的元素类型。当实际类型参数为?时,它表示某个未知类型。传递给add的任何参数都必须是这个未知类型的子类型。因为我们不知道那是什么类型,所以我们不能传入任何东西。唯一的例外是null,它是每种类型的成员。
另一方面,给定一个列表,我们可以调用get()并使用其结果。结果类型是未知类型,但我们总是知道它是一个对象。因此,可以安全地将get()的结果赋值给类型为Object的变量,或将其作为需要类型对象的参数传递。
有界通配符
考虑一个可以绘制矩形和圆形等形状的简单绘图应用程序。为了在程序中表示这些形状,你可以定义这样的类层次结构:
// 形状
public abstract class Shape {
public abstract void draw(Canvas c);
}
// 圆形
public class Circle extends Shape {
private int x, y, radius;
public void draw(Canvas c) {
...
}
}
// 长方形
public class Rectangle extends Shape {
private int x, y, width, height;
public void draw(Canvas c) {
...
}
}
这些类可以绘制在画布上:
// 画布
public class Canvas {
public void draw(Shape s) {
s.draw(this);
}
}
任何绘图通常都会包含许多形状。假设它们以列表的形式表示,那么在画布中使用一个方法将它们全部绘制出来就很方便了:
public void drawAll(List<Shape> shapes) {
for (Shape s: shapes) {
s.draw(this);
}
}
现在,drawAll()只能传入List,而不能传入List。这很不幸,因为该方法所做的只是从列表中读取形状,所以它也可以在list <Circle>上调用。我们真正想要的是让该方法接受任何形状的列表:
public void drawAll(List<? extends Shape> shapes) {
...
}
这里有一个很小但非常重要的区别:我们用List<? extends Shape>替换了List。现在drawAll()将接受Shape的任何子类的列表,所以我们现在可以在调用方法时传入List。
List<? extends Shape>是有界通配符的一个例子。?表示未知类型,就像我们前面看到的通配符一样。然而,在这种情况下,我们知道这个未知的类型实际上是Shape的子类型。(注意:它可以是Shape本身,也可以是某个子类), 我们说Shape是通配符的上界。
通常,使用通配符的灵活性是要付出代价的。这样做的代价是,现在在方法体中写入shapes是违法的。例如,这是不允许的:
public void addRectangle(List<? extends Shape> shapes) {
// Compile-time error!
shapes.add(0, new Rectangle());
}
您应该能够找出为什么上面的代码是不允许的。add()的第二个参数的类型是 ? extends Shape,一个Shape的未知子类型。因为我们不知道它是什么类型,不知道它是否是Rectangle的超类;它可能是Rectangle,也可能不是,所以在那里传递Rectangle是不安全的。
有界通配符正是处理DMV将其数据传递给人口普查局的示例所需要的。我们的示例假设数据是通过从名字(表示为字符串)到人(表示为引用类型,如Person或其子类型,如Driver)的映射来表示的。Map< K, V>是一个泛型类型的例子,它接受两个类型参数,表示映射的键和值。
再次注意形式类型参数的命名约定——K表示键,V表示值。
public class Census {
public static void addRegistry(Map<String, ? extends Person> registry) {
}
...
Map<String, Driver> allDrivers = ... ;
Census.addRegistry(allDrivers);
泛型方法
考虑编写一个方法,该方法接受对象数组和集合,并将数组中的所有对象放入集合中。以下是第一个尝试:
static void fromArrayToCollection(Object[] a, Collection<?> c) {
for (Object o : a) {
c.add(o); // compile-time error
}
}
到目前为止,您已经学会避免初学者所犯的尝试使用Collections <Object>作为集合参数的类型。您可能意识到或没有意识到使用Collection<?>也行不通。回想一下,您不能只是将对象推入一个未知类型的集合。
处理这些问题的方法是使用泛型方法。就像类型声明一样,方法声明也可以是泛型的——也就是说,由一个或多个类型参数参数化。
static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
for (T o : a) {
c.add(o); // Correct
}
}
我们可以使用元素类型是数组元素类型超类型的任何类型的集合调用此方法。
Object[] oa = new Object[100];
Collection<Object> co = new ArrayList<Object>();
// T inferred to be Object
fromArrayToCollection(oa, co);
String[] sa = new String[100];
Collection<String> cs = new ArrayList<String>();
// T inferred to be String
fromArrayToCollection(sa, cs);
// T inferred to be Object
fromArrayToCollection(sa, co);
Integer[] ia = new Integer[100];
Float[] fa = new Float[100];
Number[] na = new Number[100];
Collection<Number> cn = new ArrayList<Number>();
// T inferred to be Number
fromArrayToCollection(ia, cn);
// T inferred to be Number
fromArrayToCollection(fa, cn);
// T inferred to be Number
fromArrayToCollection(na, cn);
// T inferred to be Object
fromArrayToCollection(na, co);
// compile-time error
fromArrayToCollection(na, cs);
注意,我们不需要将实际的类型参数传递给泛型方法。编译器根据实际实参的类型为我们推断出type实参。它通常会推断出使调用类型正确的最具体的类型参数。
出现的一个问题是:什么时候应该使用泛型方法,什么时候应该使用通配符类型? 为了理解答案,让我们来看看来自Collection库的一些方法。
interface Collection<E> {
public boolean containsAll(Collection<?> c);
public boolean addAll(Collection<? extends E> c);
}
我们可以在这里使用泛型方法:
interface Collection<E> {
public <T> boolean containsAll(Collection<T> c);
public <T extends E> boolean addAll(Collection<T> c);
// Hey, type variables can have bounds too!
}
然而,在containsAll和addAll中,类型参数T只使用一次。返回类型不依赖于类型参数,也不依赖于该方法的任何其他参数(在本例中,只有一个参数)。这告诉我们,type参数被用于多态性;它唯一的作用是允许在不同的调用位置使用各种实际参数类型。如果是这种情况,应该使用通配符。通配符被设计为支持灵活的子类型,这正是我们在这里试图表达的。
泛型方法允许使用类型参数来表示一个或多个参数的类型之间对方法及其返回类型的依赖关系。如果没有这样的依赖关系,就不应该使用泛型方法。
可以同时使用泛型方法和通配符。下面是Collections.copy()方法:
class Collections {
public static <T> void copy(List<T> dest, List<? extends T> src) {
...
}
请注意这两个参数类型之间的依赖关系。从源列表src复制的任何对象都必须可分配给目标列表dest的元素类型T。所以src的元素类型可以是t的任何子类型——我们不关心是哪个。copy的签名使用类型参数表示依赖关系,但是使用通配符表示第二个参数的元素类型。
我们可以用另一种方式来编写这个方法的签名,而根本不使用通配符:
class Collections {
public static <T, S extends T> void copy(List<T> dest, List<S> src) {
...
}
这很好,但是虽然第一个类型参数在dest类型和第二个类型参数的范围中都使用,S, S本身只在src类型中使用一次——其他任何东西都不依赖于它。这表明我们可以用通配符替换S。使用通配符比声明显式类型参数更清晰、更简洁,因此应该尽可能使用通配符。
通配符还有一个优点,即它们可以在方法签名之外使用,如字段类型、局部变量和数组。下面是一个例子。
回到我们的形状绘制问题,假设我们想保存绘制请求的历史记录。我们可以在class Shape内部的静态变量中维护历史,并让drawAll()将传入的参数存储到历史字段中。
static List<List<? extends Shape>> history = new ArrayList<List<? extends Shape>>();
public void drawAll(List<? extends Shape> shapes) {
history.addLast(shapes);
for (Shape s: shapes) {
s.draw(this);
}
}
最后,让我们再次注意类型参数使用的命名约定。我们用T表示类型,当没有任何关于类型的更具体的东西来区分它的时候。这在泛型方法中很常见。如果有多个类型参数,我们可以在字母表中使用与T相邻的字母,例如s。如果泛型方法出现在泛型类中,最好避免为方法和类的类型参数使用相同的名称,以避免混淆。这同样适用于嵌套泛型类。
与遗留代码进行互操作
到目前为止,我们的所有示例都假定了一个理想化的世界,即每个人都在使用支持泛型的最新版本的Java编程语言。
唉,事实并非如此。数百万行代码已经用该语言的早期版本编写,它们不会在一夜之间全部转换。
稍后,在将遗留代码转换为使用泛型一节中,我们将处理将旧代码转换为使用泛型的问题。在本节中,我们将关注一个更简单的问题:遗留代码和泛型代码如何互操作?这个问题有两个部分:在泛型代码中使用遗留代码,以及在遗留代码中使用泛型代码。
在通用代码中使用遗留代码
如何使用旧代码,同时在自己的代码中仍然享受泛型的好处?
作为一个示例,假设您想使用com.Example.widgets包。Example.com market的工作人员开发了一套库存控制系统,如下图所示:
package com.Example.widgets;
public interface Part {...}
public class Inventory {
/**
* Adds a new Assembly to the inventory database.
* The assembly is given the name name, and
* consists of a set parts specified by parts.
* All elements of the collection parts
* must support the Part interface.
**/
public static void addAssembly(String name, Collection parts) {...}
public static Assembly getAssembly(String name) {...}
}
public interface Assembly {
// Returns a collection of Parts
Collection getParts();
}
现在,您需要添加使用上述API的新代码。最好确保总是使用适当的参数调用addAssembly()——也就是说,传入的集合确实是部件的集合。当然,泛型是为此量身定做的:
package com.mycompany.inventory;
import com.Example.widgets.*;
public class Blade implements Part {
...
}
public class Guillotine implements Part {
}
public class Main {
public static void main(String[] args) {
Collection<Part> c = new ArrayList<Part>();
c.add(new Guillotine()) ;
c.add(new Blade());
Inventory.addAssembly("thingee", c);
Collection<Part> k = Inventory.getAssembly("thingee").getParts();
}
}
当我们调用addAssembly时,它期望第二个参数是类型为Collection的。实际参数的类型是集合。这是可行的,但为什么呢?毕竟,大多数集合不包含Part对象,所以通常,编译器无法知道类型集合引用的是哪种类型的集合。
在适当的泛型代码中,集合总是伴随着类型参数。当像Collection这样的泛型类型不带类型参数时,它被称为原始类型。
大多数人的第一反应是,Collection实际上意味着Collection。然而,正如我们前面看到的,在需要Collection的地方传递Collection是不安全的。更准确的说法是,类型集合表示某个未知类型的集合,就像Collection<?>一样。
但是等等,那也不可能是对的!考虑对getParts()的调用,它返回一个集合。然后将这个值赋给k, k是一个集合。如果调用的结果是Collection<?>,赋值会出错。
实际上,赋值是合法的,但它会生成一个未检查的警告。警告是必需的,因为事实上编译器不能保证它的正确性。我们无法检查getAssembly()中的遗留代码,以确保返回的集合确实是部件的集合。代码中使用的类型是集合,可以合法地将所有类型的对象插入这样的集合中。
那么,这难道不是一个错误吗?理论上说,是的;但实际上,如果泛型代码要调用遗留代码,这必须是允许的。在这种情况下,赋值是安全的,这取决于程序员自己,因为getAssembly()的契约说它返回部件的集合,尽管类型签名没有显示这一点。
所以原始类型很像通配符类型,但是它们没有严格的类型检查。这是一个经过深思熟虑的设计决策,它允许泛型与预先存在的遗留代码互操作。
从泛型代码调用遗留代码本质上是危险的;一旦混合了泛型代码和非泛型遗留代码,泛型类型系统通常提供的所有安全保证都是无效的。然而,您仍然比没有使用泛型时好多了。至少您知道您这边的代码是一致的。
目前,非泛型代码比泛型代码多得多,而且不可避免地会出现两者混合的情况。
如果您发现必须混合遗留代码和通用代码,请密切注意未选中的警告。仔细考虑如何证明引起警告的代码的安全性。
如果您仍然犯了错误,而导致警告的代码确实不是类型安全的,该怎么办?让我们来看看这种情况。在这个过程中,我们将深入了解编译器的工作原理。
擦除和翻译
public String loophole(Integer x) {
List<String> ys = new LinkedList<String>();
List xs = ys;
xs.add(x); // Compile-time unchecked warning
return ys.iterator().next();
}
这里,我们为一个字符串列表和一个普通的旧列表添加了别名。我们向列表中插入一个整数,并尝试提取一个字符串。这显然是错误的。如果我们忽略警告并尝试执行此代码,它将在尝试使用错误类型时失败。在运行时,这段代码的行为如下:
public String loophole(Integer x) {
List ys = new LinkedList;
List xs = ys;
xs.add(x);
return(String) ys.iterator().next(); // run time error
}
当我们从列表中提取一个元素,并试图通过将其转换为字符串来将其视为字符串时,我们将得到一个ClassCastException。同样的事情也发生在泛型版本的loophole()上。
这样做的原因是,Java编译器将泛型实现为一种称为擦除的前端转换。您可以(几乎)将其视为一种源到源的转换,其中将loophole()的泛型版本转换为非泛型版本。
因此,Java虚拟机的类型安全和完整性永远不会受到威胁,即使存在未检查的警告。
基本上,擦除删除(或擦除)所有泛型类型信息。尖括号之间的所有类型信息都被抛出,因此,例如,一个参数化类型如List<String>被转换为列表。所有剩余的类型变量使用都被类型变量的上界(通常是Object)所取代。而且,只要结果代码的类型不正确,就会插入一个转换到适当类型的类型,就像loophole的最后一行一样。
关于擦除的全部细节超出了本教程的范围,但是我们刚才给出的简单描述与事实相去不远。了解一点这方面的知识是很好的,特别是如果您想做更复杂的事情,比如将现有api转换为使用泛型(请参阅将遗留代码转换为使用泛型一节),或者只是想了解为什么事情会这样。
在遗留代码中使用通用代码
现在让我们考虑相反的情况。想象一下,Example.com选择将他们的API转换为使用泛型,但是他们的一些客户端还没有这样做。所以现在的代码是这样的:
package com.Example.widgets;
public interface Part {
...
}
public class Inventory {
/**
* Adds a new Assembly to the inventory database.
* The assembly is given the name name, and
* consists of a set parts specified by parts.
* All elements of the collection parts
* must support the Part interface.
**/
public static void addAssembly(String name, Collection<Part> parts) {...}
public static Assembly getAssembly(String name) {...}
}
public interface Assembly {
// Returns a collection of Parts
Collection<Part> getParts();
}
客户端代码如下:
package com.mycompany.inventory;
import com.Example.widgets.*;
public class Blade implements Part {
...
}
public class Guillotine implements Part {
}
public class Main {
public static void main(String[] args) {
Collection c = new ArrayList();
c.add(new Guillotine()) ;
c.add(new Blade());
// 1: unchecked warning
Inventory.addAssembly("thingee", c);
Collection k = Inventory.getAssembly("thingee").getParts();
}
}
客户机代码是在引入泛型之前编写的,但它使用的是com.Example.widgets包和集合库,它们都使用泛型类型。客户端代码中所有泛型类型声明的使用都是原始类型。
第1行生成一个未检查的警告,因为将原始集合传递到预期的部件集合中,而编译器不能确保原始集合确实是部件的集合。
作为另一种选择,您可以使用source 1.4标记编译客户机代码,确保不会生成警告。但是,在这种情况下,您将不能使用JDK 5.0中引入的任何新语言特性。
要点
泛型类被其所有调用者共享
下面的代码片段打印了什么?
List <String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());
你可能会想说错,但你错了。它将输出true,因为泛型类的所有实例都具有相同的运行时类,而不管它们的实际类型参数是什么。
实际上,使类泛型的原因是它对所有可能的类型参数具有相同的行为;同一个类可以被看作有许多不同的类型。
因此,类的静态变量和方法也在所有实例中共享。这就是为什么在静态方法或初始化式中引用类型声明的类型参数,或者在静态变量的声明或初始化式中引用类型参数是非法的。
类型转换和instanceof
泛型类在其所有实例之间共享的另一个含义是,通常没有必要问一个实例它是否是泛型类型的特定调用的实例:
Collection cs = new ArrayList<String>();
// Illegal.
if (cs instanceof Collection<String>) { ... }
类似地:
// Unchecked warning,
Collection<String> cstr = (Collection<String>) cs;
给出一个未检查的警告,因为运行时系统不会为您检查这些内容。
类型变量也是如此
// Unchecked warning.
<T> T badCast(T t, Object o) {
return (T) o;
}
类型变量在运行时不存在。这意味着它们在时间和空间上都不需要性能开销,这很好。不幸的是,这也意味着您不能可靠地在类型转换中使用它们。
数组
数组对象的组件类型不能是类型变量或参数化类型,除非它是(未绑定的)通配符类型。可以声明元素类型为类型变量或参数化类型的数组类型,但不能声明数组对象。
这的确很烦人。这一限制是必要的,以避免下列情况:
// Not really allowed.
List<String>[] lsa = new List<String>[10];
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
// Unsound, but passes run time store check
oa[1] = li;
// Run-time error: ClassCastException.
String s = lsa[1].get(0);
如果允许参数化类型的数组,则前面的示例在编译时不会出现任何未检查的警告,但仍然会在运行时失败。我们将类型安全作为泛型的主要设计目标。特别是,该语言被设计成如果您的整个应用程序在使用javac -source 1.5进行编译时没有出现未检查的警告,那么它是类型安全的。
但是,您仍然可以使用通配符数组。前面代码的以下变体放弃使用元素类型为参数化的数组对象和数组类型。因此,我们必须显式地强制转换以从数组中获取字符串。
// OK, array of unbounded wildcard type.
List<?>[] lsa = new List<?>[10];
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
// Correct.
oa[1] = li;
// Run time error, but cast is explicit.
String s = (String) lsa[1].get(0);
在下一个变体中(会导致编译时错误),我们避免创建元素类型参数化的数组对象,但仍然使用带有参数化元素类型的数组类型。
// Error.
List<String>[] lsa = new List<?>[10];
类似地,试图创建元素类型为类型变量的数组对象会导致编译时错误:
<T> T[] makeArray(T t) {
return new T[100]; // Error.
}
因为类型变量在运行时不存在,所以无法确定实际的数组类型是什么。
解决这些限制的方法是使用类文字作为运行时类型标记,如下一节所述,类文字作为运行时类型标记。
Class Literals
它是由类,接口,数组或原始类型的名称或伪类型void组成的表达式,后面紧跟【.】和【class】
JDK 5.0中的一个变化是java.lang.Class是泛型的。这是一个将泛型用于容器类以外的其他东西的有趣例子。
现在这个类有一个类型参数T,您可能会问,T代表什么? 它代表类对象表示的类型。
例如,String.class的类型是Class<String>, Serializable.class的类型是Class<Serializable>。这可以用来提高反射代码的类型安全性。
特别是,由于类中的newInstance()方法现在返回T,所以在创建反射性对象时可以获得更精确的类型。
例如,假设您需要编写一个实用程序方法来执行一个数据库查询(以SQL字符串的形式给出),并返回数据库中与该查询匹配的对象集合。
一种方法是显式传入工厂对象,编写如下代码:
interface Factory<T> { T make();}
public <T> Collection<T> select(Factory<T> factory, String statement) {
Collection<T> result = new ArrayList<T>();
/* Run sql query using jdbc */
for (/* Iterate over jdbc results. */) {
T item = factory.make();
/* Use reflection and set all of item's
* fields from sql results.
*/
result.add(item);
}
return result;
}
你也可以:
select(new Factory<EmpInfo>(){
public EmpInfo make() {
return new EmpInfo();
}}, "selection string");
或者您可以声明一个类EmpInfoFactory来支持工厂接口
class EmpInfoFactory implements Factory<EmpInfo> {
...
public EmpInfo make() {
return new EmpInfo();
}
}
调用它:
select(getMyEmpInfoFactory(), "selection string");
这个解决方案的缺点是它需要:
使用类字面量作为工厂对象是很自然的,然后反射可以使用它。现在(没有泛型)代码可能会写成:
Collection emps = sqlUtility.select(EmpInfo.class, "select * from emps");
...
public static Collection select(Class c, String sqlStatement) {
Collection result = new ArrayList();
/* Run sql query using jdbc. */
for (/* Iterate over jdbc results. */ ) {
Object item = c.newInstance();
/* Use reflection and set all of item's
* fields from sql results.
*/
result.add(item);
}
return result;
}
然而,这并不能提供我们想要的精确类型的集合。现在这个类是泛型的,我们可以这样写:
Collection<EmpInfo>
emps = sqlUtility.select(EmpInfo.class, "select * from emps");
...
public static <T> Collection<T> select(Class<T> c, String sqlStatement) {
Collection<T> result = new ArrayList<T>();
/* Run sql query using jdbc. */
for (/* Iterate over jdbc results. */ ) {
T item = c.newInstance();
/* Use reflection and set all of item's
* fields from sql results.
*/
result.add(item);
}
return result;
}
上面的代码以类型安全的方式为我们提供了精确的集合类型。
这种使用类文字作为运行时类型标记的技术是一个非常有用的技巧。例如,这种习惯用法在用于操作注释的新api中被广泛使用。
通配符的更多乐趣
在本节中,我们将考虑通配符的一些更高级的用法。我们已经看到了几个例子,在这些例子中,有界通配符在读取数据结构时非常有用。现在考虑相反的情况,一个只写的数据结构。接口Sink就是这种类型的一个简单示例。
interface Sink<T> {
flush(T t);
}
我们可以想像下面的代码演示的那样使用它。方法writeAll()被设计用来将集合colll中的所有元素刷新到接收器snk,并返回最后一个刷新的元素。
public static <T> T writeAll(Collection<T> coll, Sink<T> snk) {
T last;
for (T t : coll) {
last = t;
snk.flush(last);
}
return last;
}
...
Sink<Object> s;
Collection<String> cs;
String str = writeAll(cs, s); // Illegal call.
如前所述,对writeAll()的调用是非法的,因为不能推断出有效的类型参数;String和Object都不是适合T的类型,因为集合元素和Sink元素必须是同一类型的。
我们可以通过使用通配符修改writeAll()的签名来修复这个错误,如下所示。
public static <T> T writeAll(Collection<? extends T>, Sink<T>) {...}
...
// Call is OK, but wrong return type.
String str = writeAll(cs, s);
现在调用是合法的,但是赋值是错误的,因为T与s的元素类型(Object)匹配,所以推断的返回类型是Object。
解决方案是使用一种我们还没有见过的有界通配符形式:带有下界的通配符。语法 ? super T 表示一个未知类型,它是T的超类(或T本身)。它是我们一直在使用的有界通配符的对偶,我们使用? extends T以表示一个未知类型,它是T的子类型。
public static <T> T writeAll(Collection<T> coll, Sink<? super T> snk) {
...
}
String str = writeAll(cs, s); // Yes!
使用此语法,调用是合法的,并且推断的类型是String。
现在让我们来看一个更现实的例子。java.util.TreeSet< E>表示一个有序的E类型元素tree。构造树集的一种方法是将Comparator对象传递给构造函数。该比较器将用于根据所需的顺序TreeSet的元素进行排序。
TreeSet(Comparator<E> c)
Comparator接口本质上是:
interface Comparator<T> {
int compare(T fst, T snd);
}
假设我们想要创建一个treesset。并传入一个合适的比较器,我们需要传入一个可以比较字符串的比较器。这可以用比较器字符串完成,但是比较器对象可以。就这样吧。然而,我们不能在Comparator上调用上面给出的构造函数。我们可以使用下界通配符来获得我们想要的灵活性:
TreeSet(Comparator<? super E> c)
这段代码允许使用任何适用的比较器。
作为使用下限通配符的最后一个例子,让我们看看Collections.max()方法,它返回作为参数传递给它的集合中的最大元素。现在,为了使max()工作,传递进来的集合的所有元素都必须实现Comparable。此外,它们必须相互比较。
第一次尝试泛化这个方法签名的结果是:
public static <T extends Comparable<T>> T max(Collection<T> coll)
也就是说,该方法接受与自身相当的某种类型T的集合,并返回该类型的元素。但是,这段代码限制太多了。要了解原因,请考虑一个可与任意对象相比较的类型:
class Foo implements Comparable<Object> {
...
}
Collection<Foo> cf = ... ;
Collections.max(cf); // Should work.
cf中的每个元素都可以与cf中的其他元素相媲美,因为每个这样的元素都是一个Foo,它可以与任何对象相媲美,特别是与另一个Foo。然而,使用上面的签名,我们发现呼叫被拒绝了。推断的类型必须是Foo,但是Foo没有实现Comparable<Foo>。
它没有必要完全与它本身相比较。所需要的就是它不能与它的超类型之一相比较。这给我们:
public static <T extends Comparable<? super T>> T max(Collection<T> coll)
注意,Collections.max()的实际签名涉及更多。我们将在下一节(将遗留代码转换为使用泛型)中继续讨论这个问题。这种推理几乎适用于任何可用于任意类型Comparable<? super T>
。
通常,如果你有一个API只使用类型参数T作为参数,它的使用应该利用下界通配符(?相反,如果API只返回T,你可以通过使用上界通配符给你的客户端更多的灵活性(? extends T
).
通配符捕获
现在应该很清楚了:
Set<?> unknownSet = new HashSet<String>();
...
/* Add an element t to a Set s. */
public static <T> void addToSet(Set<T> s, T t) {
...
}
下面的呼叫是非法的。
addToSet(unknownSet, "abc"); // Illegal.
实际传递的集合是字符串的集合,这没有什么区别;重要的是,作为参数传递的表达式是一个未知类型的集合,不能保证是一组字符串,或者是任何特定类型。
现在,考虑下面的代码:
class Collections {
...
<T> public static Set<T> unmodifiableSet(Set<T> set) {
...
}
}
...
Set<?> s = Collections.unmodifiableSet(unknownSet); // This works! Why?
这似乎是不应该被允许的;然而,看看这个特定的调用,允许它是绝对安全的。毕竟,unmodifiableSet()适用于任何类型的集合,无论其元素类型如何。
由于这种情况出现的相对频繁,有一个特殊的规则允许在非常特定的情况下使用这种代码,在这种情况下,代码可以被证明是安全的。该规则称为通配符捕获,允许编译器推断通配符的未知类型作为泛型方法的类型参数。
将遗留代码转换为使用泛型
前面,我们展示了新代码和遗留代码如何互操作。现在,我们来看看“泛化”旧代码这个更难的问题。
如果您决定将旧代码转换为使用泛型,则需要仔细考虑如何修改API。
你需要确保通用API没有过多的限制;它必须继续支持API的原始契约。再次考虑来自java.util.Collection的一些示例。泛型之前的API是这样的:
interface Collection {
public boolean containsAll(Collection c);
public boolean addAll(Collection c);
}
A naive attempt to generify it would be the following:
interface Collection<E> {
public boolean containsAll(Collection<E> c);
public boolean addAll(Collection<E> c);
}
虽然这当然是类型安全的,但它并不符合API的原始约定。containsAll()方法适用于任何类型的传入集合。只有当传入的集合真正只包含E的实例时,它才会成功,但是:
对于addAll(),我们应该能够添加任何由e的子类型实例组成的集合。我们在泛型方法小节中看到了如何正确地处理这种情况。
您还需要确保修改后的API与旧客户机保持二进制兼容性。这意味着API的删除必须与原始的、非泛型的API相同。在大多数情况下,这是很自然的,但也有一些微妙的情况。我们将研究我们遇到过的最微妙的情况之一,Collections.max()方法。正如我们在通配符更有趣一节中看到的,max()的一个可信签名是:
public static <T extends Comparable<? super T>>
T max(Collection<T> coll)
这很好,除了删除这个签名是:
public static Comparable max(Collection coll)
与max()的原始签名不同:
public static Object max(Collection coll)
当然可以为max()指定这个签名,但是没有这样做,并且所有调用Collections.max()的旧二进制类文件都依赖于返回Object的签名。
通过在形式类型形参T的范围内显式地指定超类,可以强制执行不同的擦除操作。
public static <T extends Object & Comparable<? super T>> T max(Collection<T> coll)
这是一个为类型参数给出多个边界的例子,使用语法T1 & T2…具有多个边界的类型变量被认为是边界中列出的所有类型的子类型。当使用多个绑定时,绑定中提到的第一个类型用作类型变量的擦除。
最后,我们应该记住,max只从它的输入集合中读取数据,因此也适用于T的任何子类型的集合。
这给我们带来了JDK中实际使用的签名:
public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)
在实践中很少出现这样的情况,但是专家库设计者应该在转换现有api时仔细考虑。
另一个需要注意的问题是协变返回,即在子类中细化方法的返回类型。您不应该在旧的API中利用这个特性。要了解原因,让我们看一个例子。
假设你最初的API是这样的:
public class Foo {
// Factory. Should create an instance of
// whatever class it is declared in.
public Foo create() {
...
}
}
public class Bar extends Foo {
// Actually creates a Bar.
public Foo create() {
...
}
}
利用协变,将其修改为:
public class Foo {
// Factory. Should create an instance of
// whatever class it is declared in.
public Foo create() {
...
}
}
public class Bar extends Foo {
// Actually creates a Bar.
public Bar create() {
...
}
}
现在,假设你的代码的第三方客户端写了以下代码:
public class Baz extends Bar {
// Actually creates a Baz.
public Foo create() {
...
}
}
Java虚拟机不直接支持覆盖具有不同返回类型的方法。编译器支持这个特性。因此,除非重新编译类Baz,否则它不会正确地覆盖Bar的create()方法。此外,Baz必须被修改,因为代码将被按照编写的那样拒绝——Baz中的create()返回类型不是Bar中的create()返回类型的子类型。