1. 程式人生 > >[Java] 淺談泛型的意義與不足

[Java] 淺談泛型的意義與不足

示意 - 泛型程式碼

// 類的泛型
class Two<A,B> {
    public A first;
    public B second;
    public Two(A a, B b) {
        first = a;
        second = b;
    }
}
class Test {
    // 方法的泛型 
    static <T> T get(T x) { return x; }
}

泛型是什麼

上面程式碼中,Two這個類使用了泛型(在類名的後面指定了<A,B>),則在例項一個Two物件時可代入兩個型別A和B。一般我們可以稱Two為一個容器。使用如下:

Two<String, Integer> a = new Two<>("Test", 2);
a.first = "modify";
System.out.print(a.first);

泛型方法的宣告,見Test類中的get方法,會根據代入的型別確定T:

System.out.println(Test.get(3)+Test.get("test..."));

有什麼用

就可以方便的代入指定型別,達到模板複用的效果。
上方對Two類的使用中,雖然在類裡first域是一個A型別,無意義。但在我呼叫a.first時,已經被IDE與編譯器視為String型別了。
效果:只要你指明瞭型別,編譯器會幫你處理好一切。
另外:
泛型類與泛型方法完全可以分開考慮,泛型類可以沒有泛型方法,泛型方法可以不在泛型類中(可參考考慮Test類)

擦除與邊界

如前面所見的Two型別,first與second貌似已經在新建中變成了String與Integer型別,在取出與存入時都可以將其視為對應型別。但實際上不是這樣的。
first與second在Two類中,是沒有其它屬性的,它們只是一個Object。
那為什麼在讀寫時我們可以使用對應的型別呢?
以下需要提兩個概念:
擦除:可以在類Two中嘗試打入a.會發現沒有什麼功能。因為編譯器在類的內部處理中,將first與second的屬性擦除了,只把它們視為Object。
邊界:編譯器唯一對泛型處理的地方是,當我們寫入如a.second = 3時,編譯器將3轉為一個Object

並賦給了a.second;當我們讀取此域如System.out.println(a.second)時,編譯器將a.second讀出後轉為了Integer型別。即是說,一切泛型的作用,都只發生在它讀取的時候

如何保持它在類中功能

這涉及到extends方法,見如下程式碼,B是A的實現類或派生類:

interface A { int get(); }
class B implements A {
    public int get() { return 1; }
}
class Test1<T extends A> {
    T x;
    Test1(T x) { this.x = x; }
    void test() { System.out.println(x.get()); }
}

public class Demo 
{
    @Test
    public void test() {
        Test1<B> t = new Test1<>(new B());
        t.test();
    }
}

使用此類寫法時,內部直接將x看作A型別了。那麼這樣有何意義呢?
我認為唯一的意義還是在於邊界處,即讀寫時,會將此成員轉為B型別。

泛型的不足

以下不足可以在面試中吹吹牛皮用:

擦除的問題

由於擦除,Java不能獲取泛型的具體資訊。這就導致以下後果:
泛型在C++中對應的功能是模板類,在模板中可以使用T x的方法,比如x.f(),然後在例項此模板時編譯器自動判斷T為是否帶有f()方法。而Java將T的一切都擦除了,導致T實際上幾乎沒有功能。雖然可以使用來補充擦除的邊界,但這樣還不如直接把T當成基類HasF來使用(即將類中的T去掉,將所有用到T的型別換成HasF),也能達到一樣的效果。因此在平時直接使用T這樣的泛型時,必須提醒自己,這是一個Object,泛型的效果僅在發生在代入與傳出值時。

不能使用一些型別

Test<T>中的T不能代入int\float等Java預設基本型別(雖然已有對應的Integer等類了)
Test<T>中的T不能是Throwable的派生類,即是代入的T不能是會丟擲異常的類。

不能同時擁有一個泛型介面的多種實現

class C<T> {}
class A implements C<Integer> {}
// error
class B extends A implements C<String> {}

這樣編譯是無法通過的,B繼承A時已經擁有了C<Integer>就無法使用的。這種限定的好壞見仁見智吧。

不能過載

如上述Two<A,B>中如果實現一個函式void f(A x)就不能再寫void f(B x)了,這不符合Java過載的機制。原因其實很好理解,因為A與B在類的內部統一被當作Object了。