Java 學習之路 之 泛型方法


前面介紹了在定義類、接口時可以使用類型形參,在該類的方法定義和 Field 定義、接口的方法定義中,這些類型形參可被當成普通類型來用。在另外一些情況下,我們定義類、接口時沒有使用類型形參,但定義方法時想自己定義類型形參,這也是可以的,Java 5 還提供了對泛型方法的支持。

1,定義泛型方法

假設需要實現這樣一個方法----該方法負責將一個 Object 數組的所有元素添加到一個 Collection 集合中。考慮采用如下代碼來實現該方法。

static void fromArrayToCollection(Object a, Collection<Object> c){ for (Object o : a){ c.add(o); } }

上 面定義的方法沒有任何問題,關鍵在於方法中的 c 形參,它的數據類型是 Collection<Object>。正如前面所介紹的,Collection<String> 不是 Collection<Object> 的子類型——所以這個方法的功能非常有限,它只能將 Object 數組的元素復制到 Object(Object的子類不行)Collection 集合中,即下面代碼將引起編譯錯誤。

String strArr = {"a", "b"}; List<String> strList = new ArrayList<>; // Collection<String> 對象不能當成 Collection<Object> 使用,下面代碼出現編譯錯誤 fromArrayToCollection(strArr, strList);

可見上面方法的參數類型不可以使用 Collection<String>,那使用通配符 Collection<?>是否可行呢?顯然也不行,我們不能把對象放進一個未知類型的集合中。

為了解決這個問題,可以使用 Java 5 提供的泛型方法(Generic Method)。所謂泛型方法,就是在聲明方法時定義一個或多個類型形參。泛型方法的用法格式如下:

修飾符 <T , S> 返回值類型 方法名(形參列表) { // 方法體... }

把上面方法的格式和普通方法的格式進行對比,不難發現泛型方法的方法簽名比普通方法的方法簽名多了類型形參聲明,類型形參聲明以尖括號括起來,多個類型形參之間以逗號隔開,所有的類型形參聲明放在方法修飾符和方法返回值類型之間。

采用支持泛型的方法,就可以將上面的 fromArrayToCollection 方法改為如下形式:

static <T> void fromArrayToCollection(T a, Collection<T> c){ for(T o: a){ c.add(o); } }

下面程序示范了完整的用法。

package com.sym.demo4; import java.util.ArrayList; import java.util.Collection; public class GenericMethodTest { // 聲明一個泛型方法,該泛型方法中帶一個 T 類型形參 static <T> void fromArrayToCollection(T a, Collection<T> c){ for(T o: a){ c.add(o); } } public static void main(String args) { Object oa = new Object[100]; Collection<Object> co = new ArrayList<>; // 下面代碼中 T 代表 Object 類型 fromArrayToCollection(oa, co); String sa = new String[100]; Collection<String> cs = new ArrayList<>; // 下面代碼中 T 代表 String 類型 fromArrayToCollection(sa, cs); // 下面代碼中 T 代表 Object 類型 fromArrayToCollection(sa, co); Integer ia = new Integer[100]; Float fa = new Float[100]; Number na = new Number[100]; Collection<Number> cn = new ArrayList<>; // 下面代碼中 T 代表 Number 類型 fromArrayToCollection(ia, cn); // 下面代碼中 T 代表 Number 類型 fromArrayToCollection(fa, cn); // 下面代碼中 T 代表 Number 類型 fromArrayToCollection(na, cn); // 下面代碼中 T 代表 Object 類型 fromArrayToCollection(na, co); // 下面代碼中 T 代表 String 類型,但 na 是一個 Number 數組 // 因為 Number 既不是 String 類型,也不是它的子類 // 所以出現編譯錯誤 //fromArrayToCollection(na, cs); } }

上面程序定義了一個泛型方法,該泛型方法中定義了一個 T 類型形參,這個 T 類型形參就可以在該方法內當成普通類型使用。與接口、類聲明中定義的類型形參不同的是,方法聲明中定義的形參只能在該方法里使用,而接口、類聲明中定義的類型形參則可以在整個接口、類中使用。

與 類、接口中使用泛型參數不同的是,方法中的泛型參數無須顯式傳入實際類型參數,如上面程序所示,當程序調用 fromArrayToCollection 方法時,無須在調用該方法前傳入 String、Object 等類型,但系統依然可以知道類型形參的數據類型,因為編譯器根據實參推斷類型形參的值,它通常推斷出最直接的類型參數。例如,下面調用代碼:

fromArrayToCollection(sa, cs);

上 面代碼中 cs 是一個 Collection<String> 類型,與方法定義時的 fromArrayToCollection(T a, Collection<T> c) 進行比較——只比較泛型參數,不難發現該T類型形參代表的實際類型是 String 類型。

對於如下調用代碼:

fromArrayToCollection(ia, cn);

上面的 cn 是 Collection<Number> 類型,與此方法的方法簽名進行比較——只比較泛型參數,不難發現該 T 類型形參代表了 Number 類型。

為了讓編譯器能准確地推斷出泛型方法中類型形參的類型,不要制造迷惑!系統一旦迷惑了,就是你錯了!看如下程序。

package com.sym.demo4; import java.util.ArrayList; import java.util.Collection; import java.util.List; public class ErrorTest { // 聲明一個泛型方法,該泛型方法中帶一個 T 類型形參 static <T> void test(Collection<T> from, Collection<T> to){ for(T ele : from){ to.add(ele); } } public static void main(String args) { List<Object> as = new ArrayList<>; List<String> ao = new ArrayList<>; // 下面代碼將引起編譯錯誤 test(as, ao); } }

上 面程序中定義了 test 方法,該方法用於將前一個集合里的元素復制到下一個集合中,該方法中的兩個形參 from、to 的類型都是 Collection<T>,這要求調用該方法時的兩個集合實參中的泛型類型相同,否則編譯器無法准確地推斷出泛型方法中類型形參的類型。

上 面程序中調用 test 方法傳入了兩個實際參數,其中 as 的數據類型是 List<String>,而 ao 的數據類型是 List<Object>,與泛型方法簽名進行對比:test(Collection<T>a, Collection<T> c).編譯器無法正確識別 T 所代表的實際類型。為了避免這種錯誤,可以將該方法改為如下形式:

package com.sym.demo4; import java.util.ArrayList; import java.util.Collection; import java.util.List; public class RightTest { // 聲明一個泛型方法,該泛型方法中帶一個 T 類型形參 static <T> void test(Collection<? extends T> from, Collection<T> to){ for(T ele : from){ to.add(ele); } } public static void main(String args) { List<Object> ao = new ArrayList<>; List<String> as = new ArrayList<>; // 下面代碼完全正常 test(as, ao); } }

上 面代碼改變了 test 方法簽名,將該方法的前一個形參類型改為 Collection<? extends T>,這種采用類型通配符的表示方式,只要 test 方法的前一個 Collection 集合里的元素類型是后一個 Collection 集合里元素類型的子類即可。

那么這里產生了一個問題:到底何時使用泛型方法?何時使用類型通配符呢?下面將具體介紹泛型方法和類型通配符的區別。

2,泛型方法和類型通配符的區別

大多數時候都可以使用泛型方法來代替類型通配符。例如,對於 Java 的 Collection 接口中兩個方法定義:

public interface Collection<E>{ boolean containAll(Collection<?> c); boolean addAll(Collection<? extends E> c); ... }

上面集合中兩個方法的形參都采用了類型通配符的形式,也可以采用泛型方法的形式,如下所示。

public interface Collection<E>{ boolean <T> containAll(Collection<T> c); boolean <T extends E> addAll(Collection<T> c); ... }

上面方法使用了 <T extends E> 泛型形式,這時定義類型形參時設定上限(其中 E 是 Collection 接口里定義的類型形參,在該接口里E可當成普通類型使用)。

上面兩個方法中類型形參 T 只使用了一次,類型形參 T 產生的唯一效果是可以在不同的調用點傳入不同的實際類型。對於這種情況,應該使用通配符:通配符就是被設計用來支持靈活的子類化的。

泛型方法允許類型形參被用來表示方法的一個或多個參數之間的類型依賴關系,或者方法返回值與參數之間的類型依賴關系。如果沒有這樣的類型依賴關系,就不應該使用泛型方法。

如 果某個方法中一個形參(a)的類型或返回值的類型依賴於另一個形參(b)的類型,則形參(b)的類型聲明不應該使用通配符----因為形參(a)或返回值 的類型依賴於該形參(b)的類型,如果形參(b)的類型無法確定,程序就無法定義形參(a)的類型。在這種情況下,只能考慮使用在方法簽名中聲明類型形參 ——也就是泛型方法。

如果有需要,我們可以同時使用泛型方法和通配符,如 Java 的 Collections.copy方法。

public class Collections{ public static <T> void copy(List<T> dest, List<? extends T> src){...} ... }

上面 copy 方法中的 dest 和 src 存在明顯的依賴關系,從源 List 中復制出來的元素,必須可以“丟進”目標 List 中,所以源 List 集合元素的類型只能是目標集合元素的類型的子類型或者它本身。但 JDK 定義 src 形參類型時使用的是類型通配符,而不是泛型方法。這是因為:該方法無須向 src 集合中添加元素,也無須修改 src 集合里的元素,所以可以使用類型通配符,不使用泛型方法。

當然,也可以將上面的方法簽名改為使用泛型方法,不使用類型通配符,如下所示。

class collections{ public static <T, S extends T> void copy(List<T> dest, List<S> scr){...} ... }

這 個方法簽名可以代替前面的方法簽名。但注意上面類型形參 S 它僅使甩了一次,沒有其他參數的類型、方法返回值的類犁依賴於它,那類型形參 S 就沒有存在的必要,即可以用通配符來代替 S 。使用通配符比使用泛型方法(在方法簽名中顯式聲明類型形參)更加清晰和准確,因此 Java 設計該方法時采用了通配符,而不是泛型方法。

類型通配符與泛型方法(在方法簽名中顯式聲明類型形參)還有一個顯著的區別:類型通配符既可以在方法簽名中定義形參的類型,也可以用於定義變量的類型;但泛型方法中的類型形參必須在對應方法中顯式聲明。

3,Java 7 的“菱形”語法與泛型構造器

正如泛型方法允許在方法簽名中聲明類型形參一樣,Java 也允許在構造器簽名中聲明類型形參,這樣就產生了所渭的泛型構造器。

一旦定義了泛型構造器,接下來在調用構造器時,就不僅可以讓 Java 根據數據參數的類型來“推斷”類型形參的類型,而且程序員也可以顯式地為構造器中的類型形參指定實際的類型。如下程序所示。

package com.sym.demo4; class Foo{ public <T> Foo(T t){ System.out.println(t); } } public class GenericConstructor { public static void main(String args) { // 泛型構造器中的 T 參數為 String new Foo("瘋狂 Java 講義"); // 泛型構造器中的 T 參數為 Integer new Foo(200); // 顯式指定泛型構造器中的 T 參數為 String // 傳給 Foo 構造器的實參也是 String 對象,完全正確 new <String> Foo("瘋狂 Android 講義");//1 // 顯式指定泛型構造器中的 T 參數為 String // 傳給 Foo 構造器的實參也是 Double 對象,下面代碼出錯 new <String> Foo(12.3);//2 } }

上 面程序中①號代碼不僅顯式指定了泛型構造器中的類型形參 T 的類型應該是 String,而且程序傳給該構造器的參數值也是 String 類型,因此程序完至正常。但在②號代碼處,程序顯式指定了泛型構造器中的類型形參 T 的類型應該是 String,但實際傳給該構造器的參數值是 Double 類型,因此這行代碼,將會出現錯誤。

前面介紹過 Java 7 新增的“菱形”語法,它允許調用構造器時在構造器后使用一對尖括號來代表泛型信息。但如果程序顯式指定了泛型構造器中聲明的類型形參的實際類型,則不可以使用“菱形”語法。如下程序所示。

package com.sym.demo4; class MyClass<E>{ public <T> MyClass(T t){ System.out.println("t 參數的值為:" + t); } } public class GenericDiamondTest { public static void main(String args) { // MyClass 類聲明中的 E 形參是 String 類型 // 泛型構造器中聲明的 T 形參是 Integer 類型 MyClass<String> mc1 = new MyClass<>(5); // 顯式指定泛型構造器中聲明的 T 形參是 Integer 類型 MyClass<String> mc2 = new <Integer> MyClass<String>(5); // MyClass 類聲明中的 E形參是 String 類型 // 如果顯式指定泛型構造器中聲明的 T 形參是 Integer 類型 // 此時就不能使用“菱形”語法,下面代碼是錯誤的 //MyClass<String> mc3 = new <Integer> MyClass<>(5); } }

上面程序中最后一行代碼既指定了泛型構造器中的類型形參是 Integer 類型,又想使用“菱形”語法,所以這行代碼無法通過編譯。

4,設定通配符下限

假 設自己實現一個工具方法:實現將 src 集合里的元素復制到 dest 集合里的功能,因為 dest 集合可以保存 src 集合里的所有元素,所以 dest 集合元素的類型應該是 src 集合元素類型的父類。為了表示兩個參數之間的類型依賴,考慮同時使用通配符、泛型參數來實現該方法。代碼如下:

public static <T> void copy(Collection<T> dest, Collection<? extends T> src){ for( T ele : src){ dest.add(ele); } }

上面方法實現了前面的功能。現在假設該方法需要一個返回值,返回最后一個被復制的元素,則可以把上面方法改為如下形式:

public static <T> T copy(Collection<T> dest, Collection<? extends T> src){ T last = null; for (T ele : src){ last = ele; dest.add(ele); } return last; }

表面上看起來,上面方法實現了這個功能,實際上有一個問題:當遍歷 src 集合的元素時,src 元素的類型是不確定的(但可以肯定它是 T 的子類),程序只能用 T 來籠統地表示各種 src 集合的元素類型。例如如下代碼:

List<Number> ln = new ArrayList<>; List<Integer> li = new ArrayList<>; // 下面代碼引起編譯錯誤 Integer last = copy(ln, li);

上 面代碼中 ln 的類型是 List<Number>,與 copy 方法簽名的形參類型進行對比即得到 T 的實際類型是 Number,而不是 Integer 類型——即 copy 方法的返回值也是 Number 類型,而不是 Integer 類型,但實際上是后一個復制元素的元素類型一定是 Integer。也就是說,程序在復制集合元素的過程中,丟失了 src 集合元素的類型。

對 於上面的 copy 方法,可以這樣理解兩個集合參數之間的依賴關系:不管 src 集合元素的類型是什么,只要 dest 集合元素的類型與前者相同或是前者的父類即可。為了表達這種約束關系,Java 允許設定通配符的下限:<? super Type>,這個通配符表示它必須是 Type 本身,或是 Type 的父類。下面程序采用設定通配符下限的方式改寫了前面的 copy 方法。

package com.sym.demo4; import java.util.ArrayList; import java.util.Collection; import java.util.List; public class MyUtils { // 下面 dest 集合元素的類型必須與 src 集合元素的類型相同,或是其父類 public static <T> T copy(Collection<? super T> dest, Collection<T> src){ T last = null; for (T ele : src){ last = ele; dest.add(ele); } return last; } public static void main(String args) { List<Number> ln = new ArrayList<>; List<Integer> li = new ArrayList<>; li.add(5); // 此處可准確地知道最后一個被復制的元素是 Integer 類型 // 與 src 集合元素的類型相同 Integer last = copy(ln, li);//1 System.out.println(ln); } }

使用這種語句,就可以保證程序的①處調用后推斷出晟后一個被復制的元素類型是 Integer,而不是籠統的 Number 類型。

實際上,Java 集合框架中的 TreeSet<E> 自一個構造器也用到了這種設定通配符下限的語法,如下所示。

// 下面的 E 是定義 TreeSet 類時的類型形參 TreeSet(Comparator<? super E> c);

正 如前章所介紹的,TreeSet 會對集合中的元素按自然順序或定制順序進行排序。如果需要 TreeSet 對集合中的所有元索進行定制排序,則要求 TreeSet 對象有一個與之關聯的 Comparator 對象。上面構造器中的參數 c 就是進行定制排序的 Comparator 對象。

Comparator接U也是個帶泛型聲明的接口:

public interface Comparator<T>{ int compare(T fst, T snd); }

通 過這種帶下限的通配符的語法,可以在創建 TreeSet 對象時靈活地選擇合適的 Comparator。假定需要創建一個 TreeSet<String> 集台,並傳人一個可以比較 String 大小的Comparator,這個 Comparator 既可以是 Comparator<String>,也可以是 Comparator<Object> ----只要尖括號里傳人的類型是 String 的父類型(或它本身)即可。如下程程序所示。

package com.sym.demo4; import java.util.Comparator; import java.util.TreeSet; public class TreeSetTest { public static void main(String args) { // Comparator 的實際類型是 TreeSet 里實際類型的父類,滿足要求 TreeSet<String> ts1 = new TreeSet<>( new Comparator<Object> { public int compare(Object fst, Object snd){ return hashCode > snd.hashCode ? 1 : hashCode < snd.hashCode ? -1 : 0; } } ); ts1.add("hello"); ts1.add("wa"); TreeSet<String> ts2 = new TreeSet<>( new Comparator<String> { @Override public int compare(String first, String second) { return first.length > second.length ? -1 : first.length < second.length ? 1 : 0; } } ); ts2.add("hello"); ts2.add("wa"); System.out.println(ts1); System.out.println(ts2); } }

通過使用這種通配符下限的方式來定義 TreeSet 構造器的參數,就可以將所有可用的 Comparator 作為參數傳入,從而增加了程序的靈活性。當然,不僅 TreeSet 有這種用法,TreeMap 也有類似的用法,具體請查閱 Java 的 API 文檔。

5,泛型方法與方法重載

因為泛型既允許設定通配符的上限,也允許設定通配符的下限,從而允許在一個類里包含如下兩個方法定義。

public class MyUtils{ public static <T> void copy(Collection<T> dest , Collection<? extends T> src) {...}//1 public static <T> copy(Collection<? super T> dest, Collection<T> src) {...}//2 }

上 面的 MyUtils 類中包含兩個 copy 方法,這兩個方法的參數列表存在一定的區別,但這種區別不是很明確:這兩個方法的兩個參數都是 Collection 對象,前一個集合里的集合元素類型是后一個集合里集合元素類型的父類。如果這個類僅包含這兩個方法不會有任何錯誤,但只要調用這個方法就會引起編譯錯誤。 例如,對於如下代碼:

List<Number> ln = new ArrayList<>; List<Integer> li = new ArrayList<>; copy(ln , li);

上 面程序中粗體字部分調用 copy 方法,但這個 copy 方法既可以匹配①號 copy 方法,此時 T 類型參數的類型是 Number:也可以匹配②號 copy 方法,此時 T 參數的類型是Integer。編譯器無法確定這行代碼想調用哪個 copy 方法,所以這行代碼將引起編譯錯誤。


免責聲明!

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



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