泛型允許對類型進行抽象。最常見的例子是容器類型,集合的類樹中的任何壹個都是。
以下是典型用法:
list Myint list = new linked list();// 1
myIntList.add(新整數(0));// 2
Integer x =(Integer)Myint list . iterator()。next();// 3
第3行的類型轉換有點煩人。通常,程序員知道什麽樣的數據放在壹個特定的列表中。但是,這種類型轉換是必要的。
(必不可少)。編譯器只能保證叠代器返回壹個對象類型。為了確保整型變量賦值的類型安全,類型轉換是必要的。
當然,這種類型轉換不僅帶來混亂,還可能產生運行時錯誤,因為程序員可能會出錯。程序員如何清楚地表達出將列表內容限制在特定數據類型的意圖?這是泛型背後的核心思想。這是上述程序片段的通用版本:
列表& lt整數& gtmyIntList = new LinkedList & lt整數& gt();// 1
myIntList.add(新整數(0));// 2
Integer x = myIntList.iterator()。next();// 3
註意變量myIntList的類型聲明。它指定這不是壹個任意的列表,而是壹個整數列表,寫成:List
我們說List是壹個接受類型參數的通用接口。在這種情況下,類型參數是整數。在創建這個列表對象時,我們還指定了壹個類型參數。
另外需要註意的是,第3行中沒有類型轉換。現在,妳可能認為我們已經成功地消除了程序中的混亂。我們將第3行中的類型轉換替換為第1行中的類型參數。然而,這裏有壹個很大的不同。編譯器現在可以在編譯時檢查程序的正確性。當我們說myIntList被聲明為List時
2.泛型的簡單定義
以下是java.util包中列表接口和叠代器接口定義的摘錄:
公共接口列表& ltE & gt{
void add(E x);
叠代器& ltE & gt叠代器();
}
公共接口叠代器& ltE & gt{
e next();
布爾型has next();
}
這些應該是熟悉的,除了尖括號中的部分,它是接口列表和叠代器中形式類型參數的聲明。類型參數可以在整個類的聲明中使用,幾乎任何地方都可以使用其他常見類型(但請參考第7部分的壹些重要限制)。
在簡介部分,我們看到了對泛型類型聲明列表的調用,比如list
在這個調用中(通常稱為參數化類型),所有出現形式類型參數(這裏是E)的地方。
替換為實際的類型參數(在本例中為Integer)。如妳所想,列舉
公共接口集成列表{
void add(整數x)
叠代器& lt整數& gt叠代器();
}
這種直覺可能是有幫助的,但也可能導致誤解。這很有幫助,因為列表
泛型類型的聲明只編譯壹次,就獲得壹個類文件,就像普通的類或接口聲明壹樣。類型參數就像方法或構造函數中的普通參數壹樣。正如方法有形式值參數來描述它所操作的參數種類壹樣,泛型聲明也有形式類型參數。當壹個方法被調用時,實參代替形參,方法體被執行。當調用泛型聲明時,實際的類型參數將替換形式類型參數。
壹個命名習慣:我們建議您使用簡潔的名稱作為形式類型參數的名稱(如果可能的話,單個字符)。最好避免使用小寫字母,這樣很容易與其他常見的形參區分開來。許多容器類型使用e作為元素的類型,就像上面給出的例子壹樣。下面的例子中還會有壹些其他的命名習慣。
3.泛型和子類繼承
讓我們測試壹下我們對泛型的理解。下面的代碼片段合法嗎?
列表& lt字符串& gtls =新數組列表& lt字符串& gt();//1
列表& ltObject & gtlo = ls//2
1行當然合法,但這個問題的狡猾之處在於第2行。這就提出了壹個問題:字符串列表是對象列表嗎?大多數人的直覺是回答:“當然!”。好,看下面幾行:
lo . add(new Object());// 3
string s = ls . get(0);// 4:嘗試將對象賦給字符串。
這裏,我們用lo指向ls。我們通過字符串列表l0訪問ls。我們可以把任何物體插入其中。結果是保存在ls中的不再是String。當我們試圖從中提取元素時,會得到意想不到的結果。java編譯器肯定會阻止這種情況發生。第2行導致編譯錯誤。
簡而言之,如果Foo是Bar的子類型(子類或子接口), G是泛型聲明,那麽G
這可能是學習泛型最難的部分,因為這與妳的直覺相反。這種直覺的問題在於,它假設集合不會改變。我們的直覺是
這些都是無法改變的。例如,如果交通部(DMV)向人口普查局提供壹份司機名單,這似乎是合理的。我們認為,壹份名單
人口普查局可能在司機名單上增加了其他人,這破壞了交通部的記錄。為了處理這種情況,考慮壹些更靈活的泛型類型是有用的。
到目前為止,我們看到的規則是相對限制性的。
4.通配符
考慮編寫壹個例程來打印集合中的所有元素。下面是您可能用舊語言編寫的代碼:
void printCollection(集合c) {
叠代器I = c . iterator();
for(int k = 0;k & ltc . size();k++) {
system . outi . next());
}
}
下面是壹個使用泛型的幼稚嘗試(使用壹個新的循環語法):
void printCollection(集合& ltObject & gtc) {
對於(對象e : c) {
system . out . println(e);
}
}
問題是新版本遠不如舊版本有用。舊版本的代碼可以使用任何類型的集合作為參數,而新版本只能使用集合
收藏& lt?& gt(讀作“未知集合”)
也就是說,集合的元素類型可以匹配任何類型。顯然,它被稱為通配符。我們可以寫:
void printCollection(集合& lt?& gtc) {
對於(對象e : c) {
system . out . println(e);
}
}
現在,我們可以使用任何類型的集合來調用它。註意,我們仍然可以讀取C中的元素,它的類型是Object。這總是安全的,
因為不管集合的真實類型是什麽,它都包含對象。但是向它添加任何元素都不是類型安全的:
收藏& lt?& gtc =新數組列表& lt字符串& gt();
c . add(new Object());//編譯時錯誤
因為不知道C的元素類型,所以不能給它添加對象。add方法有壹個類型參數e作為集合的元素類型。我們傳遞給add的任何參數都必須是未知類型的子類。因為不知道是什麽類型,所以什麽都傳不進去。唯壹的例外是null,它是所有類型的成員。另壹方面,我們可以調用get()方法並使用它的返回值。返回值是壹個未知的類型,但是我們知道,它總是壹個對象,所以把get的返回值賦給Object類型的對象或者把它放在妳想放的任何地方都是安全的。
4.1.有界通配符
考慮壹個簡單的繪圖程序,它可以用來繪制各種形狀,如矩形和圓形。為了在程序中表示這些形狀,可以定義以下類繼承結構:
公共抽象類形狀{
公共抽象虛空畫(畫布c);
}
公共班級圈延伸形狀{
private int x,y,radius
公共空繪制(畫布c) { //...}
}
公共類矩形擴展形狀{
private int x,y,width,height
公共空繪制(畫布c) { //...}
}
這些類可以繪制在畫布上:
公共類畫布{
publicvoid draw(形狀s) {
s.draw(這個);
}
}
所有的圖形通常都有許多形狀。假設它們由壹個列表表示,那麽用壹種方法在畫布上繪制所有形狀是很方便的:
公共void draw all(List & lt;形狀& gt形狀){
對於(形狀s:形狀){
s.draw(這個);
}
}
現在,類型規則導致drawAll()只能用Shape的列表來調用。它不能,例如,列表
公共void draw all(List & lt;?擴展形狀& gt形狀){ //..}
這裏有壹個很小但很重要的區別:我們把類型列表
形狀的任何子類的列表,所以我們可以做壹個列表
列表& lt?擴展形狀& gt是受限通配符的壹個示例。這裏嗎?表示未知類型,就像我們前面看到的通配符壹樣。
但是,在這裏,我們知道這個未知類型實際上是Shape的壹個子類(可以是Shape本身,也可以是Shape的壹個子類)。我們說Shape是這個通配符的上限。像往常壹樣,使用通配符的靈活性是有代價的。代價是現在寫形狀是違法的。例如,下面的代碼是不允許的:
public void add rectangle(List & lt;?擴展形狀& gt形狀){
shapes.add(0,new Rectangle());//編譯時錯誤!
}
妳應該能夠指出為什麽上面的代碼是不允許的。因為shapes.add的第二個參數類型是?extends shape-未知形狀的子類。所以我們不知道這個類型是什麽,也不知道是不是Rectangle的父類;它可能是也可能不是父類,所以在這裏傳遞壹個。
矩形不安全。限制性通配符正是我們解決DMV向人口普查局發送列表的例子所需要的。我們的示例假設數據由從字符串到人員的映射表示(由人員或其子類表示,如Driver)。地圖& ltk,V & gt是具有兩個類型參數的泛型類型的示例,表示map的鍵和值。再次註意形式類型參數的命名約定——k代表鍵,v代表值。
公共階層普查{
公共靜態void add registry(Map & lt;字符串,?擴展人員& gt註冊表){...}
}
地圖& lt字符串,驅動程序& gt所有驅動因素=...;
census . add registry(all drivers);
5.通用方法
考慮編寫壹個方法,以壹個對象數組和壹個集合作為參數,完成將數組中的所有對象放入集合的功能。
這是第壹次嘗試:
靜態void fromArrayToCollection(Object[]a,Collection & lt?& gtc) {
對於(對象o : a) {
c . add(o);//編譯時錯誤
}
}
現在,妳應該能夠學會避免初學者試圖使用集合
收藏& lt?& gt也不行。妳不能把壹個對象放入未知類型的集合中。這個問題的解決方案是使用泛型方法。就像類型聲明壹樣,方法聲明也可以通用化——也就是說,使用壹個或多個類型參數。
靜態& ltT & gtvoid fromArrayToCollection(T[] a,Collection & ltT & gtc){
對於(時間:a) {
c . add(o);//正確
}
}
我們可以使用任何集合來調用這個方法,只要它的元素類型是數組的元素類型的父類。
Object[] oa =新對象[100];
收藏& ltObject & gtco = new ArrayList & ltObject & gt();
fromArrayToCollection(oa,co);// T表示對象
String[] sa =新字符串[100];
收藏& lt字符串& gtcs = new ArrayList & lt字符串& gt();
fromArrayToCollection(sa,cs);// T推斷為字符串
fromArrayToCollection(sa,co);// T推斷為對象
Integer[] ia =新整數[100];
Float[] fa =新的Float[100];
號碼[] na =新號碼[100];
收藏& lt編號& gtcn = new ArrayList & lt編號& gt();
fromArrayToCollection(ia,cn);// T推斷為數字
fromArrayToCollection(fa,cn);// T推斷為數字
fromArrayToCollection(na,cn);// T推斷為數字
fromArrayToCollection(na,co);// T推斷為對象
fromArrayToCollection(na,cs);//編譯時錯誤
註意,我們沒有將實際的類型參數傳遞給泛型方法。編譯器根據實參為我們推斷出類型形參的值。
它通常會推斷出能使調用類型正確的最顯式的類型參數(原文:它壹般會推斷出能使調用類型正確的最具體的類型實參。).
現在有壹個問題:我們什麽時候應該使用泛型方法,什麽時候應該使用通配符類型?要理解答案,我們先來看看收藏庫中的幾種方法。
公共接口集合& ltE & gt{
boolean containsAll(集合& lt?& gtc);
布爾addAll(集合& lt?擴展E & gtc);
}
我們也可以使用泛型方法來代替:
公共接口集合& ltE & gt{
& ltT & gtboolean containsAll(集合& ltT & gtc);
& ltt擴展E & gt布爾addAll(集合& ltT & gtc);
//嘿,類型變量也可以有界限!
}
但是,在containsAll和addAll中,類型參數t只使用壹次。返回值的類型不依賴於類型參數或
不依賴於方法的其他參數(這裏只有壹個簡單的參數)。這告訴我們類型參數被用作多態性,
它唯壹的作用是允許在不同的調用點使用多種類型的實參。如果是這種情況,應該使用通配符。通配符旨在支持靈活的子類化,這也是我們在這裏想要強調的。
泛型函數允許使用類型參數來表示方法的壹個或多個參數之間的依賴關系,或者參數與其返回值之間的依賴關系。如果沒有這種依賴性,就不應該使用泛型方法。
(原文:泛型方法允許使用類型參數來表達對壹個。
方法和/或其返回類型的壹個或多個參數。如果沒有這樣的依賴,就不應該使用泛型方法。)
也可以同時使用泛型方法和通配符。下面是Collections.copy()方法:
classCollections {
公共靜態& ltT & gt無效副本(列表& ltT & gtdest,List & lt?擴展T & gtsrc){...}
}
請註意這兩個參數類型的依賴性。從源列表中復制的任何對象必須能夠將其指定為目標列表(dest)-T類型的元素的類型。因此,源類型的元素類型可以是t的任何子類型,我們不關心具體的類型。copy方法的簽名使用壹個類型參數來指示類型依賴,但使用通配符作為第二個參數的元素類型。我們還可以用其他方式編寫這個函數的簽名,而完全不使用通配符:
類集合{
公共靜態& ltS擴展了T & gt無效副本(列表& ltT & gtdest,List & ltS & gtsrc){...}
}
這也是可以的,但是第壹個類型參數既用於dst的類型,也用於第二個參數的類型參數S的上限,S本身只使用壹次。
在src類型中——沒有其他東西依賴於它。這意味著我們可以使用通配符代替s,使用通配符比使用顯式類型參數更清晰。
如果可能的話,最好使用通配符。通配符還有壹個優點,就是可以在方法簽名之外使用,比如字段類型、局部變量和數組。這裏有壹個例子。
回到我們的繪圖問題,假設我們想要保存繪圖請求的歷史。我們可以將歷史保存在Shape類的靜態成員變量中。
調用drawAll()時,將傳入參數保存到歷史記錄中:
靜態列表& lt列表& lt?擴展形狀& gt& gthistory = new ArrayList & lt列表& lt?擴展形狀& gt& gt();
公共void draw all(List & lt;?擴展形狀& gt形狀){
history.addLast(形狀);
對於(形狀s:形狀){
s.draw(這個);
}
}
最後,我們來談談類型參數的命名約定。我們用t來表示類型,任何時候都沒有更具體的類型來區分。這在泛型方法中很常見。如果有多種類型的參數,我們可能會使用字母表中與t相鄰的字母,比如s,如果泛型類中出現泛型函數,最好避免在方法的類型參數和類的類型參數中使用相同的名稱,以免混淆。內部類也是如此。