1、通配符
可以向导出类型的数组赋予基类型的数组引用
class Fruit{}
class Apple extends Fruit{}
class Jonathan extends Apple{}
class Orange extends Fruit{}
public class CovariantArrays {
public static void main(String[] args) {
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple();
fruit[1] = new Jonathan();
try{
// java.lang.ArrayStoreException
fruit[0] = new Fruit();
}catch (Exception e){
System.out.println(e);
}
try{
// java.lang.ArrayStoreException
fruit[0] = new Orange();
}catch (Exception e){
System.out.println(e);
}
}
}
创建了一个Apple数组,并将其赋值给一个Fruit数组引用,有意义的,因为Apple也是一种Fruit,因此Apple数组应该也是一个Fruit数组。
但是,实际的数组类型是Apple[],应该只能放置Apple或apple的子类型,这在编译器和运行时都可以运行。编译器运行你将Fruit放在这个数组中,是因为它有一个Fruit[]引用——因此他运行将Fruit对象或任何从Fruit继承的对象(Orange)放在这个数组中,所以,编译期是运行的,但是,运行时数组机制知道它是Apple[],因此会在向数组中放置异构类型 抛出异常。
实际中向上转型不合适用在这,我们真正做的是将一个数组赋值给另一个数组。数组的持有其他对象行为是因为向上转型而已,数组本身对自己持有的对象有检测,因此在编译期和运行时检查,我们要小心。
因为泛型的主要目标之一是将运行时错误移入到编译器,因此我们接下来尝试用泛型容器来代替数组。
// Compile Error
ArrayList<Fruit> apples = new ArrayList<Apple>();
泛型和容器相关正确的说法为:不能把一个涉及Apple的泛型赋给一个设计Fruit的泛型。如果像在数组的情况中一样,编译器对代码的了解足够多,可以确定所涉及到的容器,那么它可能运行编译时通过,但是它不知道任何有关这方面的信息,因此它拒绝向上转型。实际上着也不是向上转型——Apple的List不是Fruit的List。Apple的List将持有Apple和Apple的子类型,而Fruit的List将持有任何类型的Fruit,诚然包括Apple,但是他不是一个Apple的List,它仍旧是Fruit的List。Apple的List在类型上不等价与Fruit的List,即使Apple是一种Fruit类型。
真正的问题是容器的类型,而不是容器持有的类型。与数组不同,泛型没有内建的协变类型。这是因为数组在语言中是完全定义的,隐藏可以内建编译期和运行时的检查,但是在使用泛型时,编译器和运行时系统都不知道你想用类型做什么,以及应该采用什么规则。
有时你想要在两个类型之间建立某种类型的向上转型关系,可以通过通配符。
public static void main(String[] args) {
ArrayList<? extends Fruit> flist = new ArrayList<>();
// Compile Error
// flist.add(new Apple());
// Compile Error
// flist.add(new Fruit());
// Compile Error
// flist.add(new Orange());
flist.add(null);
Fruit fruit = flist.get(0);
}
flist类型现在是List<? extends Fruit>,读作“具有任何从Fruit继承的类型的列表”,但是,这实际上并不意味着这个List能持有任何类型的Fruit,通配符引用的是明确的类型,因此它意味着“某种flist引用没有指定的具体类型”。因此这个被赋值的List必须持有诸如Fruit或Apple这样的类型,但是为了向上转型成flist,这个类型是什么没人关系。
因为我们不知道List持有什么类型,我们就不能安全向其中添加对象,因此编译期就会阻止我们添加对象。
另一方面,如果我们调用返回Fruit的方法,则是安全的,因为这个List中的任何对象至少具有Fruit类型,因此编译器允许我们get(0).
2、编译器
你可能以为自己被阻止去调用任何接受参数的方法,其实不然
public class CompilerIntelligence {
public static void main(String[] args) {
List<? extends Fruit> flist = Arrays.asList(new Apple());
Apple apple = (Apple) flist.get(0);
System.out.println(flist.contains(new Apple()));
System.out.println( flist.indexOf(new Apple()));
}
}
contains和indexOf都可以接受Apple对象,并且可以正常执行。是否意味着编译器实际将检查代码,来查看是否有特定的方法修改了他的对象?
通过ArrayList的文档,我们可以发现add接受一个泛型参数类型的参数,但是contains和indexOf将接受Object类型的参数,因此,当指定一个 ArrayList<? extends Fruit>时,add的参数就变成了? extends Fruit.因此,编译器并不能了解这里需要Fruit的哪个具体子类型,因此它不会接受任何类型的Fruit,如果将Apple向上转型为Fruit,编译器将直接拒绝对参数列表中涉及通配符的方法(add)的调用,也就是不能直接add(new Fruit())。
在contains和indexOf,参数类型为Object,因此不涉及任何通配符,而编译器也将允许调用。意味着泛型类的设计者决定哪些调用时安全的,并用Object类作为参数类型。为了在类型中使用了通配符的情况下禁止这类调用,我们需要在参数列表中使用类型参数
public class Holder<T> {
private T value;
public Holder() {
}
public Holder(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
@Override
public boolean equals(Object o) {
return value.equals(o);
}
@Override
public int hashCode() {
return Objects.hash(value);
}
public static void main(String[] args) {
Holder<Apple> appleHolder = new Holder<>(new Apple());
Apple value = appleHolder.getValue();
appleHolder.setValue(value);
// cannot upcast
// Holder<Fruit> fruit = appleHolder;
Holder<? extends Fruit> fruit = appleHolder;
Fruit f = fruit.getValue();
value = (Apple)fruit.getValue();
try{
Orange c = (Orange)fruit.getValue();
} catch (Exception e){
System.out.println(e);
}
// cannot call set
fruit.setValue(new Apple());
// cannot call set
fruit.setValue(new Fruit());
System.out.println(fruit.equals(value));
}
}
如果创建了一个Holder<Apple>不能向上转型为Holder<Fruit>,但是可以向上转型为Holder<? extends Fruit>如果调用getValue,只会返回一个Fruit——在给定“任何扩展自Fruit的对象”这一边界后,它所能知道的一切了。如果你能够了解更多的信息,比如强制转换成Apple【某种具体的Fruit类型】,这不会导致任何警告,但是存在着ClassCastException【转成orange】。set方法不能作用于apple或Fruit,是因为setValue的参数也是“? extends Fruit”,这意味着它可以是任何事物,而编译器无法验证“任何事物”的类型安全性。
但是equals由于它接受的是Object而非T类型,因此,编译器只关注传递进来和要返回的对象类型,它并不会分析代码,以查看是否执行了任何实际的写入和读取操作。
3、逆变
超类型通配符,可以声明通配符是由某个特定类的任何基类来界定的,方法用<? super MyClass> 类型参数<? super T>尽管你不能对泛型参数给出一个超类型边界,即不能声明<T super MyClass>。super使得我们可以安全地传递一个类型对象到泛型类型中。
static void writeTo(List<? super Apple> apples){
apples.add(new Apple());
apples.add(new Jonathan());
// Error
// apples.add(new Fruit());
}
参数Apple是Apple的某种基类型的List,这样我们可以向其中安全添加Apple或Apple的子类型。既然Apple是下界,那么我们添加Fruit显然是不安全的,会导致这个List扩大接纳范围,从而可以向其中添加非Apple类型的对象,这是违反静态类型安全的。
我们可以根据能够像一个泛型类型“写入”(传递给一个方法),以及从一个泛型类型中读取(从一个方法返回),来思考子类型和超类型边界。
超类型边界放松了可以向方法传递的参数上所做的限制。
public class GenericWriting {
static <T> void writeExact(List<T> list, T item) {
list.add(item);
}
static List<Apple> apples = new ArrayList<Apple>();
static List<Fruit> fruit = new ArrayList<Fruit>();
static void f1(){
writeExact(apples,new Apple());
// 未知版本会出现不自动向上转型而 出现Error
writeExact(fruit,new Fruit());
}
static <T> void writeWithWildcard(List<? super T> list,T item){
list.add(item);
}
static void f2(){
writeWithWildcard(apples,new Apple());
writeWithWildcard(fruit,new Apple());
}
public static void main(String[] args) {
f1();
f2();
}
}
writeExact没有使用通配符可能导致不允许将Apple放到List<Fruit>中,即使知道这应该可以的。
writeWithWildcard中,其参数为List<? super T>因此这个List将持有从T导出的某种具体类型。这样就可以安全地将一个T类型的对象或者从T导出的任何对象作为参数传递给List的方法。
public class GenericReading {
static <T> T readExact(List<T> list){
return list.get(0);
}
static List<Apple> apples = Arrays.asList(new Apple());
static List<Fruit> fruits = Arrays.asList(new Fruit());
static void f1(){
Apple apple = readExact(apples);
Fruit fruit = readExact(fruits);
fruit = readExact(apples);
}
/**
* 如果是一个类,那么他的类型在这个类初始化完成后就被建立关系
*/
static class Reader<T>{
T readExact(List<T> list){return list.get(0);}
}
static void f2(){
Reader<Fruit> fruitReader = new Reader<>();
Fruit fruit = fruitReader.readExact(fruits);
// compile error readExact(List<Fruit>) cannot be applied to List<Apple>
// Fruit a = fruitReader.readExact(apples);
}
static class CovariantReader<T>{
T readCovariant(List<? extends T> list){
return list.get(0);
}
}
static void f3(){
CovariantReader<Fruit> fruitCovariantReader = new CovariantReader<>();
Fruit fruit = fruitCovariantReader.readCovariant(fruits);
Fruit fruit2 = fruitCovariantReader.readCovariant(apples);
}
public static void main(String[] args) {
f1();f2();f3();
}
}
readExact使用了精确类型。因此如果使用这个没有任何通配符的精确类型,就可以向List写入和读取这个精确类型。对于返回值,静态的泛型方法可以有效地“适应”每个方法调用,并从List<Apple>返回Apple,List<Fruit>返回一个Fruit。因此,如果可以摆脱静态泛型方法,那么当只是读取时,就不需要协变类型了。
但是,当我们使用泛型类,并创建这个类的实例时,要为这个类确定参数,从fruitReader中List<Fruit>可以读取一个Fruit,因为是它的确切类型,但是List<Apple>还应该产生Fruit对象,而fruitReader不允许这么做。
为了解决上述问题,CovariantReader方法将接受List<? extends T>。因此从这个列表中读取一个T是安全的(你知道这个列表中的所有对象至少是一个T,并且可能是从T导出的对象)
Thinking In Java Part12(通配符、超类型通配符)
最新推荐文章于 2021-03-02 18:50:25 发布