您的位置:首页 > 编程语言 > Java开发

java泛型 generics --- 第五部分 通配符

2015-04-17 15:34 429 查看

通配符

在泛型代码中,问号?,被称为通配符,代表未知的类型。
可以在很多环境下使用通配符,如参量的类型,域的类型,本地变量的类型,有时也可以作为返回类型 (though it is better programming practice to be more specific)。
通配符在以下环境中是不允许使用的:在泛型方法调用中作为类型参数,在泛型类实例化创建中作为类型参数,在声明超类型中。
下面将更加详细的介绍通配符,包括上限制通配符,下限制通配符,通配符捕获。

上限制通配符

可以使用上限制通配符来放松对变量的限制。假设你想要写一个能在 List<Integer>,List<Double>, and List<Number>上工作的方法,可以使用上限制通配符来实现。

声明上限制通配符的格式:? extends 关键字 上限制。注意这里的extends是广义上的使用:在类中是 "extends",在接口中是 "implements"


为了实现一个可以和Number及其子类型(如Integer, Double,
and Float)工作的方法,你应该指定 List<? extends Number>。

术语 List<Number>比 List<?
extends Number>更加受限制。因为前者只匹配Number列表,然而后者匹配Number及其子类型的多个列表。

考虑下面的处理方法:

public static void process(List<? extends Foo> list) { /* ... */ }


在process函数中可以Foo访问元素:

public static void process(List<? extends Foo> list) {
for (Foo elem : list) {
// ...
}
}


返回列表的总和:

public static double sumOfList(List<? extends Number> list) {
double s = 0.0;
for (Number n : list)
s += n.doubleValue();
return s;
}


List<Integer> li = Arrays.asList(1, 2, 3);
System.out.println("sum = " + sumOfList(li));


List<Double> ld = Arrays.asList(1.2, 2.3, 3.5);
System.out.println("sum = " + sumOfList(ld));


不限制通配符

不限制通配符使用?声明,如List<?>。我们成其位未知类型。在两个场景中不限制通配符是一个有用的方法:
1)在写一个函数,其功能可以使用类Object提供的功能来实现。
2) 当使用泛型类中,不依赖类型参量的方法是。如List.size 或是 List.clear。事实上,Class<?>经常被使用,因为Class<T>中方法的使用不依赖于T。

考虑下面的方法:

public static void printList(List<Object> list) {
for (Object elem : list)
System.out.println(elem + " ");
System.out.println();
}


printList的目标时打印任何类型的列表,但是,上面的函数满足预期的目标--它只能打印Object实例列表,不能打印List<Integer>,List<String>, List<Double>等等类别,因为它们不是List<Object>的子类型。为了写一个泛型printList方法,使用List<?>:

public static void printList(List<?> list) {
for (Object elem: list)
System.out.print(elem + " ");
System.out.println();
}


因为对于任何具体的类型A,List<A>是 List<?>子类型。可以使用printList打印任何类型的列表:

List<Integer> li = Arrays.asList(1, 2, 3);
List<String>  ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);


要清晰的认识到:List<Object>和List<?>是不同的。你可以插入一个Object或是其任何的类型到List<Object>中。但是,你只能插入null到 List<?>中。对于通配符的详细信息请参考 Guidelines
for Wildcard Use 。

下限值通配符

上限制符限制未知类型为一个特定的类型,或是说为某个类型的子类型,以extends关键字表现。同样,下限制符限制未知类型为一个特定的类型,或是说为该类型的父类型。

下限制符表示格式为:? super bound,例如<? super A>。

Note: 可以为通配符指定一个下限制,或是一个上限制,但是只能有一个。

假设你要写一个添加Integer对象到list中的函数。为了最大化灵活性,你期望该方法可以工作在任何可以持有Integer值得列表上,如List<Integer>, List<Number>,
andList<Object>。

为了是该方法可以工作在Integer列表和其超类列表上,如Number,Object,你应该使用List<?
super Integer>。术语List<Integer>比起List<? super Integer>更加具有限制性。因为前者只匹配Integer类型列表,然而后者匹配Integer的任何超类型列表。

public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
}


通配符使用指导 部分提供了 何时使用上限制通配符 何时使用下限制通配符的指导。

通配符与子类型化

正如在泛型、继承、子类型中所描述的那样,泛型类间或是接口间并没有什么关系,只是在其类型直接存在一种关联。然而,你可以使用通配符在泛型类直接或是接口间或是泛型类与接口间创造一种关系。

给定下面两个非泛型通用类:

class A { /* ... */ }
class B extends A { /* ... */ }


下面的代码是合理的:

B b = new B();
A a = b;


这个例子显示普通类的继承,遵循子类型化规则:如果B extends A,则class B是class A 的子类型。

但是,这个规则不适用于泛型类型:

List<B> lb = new ArrayList<>();
List<A> la = lb;   // compile-time error


假设Integer是Number的子类型,List<Integer>于List<Number>之间的关系是整样的呢?



通用父类是List<?>
为了在这两个类之间创建一个关系,以便代码可以通过List<Integer>的元素访问Number的方法,使用上限制符:

List<? extends Integer> intList = new ArrayList<>();
List<? extends Number>  numList = intList;  // OK. List<? extends Integer> is a subtype of List<? extends Number>




通配符捕获和帮助者方法

在某些情况下,编译器推断通配符的类型。例如,可能定义列表List<?>,但是当计算表达式时,编译器从代码推断特定的类型。这个过程被称作类型捕获。

大多数情况下,你不需要担心通配符捕获,除非你看到了包含"capture of"的错误信息。

下面的例子产生编译时错误:

import java.util.List;
public class WildcardError {
void foo(List<?> i) {
i.set(0, i.get(0));
}
}


