600字范文,内容丰富有趣,生活中的好帮手!
600字范文 > java泛型之自限定类型和参数协变

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

时间:2023-10-02 14:19:33

相关推荐

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》中泛型一章较后的内容了,可能需要补补前面内容才可以更好理解。此外,有一些博客对我学习过程有一些帮助在此记录一下 /p/2bf15c5265c5 , /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(),这也是可行的。

接口的版本

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,那么同时实现这两个接口编译器就傻眼了:同样的函数签名两个方法居然返回是两个不同的类型(两个类型还没有继承实现的关系)。

自限定类型介绍

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

古怪的循环泛型

先看例子:

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> {}有什么存在的必要呢?书本上还有一句很重要的话:基类用导出类替代其参数,意味者泛型基类变成了一种其所有导出类的公共功能的模板。这我认为有点像设计模式中的模板方法,你可以将公共部分提取出来,而子类可以继承这个基类,并且拥有自己额外的方法,这样子你可以省去不少子类中冗余的代码。

现在来分析这种简单版本的协变类型是否实现了:

协变返回类型:可以。这不用再说了,因为从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> {} // okinterface 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性质的基类或基接口,都必须将自己作为参数上传到基类,而不是上传其它乱七八糟的类型。上传的类型替换掉基类或基接口中的泛型参数,这种替换说明并不可能存在父类的版本,只可能存在子类作为参数的版本!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。