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()) 。这并非泛型 本身的问题,而是与语言的 演变与兼容有关 。但这些也使得泛型学习和 利用起来更让人 困惑,更加 困苦 。