zoukankan      html  css  js  c++  java
  • Java数组类型协变性、泛型类型的不变性

    Java数组类型协变性、泛型类型的不变性

    主要参考:(https://www.cnblogs.com/tjxing/p/10419993.html)

    变性是OOP语言不变的大坑,Java的数组协变就是其中的一口老坑。
    解释数组协变之前,先明确三个相关的概念,协变、不变和逆变。

    一、协变、不变、逆变

    假设,我为一家餐馆写了这样一段代码

    class Soup<T> {
        public void add(T t) {}
    }
    
    class Vegetable {}
    
    class Carrot extends Vegetable {}
    

    有一个范型类Soup,表示用食材T做的汤,它的方法add(T t)表示向汤中添加食材T。类Vegetable表示蔬菜,类Carrot表示胡萝卜。当然,Carrot是Vegetable的子类。

    那么问题来了,Soup和Soup之间是什么关系呢?

    第一反应,Soup应该是Soup的子类,因为胡萝卜汤显然是一种蔬菜汤。如果真是这样,那就看看下面的代码。其中Tomato表示西红柿,是Vegetable的另一个子类

    Soup<Vegetable> soup = new Soup<Carrot>();
    soup.add(new Tomato());
    

    第一句没问题,Soup是Soup的子类,所以可以将Soup的实例赋给变量soup。第二句也没问题,因为soup声明为Soup类型,它的add方法接收一个Vegetable类型的参数,而Tomato是Vegetable,类型正确。

    但是,两句放在一起却有了问题。soup的实际类型是Soup,而我们给它的add方法传递了一个Tomato的实例!换言之,我们在用西红柿做胡萝卜汤,肯定做不出来。所以,把Soup视为Soup的子类在逻辑上虽然是通顺的,在使用过程中却是有缺陷的。

    那么,Soup和Soup究竟应该是什么关系呢?不同的语言有不同的理解和实现。总结起来,有三种情况。

    (1)如果Soup是Soup的子类,则称泛型Soup是协变的
    (2)如果Soup和Soup是无关的两个类,则称泛型Soup是不变的
    (3)如果Soup是Soup的父类,则称泛型Soup是逆变的。(逆变偶有使用)

    理解了协变、不变和逆变的概念,再看Java的实现。
    Java的一般泛型是不变的,也就是说Soup和Soup是毫无关系的两个类,不能将一个类的实例赋值给另一个类的变量。所以,上面那段用西红柿做胡萝卜汤的代码,其实根本无法通过编译。

    二、数组协变

    Java中,数组是基本类型,不是泛型,不存在Array这样的东西。但它和泛型很像,都是用另一个类型构建的类型。所以,数组也是要考虑变性的。

    与泛型的不变性不同,Java的数组是协变的。也就是说,Carrot[]是Vegetable[]的子类。而上一节中的例子已经表明,协变有时会引发问题。比如下面这段代码

    Vegetable[] vegetables = new Carrot[10];
    vegetables[0] = new Tomato(); // 运行期错误
    

    因为数组是协变的,编译器允许把Carrot[10]赋值给Vegetable[]类型的变量,所以这段代码可以顺利通过编译。但实际上vegetables只能接收Carrot类型的元素。只有在运行期,JVM真的试图往一堆胡萝卜中插入一个西红柿的时候,才发现大事不好。所以,上面的代码在运行期会抛出一个java.lang.ArrayStoreException类型的异常。

    数组类型的协变性,是Java的著名历史包袱之一。使用数组时,千万要小心!

    三、泛型不变

    如果把例子中的数组替换为List,情况就不同了。就像这样

    ArrayList<Vegetable> vegetables = new ArrayList<Carrot>(); // 编译期错误
    vegetables.add(new Tomato());
    

    ArrayList是一个泛型类,它是不变的。所以,ArrayList和ArrayList之间并无继承关系,这段代码在编译期就会报错。
    两段代码虽然都会报错,但通常情况下,编译期错误总比运行期错误好处理一些。

    四、让泛型实现协变

    泛型是不变的,但某些场景里我们还是希望它能协变起来。比如,有一个天天喝蔬菜汤减肥的小姐姐

    class Girl {
        public void drink(Soup<Vegetable> soup) {}
    }
    

    我们希望drink方法可以接受各种不同的蔬菜汤,包括Soup和Soup。但受到不变性的限制,它们无法作为drink的参数。
    要实现这一点,应该采用一种类似于协变性的写法

    public void drink(Soup<? extends Vegetable> soup) {}
    

    意思是,参数soup的类型是泛型类Soup,而T必须是Vegetable的子类(也包括Vegetable本类)。这时,小姐姐终于可以愉快地喝上胡萝卜汤和西红柿汤了。

    但是,这种方法有一个限制。编译器只知道泛型参数是Vegetable的子类,却不知道它具体是什么。所以,所有非null的泛型类型参数均被视为不安全的。说起来很拗口,其实很简单。直接上代码

    public void drink(Soup<? extends Vegetable> soup) {
        soup.add(new Tomato()); // 错误
        soup.add(null); // 正确
    }
    

    方法内的第一句会在编译期报错。因为编译器只知道add方法的参数是Vegetable的子类,却不知道它具体是Carrot、Tomato、或者其他的什么类型。这时,传递一个具体类型的实例一律被视为不安全的。即使soup真的是Soup类型也不行,因为soup的具体类型信息是在运行期才能知道的,编译期并不知道。
    但是方法内的第二句是正确的。因为参数是null,它可以是任何合法的类型,编译器认为它是安全的。

    五、让泛型实现逆变

    同样,也有一种类似于逆变的方法

    public void drink(Soup<? super Vegetable> soup) {}
    

    这时,Soup中的T必须是Vegetable的父类。这种情况就不存在上面的限制了,下面的代码毫无问题

    public void drink(Soup<? super Vegetable> soup) {
        soup.add(new Tomato());
    }
    

    Tomato是Vegetable的子类,自然也是Vegetable父类的子类。所以,编译期就可以确定类型是安全的。

  • 相关阅读:
    Unity 3(一):简介与示例
    MongoDB以Windows Service运行
    动态SQL中变量赋值
    网站发布IIS后堆栈追踪无法获取出错的行号
    GridView Postback后出错Operation is not valid due to the current state of the object.
    Visual Studio 2010 SP1 在线安装后,找到缓存在本地的临时文件以便下次离线安装
    SQL Server 问题之 排序规则(collation)冲突
    IIS 问题集锦
    linux下安装mysql(ubuntu0.16.04.1)
    apt-get update 系列作用
  • 原文地址:https://www.cnblogs.com/JaxYoun/p/13129981.html
Copyright © 2011-2022 走看看