泛型的好處
使用泛型的好處我覺得有兩點: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> 的子類,想想上面的例子。
通常在兩種情況下會使用無限定通配符:
如果正在編寫一個方法,可以使用Object類中提供的功能來實現(xiàn)
代碼實現(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ā)過程中體會到泛型的一個好處,類似的例子還有很多。