java泛型之自限定类型和参数协变
java泛型之自限定类型和参数协变
本博文参考《thinking in java》第四版第15张“泛型”中的相关内容和网络上的各种博客,本文也是几个月前的一篇博文“java泛型(一)”的后续,主要是书本的代码加上自己的理解和感悟
背景
- 为何突然想起看这部分内容,这是由于最近有一个小项目,有几个对象的构造十分复杂,一大堆的setter和getter或者冗长的构造函数太辣眼睛。本着做一次小项目学习一些新内容的目标,加上近期看完了大话设计模式这本书,于是想用一些设计模式将代码写得好看些,最后决定使用builder模式(此构造者模式并非大话设计模式中的构造者模式),就是连续使用点操作符来赋值,最后调用build方法的。
- 问题:假设复杂的对象有继承关系,暂且叫Parent和Child,那么对应也会有一个ParentBuilder和ChildBuilder。由于对象的属性本来就很多,如果单纯分开来写(比如分别实现一个Builder接口,实现Build方法),那么ChildBuilder的方法一定很长,而且有一大部分和ParentBuilder中的意义相近(因为Child本来就有大量的属性是从父亲中继承而来的),我觉得这样子做有些笨。我的理想解决方法(可能有更好的设计方法)应该是如果是Parent的属性,那么ParentBuilder应该负责这些属性的赋值,如果是Child自己的属性,应该由ChildBuilder来负责,而且ChildBuilder可以“继承”ParentBuilder,从而让本来就是Parent的属性不用再在ChildBuilder中再写一次。
- 那么怎么大致实现这个方法呢?于是便有了下面的学习,当然,自限定内容属于《thinking in java》中泛型一章较后的内容了,可能需要补补前面内容才可以更好理解。
- 此外,有一些博客对我学习过程有一些帮助在此记录一下 https://www.jianshu.com/p/2bf15c5265c5 , https://blog.csdn.net/zwvista/article/details/78437667 。我找了网上的众多博客,发现大多数都是照书本意思翻译,我本来就是觉得书本讲得有些难懂才找博客,结果很难找到有作者有将其中解析清楚。因此本文重点谈自己对该部分的理解,或错或对,毕竟自己一大学生确实水平有限。
参数协变
- 和书本上顺序不太一样,我觉得需要先了解协变才可以更好啃下自限定类型,上面那篇博客讲得很好了,不再重复。
- 总结一下,协变可以分为两种:协变参数类型,协变返回类型
- 下面先抛开泛型,先看一些简单的例子
协变参数类型
如果不使用泛型,单纯继承是实现不了协变参数的,先看代码(Derived为Base的子类)
class OrdinaryGetter {void set(Base base) {System.out.println("OrdinarySetter.set(Base)");} }class DerivedSetter extends OrdinarySetter {// 并没有覆盖父类的方法,而是重载了这个方法void set(Derived derived) {System.out.println("DerivedSetter.set(Derived)");} }public class OrdinaryArguments {public static void main(String[] args) {Base base = new Base();Derived derived = new Derived();DerivedSetter setter = new DerivedSetter();setter.set(derived);setter.set(base);} }
分析:
- 后面两个setter会调用不同的方法,说明DerivedSetter只是重写了set方法,并不是覆盖set方法
- 换句话说,在非泛型代码中,参数的类型不可以随子类型发生变化,等下对比如果使用泛型的自限定,那么就会知道当中的差别了。
- 其实这种结果是十分好理解的,这是涉及函数签名的知识,函数签名包括函数名和参数列表(没有返回值),既然方法的类型不同了,自然函数签名就不一样,那么子类的set方法是不会覆盖父类的set方法的,它只是实现了方法的重载。可以简单这样子理解,如果子类的某个某方法的参数是协变参数,那么子类的该方法属于”覆盖“了父类的相同函数名称的方法,即只有一个版本,并不同于重载。如果子类的某个某方法的参数不是协变参数,那么该方法只是重载了父类的相同函数名的方法,并不覆盖,即子类有两个不同的版本。
协变返回类型
Java SE5中引入了协变返回类型,改造一下上面的代码:
class BaseGetter {Base get() {return new Base();} }class ChildGetter extends BaseGetter{Derived get() {return new Derived();} }public class OrdinaryArguments {public static void main(String[] args) {ChildGetter childGetter = new ChildGetter();Base base = childGetter.get();Derived child = childGetter.get();} }
分析
- 其实上述的版本并不能很好地说明协变返回类型,写在这里只是因为概念容易混淆。调用ChildGetter的get方法铁定调用的是
Derived get()
,这可以用函数签名的解释,既然子类定义的函数的函数签名和父类的一样,那么肯定是覆盖重写了父类的get方法,你甚至可以将Derived get()
改为Integer get(),这也是可行的。
- 其实上述的版本并不能很好地说明协变返回类型,写在这里只是因为概念容易混淆。调用ChildGetter的get方法铁定调用的是
接口的版本
class Base {}class Derived extends Base {}interface OrdinaryGetter {Base get(); }interface DerivedGetter extends OrdinaryGetter {Derived get(); }public class CovariantReturnTypes {void test(DerivedGetter getter) {Derived d = getter.get();} }
分析
- 上述就可以体现协变返回类型了,但在Java SE5前是不能编译的,尽管这十分合乎常理,导出类方法应该能够返回比他覆盖的基类方法更加具体的类型(如Derived比Base更加具体)。这次你可以不能将DerivedGetter的get方法改成一些不是Base的子类的类型了(例如Integer),因为一个类可以实现多个接口,如果你同时实现上述两个接口,那么你需要实现
Derived get()
,试想如果你的DerivedGetter的get方法返回Integer,那么同时实现这两个接口编译器就傻眼了:同样的函数签名两个方法居然返回是两个不同的类型(两个类型还没有继承实现的关系)。
- 上述就可以体现协变返回类型了,但在Java SE5前是不能编译的,尽管这十分合乎常理,导出类方法应该能够返回比他覆盖的基类方法更加具体的类型(如Derived比Base更加具体)。这次你可以不能将DerivedGetter的get方法改成一些不是Base的子类的类型了(例如Integer),因为一个类可以实现多个接口,如果你同时实现上述两个接口,那么你需要实现
自限定类型介绍
- 先从简单的版本入手,它没有自限定边界,但是我初学的时候已经觉得足够的古怪了。
古怪的循环泛型
先看例子:
class GenericType<T> {}public class ChildType extends GenericType<ChildType>{}
分析
- 可以这样子来理解上述当的例子:我在创建一个新类,它继承自一个泛型类型,这个泛型类型接受我的类名作为其参数
- 意思还是比较好理解,但是又有什么用呢?再看更加详细的代码
书本代码
class BasicHolder<T> {T element;void set(T arg) {element = arg;}T get() {return element;}void f() {System.out.println(element.getClass().getSimpleName());} }class SubType extends BasicHolder<SubType> {}public class CRGWithBasicHolder {public static void main(String[] args) {SubType st1 = new SubType();SubType st2 = new SubType();st1.set(st2);st1.f();// 此处是我自己增加的代码BasicHolder<SubType> basicHolder = new BasicHolder<>();basicHolder.set(st2);basicHolder.f();} }
分析
- 我在main方法中增加了一段代码,因为可以看出上下两段代码运行的结果是一样的,而下面的代码更加像我们平时的用法,例如
List<Integer> list = new ArrayList<>()
, 这时我想究竟class SubType extends BasicHolder<SubType> {}
有什么存在的必要呢? - 书本上还有一句很重要的话:基类用导出类替代其参数,意味者泛型基类变成了一种其所有导出类的公共功能的模板。这我认为有点像设计模式中的模板方法,你可以将公共部分提取出来,而子类可以继承这个基类,并且拥有自己额外的方法,这样子你可以省去不少子类中冗余的代码。
- 我在main方法中增加了一段代码,因为可以看出上下两段代码运行的结果是一样的,而下面的代码更加像我们平时的用法,例如
现在来分析这种简单版本的协变类型是否实现了:
协变返回类型:可以。这不用再说了,因为从Java SE5之后就支持这种,道理前面也提过,可以用函数签名的唯一性来理解。
协变参数类型:不可以。即它会同时存在父类和子类的两种方法版本,看书本例子:
class GenericSetter<T> {void set(T arg) {System.out.println("GenericSetter.set(Base)");} }class DerivedGs extends GenericSetter<Base> {void set(Derived derived) {System.out.println("DerivedGs.set(Derived)");} }public class PlainGenericInheritance {public static void main(String[] args) {Base base = new Base();Derived derived = new Derived();DerivedGs derivedGs = new DerivedGs();derivedGs.set(derived);derivedGs.set(base); // ok} }
DerivedGs为了试验是否支持协变参数类型,故意传递基类Base类型,然后再写set方法试图覆盖父类版本,即以Derived为参数。不幸的是,失败了,还是存在两种版本。此时,为了解决参数协变,自限定类型隆重登场!
自限定类型
形式:
class/interface SelfBounded<T extends SelfBounded<T>>{...}
,这样子一看确实有些晕。可以分步来解读:我现在定义一个类SelfBouded,它有一个泛型参数T,这个T是有要求的,它有一个上界SelfBounded<T>
。每一步都感觉懂,但是不知道干啥的对吧?但是至少有一个感觉,参数T和SelfBounded还是有一个明显的继承关系的。结合来看就懂了:
interface BaseSetterAndGetter<T extends BaseSetterAndGetter<T>> {T get();void set(T arg); }interface ChildSetterAndGetter extends BaseSetterAndGetter<ChildSetterAndGetter> {}interface A {}// Error // interface B extends BaseSetterAndGetter<A> {} // ok interface C extends BaseSetterAndGetter<ChildSetterAndGetter>{}public class GenericSetterAndGetter {void test(ChildSetterAndGetter setterAndGetter, ChildSetterAndGetter child, BaseSetterAndGetter parent) {ChildSetterAndGetter childSetterAndGetter = setterAndGetter.get();setterAndGetter.set(child);// setterAndGetter.set(parent); Error} }
先不看BaseSetterAndGetter中的具体方法是什么,先看ChildSetterAndGetter为什么定义成功:它继承BaseSetterAndGetter并且参数是自己本身即ChildSetterAndGetter,将ChildSetterAndGetter替换BaseSetterAndGetter中T可得
ChildSetterAndGetter extends BaseSetterAndGetter<ChildSetterAndGetter>
,这个式子就是我定义的本身啊!这就是传说中的自限定形式!同理,再看interface B为什么不成功,很明显,因为A和BaseSetterAndGetter并没有关系。而interface C成功定义也很容易理解,因为参数ChildSetterAndGetter网上传递替换,还是ChildSetterAndGetter extends BaseSetterAndGetter<ChildSetterAndGetter>
,这就是ChildSetterAndGetter 的定义。当然,上述代码有更重要的作用,就是为了说明自限定类型的协变类型:
- 协变返回类型:可以,不再解释了
- 协变参数类型:可以,这是和没有上界的古怪的循环泛型的差别。上述代码可以说明这一点,其实这也很好理解,每一个继承具有Selfbounded性质的基类或基接口,都必须将自己作为参数上传到基类,而不是上传其它乱七八糟的类型。上传的类型替换掉基类或基接口中的泛型参数,这种替换说明并不可能存在父类的版本,只可能存在子类作为参数的版本!
java泛型之自限定类型和参数协变相关推荐
- Java 泛型,你了解类型擦除吗?
泛型,一个孤独的守门者. 大家可能会有疑问,我为什么叫做泛型是一个守门者.这其实是我个人的看法而已,我的意思是说泛型没有其看起来那么深不可测,它并不神秘与神奇.泛型是 Java 中一个很小巧的概念,但 ...
- java泛型程序设计——无限定通配符+通配符捕获
[0]README 0.1) 本文描述+源代码均 转自 core java volume 1, 旨在理解 java泛型程序设计 的 无限定通配符+通配符捕获 的相关知识: [1]无限定通配符相关 1. ...
- java泛型程序设计——类型变量限定 + 泛型代码和虚拟机
[0]README 0.1) 本文描述+源代码均 转自 core java volume 1, 旨在理解 java泛型程序设计 的 类型变量限定 + 泛型代码和虚拟机 的知识: [1]类型变量的限定 ...
- Java泛型(11):潜在类型机制
泛型的目标之一就是能够编写尽可能广泛应用的代码. 为了实现这一点,我们需要各种途径来放松对我们的代码将要作用的类型所做的限制,同时不丢失静态类型检查的好处.即写出更加泛化的代码. Java泛型看起来是 ...
- grpc java 泛型_gRPC中Any类型的使用(Java和NodeJs端)
工作中要把原来Java服务端基于SpringMVC的服务改为使用gRPC直接调用.由于原Service的返回值为动态的Map类型,key值不确定,且value的类型不唯一,因此使用了protobuf ...
- java 泛型 通配符边界和类型形参边界的区别
通配符只能有一个边界,而类型形参可以有多个边界 通配符可以有上界或者下界,而类型形参没有下界<super> 参考: http://www.angelikalanger.com/Generi ...
- java 泛型 泛型接口(Generic Methods)类型形参(Type Parameters)
静态方法,非静态方法,还有构造器都可以使用类型形参 方法或构造器的类型形参作用于整个方法,没有例外.因为方法没有静态部分 //1.静态方法 static <T extends String> ...
- java 泛型 泛型接口(Generic Interfaces)类型形参(Type Parameters)
接口的类型形参作用于整个接口,除了一些字段和嵌套类型,因为字段和嵌套类型都是默认静态 interface Interface <T> {T value; //errorT method() ...
- java 泛型 泛型类(Generic Classes)类型形参(Type Parameters)
类(class)的类型形参的作用范围是整个class,除了静态(static)成员和静态初始化模块. class Test <T> { //类型形参的写法static {Test<T ...
最新文章
- 深度学习在三维环境重建中的应用
- 盛大游戏式管理,什么时候能管理游戏式?^o^
- 旋转字符串算法由浅入深
- PHP函数库之BC高精确度函数库
- 【音频处理】离散傅里叶变换
- python编写脚本,删除固定用户下的所有表
- lsb_release -a 查询系统版本
- 使用memcached显著提升站点性能
- python 删除断点_给 Python 开发者的四条忠告!强烈建议收藏
- 阿里电话面试题(附答案)
- java kinect_使用java来做Kinect开发
- 用计算机弹麻雀,玩麻雀弹
- 盘点免费好用的5款思维导图工具
- WinMerge文字重叠问题
- Error:Execution failed for task ':recordlib:lint'. Lint found errors in the project; aborting buil
- 四川多多开店:拼多多商家绑定银行卡怎么绑定
- 以太坊开发入门,完整入门篇(小白可以看看,高手看看自己有没有遗漏的
- canvas标签设置长宽
- 计算机学霸的电视剧,10部经典青春校园剧,每一部都让人怀念青春
- MAX() OVER() 函数