java泛型之自限定类型和参数协变

本博文参考《thinking in java》第四版第15张“泛型”中的相关内容和网络上的各种博客,本文也是几个月前的一篇博文“java泛型(一)”的后续,主要是书本的代码加上自己的理解和感悟

背景

  1. 为何突然想起看这部分内容,这是由于最近有一个小项目,有几个对象的构造十分复杂,一大堆的setter和getter或者冗长的构造函数太辣眼睛。本着做一次小项目学习一些新内容的目标,加上近期看完了大话设计模式这本书,于是想用一些设计模式将代码写得好看些,最后决定使用builder模式(此构造者模式并非大话设计模式中的构造者模式),就是连续使用点操作符来赋值,最后调用build方法的。
  2. 问题:假设复杂的对象有继承关系,暂且叫Parent和Child,那么对应也会有一个ParentBuilder和ChildBuilder。由于对象的属性本来就很多,如果单纯分开来写(比如分别实现一个Builder接口,实现Build方法),那么ChildBuilder的方法一定很长,而且有一大部分和ParentBuilder中的意义相近(因为Child本来就有大量的属性是从父亲中继承而来的),我觉得这样子做有些笨。我的理想解决方法(可能有更好的设计方法)应该是如果是Parent的属性,那么ParentBuilder应该负责这些属性的赋值,如果是Child自己的属性,应该由ChildBuilder来负责,而且ChildBuilder可以“继承”ParentBuilder,从而让本来就是Parent的属性不用再在ChildBuilder中再写一次。
  3. 那么怎么大致实现这个方法呢?于是便有了下面的学习,当然,自限定内容属于《thinking in java》中泛型一章较后的内容了,可能需要补补前面内容才可以更好理解。
  4. 此外,有一些博客对我学习过程有一些帮助在此记录一下 https://www.jianshu.com/p/2bf15c5265c5 , https://blog.csdn.net/zwvista/article/details/78437667 。我找了网上的众多博客,发现大多数都是照书本意思翻译,我本来就是觉得书本讲得有些难懂才找博客,结果很难找到有作者有将其中解析清楚。因此本文重点谈自己对该部分的理解,或错或对,毕竟自己一大学生确实水平有限。

参数协变

  1. 和书本上顺序不太一样,我觉得需要先了解协变才可以更好啃下自限定类型,上面那篇博客讲得很好了,不再重复。
  2. 总结一下,协变可以分为两种:协变参数类型,协变返回类型
  3. 下面先抛开泛型,先看一些简单的例子

协变参数类型

  1. 如果不使用泛型,单纯继承是实现不了协变参数的,先看代码(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);}
    }
    
  2. 分析:

    • 后面两个setter会调用不同的方法,说明DerivedSetter只是重写了set方法,并不是覆盖set方法
    • 换句话说,在非泛型代码中,参数的类型不可以随子类型发生变化,等下对比如果使用泛型的自限定,那么就会知道当中的差别了。
    • 其实这种结果是十分好理解的,这是涉及函数签名的知识,函数签名包括函数名和参数列表(没有返回值),既然方法的类型不同了,自然函数签名就不一样,那么子类的set方法是不会覆盖父类的set方法的,它只是实现了方法的重载。可以简单这样子理解,如果子类的某个某方法的参数是协变参数,那么子类的该方法属于”覆盖“了父类的相同函数名称的方法,即只有一个版本,并不同于重载。如果子类的某个某方法的参数不是协变参数,那么该方法只是重载了父类的相同函数名的方法,并不覆盖,即子类有两个不同的版本。

