Java泛型

泛型的好處

使用泛型的好處我覺得有兩點:1:類型安全 2:減少類型強轉(zhuǎn)

下面通過一個例子說明:

假設(shè)有一個Test類,通用的實現(xiàn)是:

class Test {
    private Object o;

    public Test(Object o) {
        this.o = o;
    }

    public Object getObject() {
        return o;
    }

    public void setObject(Object o) {
        this.o = o;
    }
}

我們可以這樣使用它:

public static void main(String[] args) {
    Test test = new Test(new Integer(1));
    //編譯時不報錯
    //運行時報  java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
    String o = (String) test.getObject();
}

看一個使用泛型的例子:

class Test<T> {
    private T o;

    public Test(T o) {
        this.o = o;
    }

    public T getObject() {
        return o;
    }

    public void setObject(T o) {
        this.o = o;
    }
}

public static void main(String[] args) {
    Test1<Integer> test = new Test1<Integer>(new Integer(1));
    //編譯時報錯,無法通過編譯
    //String o = test.getObject();

    //正常運行
    Integer o = test.getObject();
}

從上面的對比中能夠看出兩點:

1.使用泛型之后在編譯時報錯而非運行時,減少了出錯的幾率;

2.使用泛型之后編譯器不再要求強轉(zhuǎn)

定義泛型

泛型的機制能夠在定義類、接口、方法時把類型參數(shù)化,也就是類似于方法的形參一樣,把類型當做參數(shù)使用。

泛型參數(shù)部分使用<>包裹起來,比如<T>,T聲明了一種類型,習慣上,這個類型參數(shù)使用單個大寫字母來表示,指示所定義的參數(shù)類型。有如下慣例:

E:表示元素

T:表示類型

K:表示鍵

V:表示值

N:表示數(shù)字

定義泛型類

上面的例子就是一個很好的演示

class Test<T> {
    private T o;

    public Test(T o) {
        this.o = o;
    }

    public T getObject() {
        return o;
    }

    public void setObject(T o) {
        this.o = o;
    }
}

使用泛型定義類之后,泛型參數(shù) T 可以運用到該類中:可以聲明成員變量類型、可以聲明成員函數(shù)返回值類型、可以聲明成員函數(shù)參數(shù)類型。

要注意:泛型參數(shù)T不能用于聲明靜態(tài)變量,同時也不能用于new一個對象比如:T o = new T();

下面的類型擦除會說到原因。

定義泛型接口

interface Test<T> {
    public T test(T t);
}

使用泛型定義接口之后,泛型參數(shù) T 可以運用到該接口中:可以聲明接口函數(shù)返回值類型、可以聲明接口函數(shù)參數(shù)類型。

定義泛型方法

可以單獨給方法使用泛型,而不是泛化整個類:

public static <T> T getT(T t){
    return t;
}

使用泛型定義方法后,泛型參數(shù) T 可以聲明該方法的返回值類型、可以聲明該方法的參數(shù)類型。

要注意,定義方法所用的泛型參數(shù)需要在修飾符之后添加。

定義多個泛型參數(shù)

以接口為例:

interface Test<T, S> {
    public T testT(T t);
    public S testS(S s);
}

public static void main(String[] args) {
    //編譯時報錯
    //getT("s");
}

多個泛型參數(shù)在尖括號中使用逗號隔開。類的泛化與方法的泛化類似。

泛型參數(shù)的界限

定義泛型參數(shù)界限有這樣兩種意義:

1.有時候我們希望限定這個泛型參數(shù)的類型為某個類的子類或者超類;

2.上面的例子中可以看到,我們定義了泛型參數(shù),向方法中傳入某種類型,這種類型是未知的,因此我們無法使用這種類型定義的變量,不能夠調(diào)用它的方法。

public static <T extends Number> Integer getT(T t) {
    return  new Integer(t.intValue());
}

上面例子中,<T extends A>表示T是A或A的子類,他限定了傳入泛型方法參數(shù)的類型必須為A或A的子類,同時,在方法體中我們也可以使用t這個實參就像使用A的實例一樣,調(diào)用Number具有的public方法。

除了<T extends A>限定T是A或A的子類外,還可以使用<T super A>這種方式來限定T是A或A的超類。

A可以是某個類或者接口。

除此以外,還可以為泛型參數(shù)限定多個限制范圍,如<T extends A & B & C>,限定范圍中最多只能有一個類(某個類只能有一個父類~~),并且他必須是限定列表中的第一個。

