前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java泛型的协变与逆变

Java泛型的协变与逆变

作者头像
疯狂软件李刚
发布2020-06-24 17:03:04
1.2K0
发布2020-06-24 17:03:04
举报
文章被收录于专栏:疯狂软件李刚疯狂软件李刚

导读

泛型是Java最基础的语法之一,众所周知:出于安全原因,泛型默认不能支持型变(否则会引入危险),因此Java提供了通配符上限和通配符下限来支持型变,其中通配符上限就泛型协变,通配符下限就是泛型逆变。

由于来自早期的设计,所以Java的数组默认就支持型变(Variance):只要A是B的子类,那么A[]就相当于B[]的子类,比如Integer是Number的子类,因此Integer[]就相当于Number[]的子类。

但数组的型变会导致潜在的问题,例如如下程序:

代码语言:javascript
复制
public class ArrayVariance
{
  public static void main(String[] args)
  {
    Integer[] intArr = new Integer[5];
    
    // 数组默认就支持型变,因此下面代码是正确的
    Number[] numArr = intArr;
    
    // numArr只要求集合元素是Number,因此下面代码也可通过编译
    numArr[0] = 3.4;  // ①
    
  }
}

由于Java数组默认支持型变,因此Number[]类型的变量实际引用的数组完全可以是Integer[]、也可是Float[]、也可是Double[]......,因此当程序尝试向 Number[]数组中存入元素时,总有可能导致ArrayStoreException异常——原因就是你永远无法确定Number[]类型的变量实际引用的数组对象。

上面程序编译没有任何问题,但运行上面程序就会导致ArrayStoreException异常。

注意

对于一个强大的编译器来说,如果程序在编译阶段没有警告、没有错误 ,那么运行时就不应该导致简单的语法错误——上面程序编译阶段没有错误,但运行时仅仅只是因为类型不兼容(Java是强类型语言)而出错,这显然是不尽人意的。

泛型默认不支持型变

为了避免重蹈Java数组的覆辙,Java泛型显然不能再继续支持默认的型变。这意味着:即使A是B的子类,那么List<A>也不是List<B>的子类,比如Integer是Number的子类,而List<Integer>却并不是List<Number>的子类。

例如如下程序:

代码语言:javascript
复制
import java.util.*;
public class GenericNoVariance
{
  public static void main(String[] args)
  {
    List<Integer> intList = List.of(2, 4);
    
    // 泛型默认不支持型变,因此下面代码编译错误
    List<Number> numList = intList;  // ①

    numList.add(3.4);    // ②
  }
}

由于泛型不支持默认的型变,因此上面①号代码在编译阶段就会报错,因此在②号代码就无法在运行时导致错误了。

协变:通配符上限

为了让泛型支持型变,Java引入了通配符上限语法:如果A是B的子类,那么List<A>相当于是List<? extends B>的子类,比如Integer是Number的子类,List<Integer>就相当于List<? extends Number>的子类——这种型变方式被称为协变(covariance)。

对于支持协变的泛型集合,例如List<? extends Number>,Java编译器只知道该List集合的元素是Number的子类——但具体是哪个子类则无法确定。

因此对于协变的泛型集合,程序只能从集合中取出元素——取出的元素的类型肯定能保证是上限;但程序不能向集合添加元素——因此程序无法确定程序要求的集合元素具体是上限的哪个子类。例如如下程序:

代码语言:javascript
复制
import java.util.*;
public class GenericCovariance
{
  public static void main(String[] args)
  {
    List<Integer> intList = new ArrayList<>();
    intList.add(2);
    intList.add(4);

    List<Double> doubleList = new ArrayList<>();
    doubleList.add(2.1);
    doubleList.add(4.3);

    // List<? extends Number>支持协变,
    // 因此只要元素是Number子类的List集合,就可以赋值给numList集合
    List<? extends Number> numList = intList;  // ①
    // 取出的元素被当成Number处理
    Number n1 = numList.get(0);   // ②
    System.out.println(n1);

    // List<? extends Number>支持协变,
    // 因此只要元素是Number子类的List集合,就可以赋值给numList集合
    List<? extends Number> numList2 = doubleList; // ①
    // 取出的元素被当成Number处理
    Number n2 = numList2.get(0);  // ②
    System.out.println(n2);
  }
}

List<? extends Number>是支持协变的泛型,因此程序中两行①号代码可以分别将List<Integer>、List<Double>赋值给List<? extends Number>类型的变量。

List<? extends Number>类型的集合只能取出元素——取出的元素总是被当成Number处理;List<? extends Number>类型的集合不能添加元素——因此编译器无法确定该集合的元素必须是Number的哪个子类。

总结来说,支持协变的集合只能取出元素,不能添加元素——疯狂Java讲义归纳的口诀是:协变只出不进!

对于更通用的泛型来说,对于支持协变的泛型,程序只能调用以泛型为返回值类型的方法;不能调用形参为泛型的方法。例如如下代码。