协变返回类型

  1. 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();}
    }
    
  2. 分析

    • 其实上述的版本并不能很好地说明协变返回类型,写在这里只是因为概念容易混淆。调用ChildGetter的get方法铁定调用的是 Derived get(),这可以用函数签名的解释,既然子类定义的函数的函数签名和父类的一样,那么肯定是覆盖重写了父类的get方法,你甚至可以将Derived get()改为Integer get(),这也是可行的。
  3. 接口的版本

    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();}
    }
    
  4. 分析

    • 上述就可以体现协变返回类型了,但在Java SE5前是不能编译的,尽管这十分合乎常理,导出类方法应该能够返回比他覆盖的基类方法更加具体的类型(如Derived比Base更加具体)。这次你可以不能将DerivedGetter的get方法改成一些不是Base的子类的类型了(例如Integer),因为一个类可以实现多个接口,如果你同时实现上述两个接口,那么你需要实现Derived get(),试想如果你的DerivedGetter的get方法返回Integer,那么同时实现这两个接口编译器就傻眼了:同样的函数签名两个方法居然返回是两个不同的类型(两个类型还没有继承实现的关系)。

自限定类型介绍

  1. 先从简单的版本入手,它没有自限定边界,但是我初学的时候已经觉得足够的古怪了。

古怪的循环泛型

  1. 先看例子:

    class GenericType<T> {}public class ChildType extends GenericType<ChildType>{}
    
  2. 分析

    • 可以这样子来理解上述当的例子:我在创建一个新类,它继承自一个泛型类型,这个泛型类型接受我的类名作为其参数
    • 意思还是比较好理解,但是又有什么用呢?再看更加详细的代码
  3. 书本代码

    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();}
    }
    
  4. 分析

    • 我在main方法中增加了一段代码,因为可以看出上下两段代码运行的结果是一样的,而下面的代码更加像我们平时的用法,例如List<Integer> list = new ArrayList<>(), 这时我想究竟class SubType extends BasicHolder<SubType> {}有什么存在的必要呢?
    • 书本上还有一句很重要的话:基类用导出类替代其参数,意味者泛型基类变成了一种其所有导出类的公共功能的模板。这我认为有点像设计模式中的模板方法,你可以将公共部分提取出来,而子类可以继承这个基类,并且拥有自己额外的方法,这样子你可以省去不少子类中冗余的代码。
  5. 现在来分析这种简单版本的协变类型是否实现了:

    • 协变返回类型:可以。这不用再说了,因为从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为参数。不幸的是,失败了,还是存在两种版本。此时,为了解决参数协变,自限定类型隆重登场!

自限定类型

  1. 形式:class/interface SelfBounded<T extends SelfBounded<T>>{...},这样子一看确实有些晕。可以分步来解读:我现在定义一个类SelfBouded,它有一个泛型参数T,这个T是有要求的,它有一个上界SelfBounded<T>。每一步都感觉懂,但是不知道干啥的对吧?但是至少有一个感觉,参数T和SelfBounded还是有一个明显的继承关系的。

  2. 结合来看就懂了:

    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 的定义。

  3. 当然,上述代码有更重要的作用,就是为了说明自限定类型的协变类型:

    • 协变返回类型:可以,不再解释了
    • 协变参数类型:可以,这是和没有上界的古怪的循环泛型的差别。上述代码可以说明这一点,其实这也很好理解,每一个继承具有Selfbounded性质的基类或基接口,都必须将自己作为参数上传到基类,而不是上传其它乱七八糟的类型。上传的类型替换掉基类或基接口中的泛型参数,这种替换说明并不可能存在父类的版本,只可能存在子类作为参数的版本!