在这个例子中,编译器处理输入参数i为类型Object。当触发List.set(int,
E),编译器不能确定将要插入列表中的对象的类型,因此,错误产生了。

当产生这类错误,通常意味着编译器认为你给一个变量赋了错误的类型。引入泛型就是为了解决这个问题--在编译时加强类型安全。

使用Oracle JDK7 javac 编译器编译WildcardError,将产生如下错误:

WildcardError.java:6: error: method set in interface List<E> cannot be applied to given types;
i.set(0, i.get(0));
^
required: int,CAP#1
found: int,Object
reason: actual argument Object cannot be converted to CAP#1 by method invocation conversion
where E is a type-variable:
E extends Object declared in interface List
where CAP#1 is a fresh type-variable:
CAP#1 extends Object from capture of ?
1 error


在上面的例子中,代码视图执行安全操作,因此如何可以避开编译错误呢?可以使用可以捕获通配符的私有helper方法。在这里,可以创建方法fooHelper,来修复上面的问题:

public class WildcardFixed {
void foo(List<?> i) {
fooHelper(i);
}
// Helper method created so that the wildcard can be captured
// through type inference.
private <T> void fooHelper(List<T> l) {
l.set(0, l.get(0));
}
}


感谢帮助者方法,编译器使用类型推断来决定T是CAP#1,捕获变量。

通常,帮助者方法命名为: originalMethodNameHelper.

现在考虑一个更加复杂的例子,
WildcardErrorBad
:

import java.util.List;
public class WildcardErrorBad {
void swapFirst(List<? extends Number> l1, List<? extends Number> l2) {
Number temp = l1.get(0);
l1.set(0, l2.get(0)); // expected a CAP#1 extends Number,
// got a CAP#2 extends Number;
// same bound, but different types
l2.set(0, temp);	    // expected a CAP#1 extends Number,
// got a Number
}
}


在这个例子中,代码视图执行一个不安全的操作。例如考虑下面对swapFirst方法的调用:

List<Integer> li = Arrays.asList(1, 2, 3);
List<Double>  ld = Arrays.asList(10.10, 20.20, 30.30);
swapFirst(li, ld);


尽管List<Integer>和List<Double>都满足List<?
extends Number>的标准,但想把List<Integer>中的某个值放到List<Double>是明显不对的。??????
原文如下:

While List<Integer> and List<Double> both fulfill the criteria of List<? extends Number>, it is clearly incorrect to take an item from a list of Integer values and attempt to place it into a list of Double values.

用Oracle JDK
javac编译器编译以上代码将产生如下错误:

WildcardErrorBad.java:7: error: method set in interface List<E> cannot be applied to given types;
l1.set(0, l2.get(0)); // expected a CAP#1 extends Number,
^
required: int,CAP#1
found: int,Number
reason: actual argument Number cannot be converted to CAP#1 by method invocation conversion
where E is a type-variable:
E extends Object declared in interface List
where CAP#1 is a fresh type-variable:
CAP#1 extends Number from capture of ? extends Number
WildcardErrorBad.java:10: error: method set in interface List<E> cannot be applied to given types;
l2.set(0, temp);      // expected a CAP#1 extends Number,
^
required: int,CAP#1
found: int,Number
reason: actual argument Number cannot be converted to CAP#1 by method invocation conversion
where E is a type-variable:
E extends Object declared in interface List
where CAP#1 is a fresh type-variable:
CAP#1 extends Number from capture of ? extends Number
WildcardErrorBad.java:15: error: method set in interface List<E> cannot be applied to given types;
i.set(0, i.get(0));
^
required: int,CAP#1
found: int,Object
reason: actual argument Object cannot be converted to CAP#1 by method invocation conversion
where E is a type-variable:
E extends Object declared in interface List
where CAP#1 is a fresh type-variable:
CAP#1 extends Object from capture of ?
3 errors


很遗憾,没有帮助方法来解决这个问题,因为代码基本是错误的。

通配符使用指导

学习泛型编程的一个十分困惑的方面是,何时使用上限制通配符,何时使用下限制通配符。下面提供设计代码是的一些原则:

为了方便讨论,把变量考虑为下面两类功能是有帮助的:

一个输入变量:为代码提供数据。

一个输出变量:保持其他地方用到的数据。

当然某些变量可能同时有两种功能。

可以使用输入或是输出来决定使用上限制还是下限制。下面的列表提供了遵循原则:

通配符准则:

输入变量使用上限制通配符,使用extends关键字
输入变量使用下限制通配符,使用super关键字
如果输入变量需要被在Object类定义的方法访问,使用无限制通配符
如果代码需要同时访问输入和输入变量,不要使用通配符

这些原则不适用与方法的返回类型。应该避免使用通配符作为返回类型,因为这样做强迫程序员使用代码处理通配符。

List<? extends ...>可以非正式的被认为是只读的,但是这并不是一个严格的保证。假设有如下两个类:

class NaturalNumber {
private int i;
public NaturalNumber(int i) { this.i = i; }
}
class EvenNumber extends NaturalNumber {
public EvenNumber(int i) { super(i); }
}


考虑下面代码:

List<EvenNumber> le = new ArrayList<>();
List<? extends NaturalNumber> ln = le;
ln.add(new NaturalNumber(35));  // compile-time error


注释:这个继承有问题,应该是 NaturalNumber
extends EvenNumber

List<EvenNumber> 是List<? extends NaturalNumber>的子类型,所以你可以赋值ln=le。但是,不能使用ln

添加一个自然数到偶数列表中。下面的操作是可以的:

添加null
触发清除方法
获取迭代器并调用移除函数
捕获通配符并写你已经读出的元素

添加新元素,和改变现有元素,是不可以的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: