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的使用者,當使用者想要自限定時,就按照自限定的寫法創建新類就好了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM