协变和逆变以及泛型

什么是泛型,泛型中协变和逆变又是什么?
Number num = new Integer(1) ; //编译正确
ArrayList list = new ArrayList(); //编译错误
为什么同样的 Integer对象能被Number实例化, 而 ArrayList不可以被ArrayList实例化。
List<? extends Number> list = new ArrayList();
list.add(new Integer(1)); //编译错误
list.add(new Float(1.2f));//编译错误

<? extends Number>是什么意思, 为什么不能给list里面add Integer 和Float对象

涉及知识

在介绍泛型之前,我们先了解一下继承多态中引用的用法,和LSP(里氏替换原则)

引用相关

父类引用指向子类对象,反之不行(因为子类中有的属性方法,父类没有。基本原则[根据范围]:小(父类) = 大(子类))。
引用类型决定了能进行哪些操作。实际执行的是指向对象的内容

假设A extends B 
A a = new B a();   //TODO
//>> TODO  形参数的赋值相当于  B  b  (形参) =  A(实参) ;        也相当于  B b = new A();
//>> TODO  返回值的赋值相当于  a(形参) =  b(返回值实参) ;    也相当于  A b = new  B();  
a = test(a);   
public B  test (B b ){
    return b ;
}

LSP(Liskov Substitution Principle)里氏替换原则

子类对象(object of subtype/ derived class)能够替换程序(program)中父类对象(object of base /parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏

假设有类Fruit和Apple,Apple ≦ Fruit,Fruit类有一个方法fun1,返回一个Object对象:

public Fruit{
    public Object fun1() {
                return null;
    }
    public String fun2(String data){
            return data;
    }
    public String fun2(Object data){
        return data;
    }
}
public static void main(String[] arg){
        Fruit f = new Fruit();
        //某地方用到了f对象
        Object obj = f.fun1();
    
}

那么现在Aplle对象覆盖fun1,假设可以返回一个String对象:

public Aplle extends Fruit{
    @Override
    public String fun1() {
        return "";
    }
}
public static void main(String [] arg){
    Fruit f = new Apple();
    //...
    //某地方用到了f对象
    Object obj = f.fun1();
    f.fun2(new Object());
}

那么任何使用Fruit对象的地方都能替换成Apple对象吗?显然是可以的。

泛型

泛型的英文名叫做generics,一系列和泛型相关的名词都是以generic为前缀

在类型中定义泛型,即Generic Types。类型可以是类,也可以是接口

    //TODO 定义泛型,就是把需要的类型定义到类后面的尖括号里面,然后在类里面就可以把定义好的泛型像符号一样使用。多个泛型之间用逗号隔开
    public class MyGenericClass<First,Second>{
        private First first;
        private Second second;
        public  MyGenericClass(First first,Second second){
                this.first = first;
                this.second = second;
        } 
    }    

在方法中定义泛型,即Generic Methods


    public class DefineBoundedGenericTypesAppMain{
            public <Another> Another getAnother(Another val){
                return  val;
            }
    }
   //TODO 调用
DefineBoundedGenericTypesAppMain appMain = new DefineBoundedGenericTypesAppMain();
Parent another = appMain.<Parent> getAnother(new Parent());

泛型的作用

java并没有把泛型的具体类型记录在对象里面,只是在编译的时候做了一些检查,在使用的时候,做了强制类型转换(类型擦除就是把泛型信息删除了,然后变成根据泛型类型信息,加一个强制类型转换操作)

有界泛型

泛型类型不可以调用方法,因为不知道什么类型。如果需要使用某个类的方法,则需要给定类型的范围。
有界泛型解决的问题:让自己类的代码可以调用泛型类型的方法
代码如下:

    public class GrandParent{
        private Integer num;
        public Integer getNum (){
                return  num;
        }
    }
    //在泛型定义的这块如果知道这个泛型MyType继承自GrandParent这个类 就可以用下面这种方式定义
    public class MygenericClass<MyType extends GrandParent>{
            //TODO 然后这个引用就指向了GrandParent类
            private MyType mytype;
            public MygenericClass(MyType myType){
                    //TODO 所以在这里可以通过这个引用,调用GrandParent的方法
                    myType.getNem();
                    this.mytype = myType;
            }

    }

协变和逆变

定义

协变和逆变用来描述类型转换(type transformation)后的继承关系。那到底什么是协变和逆变? 先看例子:

Object [] object = new String[2];

这就是协变。
我们都知道Java中的String类型都是继承自Object,姑且记做String ≤ Object,表示String是Object的子类型,String对象可以赋值给Object的对象。左边是实例右边是引用

定义: 如果A、B表示类型, f(.) 表示类型转换,≤ 表示继承关系(比如,A ≤ B 表示A是由B派生出来的子类 )

  • f(.)是协变(covariant)的, 当A ≤ B 时,有f(A) ≤ f(B)成立 即 B b = new A();
  • f(.)是逆变(contravariant) 的, 当A ≤ B 时,有f(B) ≤ f(A)成立 即 A a = new B();
  • f(.)是不变(invariant)的, 当A ≤ B 时,上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系
    其实顾名思义,协变和逆变表示的一种类型转换的关系。构造类型之间相当于子类型之间的一种关系。[比如Object数组类型Object[] ,我们可以理解成由Object构造出来的一种新的类型,可以认为是一种构造类型]。
    表示一种自然而然的转换关系,比如String[] ≤ Object[] 。这就是继承中所说的:子类变量能赋值给父类变量,父类变量不能赋值给子类变量

返回值协变和参数逆变

JAVA和C#语言都没有把函数作为一等公民,那么那些支持一等函数的语言,即吧函数也看成一种类型的是如何支持协变和逆变 以及里式原则的呢?
换句话说,就是什么时候用一个函数g能够替代其他使用函数f的地方。答案是:
函数f可以安全替换函数g,如果与函数g相比,函数f接受更一般的参数类型,返回更特化的结果类型。《维基百科》
这就是所谓的对输入类型是逆变的,而对输出类型是协变的

虽然Java是面向对象的语言,但某种程度上它仍然遵守这个规则。
返回值协变:Java子类覆盖父类方法的时候能够返回一个“更窄”的子类型。在Java中这被当做了方法重写
参数逆变 :类似参数逆变是指子类覆盖父类方法时接受一个”更宽“的父类型,在Java和C#中这都被当做了方法重载
回头看看开头LSP的代码,把方法当做一等公民:
构造类型 :Apple ≤ Fruit
返回值: String ≤ Object
参数: Object ≧ String ---->参数中实例是调用方法实参,引用是方法中的形参

不可变的例子

比如JAVA中的List 和 List之间就是不可变的

List<String> list1 = new ArrayList<String>();
List<Object> list2 = list1;

java中协变和逆变

Java泛型是不支持协变和逆变的,比如List和List之间是不可变的。但是在Java泛型中引入通配符的时候,Java其实是支持协变和逆变的。
Java泛型对协变和逆变的支持是为了支持范围更广的参数类型。
协变和逆变是针对引用类型而言的,可以用在返回值类型,参数类型,等引用类型上。创建对象的时候,不能使用协变和逆变

泛型中的通配符

1、 实现泛型的协变和逆变
Java中泛型是不变的,可有时需要实现协变和逆变,怎么办呢? 这时,通配符 **?**就派上了用场

  • <? extends>实现了泛型的协变
List<? extends Number> list = new ArrayList<Integer>();
  • <? super>实现了泛型的逆变
List<? super Number> list = new ArrayList<Object>();

代码举例

// 不可变
List<Fruit> fruits = new ArrayList<Apple>();// 编译不通过
// 协变 Apple ≤ Fruit
List<? extends Fruit> wildcardFruits = new ArrayList<Apple>();
// 协变->方法的返回值,对返回类型是协变的:Apple ≤ Fruit
Fruit fruit = wildcardFruits.get(0);
// 不可变
List<Apple> apples = new ArrayList<Fruit>();// 编译不通过
// 逆变 Fruit  ≧  Apple
List<? super Apple> wildcardApples = new ArrayList<Fruit>();
// 逆变->方法的参数,对输入类型是逆变的:Fruit  ≧  Apple
wildcardApples.add(new Apple());

<T extends Comparable<T>><T extends Comparable<? super T>>含义

  • <T extends Comparable<T>>表明T实现了Comaprable<T>接口,此条件强制约束,泛型对象必须直接实现Comparable<T>所谓直接就是指不能通过继承或其他方式
  • <T extends Comparable<? super T>>表明T的任意一个父类实现了Comparable<? super T>接口,其中? super T 表示泛型类型是T的父类(当然包含T),因此包含上面的限制条件,且此集合包含的范围更广
    比如我们会在源码中会看到这种泛型的理解

特殊代码的理解

public static <T extends Comparable<? super T>> void sort (List<T> list)

  1. 首先:public static void sort(List list)
  2. 为了安全性加泛型:public static <T> void sort(List<T> list)
  3. 想要排序先比较,要有可比较性,因此T必须是Comparable的子类:public static <T extends Comparable> void sort(List<T> list)
  4. Comparable接口也有泛型:public static <T extends Comparable<T>> void sort(List<T> list)
  5. T的父类也行,<? super T>表示Comparable<>中的类型下限为T:public static <T extends Comparable<? super T>> void sort (List<T> list)

下面代码运行成功

import java.util.GregorianCalendar;

class Demo<T extends Comparable<? super T>>{}

public class Test1
{
    public static void main(String[] args) {
       Demo<GregorianCalendar> p = null; // 编译正确
    }
}

这个可以理解为<GregorianCalendar extends Comparable<Calendar>>是可以运行成功的!因为CalendarGregorianCalendar的父类并且实现了Comparable<Calendar>,可查看api!.

下面代码运行失败

import java.util.GregorianCalendar;
class Demo<T extends Comparable<T>>{}
//这里把? super去掉了
public class Test
{
    public static void main(String[] args) {
       Demo<GregorianCalendar> p = null; 
        }
}

因为<T extends Comparable<T>>相当于<GregorianCalendar extends Comparable<GregorianCalendar>>但是GregorianCalendar并没有实现Comparable<GregorianCalendar>而是实现的Comparable<Calendar>,这里不在限制范围之内所以会报错

为什么协变适用于读取,而逆变适用于写入

List<? extends Number> list = new ArrayList<Number>();
list.add(new Integer(1)); //error
list.add(new Float(1.2f));  //error

为什么上述代码,通过协变的list 在add 的时候会编译错误?
首先我们看下add的实现

public interface List<E> extends Collection<E> {
	boolean add(E e);
}

在调用add的时候,泛型E自动变成了<? extends Number>,其表示list所持有的类型为,在Number与Number派生子类中的某一类型,其中包含Integer类型却又不特指为Integer类型,下限不确定,只确定了上限(如果list持有类型是Integer的子类,那add Integer的时候相当于 子类引用= 父类实例 会报错),所以java在这方面直接一刀切,通过协变出来的list,不可以add,所以发生编译错误。所以协变不适合写入。
而协变读取元素的时候,里面有丰富的类型信息,也就是说读取出来的是某个类型(T)的或者其子类,这样读出来才能才能放心大胆的把它当做T使用(因为子类可以当做父类来使用),所以协变适合读取

List<? super Number> list = new ArrayList<Object>();
list.add(new Integer(1));
list.add(new Float(1.2f));

而上述逆变的代码,直接确定了list持有的类型为Number或者Number的父类,下限确定之后,add Integer的时候相当于 父类引用= 子类实例所以逆变适合写入
当逆变读取元素的时候,里面只有T的父类的信息,而父类的引用只能读取父类的信息,而你添加的元素大部分是子类的。父类的引用不能调用子类的元素,局限性大,所以逆变不适合读取

总结

如果你想从一个数据类型里获取数据,使用 ? extends 通配符
如果你想把对象写入一个数据结构里,使用 ? super 通配符
如果你既想存,又想取,那就别用通配符。

参考链接
https://www.cnblogs.com/en-heng/p/5041124.html
https://www.zybuluo.com/zhanjindong/note/34147
https://blog.csdn.net/superman_xxx/article/details/69486437?utm_source=blogxgwz4
https://www.cnblogs.com/09120912zhang/p/8319899.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值