Java泛型 自限定类型(Self-Bound Types)详解

朱雀 2023-06-06 12:29 190阅读 0赞

文章目录

  • 简介
  • 普通泛型类——构成自限定
  • 自限定类型的泛型类
  • JDK源码里自限定的应用——enum
  • JDK源码里自限定的应用——Integer

简介

java泛型里会有class SelfBounded<T extends SelfBounded<T>> { }这种写法,泛型类有一个类型参数T,但这个T有边界SelfBounded<T>。这边界里有两个疑问:

  1. SelfBounded已经在左边出现,但SelfBounded类还没定义完这里就用了;
  2. 同样,T也在左边出现过了,这是该泛型类的类型参数标识符。

这两点确实挺让人疑惑,思考这个类定义时容易陷入“死循环”。
注意,自限定的要点实际就是这两个“疑问”。

普通泛型类——构成自限定

  1. class BasicHolder<T> {
  2. T element;
  3. void set(T arg) { element = arg; }
  4. T get() { return element; }
  5. void f() {
  6. System.out.println(element.getClass().getSimpleName());
  7. }
  8. }
  9. class Subtype extends BasicHolder<Subtype> { }
  10. public class CRGWithBasicHolder {
  11. public static void main(String[] args) {
  12. Subtype st1 = new Subtype(), st2 = new Subtype(), st3 = new Subtype();
  13. st1.set(st2);
  14. st2.set(st3);
  15. Subtype st4 = st1.get().get();
  16. st1.f();
  17. }
  18. } /* Output: Subtype *///:~
  • BasicHolder<T>泛型类的类型参数并没有什么边界,在继承它的时候,你本可以class A extends BasicHolder<B> {}这样普普通通的用。
  • class Subtype extends BasicHolder<Subtype> {}这样用,就构成自限定了。从定义上来说,它继承的父类的类型参数是它自己。从使用上来说,Subtype对象本身的类型是Subtype,且Subtype对象继承而来的成员(element)、方法的形参(set方法)、方法的返回值(get方法)也是Subtype了(这就是自限定的重要作用)。这样Subtype对象就只允许和Subtype对象(而不是别的类型的对象)交互了。
  • 虽然class Subtype extends BasicHolder<Subtype> {}这样用,看起来是类定义还没有结束,就把自己的名字用到了边界的泛型类的类型参数。虽然感觉稍微有点不合理,但这里就强行理解一下吧。自限定的用法:父类作为一个泛型类或泛型接口,用子类的名字作为其类型参数
  • 以上两点,就解释了简介里的第二个“疑问”。正因为class Subtype extends BasicHolder<Subtype>这样用可以让Subtype对象只允许和Subtype对象交互,这里再把Subtype抽象成类型参数T,不就刚好变成了T extends SelfBounded<T>这样的写法。
  • 在主函数里,根据自限定的重要作用,且由于BasicHolder<T>泛型类有个成员变量和set方法,所以st1.set(st2); st2.set(st3);可以像链表一样,节点的后继指向一个节点,后者又可以指向另外的节点。

自限定类型的泛型类

下面是自限定类型的标准用法。

  1. class SelfBounded<T extends SelfBounded<T>> { //自限定类型的标准用法
  2. //所有
  3. T element;
  4. SelfBounded<T> set(T arg) {
  5. element = arg;
  6. return this;
  7. }
  8. T get() { return element; }
  9. }
  10. class A extends SelfBounded<A> { }
  11. public class SelfBounding {
  12. public static void main(String[] args) {
  13. A a = new A();//a变量只能与A类型变量交互,这就是自限定的妙处
  14. SelfBounded<A> b = new SelfBounded<A>();
  15. }
  16. } ///:~
  • 抛开自限定类型的知识点,观察SelfBounded泛型类,发现该泛型类的类型参数T有SelfBounded<T>的边界要求。根据上个章节的讲解,一个普通的泛型类我们都可以继承它来做到自限定,且因为要使用SelfBounded泛型类之前,我们必须有一个实际类型能符合SelfBounded<T>的边界要求,所以这里就模仿上一章,创建一个新类来符合这个边界,即class A extends SelfBounded<A> {},这样新类A便符合了SelfBounded<T>的边界。
  • 这时你觉得终于可以使用SelfBounded泛型类了,于是你便SelfBounded<A> b = new SelfBounded<A>();,但是这个b变量本身的类型是SelfBounded<A>,成员函数的形参或返回值的类型却是A,这个效果看起来不是我们想要的自限定的效果。(b变量不可以和别的SelfBounded<A>对象交互,因为它继承来的成员函数的类型限定是A,这样把别的SelfBounded<A>对象传给成员函数会造成ClassCastException,这属于父类对象传给子类引用,肯定不可以。所以说没有达到自限定。)
  • 其实,这里是我们多此一举了,新类class A extends SelfBounded<A> {}创建的时候就已经一举三得了。1.出现SelfBounded尖括号里面的A需要满足边界SelfBounded<T>,它自己的类定义已经满足了。2.给了SelfBounded泛型类的定义是为了使用它,新类A的对象也能使用到它,只不过这里是继承使用。3.根据上一章的讲解,新类A的类定义形成了自限定。
  • 可能一般我们以为要使用SelfBounded泛型类要有两步(1.创建新类型以符合边界 2.以刚创建的新类型的名字来创建SelfBounded泛型类对象),但由于class SelfBounded<T extends SelfBounded<T>>类定义中,SelfBounded作为了自己的泛型类型参数的边界,这样,想创建一个新类作为T类型参数以符合边界时,这个新类就必须继承到SelfBounded的所有成员(这也是我们想要的效果)。所以就可以class A extends SelfBounded<A> {}这样一步到位。这也解释了简介里的第一个“疑问”。

对了,对于第一个疑问,你可能想看一下,如果边界里的泛型类不是自己,会是什么情况:

  1. class testSelf<T> {
  2. //假设这里也有一些成员变量,成员方法
  3. }
  4. class SelfBounded<T extends testSelf<T>> { //类型参数的边界不是自己的名字SelfBounded
  5. T element;
  6. SelfBounded<T> set(T arg) {
  7. element = arg;
  8. return this;
  9. }
  10. T get() { return element; }
  11. }
  12. class testA extends testSelf<testA> { }//这个新类可作为SelfBounded的类型参数T,因为符合了边界
  13. public class SelfBounding {
  14. public static void main(String[] args) {
  15. SelfBounded<testA> a = new SelfBounded<testA>();
  16. }
  17. } ///:~

按照一般使用SelfBounded泛型类的两个步骤,首先需要创建新类class testA extends testSelf<testA> {}来符合边界,然后新类型作为类型参数使用来创建SelfBounded对象,即SelfBounded<testA> a = new SelfBounded<testA>()。但a变量的效果却不是我们想要的自限定的效果,总之看起来很奇怪。
一旦你把class SelfBounded<T extends testSelf<T>>的边界改成<T extends SelfBounded<T>>,那么新类testA,那么它的定义就应该是class testA extends SelfBounded<testA> {},然后正因为testA继承了SelfBounded<testA>,所以testA就获得了父类SelfBounded的成员方法且这些成员方法的形参或返回值都是testA。
通过这个反例便进一步解释了简介的第一个“疑问”。

对本章第一个例子作进一步的拓展吧:

  1. class SelfBounded<T extends SelfBounded<T>> {
  2. T element;
  3. SelfBounded<T> set(T arg) {
  4. element = arg;
  5. return this;
  6. }
  7. T get() { return element; }
  8. }
  9. class A extends SelfBounded<A> { }
  10. class B extends SelfBounded<A> { } // Also OK
  11. class C extends SelfBounded<C> {
  12. C setAndGet(C arg) { set(arg); return get(); }
  13. }
  14. class D { }
  15. // Can't do this:
  16. // class E extends SelfBounded<D> {}
  17. // Compile error: Type parameter D is not within its bound
  18. // Alas, you can do this, so you can't force the idiom:
  19. class F extends SelfBounded { }
  20. public class SelfBounding {
  21. public static void main(String[] args) {
  22. A a = new A();
  23. a.set(new A());
  24. a = a.set(new A()).get();
  25. a = a.get();//最终a是null
  26. C c = new C();
  27. c = c.setAndGet(new C());//c换成这行新new出来的C对象了
  28. }
  29. } ///:~
  • class B extends SelfBounded<A>这样使用也是可以的,毕竟继承SelfBounded时,给定的具体类型A确实满足了边界。不过B对象没有自限定的效果了。
  • class C extends SelfBounded<C>展示了:在自己新增的成员方法里,去调用继承来的成员方法。注意,继承来的方法被限定类型为C即自身了,这就是自限定的效果。
  • class E extends SelfBounded<D>无法通过编译,因为给定的具体类型A不符合边界。
  • class F extends SelfBounded,你可以继承原生类型,此时T会作为它的上限SelfBounded(边界)来执行。如下图:
    在这里插入图片描述

