惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

A
About on SuperTechFans
C
Cybersecurity and Infrastructure Security Agency CISA
N
News and Events Feed by Topic
C
Cisco Blogs
Cisco Talos Blog
Cisco Talos Blog
A
Arctic Wolf
Scott Helme
Scott Helme
P
Palo Alto Networks Blog
S
Schneier on Security
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Tor Project blog
量子位
G
Google Developers Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
B
Blog RSS Feed
NISL@THU
NISL@THU
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
AWS News Blog
AWS News Blog
爱范儿
爱范儿
Last Week in AI
Last Week in AI
Y
Y Combinator Blog
L
LINUX DO - 最新话题
Security Archives - TechRepublic
Security Archives - TechRepublic
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
S
Secure Thoughts
Cloudbric
Cloudbric
aimingoo的专栏
aimingoo的专栏
L
Lohrmann on Cybersecurity
TaoSecurity Blog
TaoSecurity Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Hacker News: Ask HN
Hacker News: Ask HN
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
The GitHub Blog
The GitHub Blog
有赞技术团队
有赞技术团队
S
Security @ Cisco Blogs
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Cyber Attacks, Cyber Crime and Cyber Security
G
GRAHAM CLULEY
P
Proofpoint News Feed
V
V2EX
Martin Fowler
Martin Fowler
C
CERT Recently Published Vulnerability Notes
Attack and Defense Labs
Attack and Defense Labs
C
CXSECURITY Database RSS Feed - CXSecurity.com
The Cloudflare Blog
SecWiki News
SecWiki News
罗磊的独立博客
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
小众软件
小众软件
The Last Watchdog
The Last Watchdog

Xudong's Blog

AI杀死了个人博客 让MDC在各种线程间穿梭自如 新手选购微单的策略 一些让Python代码更快的技巧 HTTP报文结构 Python日志库logging总结 写定向爬虫时遇到的问题 给Hexo主题添加LaTeX公式支持 Python3中的Iterator与Iterable Rss订阅源分享 Git Tips Redis的数据库与持久化 Redis中的数据结构 控制反转与依赖注入 Vim命令笔记 Java中的equals和hashCode方法 Java中对象域的初始化 Java中的基本类型和自动装拆箱 Hello, world. 使用Hexo搭建静态博客
Java中的协变与逆变
Xudong Sun · 2019-12-10 · via Xudong's Blog

逆变Contravariance与协变Covariance

先用一张图解释下
Contravariance&Covariance

Java数组是协变的

1
2
3
4
Number[] numbers = new Number[3];
numbers[0] = new Integer(10);
numbers[1] = new Double(3.14);
numbers[2] = new Long(99L);

包装类Integer Double LongNumber的子类,numbers中的元素的类型可以是任何Number的子类。我们称Java数组是协变的 (Covariant)

不仅如此,下面的代码也是合法的:

1
2
3
Integer[] IntArray = {1,2,3,4};
Number[] NumberArray = IntArray;
Number n = NumberArray[0];

因为**Integer[]Number[]的子类**,所以“父类的引用可以指向子类”。

但是这会导致一个有趣的问题:

1
2
3
4
Integer[] IntArray = {1,2,3,4};
Number[] NumberArray = IntArray;
NumberArray[0] = 9;
NumberArray[0] = 3.14;

在编译时,上面的代码不会报错,但是运行时第4行代码会抛出ArrayStoreException。很明显,即使通过一个Number[]引用,你也不可能把一个浮点数放入一个事实上的Integer[]数组。可以骗过编译器的代码,在运行时类型系统下并不能正确通过。我们甚至可以向协变数组中加入元素,因为Java的运行时系统知道这个数组的真实类型,所以第3行代码在添加元素时,是可以正常执行的。

Java泛型中的类型擦除

因为JDK1.5中才引入泛型机制,为了兼容旧的字节码,Java规范在编译时对泛型进行了类型擦除。也就是说我们使用的所有泛型仅仅存在于编译期间,当通过编译器检查后,泛型信息都会被删除。在运行时,JVM处理的都是没有携带泛型信息的类型。因此我们有下面的问题:

1
2
3
4
5
List<Integer> IntList = new ArrayList<Integer>();
IntList.add(1);
IntList.add(2);
List<Number> NumList = IntList;
NumList.add(3.14);

如果第4行,编译器没有报错。顺其自然,第5行也会通过编译。那么到了运行时,由于泛型信息被删除了,第5行的代码也会被正常执行,一个本应该是储存IntegerList,却被添加了一个浮点数。为了避免类型系统被破坏,编译器必须阻止我们进行第4行这样的操作。在Java泛型中,List<Integer>不是List<Number>的子类

泛型擦除的机制消弱了多态的使用。如果我们有下面这个函数:

1
2
3
4
5
6
7
static long sum(Number[] numbers) {
long summation = 0;
for(Number number : numbers) {
summation += number.longValue();
}
return summation;
}

