使用通配符简化泛型使用
2008-06-02 02:37
190 查看
使用通配符简化泛型使用理解通配符捕获 |
级别: 高级 Brian Goetz (brian.goetz@sun.com), 高级工程师, Sun Microsystems 2008 年 5 月 26 日 通配符是 Java™ 语言中最复杂的泛型之一,特别是围绕捕获通配符 的处理和令人困惑的错误消息。在这一期的 Java 理论与实践 中,资深 Java 开发人员 Brian Goetz 解释了一些由 javac 生成的怪异错误消息并提供了一些简化泛型使用的技巧和解决方法。 自从泛型被添加到 JDK 5 语言以来,它一直都是一个颇具争议的话题。一部分人认为泛型简化了编程,扩展了类型系统从而使编译器能够检验类型安全;另外一些人认为泛型添加了很多不必要的复杂性。对于泛型我们都经历过一些痛苦的回忆,但毫无疑问通配符是最棘手的部分。 通配符基本介绍 泛型是一种表示类或方法行为对于未知类型的类型约束的方法,比如 “不管这个方法的参数 x和 y是哪种类型,它们必须是相同的类型”,“必须为这些方法提供同一类型的参数” 或者 “ foo()的返回值和 bar()的参数是同一类型的”。 通配符 — 使用一个奇怪的问号表示类型参数 — 是一种表示未知类型的类型约束的方法。通配符并不包含在最初的泛型设计中(起源于 Generic Java(GJ)项目),从形成 JSR 14 到发布其最终版本之间的五年多时间内完成设计过程并被添加到了泛型中。 通配符在类型系统中具有重要的意义,它们为一个泛型类所指定的类型集合提供了一个有用的类型范围。对泛型类 ArrayList而言,对于任意(引用)类型 T, ArrayList<?>类型是 ArrayList<T>的超类型(类似原始类型 ArrayList和根类型 Object,但是这些超类型在执行类型推断方面不是很有用)。 通配符类型 List<?>与原始类型 List和具体类型 List<Object>都不相同。如果说变量 x具有 List<?>类型,这表示存在一些 T类型,其中 x是 List<T>类型, x具有相同的结构,尽管我们不知道其元素的具体类型。这并不表示它可以具有任意内容,而是指我们并不了解内容的类型限制是什么 — 但我们知道存在 某种限制。另一方面,原始类型 List是异构的,我们不能对其元素有任何类型限制,具体类型 List<Object>表示我们明确地知道它能包含任何对象(当然,泛型的类型系统没有 “列表内容” 的概念,但可以从 List之类的集合类型轻松地理解泛型)。 通配符在类型系统中的作用部分来自其不会发生协变(covariant)这一特性。数组是协变的,因为 Integer是 Number的子类型,数组类型 Integer[]是 Number[]的子类型,因此在任何需要 Number[]值的地方都可以提供一个 Integer[]值。另一方面,泛型不是协变的, List<Integer>不是 List<Number>的子类型,试图在要求 List<Number>的位置提供 List<Integer>是一个类型错误。这不算很严重的问题 — 也不是所有人都认为的错误 — 但泛型和数组的不同行为的确引起了许多混乱。 我已使用了一个通配符 — 接下来呢? 清单 1 展示了一个简单的容器(container)类型 Box,它支持 put和 get操作。 Box由类型参数 T参数化,该参数表示 Box 内容的类型, Box<String>只能包含 String类型的元素。 清单 1. 简单的泛型 Box 类型
Box<?>类型的变量,比如清单 2 unbox()方法中的 box参数。 unbox()如何处理已传递的 box? 清单 2. 带有通配符参数的 Unbox 方法
get()方法,并且能调用任何从 Object继承而来的方法(比如 hashCode())。它惟一不能做的事是调用 put()方法,这是因为在不知道该 Box实例的类型参数 T的情况下它不能检验这个操作的安全性。由于 box是一个 Box<?>而不是一个原始的 Box,编译器知道存在一些 T充当 box的类型参数,但由于不知道 T具体是什么,您不能调用 put()因为不能检验这么做不会违反 Box的类型安全限制(实际上,您可以在一个特殊的情况下调用 put():当您传递 null字母时。我们可能不知道 T类型代表什么,但我们知道 null字母对任何引用类型而言是一个空值)。 关于 box.get()的返回类型, unbox()了解哪些内容呢?它知道 box.get()是某些未知 T的 T,因此它可以推断出 get()的返回类型是 T的擦除(erasure),对于一个无上限的通配符就是 Object。因此清单 2 中的表达式 box.get()具有 Object类型。 通配符捕获 清单 3 展示了一些似乎应该 可以工作的代码,但实际上不能。它包含一个泛型 Box、提取它的值并试图将值放回同一个 Box。 清单 3. 一旦将值从 box 中取出,则不能将其放回
Object不兼容的错误消息。 “capture#337 of ?” 表示什么?当编译器遇到一个在其类型中带有通配符的变量,比如 rebox()的 box参数,它认识到必然有一些 T,对这些 T而言 box是 Box<T>。它不知道 T代表什么类型,但它可以为该类型创建一个占位符来指代 T的类型。占位符被称为这个特殊通配符的捕获(capture)。这种情况下,编译器将名称 “capture#337 of ?” 以 box类型分配给通配符。每个变量声明中每出现一个通配符都将获得一个不同的捕获,因此在泛型声明 foo(Pair<?,?> x, Pair<?,?> y)中,编译器将给每四个通配符的捕获分配一个不同的名称,因为任意未知的类型参数之间没有关系。 错误消息告诉我们不能调用 put(),因为它不能检验 put()的实参类型与其形参类型是否兼容 — 因为形参的类型是未知的。在这种情况下,由于 ?实际表示 “?extends Object” ,编译器已经推断出 box.get()的类型是 Object,而不是 “capture#337 of ?”。它不能静态地检验对由占位符 “capture#337 of ?” 所识别的类型而言 Object是否是一个可接受的值。 捕获助手 虽然编译器似乎丢弃了一些有用的信息,我们可以使用一个技巧来使编译器重构这些信息,即对未知的通配符类型命名。清单 4 展示了 rebox()的实现和一个实现这种技巧的泛型助手方法(helper): 清单 4. “捕获助手” 方法
reboxHelper()是一个泛型方法,泛型方法引入了额外的类型参数(位于返回类型之前的尖括号中),这些参数用于表示参数和/或方法的返回值之间的类型约束。然而就 reboxHelper()来说,泛型方法并不使用类型参数指定类型约束,它允许编译器(通过类型接口)对 box 类型的类型参数命名。 捕获助手技巧允许我们在处理通配符时绕开编译器的限制。当 rebox()调用 reboxHelper()时,它知道这么做是安全的,因为它自身的 box参数对一些未知的 T而言一定是 Box<T>。因为类型参数 V被引入到方法签名中并且没有绑定到其他任何类型参数,它也可以表示任何未知类型,因此,某些未知 T的 Box<T>也可能是某些未知 V的 Box<V>(这和 lambda 积分中的 α 减法原则相似,允许重命名边界变量)。现在 reboxHelper()中的表达式 box.get()不再具有 Object类型,它具有 V类型 — 并允许将 V传递给 Box<V>.put()。 我们本来可以将 rebox()声明为一个泛型方法,类似 reboxHelper(),但这被认为是一种糟糕的 API 设计样式。此处的主要设计原则是 “如果以后绝不会按名称引用,则不要进行命名”。就泛型方法来说,如果一个类型参数在方法签名中只出现一次,它很有可能是一个通配符而不是一个命名的类型参数。一般来说,带有通配符的 API 比带有泛型方法的 API 更简单,在更复杂的方法声明中类型名称的增多会降低声明的可读性。因为在需要时始终可以通过专有的捕获助手恢复名称,这个方法让您能够保持 API 整洁,同时不会删除有用的信息。 类型推断 捕获助手技巧涉及多个因素:类型推断和捕获转换。Java 编译器在很多情况下都不能执行类型推断,但是可以为泛型方法推断类型参数(其他语言更加依赖类型推断,将来我们可以看到 Java 语言中会添加更多的类型推断特性)。如果愿意,您可以指定类型参数的值,但只有当您能够命名该类型时才可以这样做 — 并且不能够表示捕获类型。因此要使用这种技巧,要求编译器能够为您推断类型。捕获转换允许编译器为已捕获的通配符产生一个占位符类型名,以便对它进行类型推断。 当解析一个泛型方法的调用时,编译器将设法推断类型参数它能达到的最具体类型。 例如,对于下面这个泛型方法:
T是 Integer、 Number、 Serializable 或 Object,但它选择 Integer作为满足约束的最具体类型。 当构造泛型实例时,可以使用类型推断减少冗余。例如,使用 Box类创建 Box<String>要求您指定两次类型参数 String:
BoxImpl提供一个类似清单 5 的泛型工厂方法(这始终是个好主意),则可以减少客户机代码的冗余: 清单 5. 一个泛型工厂方法,可以避免不必要地指定类型参数
BoxImpl.make()工厂实例化一个 Box,您只需要指定一次类型参数:
make()方法为一些类型 V返回一个 Box<V>,返回值被用于需要 Box<String>的上下文中。编译器确定 String是 V能接受的满足类型约束的最具体类型,因此此处将 V推断为 String。您还可以手动地指定 V的值:
结束语 通配符无疑非常复杂:由 Java 编译器产生的一些令人困惑的错误消息都与通配符有关,Java 语言规范中最复杂的部分也与通配符有关。然而如果使用适当,通配符可以提供强大的功能。此处列举的两个技巧 — 捕获助手技巧和泛型工厂技巧 — 都利用了泛型方法和类型推断,如果使用恰当,它们能显著降低复杂性。 |
相关文章推荐
- Java 理论与实践: 使用通配符简化泛型使用
- 使用通配符简化泛型使用
- Java 理论与实践:使用通配符简化泛型使用
- [转贴] Java 理论与实践: 使用通配符简化泛型使用
- java使用通配符简化泛型使用
- Java 理论与实践: 使用通配符简化泛型使用
- 使用通配符简化泛型使用
- 转载 Java 理论与实践: 使用通配符简化泛型使用
- Java 理论与实践: 使用通配符简化泛型使用
- Java 理论与实践: 使用通配符简化泛型使用 (二)
- Java 理论与实践: 使用通配符简化泛型使用
- Java 理论与实践:使用通配符简化泛型使用
- Java 理论与实践:使用通配符简化泛型使用,第 2 部分
- [转贴] Java 理论与实践: 使用通配符简化泛型使用
- Java 理论与实践: 使用通配符简化泛型使用
- Java 理论与实践: 使用通配符简化泛型使用
- Java 理论与实践: 使用通配符简化泛型使用
- 数据库查询性能优化(合理使用索引|避免或简化排序|避免对大型表进行全表顺序扫描|避免使用相关的子查询|避免使用通配符匹配 )
- 泛型通配符的使用及类型限定上限
- 使用泛型和反射技术简化Spring jdbcTemplate的使用