当前位置: 首页 > 编程笔记 >

Java中数组协变和范型不变性踩坑记录

应煌
2023-03-14
本文向大家介绍Java中数组协变和范型不变性踩坑记录,包括了Java中数组协变和范型不变性踩坑记录的使用技巧和注意事项,需要的朋友参考一下

前言

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

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

下面话不多说了,来一起看看详细的介绍吧

一、协变、不变、逆变

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

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

class Vegetable { }

class Carrot extends Vegetable { }

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

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

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

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

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

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

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

(1)如果Soup<Carrot>是Soup<Vegetable>的子类,则称泛型Soup<T>是协变的

(2)如果Soup<Carrot>和Soup<Vegetable>是无关的两个类,则称泛型Soup<T>是不变的

(3)如果Soup<Carrot>是Soup<Vegetable>的父类,则称泛型Soup<T>是逆变的。(不过逆变不常见)

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

二、数组协变

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

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

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

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

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

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

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

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

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

三、当泛型也想要协变、逆变

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

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

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

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

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

意思是,参数soup的类型是泛型类Soup<T>,而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<Tomato>类型也不行,因为soup的具体类型信息是在运行期才能知道的,编译期并不知道。

但是方法内的第二句是正确的。因为参数是null,它可以是任何合法的类型。编译器认为它是安全的。

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

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

这时,Soup<T>中的T必须是Vegetable的父类。

这种情况就不存在上面的限制了,下面的代码毫无问题

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

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

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对小牛知识库的支持。

 类似资料:
  • 问题内容: 有人告诉我,Java允许协变数组子类型化,换句话说,如果A是B的子类型,那么A []是B []的子类型,但这是一个不好的功能,因为它可能导致运行时错误。有人可以给我一个具体的例子来说明它如何导致运行时错误,以及Java是否/如何解决此问题? 谢谢! 问题答案: 很简单。 只要您将内容取出,协变类型就不错,但是放入内容的那一刻,整个事情就破裂了。假设您有一个采用Object []作为参数

  • 本文向大家介绍vue踩坑记录之数组定义和赋值问题,包括了vue踩坑记录之数组定义和赋值问题的使用技巧和注意事项,需要的朋友参考一下 问题场景:vue页面初始化展示请求后台返回的数据失败,没有报错 输出的this.effective_date: 页面部分: 发现问题:data定义的数据为[]空数组,没有定义数组长度,而赋值的时候用数组下标赋值 修改代码后如下: 打印effective_date的值:

  • 本文向大家介绍浅谈vuepress 踩坑记,包括了浅谈vuepress 踩坑记的使用技巧和注意事项,需要的朋友参考一下 vuepress是尤大大4月12日发布的一个全新的基于vue的静态网站生成器,实际上就是一个vue的spa应用,内置webpack,可以用来写文档。恰好最近需要为一些组件写文档,就动手撸了一波,毕竟刚发布,遇到不少坑,最终还是磕磕碰碰的运行起来了,为了避免大家踩同样的坑,特意将搭

  • 这可能是一个很傻的问题,但我挠头了很久也弄不明白其中的区别。 我正在浏览scala泛型页面:https://docs.scala-lang.org/tour/generic-classes.html 注意:泛型类型的子类型是不变的。这意味着,如果我们有一个stack[Char]类型的字符堆栈,那么它就不能用作stack[Int]类型的整数堆栈。这是不合理的,因为它使我们能够将真整数输入到字符堆栈中

  • 问题内容: 摘自Joshua Bloch的Effective Java, 数组在两个重要方面不同于通用类型。第一数组是协变的。泛型是不变的。 协变量仅表示如果X是Y的子类型,则X []也将是Y []的子类型。数组是协变的,因为字符串是Object的子类型,所以 不变式仅表示X是否为Y的子类型, 问题答案: Java和C#的早期版本不包含泛型(又称参数多态性)。 在这种情况下,使数组不变会排除有用的

  • 从Joshua Bloch的Effective Java中, > 数组与泛型类型有两个重要的区别。第一个数组是协变的。泛型是不变的。 协变简单地说,如果X是Y的子型,那么X[]也将是Y[]的子型。数组是协变的,因为字符串是对象的子类型,所以 不变简单地说,不管X是不是Y的子类型, 我的问题是为什么决定在Java中使数组是协变的?还有其他的SO帖子,比如为什么数组是不变的,但是列表是协变的?,但它们