我们可以很自然的以下面的方式使用这个函数:

1
2
3
4
5
6
Integer[] IntArray = {1,2,3,4,5};
Long[] LongArray = {1L, 2L, 3L, 4L, 5L};
Double[] DoubleArray = {1.0, 2.0, 3.0, 4.0, 5.0};
System.out.println(sum(IntArray));
System.out.println(sum(LongArray));
System.out.println(sum(DoubleArray));

因为数组是协变的,所以把子类型实参传入父类型形参是完全没问题的。多态很正确的被运用了。

但是,如果我们的函数使用了泛型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static long sum(List<Number> numbers) {
long summation = 0;
for(Number number : numbers) {
summation += number.longValue();
}
return summation;
}

public static void Main(String[] args) {
List<Integer> IntArray = asList(1,2,3,4,5);
List<Long> LongArray = asList(1L, 2L, 3L, 4L, 5L);
List<Double> DoubleArray = asList(1.0, 2.0, 3.0, 4.0, 5.0);
System.out.println(sum(IntArray));
System.out.println(sum(LongArray));
System.out.println(sum(DoubleArray));
}

因为泛型擦除可能导致的List被污染的原因,我们不能把List<Integer>当成List<Number>的子类型,所以把Java的泛型称作是不可变的 (invariant)

很显然,这是一个问题,我们需要通过某种机制,让泛型也可以用在多态中。

泛型中的协变 Covariance

使用通配符? extends T,其中T是一个基类型,或者说父类。然后我们就可以实现如下代码:

1
2
3
List<? extends Number> NumList = new ArrayList<Integer>();
List<? extends Number> NumList = new ArrayList<Float>();
List<? extends Number> NumList = new ArrayList<Double>();

并且,我们可以从NumList中读取元素:

1
Number n = NumList.get(0);

因为我们可以确定,不管从NumList中拿到什么元素,都一定是Number的子类(泛型通配符? extends Number所规定的),所以父类Number的引用一定可以指向这个子类元素。

上面的出现的函数可以改写为:

1
static long sum(List<? extends Number> numbers) {...}

但是,我们不能向一个协变泛型的结构中加入任何元素。

1
NumList.add(45L); 

因为运行时的泛型擦除,而编译时的信息不足够确定NumList的真实类型,编译器并不知道添加什么类型的元素才是合法的。

所以如果一个容器是协变的,就只能设置为只读。否则我们能把一些特殊的容器协变到更一般的容器后,就可以再往里面添加进不应该储存的类型。协变结构可读,不可写

逆变 Contravariance

使用通配符? super T,其中T是一个基类型,或者说父类,我们可以向逆变结构中添加任何T的子类。逆变结构可写,不可读。

1
2
3
4
5
List<? super Number> NumList = new ArrayList<Object>();
NumList.add(new Integer(1));
NumList.add(new Double(3.14));
NumList.add(new Object());
Number n = NumList.get(0);

一个储存ObjectList,自然可以放入Number类型的元素。逻辑上的List<Object>应该是List<Number>的父类,但父类被反过来赋值给子类的引用,所以称作逆变。

但是由于规定了 ? super Number的限制,这表明我们需要在容器中储存Number及其子类型,在第4行我们不能再向这个List中添加Object类型的元素。

从逆变结构中读取元素是不被允许的。在第5行,我们想要得到一个Number,但是这不会被编译器允许,因为不能保证这个List中均是Number类型的元素。另外说明一下,Object n = NumList.get(0)是允许的,但是这和原本的泛型类型<Object>没有任何关系,因为Object在Java中是所有类的父类,所以Java允许我们把任何类型当作Object对象,造成了这种可以从逆变容器中取出元素的特殊情况。

PECS原则

Producer Extends Consumer Super Principle

PECE原则是从一个容器角度来描述的

  • 一个泛型结构的用途是让别人来读取内容(生产者协变),需要使用? extends T
  • 一个泛型结构的用途是供别人写入内容(消费者逆变),需要使用? super T
1
2
3
4
5
public static void copy(List<? extends Number> source, List<? super Number> destination) {
for(Number number : source) {
destination.add(number);
}
}
1
2
3
4
5
List<Integer> IntList = Arrays.asList(1,2,3,4); 
List<Double> DoubleList = Arrays.asList(3.14, 6.28);
List<Object> ObjList = new ArrayList<Object>();
copy(IntList, ObjList);
copy(DoubleList, ObjList);

总结

在泛型中使用逆变和协变,可以很好的克服Java中泛型擦除带来的负面影响,让泛型和多态可以快乐地一起玩耍。

当我们已经知道类型之间的继承关系,比如Number 类型是Integer类型的父类。我们就可以有以下结论:

  • 协变(covariance):List<Number>List<Integer>的父类,它们维持内部参数的关系不变。
  • 逆变(contravariance):List<Number>List<Integer>的子类,它们的关系被反转了。
  • 不变(invariance):两者没有任何子类型关系,不能互相替代。