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

简介

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

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

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

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

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(), st2 = new Subtype(), st3 = new Subtype();
        st1.set(st2);
        st2.set(st3);
        Subtype st4 = st1.get().get();
        st1.f();
    }
} /* 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);可以像链表一样,节点的后继指向一个节点,后者又可以指向另外的节点。

自限定类型的泛型类

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

class SelfBounded<T extends SelfBounded<T>> {//自限定类型的标准用法
    //所有
    T element;
    SelfBounded<T> set(T arg) {
        element = arg;
        return this;
    }
    T get() { return element; }
}

class A extends SelfBounded<A> {}

public class SelfBounding {
    public static void main(String[] args) {
        A a = new A();//a变量只能与A类型变量交互,这就是自限定的妙处
        SelfBounded<A> b =  new SelfBounded<A>();
    }
} ///:~
  • 抛开自限定类型的知识点,观察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> {}这样一步到位。这也解释了简介里的第一个“疑问”。

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

class testSelf<T> {
    //假设这里也有一些成员变量,成员方法
}

class SelfBounded<T extends testSelf<T>> {//类型参数的边界不是自己的名字SelfBounded
    T element;
    SelfBounded<T> set(T arg) {
        element = arg;
        return this;
    }
    T get() { return element; }
}

class testA extends testSelf<testA> {}//这个新类可作为SelfBounded的类型参数T,因为符合了边界

public class SelfBounding {
    public static void main(String[] args) {
        SelfBounded<testA> a = new SelfBounded<testA>();
    }
} ///:~

按照一般使用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。
通过这个反例便进一步解释了简介的第一个“疑问”。

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

class SelfBounded<T extends SelfBounded<T>> {
    T element;
    SelfBounded<T> set(T arg) {
        element = arg;
        return this;
    }
    T get() { return element; }
}

class A extends SelfBounded<A> {}
class B extends SelfBounded<A> {} // Also OK

class C extends SelfBounded<C> {
    C setAndGet(C arg) { set(arg); return get(); }
}

class D {}
// Can't do this:
// class E extends SelfBounded<D> {}
// Compile error: Type parameter D is not within its bound

// Alas, you can do this, so you can't force the idiom:
class F extends SelfBounded {}

public class SelfBounding {
    public static void main(String[] args) {
        A a = new A();
        a.set(new A());
        a = a.set(new A()).get();
        a = a.get();//最终a是null
        C c = new C();
        c = c.setAndGet(new C());//c换成这行新new出来的C对象了
    }
} ///:~
  • 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(边界)来执行。如下图:
    在这里插入图片描述

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

//借用之前定义好的SelfBounded
class testNoBoundary<T> {}//我自己新加的

public class SelfBoundingMethods {
    static <T extends SelfBounded<T>> T f(T arg) {
        return arg.set(arg).get();
    }

    static <T extends testNoBoundary<T>> T f1(T arg) {
        return arg;
    }
    public static void main(String[] args) {
        A a = f(new A());

        class selfBound extends testNoBoundary<selfBound> {}
        selfBound b = f1(new selfBound());
    }
} ///:~
  • 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。

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

public enum WeekDay {
    Mon("Monday"), Tue("Tuesday"), Wed("Wednesday"), Thu("Thursday"), Fri( "Friday"), Sat("Saturday"), Sun("Sunday");
    private final String day;
    private WeekDay(String day) {
        this.day = day;
    }
    public static void printDay(int i){
        switch(i){
            case 1: System.out.println(WeekDay.Mon); break;
            case 2: System.out.println(WeekDay.Tue);break;
            case 3: System.out.println(WeekDay.Wed);break;
            case 4: System.out.println(WeekDay.Thu);break;
            case 5: System.out.println(WeekDay.Fri);break;
            case 6: System.out.println(WeekDay.Sat);break;
            case 7: System.out.println(WeekDay.Sun);break;
            default:System.out.println("wrong number!");
        }
    }
    public String getDay() {
        return day;
    }
    public static void main(String[] args) {
        WeekDay a = WeekDay.Mon;
    }
}

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

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

截取部分汇编来看:

public final class WeekDay extends java.lang.Enum<WeekDay> {
  public static final WeekDay Mon;

  public static final WeekDay Tue;

  public static final WeekDay Wed;

  public static final WeekDay Thu;

  public static final WeekDay Fri;

  public static final WeekDay Sat;

  public static final WeekDay Sun;

  public static WeekDay[] values();
  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的类定义是:

public interface Comparable<T> {
    public int compareTo(T o);
}

public final class Integer extends Number implements Comparable<Integer> {//省略}

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

posted @ 2019-10-13 11:34  allMayMight  阅读(1343)  评论(0编辑  收藏  举报