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

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/2bf15c5265c5https://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性质的基类或基接口,都必须将自己作为参数上传到基类,而不是上传其它乱七八糟的类型。上传的类型替换掉基类或基接口中的泛型参数,这种替换说明并不可能存在父类的版本,只可能存在子类作为参数的版本!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值