代码语言:javascript
复制
class Apple<T>
{
    private T info;
    public Apple(T info) {
        this.info = info;
    }
    public void setInfo(T info) {
        this.info = info;
    }
    public T getInfo() {
        return this.info;
    }
}
public class GenericCovariance2 {
    public static void main(String[] args) {
        // 指定泛型T为Integer类型
        Apple<Integer> intApp = new Apple<>(2);

        // 协变
        Apple<? extends Number> numApp = intApp;
        
        // 协变的泛型,调用以泛型为返回值的方法,正确。
        // 该方法的返回值是T,该T总是Number类或其子类
        Number n = numApp.getInfo();
        System.out.println(n);

        // 协变的泛型,不能调用以泛型为参数的方法,编译报错
        // 因此编译器只能确定T必须是Number的子类,但具体是哪个子类则无法确定,因此编译出错
        numApp.setInfo(3);  // ①
    }
}

上面程序中Apple<? extends Number>也是支持协变的泛型,因此该类型的变量只能调用返回值为泛型的方法,不能调用形参为泛型的方法——如上①号代码所示。

逆变:通配符下限

Java引入了通配符下限语法是为支持逆变(controvariance):如果A是B的父类,那么List<A>反而相当于是List<? super B>的子类,比如Number是Integer的父类,List<Number>反而相当于List<? super Integer>的子类——这种型变方式被称为逆变(contravariance)。

对于支持逆变的泛型集合,例如List<? super Integer>,Java编译器只知道该List集合的元素是Integer的父类——但具体是哪个父类则无法确定。

因此对于逆变的泛型集合,程序只能向集合中添加元素——添加元素的类型总能符合上限——而集合元素总是上限的父类,因此完全没问题;但程序不能从集合中取出元素——因为编译器无法确定集合元素具体是下限的哪个父类——除非你把取出的集合元素总是当成Object处理(众生皆Object)

例如如下程序:

代码语言:javascript
复制
import java.util.*;
public class GenericContravariance
{
    public static void main(String[] args)
    {
        List<Number> numList = new ArrayList<>();
        numList.add(2);
        numList.add(4.3);

        List<Object> objList = new ArrayList<>();
        objList.add("Java");
        objList.add(3.5f);
        
        // List<? super Integer>支持逆变,
        // 因此只要元素是Integer父类的List集合,就可以赋值给intList1集合
        List<? super Integer> intList1 = numList;  // ①
        // 逆变的集合添加元素完全没问题——集合元素肯定是Integer的父类(此处为Number)
        intList1.add(20);   // ②
        System.out.println(intList1);

        // List<? super Integer>支持逆变,
        // 因此只要元素是Integer父类的List集合,就可以赋值给intList2集合
        List<? super Integer> intList2 = objList; // ①
        // 逆变的集合添加元素完全没问题——集合元素肯定是Integer的父类(此处为Object)
        intList2.add(30);   // ②
        System.out.println(intList2);

        // 取出集合元素时,集合元素只能被当成Object处理
        Object ob1 = intList1.get(0);
        Object ob2 = intList2.get(0);
    }
}

List<? super Integer>是支持逆变的泛型,因此程序中两行①号代码可以分别将List<Number>、List<Object>赋值给List<? super Integer>类型的变量。

List<? super Integer>类型的集合只能添加元素——添加的Integer元素一定匹配下限,而下限一定是集合元素的子类。因此上面两行②号代码都可以添加元素。但如果程序尝试从泛型逆变的集合中取出元素,那么取出的元素只能被当成Object处理(众生皆Object)。

总结来说,支持逆变的集合只能添加元素,不能取出元素(除非取出元素都当成Object)——疯狂Java讲义归纳的口诀是:逆变只进不出!

对于更通用的泛型来说,对于支持逆变的泛型,程序只能调用以泛型为形参的方法;不能调用形参为返回值类型的方法(除非将返回值当成Object处理)。例如如下代码。

代码语言:javascript
复制
class Apple<T>
{
    private T info;
    public Apple(T info) {
        this.info = info;
    }
    public void setInfo(T info) {
        this.info = info;
    }
    public T getInfo() {
        return this.info;
    }
}
public class GenericContravariance2 {
    public static void main(String[] args) {
        // 指定泛型T为Object类型
        Apple<Object> objApp = new Apple<>("疯狂Java");

        // 逆变
        Apple<? super Integer> intApp = objApp;

        // 逆变的泛型,调用以泛型为形参的方法,正确。
        // 该方法的Integer参数总符合下限,下限一定派生自父类
        intApp.setInfo(3);

        // 逆变的泛型,调用以泛型为返回值的方法,该返回值只能被当成Object处理
        Object o = intApp.getInfo();
        System.out.println(o);
    }
}

本文结束

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-06-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 疯狂软件李刚 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档