其他分享
首页 > 其他分享> > 关于泛型和协变、逆变

关于泛型和协变、逆变

作者:互联网

关于泛型和协变、逆变

写在前边

在复习的过程中,发现对于泛型的一些性质以及其实现了解的不到位,所以写一个关于泛型及泛型中的协变、逆变等内容的相关博客。

泛型

泛型是参数化类型,即将原来具体的类型定义为参数形式,使用统一的类型遍历表达多种类型,而不明确具体的类型。

从本质上看,泛型实际上是一种延迟明确的方式,即把类型明确的任务推迟到调用方法或创建对象时完成。在运行时根据具体指定类型确定具体类型(编译成class文件时,会用指定类型替换类型变量“擦除”)。

泛型在运行时会被擦除,因此不能使用instanceof()方法来检查泛型内的类型。

泛型中传入不同的父子类型的泛型不具有父子关系。

泛型变量的三种使用形式:泛型类、泛型接口和泛型方法。

泛型类

类中声明了一个或多个泛型变量,它定义了一个或多个充当参数的类型变量,所有这些参数化类型在运行时共享同一个类。泛型类结构如下图所示。

image

泛型接口

如果接口声明了一个或多个类型变量,那么它就是泛型的。定义了一个或多个类型变量作为参数。

与泛型类相似,将类转化为接口理解即可。

Java Set

Set就是一个泛型接口的典型实例,设计并实现了Set<E>.
image

其部分操作如下:

image

实现类

泛型接口的实现类可以是非泛型的实现类、也可以是泛型的实现类。

非泛型的实现类如下图所示,在实现时直接给定类型变量的具体类型。
image

泛型的实现类,其实现类仍然是泛型。

image

泛型方法

泛型方法就是声明了一个或多个类型变量的方法,这些类型变量被称为方法的形式类型参数,形式类型参数的形式与类或接口的类型参数列表相同。其结构如下图所示:

image

注:

普通类中的泛型方法如下图所示:

image

返回类型前记得要写<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方法实现,就将此静态方法定义成了泛型方法。

image-20220611005542789

协变与逆变

概念

首先,协变和逆变是用来描述类型转换前后的继承关系的两个概念。以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)相互之间没有继承关系。

数组是协变的

我们创建类型为 NumberInteger 类型的两个数组,并分别输出数组的类名,代码以及运行结果如下:

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 类,因为经过类型擦除后,列表中的 StringInteger 都被擦除掉,被替换为其上限类型(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 ,以及水果类 Fruit 以及其子类 Apple 定义如下:

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