设计手机网站公司,怎么才能在网上卖货,英文版wordpress改中文,中国设计之家官网有关泛型的基本概念#xff0c;参见我的前一篇博客 JAVA 学习泛型#xff08;一#xff09;。 协变性
泛型不具备协变性 在介绍通配泛型之前#xff0c;先来看一下下面的例子。我们定义了一个泛型栈#xff1a;
import java.util.ArrayList;
class GenericStackE参见我的前一篇博客 JAVA 学习·泛型一。 协变性
泛型不具备协变性 在介绍通配泛型之前先来看一下下面的例子。我们定义了一个泛型栈
import java.util.ArrayList;
class GenericStackE {private ArrayListE list new ArrayListE();public boolean isEmpty() {return list.isEmpty();}public int getSize() {return list.size();}public E peek() {return list.get(getSize() - 1);//取值不出栈}public E pop() {E o list.get(getSize() - 1) ;list.remove(getSize() - 1);return o;}public void push(E o) {list.add(o);}public String toString() {return stack: list.toString();}
}现在我们写了一个方法max用来求一个GenericStack容器中元素的最大值。如下面的代码所示
public class WildCardNeedDemo {public static double max(GenericStackNumber stack){double max stack.pop().doubleValue();while (! stack.isEmpty()){double value stack.pop().doubleValue();if(value max)max value;}return max;}public static void main(String[] args){GenericStackInteger intStack new GenericStack();intStack.push(1);intStack.push(2);intStack.push(3);System.out.println(Th max value is max(intStack));}
}上面的main函数意图在于借助WildCardNeedDemo.max方法找出intStack中的最大值3。但是实际运行时程序报错说GenericStackInteger无法转换为GenericStackNumber类型。这是因为泛型不具备协变性。 所谓的协变性在泛型中是指有泛型类GenericT如果B是A的子类那么GenericB也是GenericA的子类。
数组具备协变性 协变性在数组中是指如果类A是类B的父类那么A[]就是B[]的父类。数组具有协变性。 数组的协变性是 Java 开发者和使用者所公认的一个瑕疵因为它会导致编译通过的地方运行时出错的问题。比如下面这个例子
class Fruit{}
class Apple extends Fruit{}
class Jonathan extends Apple{} //一种苹果
class Orange extends Fruit{}
//由于数组的协变性可以把Apple[]类型的引用赋值给Friut[]类型的引用
Fruit[] fruits new Apple[10];
fruits[0] new Apple();
fruits[1] new Jonathan(); // Jonathan是Apple的子类
try{//下面语句fruits的声明类型是Fruit[]因此编译通过但运行时将Fruit转型为Apple错误//数组是在运行时才去判断数组元素的类型约束fruits[2] new Fruit();//运行时抛出异常 java.lang.ArrayStoreException这是数组协变性导致的问题
}catch(Exception e){System.out.println(e);
}在前一篇博客中提到过泛型的设计就是为了防止编译通过的地方运行时出错问题的发生。如果泛型也和数组一样具备协变性那这个问题就无法防止所以 Java 的开发者规定泛型不具有协变性。
通配泛型 但是规定泛型不具备协变性又会带来很多不方便。为了让泛型具有更好的性能 Java 开发者设计出了通配泛型。通配泛型具有三种形式上界通配、下界通配和非受限通配。
上界通配 形式为? extends T表示只要是T的子类即可T定义了类型的上限父类为上子类为下。 在上面WildCarNeedDemo中只需要将max的形参列表改为(GenericStack? extends Number stack)就能够正常运行。因为Integer是Number的子类所以GenericStack? extends Number是GenericStackInteger的父类。 以上界通配符声明的泛型容器是不能添加除null之外的元素的。如
ArrayList? extends Fruit list new ArrayListApple();
list.add(new Apple()); list.add(new Fruit()); //编译都报错
//可加入null
list.add(null);这是因为编译器在编译时根本看不到运行时类型ArrayListApple它只认list的声明类型ArrayList? extends Fruit。编译器无法知道list指向的容器的元素的类型下界自然无法判断加进来的元素是否与容器相容。所以编译器就干脆什么不让加进来。 然而不管list究竟指向什么类型的容器容器的元素一定是Fruit的子类。所以可以从容器里取元素并用Fruit类型的引用变量指向它。 所以上界通配的泛型容器相当于一个只读不存(注意不能存但是能删所以是可写的)的容器。只读不写的特性让上界通配泛型容器具有特殊的意义作为方法参数。例如定义一个方法handle(ArrayList? extends Fruit list)方法中可以对传进来的list中的元素引用为Fruit进行处理但是不能添加新的元素。 非受限通配的形式为?它是一种特殊的上界通配等价于? extends Object。因此非受限通配的所有性质都可以参照上界通配。 下界通配 形式为? super T表示只要是T的父类即可T定义了类型的下限。 以下界通配符声明的泛型容器只能添加T和T的子类对象。
ArrayList? super Fruit list new ArrayListObject();
list.add(new Fruit()); //OK
list.add(new Apple()); //OK
list.add(new Jonathan()); //OK
list.add(new Orange()); //OK
list.add(new Object()); //添加Fruit父类则编译器禁止报错道理和上界通配是一样的编译器只知道list指向的容器的元素的类型下界是Fruit看不到运行时类型ArrayListObject。所以编译器知道加入Fruit和Fruit子类对象时安全的至于Fruit的父类就无法保证了。 从这种容器中取元素都解释为Object类也可以强制类型转换为其他类但是调用方法就行不通了因为不知道取出来的对象是否有我们调用的方法。
PECS 原则 Producer ExtendsConsumer Super. 如果需要一个只读泛型类用来Produce T那么用 ? extends T。如果需要一个只写泛型类用来Consume T那么用 ? super T。如果一个泛型容器需要同时读取和写入那么就不能用通配符。 实际上? extends T也可以写删除元素所以说它只读是不准确的意思是想表达不能往里面加东西。? super T也可以读作为Object读出来说它只写也是不准确的但是想表达的意思是从里面取出来的对象也不知道有没有我们想要的数据成员或方法所以一般不读。 泛型容器中元素的转移——PECS的一个应用实例 泛型类GenericStackE的定义仍然沿用上文的定义。下面的代码实现了GenericStack的两个实例泛型
GenericStackString strStack new GenericStack();
GenericStackObject objStack new GenericStack();
objStack.push(Java);
objStack.push(2); //装箱
strStack.push(Sun); 现在我想写一个方法add通过调用add(strStack,objStack)将strStack中的元素全部加入objStack中。可以定义下面的方法
public static T void add(GenericStackT stack1,GenericStack? super T stack2){while(!stack1.isEmpty())stack2.push(stack1.pop());
}实际编译add(strStack,objStack)时编译器自动推断T应该是String并推断这条语句运行时不会出错。也可以显式地使用Stringadd(strStack,objStack)但是不建议一旦编译器推断出的实际类型和你给出的实际类型不一致就会报错。 当然add的函数头还可以是
public static T void add(GenericStack? extends T stack1,GenericStackT stack2);这时编译add(strStack,objStack)编译器推断出T应是Object。
Java泛型变量推论机制浅讨论 上面的这个实例中都是编译器推断出T时什么类型。这是因为我们在形参列表中使用了普通泛型T编译器直接根据传入对象的引用类型来推断。 形参列表中的普通泛型给了编译器可乘之机编译器直接通过普通泛型得到T的实际类型然后依次检查形参列表中其他的泛型是否合法。那我如果不给编译器可乘之机呢比如下面这样
public static T void add(GenericStack? extends T stack1,GenericStack? super T stack2);编译器依然可以解释T虽然这个时候编译器只能得到T的一个范围。比如对于add(strStack,objStack)语句编译器能得到的信息是String是T的子类而Object是T的父类。显然这样的T是存在的编译器就不会报错。那么编译器到底将T解释称什么呢 这种情况下T被解释为它所能够达到的下限。下面是解释 栈还是上面定义的GenericStack现在我写下面一个入口类
public class SuperWildCarDemo {public static void main(String[] args) {GenericStackInteger intStack new GenericStack();GenericStackObject objStack new GenericStack();GenericStackObject tempStack SuperWildCarDemo.Numberadd(intStack, objStack);}public static T T add(GenericStack? extends T stack1, GenericStack? super T stack2){return (T) new Object();}
}上述代码的第 5 5 5 行报错。报错内容如下 由于我们显示提供了T是Number那么自然返回的stack1也被强制类型转化为Number类型了。现在我们不显式提供类型参数看看会是怎么报错 我们没有告诉编译器T应该是什么类型但是编译器说这个add函数返回的是Interger类型的对象。这说明编译器自行推断出T是Integer。 这个例子中换成了心的泛型实例GenericStackInteger是因为Object和Integer之间还有一个中间类Number。我想说的是在不显式提供类型实参且编译器根据传入对象无法确定类型形参的具体类型时编译器会把类型形参解释为它能够到达的下限。不会解释为上限更不会解释为其他的中间类型。 当然如果编译器发现根据你传入的对象推断出的T的范围是空集那就直接报错。