Java理论与实践: 使用通配符简化泛型使用

日期: 2009-11-29 来源:TechTarget中国 英文

  在使用Java™语言的泛型时,通配符非常令人困惑,并且最常见的一个错误就是在使用有界通配符的两种形式的其中之一(“? super T” 和 “? extends T”)时出现错误。您出错了吗?别沮丧,即使是专家也会犯这种错误,本文中Brian Goetz将展示如何避免这个错误。

  在Java语言中,数组是协变的(因为一个Integer同时也是一个Number,一个Integer数组同时也是一个Number数组),但是泛型不是这样的(List并不等于List)。人们会争论哪些选择是“正确的”,哪些选择是 “错误的” — 当然,每种选择都各有优缺点—但有一点毫无疑问,存在两种使用差别很小的语义构造派生类型的类似机制,这将导致大量错误和误解。

  有界通配符(一些有趣的 “? extends T” 通用类型说明符)是语言提供的一种工具,用来处理协变性缺乏—有界通配符允许类声明方法参数或返回值何时具有协变性(或相反,声明方法参数或返回值何时具有逆变性(contravariant))。虽然了解何时使用有界通配符是泛型较为复杂的方面,但是,使用有界通配符的压力通常都落在库作者的身上,而非库用户。最常见的有界通配符错误就是忘记使用它们,这就限制了类的使用,或是强制用户不得不重用现有的类。

  有界通配符的作用

  让我们从一个简单的泛型类开始(一个称为Box的值容器),它持有一个具有已知类型的值:

以下是引用片段:
  public interface Box {
  public T get();
  public void put(T element);
  }

  由于泛型不具备协变性,Box并不等同于Box,尽管Integer属于Number。但是对于Box这样的简单泛型类来说,这不成问题,并且常常被忽略,因为Box的接口完全指定为T类型的变量—而不是通过T泛型化的类型。直接处理类型变量允许实现多态性。清单 1 展示了这种多态性的两个示例:获取Box的内容,并将它作为一个Number,然后将一个Integer放入Box中:

  清单 1. 通过泛型类利用固有的多态性

以下是引用片段:
  Box iBox = new BoxImpl(3);
  Number num = iBox.get();
  Box nBox = new BoxImpl(3.2);
  Integer i = 3;
  nBox.put(i);

  通过使用简单的Box类,使我们确信可以没有协变性,因为在需要实现多态的位置,数据已经具有某种形式,使编译器能够应用适当的子类型规则。

  然而,如果希望API不仅能够处理T类型的变量,还能处理通过T泛型化的类型,事情将变得更加复杂。假设希望将一个新的方法添加到Box,该方法允许获得另一个Box的内容并其放到清单2所示的Box中:

  清单 2. 扩展的Box接口并不灵活

以下是引用片段:
  public interface Box {
  public T get();
  public void put(T element);
  public void put(Box box);
  }

  这个扩展Box的问题是,只能将内容放到类型参数与原box完全相同的Box中。因此,清单 3 中的代码就不能进行编译:

  清单 3. 泛型不具备协变性

以下是引用片段:
  Box nBox = new BoxImpl();
  Box iBox = new BoxImpl();
  nBox.put(iBox); // ERROR

  显示一条错误消息,表示无法在Box中找到方法put(Box)。如果认为泛型是不具有协变性的,这条错误还讲得通;一个Box 不是Box,尽管Integer是Number,但是这使得Box类的 “泛型性” 比我们期望的要弱。要提高泛型代码的有效性,可以指定一个上限(或下限),而不是指定某个泛型类型参数的精确类型。这可以使用有界通配符来实现,它的形式为 “? extends T” 或 “? super T”。(有界通配符只能用作类型参数,而不能作为类型本身—因此,需要一个有界的命名的类型变量)。在清单 4 中,修改了 put() 的签名以使用一个上限通配符—Box,这表示Box的类型参数可以是T或T的任何子类。

  清单 4. 对清单 3 的 Box 类的改进解释了协变性

以下是引用片段:
  public interface Box {
  public T get();
  public void put(T element);
  public void put(Box box);
  }

  现在,清单 3 中的代码可以进行编译并执行,因为put()的参数现在可以是参数类型为T或T的子类型的Box。由于Integer是Number的子类型,编译器能够解析方法引用put(Box),因为Box匹配有界通配符Box。

  很容易犯清单 3 中的Box错误,即使是专家也难以避免 — 在平台类库中,许多地方都使用Collection,而不是Collection。例如,在java.util.concurrent包的AbstractExecutorService中,invokeAll() 的参数最初是一个 Collection>。但是,这样使用 invokeAll() 非常麻烦,因为这要求必须由Callable参数化的集合持有任务集,而不是由实现 Callable 的类参数化的集合。在 Java 6 中,这种签名被修改为Collection> —这只是为了演示非常容易犯这个错误,正确的修复应该是使 invokeAll()包含一个 Collection>参数。这个参数无疑更加难看,但不会给客户机带来麻烦。

  下限通配符

  上面的大多数有界通配符都进行了限定;“? extends T” 符号为类型添加了一个上限。但是,虽然比较少见,仍然可以使用 “? super T” 符号为类型添加一个下限,表示 “类型T以及它的任何超类”。当您希望指定一个回调对象(例如一个比较器)或存放某个值的数据结构,可以使用下限通配符。

  假设我们希望增强Box,使它能够与另一个box的内容进行比较。可以通过containsSame()方法和Comparator回调对象的定义扩展Box,如清单5所示:

   清单 5. 尝试向Box添加一个比较方法

以下是引用片段:
  public interface Box {
  public T get();
  public void put(T element);
  public void put(Box box);
  boolean containsSame(Box other,
  EqualityComparator comparator);
  public interface EqualityComparator {
  public boolean compare(T first, T second);
  }
  }

我们一直都在努力坚持原创.......请不要一声不吭,就悄悄拿走。

我原创,你原创,我们的内容世界才会更加精彩!

【所有原创内容版权均属TechTarget,欢迎大家转发分享。但未经授权,严禁任何媒体(平面媒体、网络媒体、自媒体等)以及微信公众号复制、转载、摘编或以其他方式进行使用。】

微信公众号

TechTarget微信公众号二维码

TechTarget

官方微博

TechTarget中国官方微博二维码

TechTarget中国

相关推荐