java泛型之自限定类型和参数协变相关推荐

  1. Java 泛型,你了解类型擦除吗?

    泛型,一个孤独的守门者. 大家可能会有疑问,我为什么叫做泛型是一个守门者.这其实是我个人的看法而已,我的意思是说泛型没有其看起来那么深不可测,它并不神秘与神奇.泛型是 Java 中一个很小巧的概念,但 ...

  2. java泛型程序设计——无限定通配符+通配符捕获

    [0]README 0.1) 本文描述+源代码均 转自 core java volume 1, 旨在理解 java泛型程序设计 的 无限定通配符+通配符捕获 的相关知识: [1]无限定通配符相关 1. ...

  3. java泛型程序设计——类型变量限定 + 泛型代码和虚拟机

    [0]README 0.1) 本文描述+源代码均 转自 core java volume 1, 旨在理解 java泛型程序设计 的 类型变量限定 + 泛型代码和虚拟机 的知识: [1]类型变量的限定 ...

  4. Java泛型(11):潜在类型机制

    泛型的目标之一就是能够编写尽可能广泛应用的代码. 为了实现这一点,我们需要各种途径来放松对我们的代码将要作用的类型所做的限制,同时不丢失静态类型检查的好处.即写出更加泛化的代码. Java泛型看起来是 ...

  5. grpc java 泛型_gRPC中Any类型的使用(Java和NodeJs端)

    工作中要把原来Java服务端基于SpringMVC的服务改为使用gRPC直接调用.由于原Service的返回值为动态的Map类型,key值不确定,且value的类型不唯一,因此使用了protobuf ...

  6. java 泛型 通配符边界和类型形参边界的区别

    通配符只能有一个边界,而类型形参可以有多个边界 通配符可以有上界或者下界,而类型形参没有下界<super> 参考: http://www.angelikalanger.com/Generi ...

  7. java 泛型 泛型接口(Generic Methods)类型形参(Type Parameters)

    静态方法,非静态方法,还有构造器都可以使用类型形参 方法或构造器的类型形参作用于整个方法,没有例外.因为方法没有静态部分 //1.静态方法 static <T extends String> ...

  8. java 泛型 泛型接口(Generic Interfaces)类型形参(Type Parameters)

    接口的类型形参作用于整个接口,除了一些字段和嵌套类型,因为字段和嵌套类型都是默认静态 interface Interface <T> {T value; //errorT method() ...

  9. java 泛型 泛型类(Generic Classes)类型形参(Type Parameters)

    类(class)的类型形参的作用范围是整个class,除了静态(static)成员和静态初始化模块. class Test <T> { //类型形参的写法static {Test<T ...

最新文章

  1. 深度学习在三维环境重建中的应用
  2. 盛大游戏式管理,什么时候能管理游戏式?^o^
  3. 旋转字符串算法由浅入深
  4. PHP函数库之BC高精确度函数库
  5. 【音频处理】离散傅里叶变换
  6. python编写脚本,删除固定用户下的所有表
  7. lsb_release -a 查询系统版本
  8. 使用memcached显著提升站点性能
  9. python 删除断点_给 Python 开发者的四条忠告!强烈建议收藏
  10. 阿里电话面试题(附答案)
  11. java kinect_使用java来做Kinect开发
  12. 用计算机弹麻雀,玩麻雀弹
  13. 盘点免费好用的5款思维导图工具
  14. WinMerge文字重叠问题
  15. Error:Execution failed for task ':recordlib:lint'. Lint found errors in the project; aborting buil
  16. 四川多多开店:拼多多商家绑定银行卡怎么绑定
  17. 以太坊开发入门,完整入门篇(小白可以看看,高手看看自己有没有遗漏的
  18. canvas标签设置长宽
  19. 计算机学霸的电视剧,10部经典青春校园剧,每一部都让人怀念青春
  20. MAX() OVER() 函数

热门文章

  1. 理解ClangAST
  2. 动态规划法求解资源分配问题
  3. 深入剖析 redis 事务机制
  4. 第6贴:前置音频电路 音频功放
  5. 经典通用的Pbootcms花卉网站模板源码,自适应手机端,带后台管理
  6. R语言FOR循环打印9*9乘法表
  7. JavaScript的案例:模拟聊天界面发送信息
  8. 手把手搭建redis集群-三台虚拟机(三主三从)
  9. Android 编译报Run with --stacktrace option to get the stack trace
  10. L0、L1、L2范数