Class A { // }
interface B { // }
interface C { // }

//正確
class D <T extends A & B & C> { // }
//編譯時報錯
class D <T extends A & B & C> { // }

泛型的繼承

看一下jdk中List的泛型繼承例子:

public interface List<E> extends Collection<E>{//...}

List<String> 就是 Collection<String> 的子類。

假如定義自己的接口:

interface MyList<E,P> extends List<E> {
  void setPay(E e,P p);
  //...
}

MyList<String,String>、MyList<String,Integer>、MyList<String,Exception>都是List<String>的子類。

使用泛型

上面的例子中已經(jīng)列舉了一些使用泛化類或者泛化函數(shù)的例子,但是還有一些問題需要指出:

1.泛型參數(shù)只接受引用類型,不適用于基本類型

比如:

class Test<T> {}

public static void main(String[] args) {
    //無法通過編譯,不接受int類型的泛化參數(shù)
    //Test<int> test = new Test();
}

而我們使用泛化函數(shù)時:

public static void main(String[] args) {
    getT(1);
}
public static <T> void getT(T t) {
}

是沒有問題的,通過查看生成的字節(jié)碼,發(fā)現(xiàn)getT(1)這個方法的字節(jié)碼中1被自動裝箱為Integer類型。

2.通配符的使用

考慮下面的情況:

class Test<T> {}
public static void getT(Test<Number> t) {
}

public static void main(String[] args) {
    Test<Double> test = new Test();
    //編譯時報錯
    //getT(test);
}

報錯的原因很好理解,雖然Double是Number的子類,但Test<Double>并不是Test<Number>的子類,故類型檢查無法通過。這一點一定要明白。

那么如果我們確實想要傳入一個Test<Double>類型的形參呢?可以使用通配符:

class Test<T> {}
public static void main(String[] args) {
        Test<Double> test = new Test();
        //正常運行
        getT(test);
}
public static void getT(Test<? extends Number> t) {
}

Test<? extends Number>擴展了形參的類型,可以是Test<Double>Test<Integer>等,尖括號中的類型必須是Number或繼承于Number。同樣的,通配符也適用于super,如Test<? super A>

如果類型參數(shù)中既沒有extends 關(guān)鍵字,也沒有super關(guān)鍵字,只有一個?,代表無限定通配符。

Test<?>Test<Object>并不相同,無論T是什么類型,Test<T>Test<?>的子類,但是,Test<T> 不是 Test<Object> 的子類,想想上面的例子。

通常在兩種情況下會使用無限定通配符:

  1. 如果正在編寫一個方法,可以使用Object類中提供的功能來實現(xiàn)

  2. 代碼實現(xiàn)的功能與類型參數(shù)無關(guān),比如List.clear()與List.size()方法,還有經(jīng)常使用的Class<?>方法,其實現(xiàn)的功能都與類型參數(shù)無關(guān)。

一般情況下,通配符<? extends Number>只是出現(xiàn)在使用泛型的時候,而不是定義泛型的時候,就像上面的例子那樣。而<T extends Number>這種形式出現(xiàn)在定義泛型的時候,而不是使用泛型的時候,不要搞混了。

結(jié)合泛型的繼承和通配符的使用,理解一下泛型的類型系統(tǒng),也就是泛型類的繼承關(guān)系:

以下內(nèi)容來自:Java深度歷險(五)——Java泛型


引入泛型之后的類型系統(tǒng)增加了兩個維度:一個是類型參數(shù)自身的繼承體系結(jié)構(gòu),另外一個是泛型類或接口自身的繼承體系結(jié)構(gòu)。第一個指的是對于 List<String>List<Object>這樣的情況,類型參數(shù)String是繼承自O(shè)bject的。而第二種指的是 List接口繼承自Collection接口。對于這個類型系統(tǒng),有如下的一些規(guī)則:

相同類型參數(shù)的泛型類的關(guān)系取決于泛型類自身的繼承體系結(jié)構(gòu)。即List<String>Collection<String> 的子類型,List<String>可以替換Collection<String>。這種情況也適用于帶有上下界的類型聲明。
當泛型類的類型聲明中使用了通配符的時候, 其子類型可以在兩個維度上分別展開。如對Collection<? extends Number>來說,其子類型可以在Collection這個維度上展開,即List<? extends Number>Set<? extends Number>等;也可以在Number這個層次上展開,即Collection<Double>Collection<Integer>等。如此循環(huán)下去,ArrayList<Long>HashSet<Double>等也都算是Collection<? extends Number>的子類型。
如果泛型類中包含多個類型參數(shù),則對于每個類型參數(shù)分別應(yīng)用上面的規(guī)則。


關(guān)于通配符的理解可以參考Java 泛型 <? super T> 中 super 怎么 理解?與 extends 有何不同?

類型擦除

類型擦除發(fā)生在編譯階段,對于運行期的JVM來說,List<int>List<String>就是同一個類,因為在編譯結(jié)束之后,生成的字節(jié)碼文件中,他們都是List類型。

1.java編譯器會在編譯前進行類型檢查

java編譯器承擔了所有泛型的類型安全檢查工作。

2.類型擦除后保留的原始類型

原始類型(raw type)就是擦除去了泛型信息,最后在字節(jié)碼中的類型變量的真正類型。無論何時定義一個泛型類型,相應(yīng)的原始類型都會被自動地提供。類型變量被擦除(crased),并使用其限定類型(無限定的變量用Object)替換。

3.自動類型轉(zhuǎn)換

因為類型擦除的問題,所以所有的泛型類型變量最后都會被替換為原始類型。這樣就引起了一個問題,既然都被替換為原始類型,那么為什么我們在獲取的時候,不需要進行強制類型轉(zhuǎn)換呢?

比如:

public class Test {  
    public static void main(String[] args) {  
        ArrayList<Date> list=new ArrayList<Date>();  
        list.add(new Date());  
        Date myDate=list.get(0);  
    }
}  

Date myDate=list.get(0);這里我們并沒有對其返回值進行強轉(zhuǎn)就可以直接獲取Date類型的返回值。原因在于在字節(jié)碼當中,有checkcast這么一個操作幫助我們進行了強轉(zhuǎn),這是java自動進行的。

更多的關(guān)于類型擦除的知識,參考 java泛型(二)、泛型的內(nèi)部原理:類型擦除以及類型擦除帶來的問題

實踐

最近在重構(gòu)公眾號服務(wù)器的過程中,用到了泛型編程的知識。

public interface BaseServiceContext<T extends ReqBaseMessage, R> {
    public void selectService(T reqMeg);

    public R executeRequest();
}

上面是一個選擇service的上下文接口,接收到用戶請求后通過這個接口選擇對應(yīng)的service并且執(zhí)行service。這個接口相當于一個工廠和策略模式的結(jié)合體。下面是這個接口的一種實現(xiàn):

//請求為文本類型,返回string類型的處理結(jié)果
public class TextServiceContext implements BaseServiceContext<ReqTextMessage,String> {

    @Override
    public void selectService(ReqTextMessage reqMeg) {
        //.....
    }

    @Override
    public String executeRequest() {
        //.....
    }
}

可以看到,BaseServiceContext<ReqTextMessage,String>限定了selectService方法的參數(shù)類型和executeRequest方法的返回值類型,使其能夠靈活的支持各種類型的參數(shù)和返回值。

看一下在沒有學習泛型之前,這個接口是怎么實現(xiàn)的:

public interface BaseServiceContext {
    public void selectService(ReqBaseMessage reqMeg);

    public Object executeRequest();
}

public class TextServiceContext implements BaseServiceContext {

    @Override
    public void selectService(ReqBaseMessage reqMeg) {
        //根據(jù)業(yè)務(wù)邏輯對reqMeg進行強轉(zhuǎn),需要程序員自己判斷
        //很有可能強轉(zhuǎn)失敗
    }

    @Override
    public Object executeRequest() {
        //返回類型為object,在調(diào)用方法的外部強轉(zhuǎn)為需要的類型
        //很有可能強轉(zhuǎn)失敗
    }
}

可以看到?jīng)]有使用泛型接口的情況下,類型不安全且增大了強轉(zhuǎn)失敗的風險。同時也需要程序員根據(jù)業(yè)務(wù)邏輯去判斷該強轉(zhuǎn)成什么類型。使用泛型接口之后就沒有了這些問題,只需要在使用接口時聲明好他的泛型參數(shù)就o了。

上面只是我在開發(fā)過程中體會到泛型的一個好處,類似的例子還有很多。

注意事項

參考

java 泛型編程(一)

泛型:工作原理及其重要性

Java深度歷險(五)——Java泛型

java泛型詳解

Java 泛型 <? super T> 中 super 怎么 理解?與 extends 有何不同?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 開發(fā)人員在使用泛型的時候,很容易根據(jù)自己的直覺而犯一些錯誤。比如一個方法如果接收List作為形式參數(shù),那么如果嘗試...
    時待吾閱讀 1,122評論 0 3
  • 為什么需要泛型? 通過泛型可以定義類型安全的數(shù)據(jù)結(jié)構(gòu),而無須使用實際的數(shù)據(jù)類型(可擴展)。這能夠顯著提高性能并得到...
    一只好奇的茂閱讀 1,349評論 2 39
  • 我們知道,使用變量之前要定義,定義一個變量時必須要指明它的數(shù)據(jù)類型,什么樣的數(shù)據(jù)類型賦給什么樣的值。 假如我們現(xiàn)在...
    今晚打肉山閱讀 1,213評論 0 1
  • 一、泛型簡介1.引入泛型的目的 了解引入泛型的動機,就先從語法糖開始了解。 語法糖 語法糖(Syntactic S...
    Android進階與總結(jié)閱讀 1,073評論 0 9
  • 大人有吃夜宵的習慣,但是我們都明白吃夜宵對身體危害很大,孩子更是要從小培養(yǎng)好的飲食和生活習慣。 有的孩子睡覺晚,肚...
    米妮老師說兒推閱讀 454評論 0 0

友情鏈接更多精彩內(nèi)容