Java数组协变与范型不变性的案例

  

Java数组协变与范型不变性的案例?这个问题可能是我们日常学习或工作经常见到的。希望通过这个问题能让你收获颇深。下面是小编给大家带来的参考内容,让我们一起来看看吧!

变性是OOP语言不变的大坑,Java的数组协变就是其中的一口老坑。因为最近踩到了,便做一个记录。顺便也提一下范型的变性。

解释数组协变之前,先明确三个相关的概念,协变,不变和逆变。

<强>

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

类Soup{
  公共空间添加(T T) {}
  }
  类蔬菜{}
  类胡萝卜延伸蔬菜{}

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

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

Soup汤=new Soup ();   汤。添加(新番茄());

第一句没问题,Soup是Soup的子类,所以可以将Soup的实例赋给变量汤。第二句也没问题,因为汤声明为Soup类型,它的增加方法接收一个蔬菜类型的参数,而番茄是蔬菜、类型正确。

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

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

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

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

<强>

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

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

蔬菜蔬菜[]=new胡萝卜[10];
  番茄蔬菜[0]=new ();//运行期错误

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

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

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

ArrayList蔬菜=new ArrayList ();//编译期错误   蔬菜。添加(新番茄());

ArrayList是一个泛型类,它是不变的,所以,ArrayList和ArrayList之间并无继承关系,这段代码在编译期就会报错。

两段代码虽然都会报的错,但通常情况下,编译期错误总比运行期错误好处理一些。

<强>

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

类女孩{
  公共空饮料(Soup汤){}
  }

我们希望喝方法可以接受各种不同的蔬菜汤,包括Soup和Soup。但受到不变性的限制,它们无法作为喝的参数。

要实现这一点,应该采用一种类似于协变性的写法

公共空饮料(Soup<?Vegetable>延伸;汤){}

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

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

Java数组协变与范型不变性的案例