JDK 5.0中的泛型类型学习 |
JDK 5.0 中增加的泛型类型,是 Java 语言中类型安全的一次主要改良 。然而,关于 首次 使用泛型类型的消费者来说,泛型的某些方面看起来可能不方便清楚,甚至十分奇怪 。在本月的“Java 实际和 实际”中,Brian Goetz 综合了 禁锢第一次 使用泛型的消费者的常见陷阱 。您 可以通过 探讨论坛与作者和 其余读者分享您对本文的 意见 。(也 可以单击本文顶端或底端的 探讨来 拜访这个论坛 。) 表面上看起来,无论语法还是 利用的环境( 比方容器类),泛型类型(或者泛型)都 类似于 C++ 中的模板 。然而这种 类似性仅限于表面,Java 语言中的泛型 根本上 彻底在编译器中实现,由编译器执行类型 审查和类型判断, 而后生成一般的非泛型的字节码 。这种实现技术称为擦除(erasure)(编译器 使用泛型类型信息 保障类型安全, 而后在生成字节码之前将其 革除),这项技术有一些奇怪,而且有时会带来一些令人 困惑的 后果 。 固然范型是 Java 类走向类型安全的一大步,然而在学习 使用泛型的过程中 几乎 确定会遇到头痛(有时候让人 无奈 忍耐)的问题 。 留神:本文 假如您对 JDK 5.0 中的范型有 根本的了解 。 泛型不是协变的 固然将 集中看作是数组的 形象会有所协助,然而数组还有一些 集中不具备的特别性质 。Java 语言中的数组是协变的(covariant),也便是说,假如 Integer 扩大了 Number(事实也是如此),那么不只 Integer 是 Number,而且 Integer[] 也是 Number[],在要求 Number[] 的地方 彻底 可以传递或者给予 Integer[] 。(更正式地说,假如 Number 是 Integer 的超类型,那么 Number[] 也是 Integer[] 的超类型) 。您 兴许认为这一原理同样 实用于泛型类型 —— List<Number> 是 List<Integer> 的超类型,那么 可以在需求 List<Number> 的地方传递 List<Integer> 。 可怜的是,状况并非如此 。 不同意这样做有一个很 充足的理由:这样做将 毁坏要提供的类型安全泛型 。假如 可以将 List<Integer> 赋给 List<Number> 。那么下面的代码就同意将非 Integer 的内容放入 List<Integer>: List<Integer> li = new ArrayList<Integer>(); List<Number> ln = li; // illegal ln.add(new Float(3.1415)); 由于 ln 是 List<Number>,所以向其增加 Float 仿佛是 彻底合法的 。然而假如 ln 是 li 的别名,那么这就 毁坏了蕴含在 li 定义中的类型安全承诺 —— 它是一个整数列表,这便是泛型类型不能协变的缘由 。 其余的协变问题 数组 可以协变而泛型不能协变的另一个 后果是,不能实例化泛型类型的数组(new List<String>[3] 是不合法的),除非类型参数是一个未绑定的通配符(new List<?>[3] 是合法的) 。让我们看看假如同意申明泛型类型数组会造成什么 后果: List<String>[] lsa = new List<String>[10]; // illegal Object[] oa = lsa; // OK because List<String> is a subtype of Object List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[0] = li; String s = lsa[0].get(0); 最终一 即将抛出 ClassCastException,由于这样将把 List<Integer> 填入本应是 List<String> 的位置 。由于数组协变会 毁坏泛型的类型安全,所以不同意实例化泛型类型的数组(除非类型参数是未绑定的通配符, 比方 List<?>) 。 构造延迟 由于 可以擦除 性能,所以 List<Integer> 和 List<String> 是同一个类,编译器在编译 List<V> 时只生成一个类(和 C++ 不同) 。 因此,在编译 List<V> 类时,编译器不晓得 V 所 示意的类型,所以它就不能像晓得类所 示意的具体类型那样 解决 List<V> 类定义中的类型参数(List<V> 中的 V) 。 由于运行时不能 划分 List<String> 和 List<Integer>(运行时都是 List),用泛型类型参数标识类型的变量的 构造就成了问题 。运行时不足类型信息,这给泛型容器类和 盼望 缔造 掩护性副本的泛型类提出了难题 。 比方泛型类 Foo: class Foo<T> { public void doSomething(T param) { ... } } 在这里 可以看到一种模式 —— 与泛型有关的众多问题或者折衷并非来自泛型 本身,而是 维持和已有代码兼容的要求带来的副作用 。 泛化已有的类 在转化现有的库类来 使用泛型方面没有多少技巧,但与寻常的状况 雷同,向后兼容性不会凭空而来 。我已经 探讨了两个例子,其中向后兼容性 制约了类库的泛化 。 另一种不同的泛化 步骤可能不存在向后兼容问题,这便是 Collections.toArray(Object[]) 。传入 toArray() 的数组有两个 目标 —— 假如 集中足够小,那么 可以将其内容直接放在提供的数组中 。不然,利用反射(reflection) 缔造 雷同类型的新数组来 承受 后果 。假如从头开始重写 Collections 框架,那么很可能传递给 Collections.toArray() 的参数不是一个数组,而是一个类文字: interface Collection<E> { public T[] toArray(Class<T super E> elementClass); } 由于 Collections 框架作为良好类设计的例子被 宽泛模仿,然而它的设计受到向后兼容性 束缚,所以这些地方值得您 留神,不要盲目模仿 。 首先, 一般被 混同的泛型 Collections API 的一个主要方面是 containsAll()、removeAll() 和 retainAll() 的签名 。您可能认为 remove() 和 removeAll() 的签名应该是: interface Collection<E> { public boolean remove(E e); // not really public void removeAll(Collection<? extends E> c); // not really } 但实际上却是: interface Collection<E> { public boolean remove(Object o); public void removeAll(Collection<?> c); } 为何呢?答案同样是由于向后兼容性 。x.remove(o) 的接口表明“假如 o 包括在 x 中,则删除它,不然什么也不做 。”假如 x 是一个泛型 集中,那么 o 不 定然与 x 的类型参数兼容 。假如 removeAll() 被泛化为惟独类型兼容时 威力调用(Collection<? extends E>),那么在泛化之前,合法的代码序列就会变得不合法, 比方: // a collection of Integers Collection c = new HashSet(); // a collection of Objects Collection r = new HashSet(); c.removeAll(r); 假如上述片段用直观的 步骤泛化(将 c 设为 Collection<Integer>,r 设为 Collection<Object>),假如 removeAll() 的签名要求其参数为 Collection<? extends E> 而不是 no-op,那么就 无奈编译上面的代码 。泛型类库的一个主要 指标便是不 攻破或者转变已有代码的语义, 因此,必须用比从头再一次设计泛型所 使用类型 束缚更弱的类型 束缚来定义 remove()、removeAll()、retainAll() 和 containsAll() 。 在泛型之前设计的类可能妨碍了“显然的”泛型化 步骤 。这种状况下就要像上例这样进行折衷,然而假如从头设计新的泛型类, 了解 Java 类库中的哪些东西是向后兼容的 后果很有 意思,这样 可以幸免不适当的摹仿 。 擦除的实现 由于泛型 根本上都是在 Java 编译器中而不是运行库中实现的,所以在生成字节码的时候,差不多全部关于泛型类型的类型信息都被“擦掉”了 。换句话说,编译器生成的代码与您手工编写的不用泛型、 审查程序的类型安全后进行强制类型转换所得到的代码 根本 雷同 。与 C++ 不同,List<Integer> 和 List<String> 是同一个类( 固然是不同的类型但都是 List<?> 的子类型,与以往的版本相比,在 JDK 5.0 中这是一个更主要的区别) 。 擦除 象征着一个类不能同时实现 Comparable<String> 和 Comparable<Number>,由于事实上两者都在同一个接口中,指定同一个 compareTo() 步骤 。申明 DecimalString 类以便与 String 与 Number 比较 仿佛是 理智的,但关于 Java 编译器来说,这相当于对同一个 步骤进行了两次申明: public class DecimalString implements Comparable<Number>, Comparable<String> { ... } // nope 擦除的另一个 后果是,对泛型类型参数是用强制类型转换或者 instanceof 毫无 意思 。下面的代码 彻底不会改善代码的类型安全性: public <T> T naiveCast(T t, Object o) { return (T) o; } 编译器仅仅发出一个类型未 审查转换 忠告,由于它不晓得这种转换是不是安全 。naiveCast() 步骤实际上 根本不作任何转换,T 直接被替换为 Object,与 期冀的相反,传入的对象被强制转换为 Object 。 擦除也是造成上述 构造问题的缘由,即不能 缔造泛型类型的对象,由于编译器不晓得要调用什么 构造函数 。假如泛型类需求 构造用泛型类型参数来指定类型的对象,那么 构造函数应该 承受类文字(Foo.class)并将它们 保留起来,以便通过反射 缔造实例 。 完毕语 泛型是 Java 语言走向类型安全的一大步,然而泛型设施的设计和类库的泛化并非未 通过 斗争 。 扩大 虚构机指令集来 支撑泛型被认为是 无奈 承受的,由于这会为 Java 厂商 晋级其 JVM 造成难以 跨越的 妨碍 。 因此采纳了 可以 彻底在编译器中实现的擦除 步骤 。 类似地,在泛型 Java 类库时, 维持向后兼容也为类库的泛化 模式设置了众多 制约,产生了一些 混乱的、令人 懊丧的 构造(如 Array.newInstance()) 。这并非泛型 本身的问题,而是与语言的 演变与兼容有关 。但这些也使得泛型学习和 利用起来更让人 困惑,更加 困苦 。 |