Java 逆变与协变的名词说明

2022-11-24,,

  最近在研究Thinking in Java的时候,感觉逆变与协变有点绕,特意整理一下,方便后人。我参考于Java中的逆变与协变,但是该作者整理的稍微有点过于概念化,我在这里简单的说一下

我对于协变于逆变的理解

一:协变

  协变返回类型指的是子类中的成员函数的返回值类型不必严格等同于父类中被重写的成员函数的返回值类型,而可以是更 "狭窄" 的类型。当然协变也会出现在数据,泛型等地方。

1:协变的简单实例

  参考于 “理解Java中的协变返回类型”。 下边代码中,子类方法的返回值类型是父类方法返回值类型的子类型,这就是简单的协变示意。

import java.io.ByteArrayInputStream;
import java.io.InputStream; class Base
{
//子类Derive将重写此方法,将返回类型设置为InputStream的子类
public InputStream getInput()
{
  return System.in;
}
}
public class Derive extends Base
{ @Override
public ByteArrayInputStream getInput()
{ return new ByteArrayInputStream(new byte[1024]);
}
public static void main(String[] args)
{
Derive d=new Derive();
System.out.println(d.getInput().getClass());
}
}
/*程序输出:
class java.io.ByteArrayInputStream
*/

2:数组使用协变

  数组支持协变, 比如 Parent [] pets =new  Son[10] 。如果 son是parent的子类,那么这种定义形式在Java编译期是允许的。

但是,java中 数组协变 很容易导致错误:

出错的例子:

public static void main(String[] args) {
//编译期不报错
Object[] number = new Integer[10];
number[0] = "123";
System.out.println(number[0]);
}
/**
结果:
Exception in thread "main" java.lang.ArrayStoreException: java.lang.String
at com.generic.TestN.main(TestN.java:8)
**/

正常的例子:

public static void main(String[] args) {
//下边就是数组类型的协变,感觉有点像是上转型
Number[] number = new Integer[];
number[] = ;
System.out.println(number[]);
}

3:数组不支持泛型,作为对比,容器支持泛型但不支持协变(不包括通配符)

 在这里 说一下 Java数组的特殊性,也是Java数组为什么敢使用协变的原因:

  数组记得它内部元素的具体类型,并且会在运行时做类型检查。虽然向上转型以后,编译期类型检查放松了,但因为数组运行时对内部元素类型看得紧,不匹配的类型还是插不进去的.

这也是为什么容器Collection不能设计成协变的原因。Collection不做运行时类型检查,比较耿直。所以容器是不支持协变的(当然,引入通配符之后这可以解决这一问题,我们待会在说)

  java数组在创建的时候必须知道内部元素的类型,而且会一直记得类型信息。每次往数组内添加元素都会做类型检查

Java泛型是用擦除(Erasure)实现的,运行时类型参数会被擦掉。

List<String> l = new ArrayList<String>();
l.add("hello");
String str=l.get(0)
//这里简单说一下擦除,上边是编译器的代码,运行时,泛型会被擦除

而且 ,java中数组明确规定

  Java Language Specification明确规定:数组内的元素必须是“物化”的,对“物化”的第一条定义就是不能是泛型。

在这里,我从知乎上找到了一个反编译Array的例子。

String[] s=new String[]{"hello"};
int[] i=new int[]{1,2,3};
//Array的具体实现是在虚拟机层面,嵌地非常深,也查不到源码
//只能用javap反编译看看具体初始化数组的字节码

下面是具体的反编译的字节码: 看注释说明,创建int数组和String数组的指令都不一样,换句话说,数组是Java中的特例,它嵌在虚拟机层面,从底层就决定了不支持泛型

 Code:
0: iconst_1
1: anewarray #2 // class java/lang/String
4: dup
5: iconst_0
6: ldc #3 // String hello
8: aastore
9: astore_1
10: iconst_3
11: newarray int
13: dup
14: iconst_0
15: iconst_1
... ...

4:泛型中的协变(带通配符的泛型)

   看来协变的概念就应该很清楚的知道,泛型是不支持协变的。List<integer> 并不是 List<Number>的儿子。编译期就会报错,如下截图

  于是,java设计者引入了通配符的概念,用于在泛型中提供协变这一功能。比如 我们希望有一个方法,它即可以接受宠物狗列表,也可以接受田园犬(是宠物狗的子类)列表,

于是我们引入协变。

package com.generic;

import java.util.ArrayList;
import java.util.List; public class PetShow { public void run(List<? extends ChongWuGou> dogs){
System.out.println("running");
}
public static void main(String [] args){
List<ChongWuGou> cDogs = new ArrayList<ChongWuGou>();
List<TianYuanQuan> tDogs = new ArrayList<TianYuanQuan>();
new PetShow().run(tDogs);//该方法可以正确运行
}
}
//宠物狗
class ChongWuGou{ }
class TianYuanQuan extends ChongWuGou{ }

二:逆变

   在Java中不允许将父类变量赋值给子类变量。泛型自然也不支持逆变。但是在泛型中可以通过通配符进行模拟(第六节:协变和逆变 )

public class Test
{
public static void main(String[] args)
{
List<? super Integer> list = new ArrayList<Number>();
}
}

  ? super Integer的含义是:支持Integer的父类,也包括Integer类,作为泛型的参数。

Java 逆变与协变的名词说明的相关教程结束。

《Java 逆变与协变的名词说明.doc》

下载本文的Word格式文档,以方便收藏与打印。