小知识,大挑战!本文正在参与“程序员必备小知识”创作活动
本文很多内容来源于Java官方文档。
泛型和泛型方法有参数类型,这使得它们可以准确地用描述用特定类型实例化时会发生什么。
一、泛型概述
泛型允许您抽象类型。最常见的示例是容器类型。例如集合中的那些类型。下面这个是个典型的用法
List myIntList = new LinkedList();
myIntList.add(new Integer(0));
Integer x = (Integer) myIntList.iterator().next();
第三行的类型转换有点烦人,通常,程序员知道将哪类数据放入特定列表中。编译器只能保证迭代器将返回 Object。为了确保对类型为 Integer 的变量的分配类型是安全的,需要强制转换。
转换带来了混乱,它还引入了运行时错误的可能性,因为程序员可能记错了取出的数据类型。
如果程序员可以实际表达自己的意图,并 将列表标记为包含特定数据类型,该怎么办?这是泛型背后的 核心思想。下面代码是对上面一段无泛型代码给出的泛型版本
List<Integer> myIntList = new LinkedList<Integer>();
myIntList.add(new Integer(0));
Integer x = myIntList.iterator().next();
注意 myIntList 的声明类型,List <Integer> 这样的声明称之为 带有类型参数的 通用接口。在创建列表对象的时候,还指定一个类型参数。
还需要注意的是:第 3 行行的强制转换已消失。
现在,你可能认为我们只是将转换转移了,从第三行转移到了第一行中,其实不是,这里有很大的不同。编译器现在可以在 编译时检查程序的类型 是否正确。 当声明 myIntList = new LinkedList<Integer>() 时,它告诉了我们 myIntList 的信息,该变量无论何时使用都只能添加进 integer 类型的数据,这个编译器对此进行保证。相反的是,如果是强制转换,那么程序员需要在使用到的地方都去做强制转换。
最终效果是提高 可读性 和 健壮性,尤其是在大型程序中。
二、定义简单泛型
这是 java.util 包中接口 List 和 Iterator 的定义的一小部分摘录:
public interface List <E> {
void add(E x);
Iterator<E> iterator();
}
public interface Iterator<E> {
E next();
boolean hasNext();
}
除了尖括号中的内容外,这段代码都应该很熟悉。这些是 List 和 Iterator 接口 的形式类型参数的声明。
类型参数可以在整个泛型声明中使用,几乎可以在使用普通类型的地方使用(尽管有一些重要限制:请参见 [细节]部分 )
你可能会这样想:这里的 List<Integer> 对于 List<E> 来说,E 被统一替换成了 Integer。
public interface List <Integer> {
void add(Integer x);
Iterator<Integer> iterator();
}
这种想法是对于理解有帮助,但是会引起误解,因为泛型的声明实际上不会以这样的方式进行扩展,在内存中、编译后等都不会有这种方式存在
类型参数类似于方法或构造中的普通参数。就像方法具有描述其操作值得 形参,泛型也一样。调用方法时,将实际参数替换为形参。
对于类型参数的命名,使用大写的单字母,比如 List<E> 中的 E 是元素 Element 的首字母。
三、泛型和子类型化
以下代码合法吗?
List<String> ls = new ArrayList<String>(); // 1
List<Object> lo = ls; // 2
第一行是合法的,对于第二行,大多数人的本能反应是可以的。继续看看下面的代码
lo.add(new Object()); // 3
String s = ls.get(0); // 4 尝试将 Object 赋值给字符串
这里通过 lo 访问 ls,也就是说,第三行:ls 里面被添加进了一个 Object 对象,第 4 行,将 Object 对象赋值给 String 对象。通过 lo 可以插入任意对象,结果已经不再是 String 了。
所以 Java 编译器防止这种情况的产生,第 2 行将导致编译时错误。
通常,如 Foo 是 Bar 的子类,并且 G 是一个泛型类型声明,G <Foo> 并非是 G <Bar> 的子类型。这可能是你学习泛型最困难的事情,因为他违背了我们一贯的直觉。
有一个例子可以帮助你来理解:
- Driver 驾驶员是 Person 人类的子类
List<Driver>如果是等同于List<Person>
那么:不是驾驶员的人员可能被添加到列表中,这是很危险的事情;不会开车的人成为了驾驶人员。
为了应对这种情况,考虑更灵活的泛型类型很有用。到目前为止,我们看到的规则非常严格。
四、通配符
看下面两段代码
/**
jdk 1.5 之前的写法
*/
void printCollectionOld(Collection c) {
Iterator i = c.iterator();
for (int k = 0; k < c.size(); k++) {
System.out.println(i.next());
}
}
/**
使用泛型和新的循环语法
*/
void printCollectionNew(Collection<Object> c) {
for (Object e : c) {
System.out.println(e);
}
}
两个方法的功能都是打印集合中的元素:
- old 方法未使用泛型
- new 方法使用泛型,类型参数为 Object 类型
old 版本能接受任意类型的集合,然而 new 却只能接收类型参数为 Object 类型的集合,因为它不是 所有种类集合的超类型
Collection<?> 这个才是所有种类集合的超类型,读作 未知集合,匹配任意内容的集合。被称为 通配符类型,可以这样重写上面的代码
void printCollectionNew(Collection<?> c) {
for (Object e : c) {
System.out.println(e);
}
}
在 new 方法的内部,我们可以把集合中元素指定为 Object 类型,这对于集合元素来说是正确的。但是不能往里面添加 Object 类型的元素,因为并不安全
Collection<?> c = new ArrayList<String>();
// 尝试将 Object 元素添加到未知集合中
c.add(new Object()); // 编译时错误
由于我们不知道 c 代表什么元素类型,因此无法向其中添加对象。
add() 方法接受元素类型为 E 的参数。当实际类型参数为 ? 时,它代表某种未知类型。我们传递给 add() 方法的任何参数都必须是 未知类型的子类型。由于我们不知道是什么类型,因此无法传递任何内容。null 是例外,因为它是每种类型的成员
一样的道理:定义 List<?>,可以写成 Object o = list.get(),元素类型是未知的,但是我们始终知道它是一个 Object。因此将 get() 的结果赋值给任何期望的类型,都是安全的。
上限通配符
考虑一个简单的绘图应用程序,它可以绘制诸如矩形和圆形的形状。为了在程序中表示这些形状,您可以定义这样的类层次结构:
// 形状
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<Shape> 类型的,而不能接受 List<Circle> 类型,由于 drawAll 方法只是调用 Shape 上的 draw 方法,因此我们需要一个可以 接受任何形状(Shape)的列表
我们可以如下定义,它将接受任何形状的列表
public void drawAll(List<? extends Shape> shapes) {
...
}
List<? extends Shape> 是有界通配符的示例,在这种情况下,我们其实知道 ? 这个未知类型实际上是 Shape 的子类型(它可以是 Shape 它本身,也可以是某些子类)。 这里的含义是 Shape 是通配符的 上限。
同样的,使用 通配符的灵活性 是要付出一定的代价。代价是:它不能写入一个形状了,如下
public void addRectangle(List<? extends Shape> shapes) {
// 产生编译时错误
shapes.add(0, new Rectangle());
}
因为 shapes.add() 需要的参数是一个 ? 未知类型的,我们无法传递一个未知的类型。
Map<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
}
}
上面讲过,这里不能添加元素到一个位置类型集合中。
要解决这些问题就是使用 通用方法。 就像类型声明一样,方法声明可以是通用 的,即由 一个或多个类型参数进行参数化
static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
for (T o : a) {
c.add(o); // 正确
}
}
我们可以使用任何类型的集合(其 元素类型 是 数组的元素类型 的 超类型)来调用此方法。
Object[] oa = new Object[100];
Collection<Object> co = new ArrayList<Object>();
// T 被推导为 Object
fromArrayToCollection(oa, co);
String[] sa = new String[100];
Collection<String> cs = new ArrayList<String>();
// T 被推导为 Object
fromArrayToCollection(sa, cs);
// T 被推导为 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 被推导为 Number
fromArrayToCollection(ia, cn);
// T 被推导为 Number
fromArrayToCollection(fa, cn);
// T 被推导为 Number
fromArrayToCollection(na, cn);
// T 被推导为 Object
fromArrayToCollection(na, co);
// 编译错误:cs 元素类型是 String,na 的元素类型为 Number
fromArrayToCollection(na, cs);
注意,我们不必将实际的类型参数传递给泛型方法。编译器根据实际参数的类型为我们推断出类型参数。通常,它将推断出使调用类型正确的 最具体的类型参数。
通用方法和通配符什么时候使用?
出现的一个问题是:什么时候应该使用通用方法,什么时候应该使用通配符类型? 为了理解答案,让我们研究一下 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);
// 类型变量也可以有边界!
}
在上述场景中, T 只被使用一次(只有一个参数),返回类型也不依赖于 T,应该使用通配符,通配符旨在 支持灵活的子类型,这是我们在这里试图表达的。
通用方法允许 使用类型参数 来表示 方法的一个或多个参数的类型 和/或其 返回类型之间的依赖性。如果没有这种依赖性,则不应使用通用方法。
可以 同时 使用 通用方法和通配符。比如方法 Collections.copy():
class Collections {
public static <T> void copy(List<T> dest, List<? extends T> src) {
...
}
注意两个参数的 类型之间的依赖关系。从源列表 src 复制的任何对象都必须分配给目标列表 dst 的元素类型 T。因此,src 的元素类型可以是 T 的任何子类型,我们不在乎是哪个子类型。copy 的签名使用 类型参数 表示依赖项,但对第二个参数的元素类型使用 通配符。
我们可以使用另一种方式编写此方法签名,而不使用通配符
class Collections {
public static <T, S extends T> void copy(List<T> dest, List<S> src) {
...
}
第一个类型参数在边界中被 S 使用,但是 S 本身只被使用了一次,这说明我们可以用通配符来代替 S。使用通配符比声明显式类型参数更清晰,更简洁,因此尽可能使用通配符
通配符还具有可以 在方法签名之外使用的优点,例如字段,局部变量和数组的类型。这是一个例子。
回到我们的形状绘图问题,假设我们要保留绘图请求的历史记录,可以将历史记录保存在 class 的静态变量内
static List<List<? extends Shape>>
history = new ArrayList<List<? extends Shape>>();
public void drawAll(List<? extends Shape> shapes) {
// 这里将请求绘图的记录添加到历史记录中
history.add(shapes);
for (Shape s : shapes) {
s.draw(this);
}
}
类型参数命名约定
最后,让我们再次注意类型参数使用的命名约定。
当 没有任何关于类型的更具体的东西来区分它 时,我们使 用 T 表示类型。这在泛型方法中经常出现。
如果有 多个类型参数,我们可能会使用 字母表中与 T 相邻的字母,比如 s。
如果 泛型方法出现在泛型类中,最好 避免对方法和类的类型参数使用相同的名称,以避免混淆。嵌套泛型类也是如此。
六、与旧版代码互操作
到目前为止,我们所有的示例都假设了一个理想化的世界:每个人都在使用支持泛型的 Java 编程语言的最新版本。实际上并非如此。数百万行代码已用该语言的早期版本编写。
在将旧版代码转换为使用泛型部分中,我们将解决 将旧代码转换为使用泛型的问题。
我们将关注一个简单的问题:旧代码和泛型代码如何互操作?这个问题分为两个部分:
- 在泛型代码中使用旧代码
- 在旧代码中使用泛型代码
在泛型代码中使用旧代码
在仍然享受自己代码中泛型的好处的同时,如何使用旧代码?
例如,假设您要使用 package com.Example.widgets。Example.com 是一个销售库存控制系统,其要点如下所示:
package com.Example.widgets;
public interface Part {...}
public class Inventory {
public static void addAssembly(String name, Collection parts) {...}
public static Assembly getAssembly(String name) {...}
}
public interface Assembly {
Collection getParts();
}
现在,您想添加使用上述 API 的新代码。始终确保 addAssembly() 使用正确的参数进行调用是一件好事,也就是说,您传入的集合 Collection 中的元素的确是 Part。当然,泛型是为此而量身定制的:
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。实际参数的类型是 Collection<Part>。这是可行的,但为什么呢?毕竟,大多数集合不包含 Part 对象,因此通常,编译器无法知道 Collection 类型所指的集合类型。
在适当的 泛型代码中,集合总是伴随着 类型参数。当使用像 Collection 这样的泛型类型而 不带类型参数 时,它被称为 原始类型
多数人的本能会认为 Collection 是 Collection<Object>,正如前面所看到的,在需要 Collection<Object> 的地方传递 Collection<Part> 是不安全的。更准确的说,Collection 表示某种未知类型的集合,就像 Collection<?>
但是,等等,那也不对!考虑对 getParts() 的调用,该调用返回 Collection。然后将其分配给 Collection<Part> k 。如果调用的结果为 Collection<?>,则分配将为错误。
实际上,该分配是合法的,但是会生成 未经检查的警告。需要警告,因为事实是编译器无法保证其正确性。我们无法检查旧代码,getAssembly() 以确保确实返回的集合的元素是 Part。代码中使用的类型是 Collection,并且可以合法地将各种对象插入此类集合中。
所以,这不应该是一个错误吗?从理论上讲,是的。但是实际上,如果泛型代码要调用旧代码,则必须允许这样做。在这种情况下,由程序员(您自己)决定,这是安全的,因为即使类型签名没有显示,契约 getAssembly() 仍会返回 Part 的集合,这是安全的。
因此,原始类型 非常 类似于通配符类型,但是没有严格地对它们进行类型检查。这是一个经过深思熟虑的设计决策,以允许泛型与现有的旧代码互操作。
在泛型代码中调用旧代码本质上是危险的,他们一混合,泛型提供的安全保障将都失效,但是,与完全不使用泛型的情况相比,至少您知道自己的代码是一致的。
目前,存在更多的非泛型代码,然后是泛型代码,并且不可避免地会出现必须混合使用的情况。
如果发现 必须混合使用旧代码和泛型代码,请密切 注意未检查的警告。仔细考虑如何才能证明引起警告的代码的安全性。
如果您仍然犯了一个错误,并且引起警告的代码确实不是安全的,会发生什么?让我们来看看这种情况。在此过程中,我们将深入了解编译器的工作原理。
泛型擦除
public String loophole(Integer x) {
List<String> ys = new LinkedList<String>();
List xs = ys;
xs.add(x); // 编译时检查警告
return ys.iterator().next();
}
这里将 List<String> ys 取了一个别名 List xs,将 Integer 类型放入了 String 类型的集合中。在程序运行时,将失败。
在程序运行时,上述代码行为类似与如下代码
public String loophole(Integer x) {
List ys = new LinkedList;
List xs = ys;
xs.add(x);
return(String) ys.iterator().next(); // 运行时异常
}
当我们从列表中提取一个元素,并尝试通过将其强制转换为 String 时,我们将得到一个 ClassCastException。
这样做的原因是,泛型由 Java 编译器实现为称为 erasure 的前端转换,将泛型版本,转换为非泛型版本。
结果,即使存在未经检查的警告,Java 虚拟机的类型安全性和完整性也永远不会受到威胁。
基本上会 擦除 所有泛型类型信息,擦除了尖括号之间的所有类型信息,例如,将类似的参数化类型List<String>转换为 List,类型变量的所有其余用法都由类型变量的上限(通常为 Object )代替。而且,只要结果代码的类型不正确,就会插入对相应类型的强制类型转换,例如上述代码的最后一行。
在旧代码中使用泛型
现在让我们考虑相反的情况。想象一下 Example.com 选择将其 API 转换为使用泛型,但其中一些客户端尚未使用。因此,现在的代码如下所示:
package com.Example.widgets;
public interface Part {
...
}
public class Inventory {
public static void addAssembly(String name, Collection<Part> parts) {...}
public static Assembly getAssembly(String name) {...}
}
public interface Assembly {
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 行产生一个未检查的警告,因为编译器不能确保 Collection 的元素是 Part 类型的
七、细节
泛型类由其所有调用共享
以下代码打印信息是?
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());
你可能会想说 false,但是你错了,它打印 true;因为泛型类的 所有实例都具有相同的运行时类,而 不管实际类型参数如何。他们的 class 都是 class java.util.ArrayList
使类具有泛型的原因是,它对所有可能的 类型参数都具有相同的行为。可以将同一类视为具有许多不同的类型。
结果,类的 静态变量 和 方法 也将在所有实例之间共享。因此,在静态方法、初始化块、静态变量,静态初始化块声明中,引用类型声明的类型参数是非法 的。
public static class Test<T> {
// 比如在静态变量中使用泛型类型,就是非法的
static T xx = null;
}
强制转换和 InstanceOf
泛型类在其所有实例之间共享的事实的另一个含义是:询问实例是否是 泛型类型的特定调用的实例 通常没有任何意义:
Collection cs = new ArrayList<String>();
// 非法.
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;
}
类型变量在运行时不存在。这意味着它们在时间和空间上都没有性能开销,这很好。不幸的是,这也意味着您不能在类型转换中可靠地使用它们。
数组
数组对象的 组件类型 不能是 类型变量 或 参数化类型,除非它是(无界的) 通配符类型。。您可以声明其元素类型为 类型变量 或 参数化类型 的 数组类型,但不能声明数组对象。
// 不允许的
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));
// 不健全,但通过运行时存储检查
oa[1] = li;
// 运行时异常: ClassCastException.
String s = lsa[1].get(0);
如果允许使用参数化类型的数组,则前面的示例将在编译时没有任何未经检查的警告,但在运行时会失败。我们已经将类型安全性作为泛型的主要设计目标。特别是,该语言旨在确保 如果使用javac-source 1.5编译了整个应用程序而没有未经检查的警告,则该语言是安全的。
// 无界通配符类型的数组。
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 类作为运行时类型令牌,如下一节中所述,Class 类作为运行时类型令牌。
八、Class 类作为运行时类型令牌
Jdk 5.0 中的一个变化是 java. lang. class 类是通用的。这是一个有趣的示例,用于容器类以外的其他对象。
有 Class 和 T,您可能会问,T 代表什么?它表示 Class 对象所表示的类型.
例如,String. Class 的类型是 Class<String>, Serializable. Class 的类型是 Class<Serializable>。 这可以用来提高反射代码的类型安全性。
// java.lang.Class#newInstance
public T newInstance()
特别是:newInstance 返回 T,因此在反射创建对象时,您可以获得更精确的类型。
例如,假设您需要编写一个实用程序方法来执行数据库查询(以 SQL 字符串形式给出),并在数据库中返回与该查询匹配的对象的集合。
一种方法是显式传递工厂对象,编写如下代码:
interface Factory<T> { T make();}
public <T> Collection<T> select(Factory<T> factory, String statement) {
Collection<T> result = new ArrayList<T>();
/* 使用jdbc运行sql查询 */
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;
}
可以如下调用
xx.select(new Factory<EmpInfo>() {
public EmpInfo make() {
return new EmpInfo();
}
}, "selection string");
或者您可以声明一个 EmpInfoFactory 类来实现 Factory 接口
class EmpInfoFactory implements Factory<EmpInfo> {
...
public EmpInfo make() {
return new EmpInfo();
}
}
调用则如下
select(getMyEmpInfoFactory(), "selection string");
该解决方案的缺点是它需要:
- 在调用时:需要使用比较长的匿名工厂类
- 或则为每种类型声明一个工厂类,并在调用时,传递一个工厂实例,这有点不自然
将 Class 类作为工厂对象是很自然的,然后通过反射使用它,这里没有泛型,代码可能如下
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;
}
但是,这不能为我们提供所需的精确类型的集合。现在 Class 是通用的,我们可以改写以下内容
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;
}
上面的代码以类型安全的方式为我们提供了精确的集合类型。
使用 Class 类作为运行时类型标记的这种技术是一个非常有用的技巧,例如,它是一种新用法,在新的 API 中广泛使用来处理注释。
九、通配符带来更多乐趣
在本节中,我们将考虑通配符的一些更高级的用法。前面讲解了几个示例,其中从数据结构中读取数据时,有界通配符很有用。现在考虑相反的,只写数据结构。
interface Sink<T> {
void flush(T t);
}
如下使用:writeAll 将列表中的数据通过 flush 方法写入,然后返回最后一个写入的数据元素
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 元素和 Collection 元素类型要是相同的。
我们可以使用通配符来改写 writeAll 方法声明
public static <T> T writeAll(Collection<? extends T>, Sink<T>) {...}
...
// 调用正常,但是返回类型错误
String str = writeAll(cs, s);
调用正常,但是返回类型错误,推导出的返回类似是 Objec,是由 s 变量推导出来的,cs 是 T 的子类,S 是 T。
下界通配符
解决方案是使用 下界通配符,? super T,表示未知类型是 T 的超类或则就是 T.
public static <T> T writeAll(Collection<T> coll, Sink<? super T> snk) {
...
}
String str = writeAll(cs, s); // Yes!
? super T:下界通配符,至少是 T 类型? extends T:上限通配符,必须是 T 的子类或 T
让我们再来看一个例子, java.util.TreeSet<E> 的 E 表示要排序的元素类型。在构造函数可以传入一个比较器
TreeSet(Comparator<E> c)
interface Comparator<T> {
int compare(T fst, T snd);
}
假设我们想要创建一个 TreeSet<String> 并传递一个合适的比较器,我们需要传递一个可以比较字符串的比较器。这可以通过 Comparator<String> 完成,但是 Comparator<Object> 也可以完成。
但是,我们无法把 Comparator<Object> 传递给上面的构造函数,这个时候就可以使用下界通配符来获得我们想要的灵活性
TreeSet(Comparator<? super E> c)
这个测试用例如下,因为 TreeSet 已经使用下界通配符了,我们用自己的例子来演示
public class Demo<E> {
public static void main(String[] args) {
new Demo<String>(new Comparator<Object>() {
@Override
public int compare(Object o1, Object o2) {
return 0;
}
});
}
public Demo(Comparator<? super E> xx) {
}
interface Comparator<T> {
int compare(T fst, T snd);
}
}
再来看一个下界通配符的示例 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); // 应该正常工作
Foo 实现了 Comparable,因此每个 Foo 元素都可以与另一个 Foo 元素比较,但是调用被拒绝,失败了。
被推导为,T 必须和 T 比较,T 不必与自己完全可比。所需要做的就是让 T 与其超类型之一可比。这给我们:
public static <T extends Comparable<? super T>> T max(Collection<T> coll)
通常
-
? super T:下界通配符如果 API 用 T 作为参数
-
? extends T:上限通配符如果 API 只返回 T
而上面的 max,又用 T 作为参数,又用 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.
作为参数传递的表达式是一组未知类型,不能保证是一组字符串,或者特别是任何类型。
因为可以从第 2 个传入的参数推导出是字符串类型,但是第一个参数是一个 ? 未知类型。 他们又公用一个 T,所以调用非法。
看下面一个例子:
class Collections {
...
<T> public static Set<T> unmodifiableSet(Set<T> set) {
...
}
}
...
Set<?> s = Collections.unmodifiableSet(unknownSet); // 这个调用正常,为什么?
从前面看起来,也是不允许的,但是因为这里只有一个参数,它是绝对安全的,毕竟,无论元素类型是什么, unmodifiableSet 都适用于任何类型的 Set。
由于这种情况经常发生,因此有一条特殊的规则允许在非常特殊的情况下使用此类代码,在这种情况下,可以证明该代码是安全的。该规则称为 通配符捕获,它允许编译器将通配符的未知类型作为通用方法的类型参数进行推断。
十、将旧版代码转换为使用泛型
入参转换
如果您决定将旧代码转换为使用泛型,则需要仔细考虑如何修改 API。
你必须确保泛型 API 不受限制,它必须支持原来的旧的调用。考虑一个示例:java.util.Collection
interface Collection {
public boolean containsAll(Collection c);
public boolean addAll(Collection c);
}
尝试把他修改为泛型的
interface Collection<E> {
public boolean containsAll(Collection<E> c);
public boolean addAll(Collection<E> c);
}
虽然这个 API 保证了类型的安全,对于 containsAll() API 不符合原来的 API 了。
containsAll(Collection c):不同类型的集合是可以比较的,而新的必须要是两个 E 类型的集合- 传入不同类型的集合不行了,可能是因为调用方不知道传入的集合的确切类型,或者因为它是集合
<S>,其中 S 是 E 的子类型。
对于 addAll 应该可以添加任何 E 类型的或 E 子类型的元素。这个在前面通用方法中讲解过了。
您还需要确保修订后的 API 保留与旧客户端的二进制兼容性。这意味着该 API 的擦除必须与原始的未经过增强的API 相同。在大多数情况下,这是自然而然的事情,但是有一些微妙的情况。我们将研究我们遇到的最微妙的情况之一:Collections.max(),在前面也讲解过。合理的签名是
public static <T extends Comparable<? super T>> T max(Collection<T> coll)
删除泛型后
public static Comparable max(Collection coll)
可以看到与原始 API 不同了
public static Object max(Collection coll)
所有旧的二进制文件依赖返回 Object 的方法签名。
这个时候我们可以使用显示指定 超类型来作为泛型变量的擦除
public static <T extends Object & Comparable<? super T>> T max(Collection<T> coll)
已知具有多个界限的类型变量是界限中列出的所有类型的子类型,边界中第一个类型将用作类型变量的擦除
JDK 中该方法的实际签名是
public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)
返回参数类型转换
另一个问题是返回参数类型的优化,你 不应该在旧的 API 中利用泛型功能,下面我们来探讨下为什么
假设旧 API 为
public class Foo {
// 工厂方法,返回 Foo 或他的子类
public Foo create() {
...
}
}
public class Bar extends Foo {
// 重写后,实际返回 Bar 类型
public Foo create() {
...
}
}
利用重载原理,我们可以把返回类型修改为实际的类型
public class Foo {
public Foo create() {
...
}
}
public class Bar extends Foo {
// 修改了返回类型
public Bar create() {
...
}
}
现在,假设您的代码的第三方客户端编写了以下内容:
public class Baz extends Bar {
// Actually creates a Baz.
public Foo create() {
...
}
}
Java 虚拟机 不直接支持具有不同返回类型的方法的覆盖。编译器支持此功能,因此 Baz 类必须重新,因为它不能覆盖 Bar 中的 create 方法,因为它返回的类型不是 Bar 中 create 返回的子类型。