在 Java 语言中,数组是协变的(因为一个 Integer
同时也是一个 Number
,一个 Integer
数组同时也是一个 Number
数组),但是泛型不是这样的(List<Integer>
并不等于 List<Number>
)。人们会争论哪些选择是 “正确的”,哪些选择是 “错误的” — 当然,每种选择都各有优缺点 — 但有一点毫无疑问,存在两种使用差别很小的语义构造派生类型的类似机制,这将导致大量错误和误解。
有界通配符(一些有趣的 “? extends T
” 通用类型说明符)是语言提供的一种工具,用来处理协变性缺乏 — 有界通配符允许类声明方法参数或返回值何时具有协变性(或相反,声明方法参数或返回值何时具有逆变性(contravariant))。虽然了解何时使用有界通配符是泛型较为复杂的方面,但是,使用有界通配符的压力通常都落在库作者的身上,而非库用户。最常见的有界通配符错误就是忘记使用它们,这就限制了类的使用,或是强制用户不得不重用现有的类。
有界通配符的作用
让我们从一个简单的泛型类开始(一个称为 Box
的值容器),它持有一个具有已知类型的值:
public interface Box<T> {
public T get();
public void put(T element);
}
|
由于泛型不具备协变性,Box<Integer>
并不等同于 Box<Number>
,尽管 Integer
属于 Number
。但是对于 Box
这样的简单泛型类来说,这不成问题,并且常常被忽略,因为 Box<T>
的接口完全指定为 T 类型的变量 — 而不是通过 T 泛型化的类型。直接处理类型变量允许实现多态性。清单 1 展示了这种多态性的两个示例:获取 Box<Integer>
的内容,并将它作为一个 Number
,然后将一个 Integer
放入 Box<Number>
中:
清单 1. 通过泛型类利用固有的多态性
Box<Integer> iBox = new BoxImpl<Integer>(3);
Number num = iBox.get();
Box<Number> nBox = new BoxImpl<Number>(3.2);
Integer i = 3;
nBox.put(i);
|
通过使用简单的 Box
类,使我们确信可以没有协变性,因为在需要实现多态的位置,数据已经具有某种形式,使编译器能够应用适当的子类型规则。
然而,如果希望 API 不仅能够处理 T 类型的变量,还能处理通过 T 泛型化的类型,事情将变得更加复杂。假设希望将一个新的方法添加到 Box
,该方法允许获得另一个 Box
的内容并其放到清单 2 所示的 Box
中:
清单 2. 扩展的 Box 接口并不灵活
public interface Box<T> {
public T get();
public void put(T element);
public void put(Box<T> box);
}
|
这个扩展 Box
的问题是,只能将内容放到类型参数与原 box 完全相同的 Box
中。因此,清单 3 中的代码就不能进行编译:
清单 3. 泛型不具备协变性
Box<Number> nBox = new BoxImpl<Number>();
Box<Integer> iBox = new BoxImpl<Integer>();
nBox.put(iBox); // ERROR
|
显示一条错误消息,表示无法在 Box<Number>
中找到方法 put(Box<Integer>)
。如果认为泛型是不具有协变性的,这条错误还讲得通;一个 Box<Integer>
不是 Box<Number>
,尽管 Integer
是 Number
,但是这使得 Box
类的 “泛型性” 比我们期望的要弱。要提高泛型代码的有效性,可以指定一个上限(或下限),而不是指定某个泛型类型参数的精确类型。这可以使用有界通配符来实现,它的形式为 “? extends T
” 或 “? super T
”。(有界通配符只能用作类型参数,而不能作为类型本身 — 因此,需要一个有界的命名的类型变量)。在清单 4 中,修改了 put()
的签名以使用一个上限通配符 — Box<? extends T>
,这表示 Box
的类型参数可以是 T
或 T
的任何子类。
清单 4. 对清单 3 的 Box 类的改进解释了协变性
public interface Box<T> {
public T get();
public void put(T element);
public void put(Box<? extends T> box);
}
|
现在,清单 3 中的代码可以进行编译并执行,因为 put()
的参数现在可以是参数类型为 T 或 T 的子类型的 Box
。由于 Integer
是 Number
的子类型,编译器能够解析方法引用 put(Box<Integer>)
,因为 Box<Integer>
匹配有界通配符 Box<? extends Number>
。
很容易犯清单 3 中的 Box
错误,即使是专家也难以避免 — 在平台类库中,许多地方都使用 Collection<T>
,而不是 Collection<? extends T>
。例如,在 java.util.concurrent 包的 AbstractExecutorService
中,invokeAll()
的参数最初是一个 Collection<Callable<T>>
。但是,这样使用 invokeAll()
非常麻烦,因为这要求必须由 Callable<T>
参数化的集合持有任务集,而不是由实现 Callable<T>
的类参数化的集合。在 Java 6 中,这种签名被修改为 Collection<? extends Callable<T>>
— 这只是为了演示非常容易犯这个错误,正确的修复应该是使 invokeAll()
包含一个 Collection<? extends Callable<? extends T>>
参数。这个参数无疑更加难看,但不会给客户机带来麻烦。
下限通配符
上面的大多数有界通配符都进行了限定;“? extends T
” 符号为类型添加了一个上限。但是,虽然比较少见,仍然可以使用 “? super T
” 符号为类型添加一个下限,表示 “类型 T 以及它的任何超类”。当您希望指定一个回调对象(例如一个比较器)或存放某个值的数据结构,可以使用下限通配符。
假设我们希望增强 Box
,使它能够与另一个 box 的内容进行比较。可以通过 containsSame()
方法和 Comparator
回调对象的定义扩展 Box
,如清单 5 所示:
清单 5. 尝试向 Box 添加一个比较方法
public interface Box<T> {
public T get();
public void put(T element);
public void put(Box<? extends T> box);
boolean containsSame(Box<? extends T> other,
EqualityComparator<T> comparator);
public interface EqualityComparator<T> {
public boolean compare(T first, T second);
}
}
|
可以使用一个通配符定义 containsSame()
中另一个 box 的类型,这将避免前面遇到的问题。但是仍然会遇到一个类似的问题;比较器参数必须是 EqualityComparator<T>
。这意味着我们不能编写如清单 6 所示的代码:
清单 6. 使用清单 5 中的比较方法会导致失败
public static EqualityComparator<Object> sameObject
= new EqualityComparator<Object>() {
public boolean compare(Object o1, Object o2) {
return o1 == o2;
}
};
...
BoxImpl<Integer> iBox = ...;
BoxImpl<Number> nBox = ...;
boolean b = nBox.containsSame(iBox, sameObject);
|
在这里使用一个 EqualityComparator<Object>
似乎非常合理。既然可以使用泛型指定,客户机就不必为每一个可能的 Box
类型创建独立的比较器了!解决方法是使用一个下限通配符 “? super T
”。使用 compareTo()
方法扩展的正确 Box
类如清单 7 所示:
清单 7. 清单 5 中的比较操作在使用有界通配符后更加灵活
public interface Box<T> {
public T get();
public void put(T element);
public void put(Box<? extends T> box);
boolean containsSame(Box<? extends T> other,
EqualityComparator<? super T> comparator);
public interface EqualityComparator<T> {
public boolean compare(T first, T second);
}
}
|
通过使用一个下限通配符,containsSame()
方法表示需要能够比较 T 或它的任何超类型 的工具,这就允许我们提供一个能够比较对象的比较器,并且不需要使用 EqualityComparator<Number>
封装它。
get-put 原则
有一个流传已久的笑话:“佩戴一只手表的人常常知道时间,而佩戴两只手表后反而难以确定了”。由于 Java 语言同时支持上限和下限通配符,那么如何判断何时使用哪一种呢?
这里有一条简单的规则,称为 get-put 原则,它解释了应该使用哪一种通配符。get-put 原则首次出现在 Naftalin 和 Wadler 所著的有关泛型的 Java Generics and Collections 一书中(参见参考资料),它是这样描述的:
仅从某个结构中获取值时使用 extends
通配符;仅将值放入某个结构时使用 super
通配符;同时执行以上两种操作时不要使用通配符。
在应用到 Box
等容器类或 Collections 类时,get-put 原则很好理解,因为 get 和 put 概念和这些类的作用有着自然的联系:存储内容。因此,如果希望应用 get-put 原则来创建一个可以在 Box
之间进行复制的方法,最常见的形式如清单 8 所示,其中复制源使用上限通配符,目标使用下限通配符:
清单 8. 同时使用上限和下限通配符的 Box 复制方法
public static<T> void copy(Box<? extends T> from, Box<? super T> to) {
to.put(from.get());
}
|
如果对前面的 containsSame()
方法(对 box 使用了上限通配符而对比较器使用了下限通配符)应用 get-put 原则?第一步很简单:需要从其他 box 获取一个值,因此使用一个 extends
通配符。但第二步有点复杂 — 因为比较器并不是容器,因此与从一个数据结构获得或存入值有所不同。
当数据类型并不是一个明显的容器类(例如集合)时,应该这样考虑 get-put 原则:尽管 EqualityComparator
不是一个数据结构,仍然可以向它 “存入” 值 — 即将值传递给它的一个方法。在 containsSame()
方法中,使用 Box
作为值的生成器(从 Box
获取值)并使用比较器作为值的使用者(将值传递给比较器)。因此可以对 Box
使用 extends
通配符,而对比较器使用 super
通配符。
我们可以看到 get-put 应用到了 Collections.sort()
的声明中,如清单 9 所示:
清单 9. 使用下限通配符的另一个示例
public static <T extends Comparable<? super T>> void sort(List<T>list) { ... }
|
相关推荐
通过使用泛型,编译器可以检查类型匹配,避免了在运行时可能出现的ClassCastException。通配符在一定程度上保持了这种类型安全,尽管它们允许处理未知类型,但仍然遵守类型约束。 8. 泛型与原始类型(Raw Types):...
实例189 - 使用通配符增强泛型,着重探讨了如何通过通配符来提升泛型的灵活性和可复用性。这个主题与源码理解和工具应用紧密相关。 首先,让我们理解什么是通配符。在Java的泛型中,通配符(Wildcard)是问号(?)...
实际项目中需要使用泛型来增加类型安全性和重用性的开发任务。 目标: 本代码资源的目标是帮助读者理解泛型的用法和优势,并通过实际的示例代码加深对泛型的掌握。读者可以通过运行这些示例代码来观察泛型的行为和...
"泛型讲解 类型通配符" 泛型是Java语言中的一种机制,它允许在定义类、接口时指定类型形参,这个类型形参将在声明变量、创建对象时确定。...通过使用泛型,可以避免ClassCastException,提高代码的安全性和可靠性。
Java 中泛型通配符的使用方法示例主要介绍了 Java 中泛型通配符的使用方法,结合实例形式分析了 Java 中泛型通配符的功能、语法及在泛型类创建泛型对象中的使用方法。以下是 Java 中泛型通配符的使用方法示例的知识...
例如,使用泛型容器类来代替数组,可以避免运行时期的错误。然而,泛型不支持协变,这意味着我们不能将 ArrayList<Apple> 赋给 ArrayList<Fruit> 的引用,即使 Apple 是 Fruit 的子类型。 这时,通配符就发挥作用了...
在处理像Activiti这样的工作流框架时,理解并正确使用泛型通配符尤其重要,因为它们经常涉及到复杂的对象模型和数据操作,确保类型安全可以避免潜在的运行时错误。 总的来说,Java泛型通配符是Java编程中不可或缺的...
参数化类型是指在使用泛型类时指定了具体的类型。例如,`List<String>` 是一个参数化类型,`List` 是泛型类,`String` 是具体的类型。 4. 原始类型 原始类型是指参数化类型的泛型类的 Class。例如,`List` 的原始...
泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。 Java语言引入泛型的好处是...
除了泛型类,我们还可以在方法中使用泛型。泛型方法的声明方式是在方法返回类型前加上类型参数。例如,`public static <T> void printArray(T[] array) {...}`,`printArray`方法可以接受任何类型的数组。 4. 泛型...
通过正确地使用泛型方法,我们可以避免运行时错误,提高代码的可读性和可重用性。在实际开发中,理解并熟练掌握泛型方法的使用对于提升编程效率和代码质量至关重要。 以上就是关于Java泛型方法的基本介绍和使用示例...
如何使用泛型和通配符来实现? 5. 为什么不能在泛型方法中使用instanceof关键字?(因为泛型类型信息在运行时被擦除) 了解并熟练运用泛型和通配符是Java开发人员必备的技能,它们能够帮助写出更安全、可维护的...
限定通配符是指在泛型中使用extends关键字来限定类型的通配符,语法格式为`<? extends E>`,其中E是某个类型的名称。例如,在上述代码中,我们可以使用`Gys<? extends T>`来限定addAll方法的参数类型,表示该方法...
如果需要指定泛型类型参数,可以使用T,如果需要表示未知类型的泛型参数,可以使用问号(通配符)。 此外,Java泛型还有其他重要的概念,如有界类型、通配符类型等。有界类型是指使用extends语句来限制泛型类型参数的...
* 一个参数通配符的实例 * 说明:对一个包含了数值元素的集合进行汇总运算。在这种情况下,用户并不关心 * 集合中的每一个对象是什么类型,只要它是数值型即可,而且,用户也希望集合中可以 * 存放不同类型的数值...
通过使用泛型,开发者可以在运行时避免强制类型转换,并且能够编写更易于理解、维护和扩展的代码。 #### 三、定义简单的泛型 ##### 3.1 定义泛型类 泛型类允许我们在定义类时指定一个或多个类型参数。例如,我们...
通过使用泛型,我们可以定义类型参数化的类或方法,从而避免了代码重复并且可以在运行时提供类型检查。 #### 2. 泛型类Stack 在这个实验中,我们需要实现一个泛型类`Stack<E>`。其中`E`表示任何类型的元素。为了...
泛型的通配符使用也是泛型机制的一部分,它允许在泛型类或接口的类型参数中使用一个问号(?)来代表任何类型。通配符主要用于表示未知的类型参数,或者表示类型参数的集合。常见的通配符使用场景包括使用List表示...
泛型方法是指使用泛型类型参数的方法。例如,`public static <T> T identity(T t) { return t; }` 定义了一个名为 identity 的泛型方法。 6. 与旧代码交互 泛型代码可以与旧代码交互,例如使用老代码中的方法或...