也可以将自限定用于泛型方法:

  1. //借用之前定义好的SelfBounded
  2. class testNoBoundary<T> { }//我自己新加的
  3. public class SelfBoundingMethods {
  4. static <T extends SelfBounded<T>> T f(T arg) {
  5. return arg.set(arg).get();
  6. }
  7. static <T extends testNoBoundary<T>> T f1(T arg) {
  8. return arg;
  9. }
  10. public static void main(String[] args) {
  11. A a = f(new A());
  12. class selfBound extends testNoBoundary<selfBound> { }
  13. selfBound b = f1(new selfBound());
  14. }
  15. } ///:~
  • f静态方法要求T自限定,且边界是SelfBounded。那么之前定义的A类型就符合要求了。
  • 我加了个f1静态方法,它也要求T自限定,且边界是testNoBoundary。注意testNoBoundary泛型类对类型参数T没有边界要求。class selfBound extends testNoBoundary<selfBound> {}这里用了局部内部类创建了一个符合边界要求的新类型。

JDK源码里自限定的应用——enum

java中使用enum关键字来创建枚举类,实际创建出来的枚举类都继承了java.lang.Enum。也正因为这样,所以enum不能再继承别的类了。其实enum就是java的一个语法糖,编译器在背后帮我们继承了java.lang.Enum。

下面就是一个枚举类的使用:

  1. public enum WeekDay {
  2. Mon("Monday"), Tue("Tuesday"), Wed("Wednesday"), Thu("Thursday"), Fri( "Friday"), Sat("Saturday"), Sun("Sunday");
  3. private final String day;
  4. private WeekDay(String day) {
  5. this.day = day;
  6. }
  7. public static void printDay(int i){
  8. switch(i){
  9. case 1: System.out.println(WeekDay.Mon); break;
  10. case 2: System.out.println(WeekDay.Tue);break;
  11. case 3: System.out.println(WeekDay.Wed);break;
  12. case 4: System.out.println(WeekDay.Thu);break;
  13. case 5: System.out.println(WeekDay.Fri);break;
  14. case 6: System.out.println(WeekDay.Sat);break;
  15. case 7: System.out.println(WeekDay.Sun);break;
  16. default:System.out.println("wrong number!");
  17. }
  18. }
  19. public String getDay() {
  20. return day;
  21. }
  22. public static void main(String[] args) {
  23. WeekDay a = WeekDay.Mon;
  24. }
  25. }

发现通过idea看WeekDay.class文件时看不出继承java.lang.Enum的。只有通过javap命令才能看出来。先看一下java.lang.Enum的定义,Enum<E extends Enum<E>>是自限定类型的标准写法:

  1. public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable { }

截取部分汇编来看:

  1. public final class WeekDay extends java.lang.Enum<WeekDay> {
  2. public static final WeekDay Mon;
  3. public static final WeekDay Tue;
  4. public static final WeekDay Wed;
  5. public static final WeekDay Thu;
  6. public static final WeekDay Fri;
  7. public static final WeekDay Sat;
  8. public static final WeekDay Sun;
  9. public static WeekDay[] values();
  10. public static WeekDay valueOf(java.lang.String);

发现确实WeekDay做到了自限定,因为继承来的成员和方法的类型都被限定成WeekDay它自己了。

分析一下java.lang.Enum这么设计的好处:

  • Enum作为一个抽象类,我们使用enum关键字创建出来的枚举类实际都是Enum的子类,因为class Enum<E extends Enum<E>>的类定义是这种标准的自限定类型,所以编译器直接生成的类必须是WeekDay extends java.lang.Enum<WeekDay>(即本文中讲的:需先创建一个符合边界条件的实际类型,但创建的同时又继承Enum本身,所以就一步到位了)。
  • 正因为编译器生成的枚举类都是Enum的子类,结合上条分析,每种Enum子类的自限定类型都是Enum子类自身。这样WeekDay的实例就只能和WeekDay的实例交互(星期几和星期几比较),Month的实例就只能和Month的实例交互(月份和月份比较)。

JDK源码里自限定的应用——Integer

Integer的类定义是:

  1. public interface Comparable<T> {
  2. public int compareTo(T o);
  3. }
  4. public final class Integer extends Number implements Comparable<Integer> { //省略}

可以看到Integer实现了Comparable<Integer>,这也是自限定,这样,从Comparable接口继承来的compareTo方法的形参类型就是Integer它自己了。和章节《普通泛型类——构成自限定》里的例子一样。
但接口Comparable的定义可没要求类型参数T必须自限定啊,它甚至连T的边界都没有,当然,这样的好处就是把决定权交给了Comparable的使用者,当使用者想要自限定时,就按照自限定的写法创建新类就好了。

发表评论

表情:
评论列表 (有 0 条评论,190人围观)

还没有评论,来说两句吧...

相关阅读