[转载] JAVA泛型杂谈--擦除,协变,逆变,通配符等
参考链接: Java中的协变返回类型
在《JAVA核心思想》这本书里,关于泛型的章节意外的很多,小小的泛型里其实有很多可以学习的内容,我总结下最近看书的成果。
一. 泛型的好处和应用
最基础的用到泛型的地方无非是在容器里 使用泛型用以保证容器内数据的类型统一,所以我们先总结下泛型使用的好处:
可以统一集合类型容器的存储类型,防止在运行期出现类型装换异常,增加编译时类型的检查解决重复代码的编写,能够复用算法。可以起到重载的作用
第二个作用很好理解,泛型 的 ‘泛’ 即代表着 ‘泛化’,不仅仅是保证容器的安全性,更重要的是减少类型的限制,让我们编写更加通用的代码。我们举个例子: 在javaweb中,我们经常向前台返回JSON对象信息,不同的业务可以会组装不同的bean,为了节省提高代码的复用性,我们可以这么写一个类:
public class ReturnObject<A, B> {
public final A a;
public final B b;
public ReturnObject(A a, B b) {
this.a = a;
this.b = b;
}
public <A> A t(A a) {
return a;
}
@Override
public String toString() {
return "ReturnObject{" +
"a=" + a +
", b=" + b +
'}';
}
}
这个类没有具体的类型,意思也很简单,就是不限定变量的类型,根据你业务的不同,你可以传不同的类型进去(通过构造器),更方便的是,java泛型支持继承,你可以随意拓展你的业务字段,也就不再需要为了一种业务专门创建一个bean类了。
// 业务字段拓展
public class ReturnObjectExtender<A, B, C> extends ReturnObject<A, B> {
public final C c;
public ReturnObjectExtender(A a, B b, C c) {
super(a, b);
this.c = c;
}
@Override
public String toString() {
return "ReturnObjectExtender{" +
"c=" + c +
", a=" + a +
", b=" + b +
'}';
}
}
二. 重要!泛型的擦除
JAVA的泛型都是通过擦除来实现的,这句话的意思是 当你的程序真正跑起来的时候,任何具体的类型其实都已经被擦除了,所以在下面的例子中,输出的结果是true,aClass和aClass1都是一样的class生成的对象。
Class aClass = new ArrayList<Integer>().getClass();
Class aClass1 = new ArrayList<String>().getClass();
System.out.println(aClass == aClass1);
擦除的负面效应直接体现在如果你写下面这段代码,T类型并不能认出你传给它的是String类型,T直接会被Object替代,
public class WildCardTest<T> {
public void f(T t) {
//这一段编译报错
t.isEmpty();
}
public static void main(String[] args) {
WildCardTest<String> stringWildCardTest = new WildCardTest<>();
stringWildCardTest.f("");
}
}
要让String调用它的isEmpty()方法,需要给泛型一个边界,代码只需要重新改一下,T extends String表明T可以是String类或是String的子类,如果传入的没有问题,那就可以调用isEmpty()方法。
public class WildCardTest<T extends String> {
public void f(T t) {
t.isEmpty();
}
public static void main(String[] args) {
WildCardTest<String> stringWildCardTest = new WildCardTest<>();
stringWildCardTest.f("");
}
}
擦除是历史遗留问题
java的泛型不是从jdk1.0就出现的,为了跟以往没有泛型代码的源代码兼容,例如List被擦除为List,而普通的类型变量在未指定边界的时候被擦除为Object,从而实现泛型的功能并且向后兼容。
三. 泛型的通配符(逆变与协变)
这是两个赋值,一个是数组,ArrayList因为是Collection的子类,所以数组向上转型是可以的;另一个是带泛型的ArrayList,带ArrayList的泛型并不能赋值给带Collection的泛型。
Collection[] collections = new ArrayList[]{};
//泛型会报错
ArrayList<Collection> collections1 = new ArrayList<ArrayList>();
逆变,协变与不变
为什么会导致这样的差异呢?这里又引出了一个概念– 逆变,协变与不变。逆变与协变用来描述类型转换后的继承关系。
f(⋅)是逆变(contravariant)的,当A≤B时有f(B)≤f(A)成立;
f(⋅)是协变(covariant)的,当A≤B时有成立f(A)≤f(B)成立;
f(⋅)是不变(invariant)的,当A≤B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系。
根据上面两个赋值语句做解释,A 是ArrayList ,B是Collection,所以B > A,这个没有问题,然后 A[] 数组当作f(A),B[] 数组当作f(B),并且A[]可以赋值给B[]数组,说明 f(B) >=f(A),符合协变原则,所以数组是协变的。 而泛型也套用这个规则,发现 泛型其实是不变的。
Java中泛型是不变的,可有时需要实现逆变与协变,怎么办呢?这时,通配符?派上了用场:
// <? extends>实现了泛型的协变,它意味着某种Collection或Collection子类的类型,规定了该容器持有类型的上限,它是具体的类型,只是这个类型是什么没有人关心 比如:
List<? extends Collection> list = new ArrayList<ArrayList>();
// <? super>实现了泛型的逆变,它意味着某种ArrayList或ArrayList父类的类型,规定了该容器持有类型的下限,比如:
List<? super ArrayList> list = new ArrayList<Collection>();
逆变,协变的应用
在协变中,还是先以数组做举例,协变中对持有对象的存入有严格限制:
Collection[] collections = new ArrayList[2];
collections[0] = new ArrayList();
//这一步编译没问题。但是运行时发现数组里已经定好了只能存ArrayList类型,所以会抛ArrayStoreException
collections[1] = new LinkedList();
所以在泛型的协变中,例如ArrayList的add方法是不能调用了,在编译期间直接报错。
这是ArrayList 的add,get方法定义,当使用协变时,E e 会被直接替换成 ? extends E
public boolean add(E e);
public E get(int index);
具体事例:
ArrayList<? extends Set> sets = new HashSet<>();
//因为 ? extends Set 编译器不知道sets引用指向什么对象,有可能是 HashSet,可能是TreeSet,这种不确定性导致sets不能使用add方法。
//sets.add(new HashSet());
//能插入null值
sets.add(null);
//因为这个泛型参数的上限是Set,为了安全性,所以只返回set类型
Set set = sets.get(0);
在逆变中,因为规定了泛型的下界,所以get set 方法的使用限制又有所不同:
ArrayList<? super List> list = new ArrayList<Collection>();
//对于 add方法,只能放List或List的子类,因为list容器泛型参数都是List的父类 不会出现问题
list.add(new ArrayList());
//不能add HashSet
//list.add(new HashSet());
//不能add Object
//list.add(new Object());
//因为这个泛型参数的下限是List 所以无法确定这是个什么类型,只能返回Object
Object object = list.get(0);
无界通配符
除了extends和super,还有一种 List<\?>这种通配符,代表着任何事物,但它与List不同的是:
List<\Object> = List = ‘持有任何Object类型的原生List’List<\?>表示–’具有某种特定类型的非原生List,只是我们不知道那种类型是什么’。
具体用代码看出区别:
ArrayList<Collection> collections2 = new ArrayList<>();
ArrayList<?> objects = new ArrayList<>();
//?代表持有某种特定类型 ,所以也可以是Collection,这种赋值时合法的
objects = collections2;
//?代表持有某种特定类型,d但是不确定具体哪种,所以只能返回Object
Object o = objects.get(0);
//?代表持有某种特定类型,但是什么类型编译器并不知道,所以为了安全起见,不会让你用add方法
//objects.add(new Object());
//可以add null
objects.add(null);
总结来说:
要从泛型类取数据时,用extends;要往泛型类写数据时,用super;既要取又要写,就不用通配符(即extends与super都不用)。
四. 总结
这篇文章总结的是书里比较重要的知识点,跳过了简单的应用,写了那么多,感觉总结还是很有必要的,你第一遍看书也许概念会有些懵懂,但是再记录总结下,你会解开第一遍看书时有点么棱两可的知识点。
[转载] JAVA泛型杂谈--擦除,协变,逆变,通配符等相关推荐
- 泛型型协变逆变_Java泛型类型简介:协变和逆变
泛型型协变逆变 by Fabian Terh 由Fabian Terh Java泛型类型简介:协变和逆变 (An introduction to generic types in Java: cova ...
- 大数据技术之_16_Scala学习_12_设计模式+泛型、上下界、视图界定、上下文界定、协变逆变不变
大数据技术之_16_Scala学习_12 第十七章 设计模式 17.1 学习设计模式的必要性 17.2 掌握设计模式的层次 17.3 设计模式的介绍 17.4 设计模式的类型 17.5 简单工厂模式( ...
- 12:设计模式、泛型、上下界、视图界定、上下文界定、协变逆变不变
经典的 WordCount 的讲解 示例代码如下: package com.atguigu.chapter14.homework.wordcount/*val lines = List("a ...
- 10天学会kotlin DAY7 接口 泛型 协变 逆变
kotlin 接口 泛型 协变 逆变 前言 1.接口的定义 2.抽象类 3.定义泛型类 4.泛型函数 5.泛型变换 6.泛型类型约束 7.vararg 关键字(动态参数) 8.[] 操作符 9.out ...
- C#泛谈 —— 变体(协变/逆变)
有如下四个类. public class Animal{}public class Mammal : Animal{}public class Dog : Mammal{public void Eat ...
- 协变逆变java_Java中的逆变与协变
什么是逆变与协变 协变(Covariance) 如果B是A的子类,并且F(B)也是F(A)的子类,那么F即为协变 逆变(Contravariance) 如果B是A的子类,并且F(B)成了F(A)的父类 ...
- 协变逆变java_Java中的协变与逆变
Java作为面向对象的典型语言,相比于C++而言,对类的继承和派生有着更简洁的设计(比如单根继承). 在继承派生的过程中,是符合Liskov替换原则(LSP)的.LSP总结起来,就一句话: 所有引用基 ...
- Java 泛型总结(三):通配符的使用
简介 前两篇文章介绍了泛型的基本用法.类型擦除以及泛型数组.在泛型的使用中,还有个重要的东西叫通配符,本文介绍通配符的使用. 这个系列的另外两篇文章: Java 泛型总结(一):基本用法与类型擦除 J ...
- Scala语言学习笔记——泛型、上下界、视图界定、上下文界定、协变逆变不变、闭包、柯里化
1.Scala泛型 应用案例1 /*** @author huleikai* @create 2019-05-27 11:23*/ object TestFanXing {def main(args: ...
最新文章
- maven添加本地jar包依赖
- VC++程序开机自启动(注册表上注册)
- 剑指offer:26-30记录
- JDK源码解析之 java.lang.Long
- 可逆矩阵的特征值和原来矩阵_线性代数——相似矩阵的可逆变换矩阵P是否唯一...
- HDU2019 数列有序!
- 单词压缩编码--Trie树
- BZOJ 4552 [Tjoi2016Heoi2016]排序 ——线段树 二分答案
- 51nod1174--区间中最大的数--线段树
- 计算机考试一级b软件未来教育,2019.9全国计算机一级MS Office考试每日一练
- ltm4650_LTM4650IY-1APBF_代理全新进口【linear】现货商
- 万年历、黄历,获取每日的宜忌、五行、冲煞、值神、彭祖百忌、吉神宜趋、今日胎神、凶神宜忌、二十八星宿、建除十二神
- 24_摘录的一些精彩语句1
- 【Red Team——基础】通过钓鱼攻击获得访问权限
- GetPixel算法
- gallary 实现类似viewpage 的效果 左右可见
- 安装gtsam遇到的错误
- linux运行魔力宝贝,魔力宝贝私服架设(仅供朋友交流学习 绝无商业用途 如有雷同纯熟巧合)...
- 【Vue】Vue2生命周期详解
- 把WinRAR默认压缩格式换为ZIP
热门文章
- 解决“Dynamic Web Module 3.0 requires Java 1.6 or newer.”错误
- javaBean和jsp应用
- android音量键广播,音量控制键控制的音频流(setVolumeControlStream)描述
- oracle授权只读用户,Oracle创建只读用户(账号)的方法
- v7000更换电池步骤_ups电源运行中是否可以更换电池?应如何操作呢
- 2.2线性表的顺序表
- 如何在ps添加箭头_「PS精选案例教程」制作斑驳生锈字体
- python爬取哔哩哔哩视频_Python实现视频爬取下载
- html自适应_web前端入门到实战:HTML 文档流,设置元素浮动,导致父元素高度无法自适应的解决方法...
- python3.8.0安装_Python3.8.0