关于泛型和协变、逆变
作者:互联网
关于泛型和协变、逆变
写在前边
在复习的过程中,发现对于泛型的一些性质以及其实现了解的不到位,所以写一个关于泛型及泛型中的协变、逆变等内容的相关博客。
泛型
泛型是参数化类型,即将原来具体的类型定义为参数形式,使用统一的类型遍历表达多种类型,而不明确具体的类型。
从本质上看,泛型实际上是一种延迟明确的方式,即把类型明确的任务推迟到调用方法或创建对象时完成。在运行时根据具体指定类型确定具体类型(编译成class文件时,会用指定类型替换类型变量“擦除”)。
泛型在运行时会被擦除,因此不能使用instanceof()方法来检查泛型内的类型。
泛型中传入不同的父子类型的泛型不具有父子关系。
泛型变量的三种使用形式:泛型类、泛型接口和泛型方法。
泛型类
类中声明了一个或多个泛型变量,它定义了一个或多个充当参数的类型变量,所有这些参数化类型在运行时共享同一个类。泛型类结构如下图所示。
泛型接口
如果接口声明了一个或多个类型变量,那么它就是泛型的。定义了一个或多个类型变量作为参数。
与泛型类相似,将类转化为接口理解即可。
Java Set
Set就是一个泛型接口的典型实例,设计并实现了Set<E>.
其部分操作如下:
实现类
泛型接口的实现类可以是非泛型的实现类、也可以是泛型的实现类。
非泛型的实现类如下图所示,在实现时直接给定类型变量的具体类型。
泛型的实现类,其实现类仍然是泛型。
泛型方法
泛型方法就是声明了一个或多个类型变量的方法,这些类型变量被称为方法的形式类型参数,形式类型参数的形式与类或接口的类型参数列表相同。其结构如下图所示:
注:
- 泛型类/接口,是在实例化类的时候指明泛型的具体类型
- 泛型方法,是在调用方法的时候指明泛型的具体类型
普通类中的泛型方法如下图所示:
返回类型前记得要写<T>!!!
泛型类中的泛型方法
下面给出一段泛型类中的泛型方法实例:
class GenericTest<T>{
//下面的T同所在类的类型变量一致,show1不是泛型方法
public void show1(T t){
System.out.println(t.toString());
}
//下面的E是新的类型变量,只适用于此方法,show2是泛型方法
public <E> void show2(E t){
System.out.println(t.toString());
}
//下面的T是新的类型变量,同类的类型变量无关(即使名字一样)
//show3是泛型方法
public <T> void show3(T t){
System.out.println(t.toString());
}
}
通过这个实例可以得出结论:
泛型方法需要在返回类型前加一个新的类型变量声明,这个类型变量与同类的类型变量无关。
对于上述这个例子,给出如下客户端代码:
public static void main(String[] args){
GenericTest<String> genericTest = new GenericTest<>();
genericTest.show1("genericTest!"); //succeed, "genericTest!"
genericTest.show1(Integer.valueOf("1")); //compile error
genericTest.show2(Integer.valueOf("1")); //succeed, 1
genericTest.show2(person); //succeed, maybe name of person
genericTest.show3(Integer.valueOf("1")); //succeed, 1
genericTest.show3(person); //succeed, maybe name of person
}
泛型方法与静态方法之间的一个关系
- 静态方法不能使用所在泛型类定义的泛型变量。
- 如果静态方法也要使用泛型的话,必须将静态方法定义为泛型方法。
比如我们在实验2中,Graph接口的empty方法实现,就将此静态方法定义成了泛型方法。
协变与逆变
概念
首先,协变和逆变是用来描述类型转换前后的继承关系的两个概念。以A 、B表示类型,f(⋅)表示类型转换(f(A)表示A转换后的类型),≤表示继承关系(比如,A≤B表示A是由B派生出来的子类),其定义可概括如下:
f(⋅)是协变(covariant)的,当A≤B时有f(A)≤f(B)成立;
f(⋅)是逆变(contravariant)的,当A≤B时有f(B)≤f(A)成立;
f(⋅)是不变(invariant)的,当A≤B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系。
数组是协变的
我们创建类型为 Number 和 Integer 类型的两个数组,并分别输出数组的类名,代码以及运行结果如下:
Number[] n1 = new Number[1];
Integer[] n2 = new Integer[1];
System.out.println(n1.getClass().getName());
System.out.println(n2.getClass().getName());
//java.lang.Number;
//java.lang.Integer;
//说明数组是协变的
泛型是不变的
给定一个以泛型编程构建的列表List<T>,并且B是A的子类型,范式List<B>不是List<A>的的子类型。由于泛型只存在于编译阶段,在运行时会进行类型擦除,所以导致泛型的不变性,List<A>和List<B>实际上是一种兄弟关系,其均属于Object的子类型。即如果我们运行以下代码:
List<Number> l1 = new ArrayList<>();
List<Integer> l2 = new ArrayList<>();
System.out.println(l1.getClass().getName());
System.out.println(l2.getClass().getName());
//结果如下
java.util.ArrayList
java.util.ArrayList
输出结果表面两个列表均属于 java.util.ArrayList 类,因为经过类型擦除后,列表中的 String 和 Integer 都被擦除掉,被替换为其上限类型(E,T extends E),导致其均属于 java.util.ArrayList 类。
类型擦除
编译后,泛型会被替换为其上限类型,如果未指定其上限,缺省为Object类型。
泛型中的协变
泛型是不支持数据协变的,但在实际设计数据类型的时候,这种协变却又是我们需要的,这个时候,我们就可以通过 通配符 以达到泛型协变的目的。
通配符
常用的通配符符号有T、E、K、V、?,运用通配符时会遵循约定:
- ? 表示不确定的java类型
- T 表示具体的java类型
- K V 分别表示java键值中的key和value
- E 表示Element
上界通配符 形如 <? extends E>
规定了参数化的类型可以是E及其子类,即确定了类型的上界,规定上界后,就可以使用对应的类型的操作。
下界通配符 形如<? super E>
规定了参数化的类型可以是E及其父类,即确定了类型的下界。
例如我们创建了一个盘子类 Plate
class Plate<T> {
private T item;
public Plate(T t){
item = t;
}
}
class Fruit {}
class Apple extends Fruit {}
按照泛型常规方法实现我们的需求如下所示
Plate<Fruit> p = new Plate<Apple>(new Apple());
//代码编译报错
//Error: cannot convert from Plate<Apple> to Plate<Fruit>
这时我们可以在定义水果盘子的时候加上通配符 “?” ,以解决相应的问题:
Plate<? extends Fruit> p = new Plate<Apple>(new Apple());
泛型中的逆变
泛型中的逆变与泛型中的协变相似,只不过完成的是从子类型到父类型的抽象。
仍然以上边的例子为基础,给出如下逆变代码:
Plate<? super Apple>wildcardApples =new Plate<Fruit>(new Fruit());
标签:Plate,变量,逆变,类型,协变,泛型,new 来源: https://www.cnblogs.com/hit-rhino54/p/16365114.html