15.4 泛型方法
到目前爲止,我們看到的泛型,都是應用於整個類上。但同樣可以在類中包含參數化方法,而這個方法所在的類可以是泛型類,也可以不是泛型類。也就是說,是否擁有泛型方法,與其所在的類是否是泛型沒有關係。
泛型方法使得該方法能夠獨立於類而產生變化。以下是一個基本的指導原則無論何時,只要你能做到,你就應該盡最使用泛型方法。也就是說,如果使用泛型方法可以取代將整個類泛型化,那麼就應該只使用泛型方法,因爲它可以使事情更清楚明白。另外,對於一個static的方法而言,無法訪向泛型類的類型參數,所以,如果static方法需要使用泛型能力,就必須使其成爲泛型方法。
要定義泛型方法,只需將泛型參數列表置於返回值之前,就像下面這樣:
public class GenericMethods {
public <T> void f(T x) {
System.out.println(x.getClass().getName());
}
public static void main(String[] args) {
GenericMethods gm = new GenericMethods();
gm.f("");
gm.f(1);
gm.f(1.0);
gm.f(1.0F);
gm.f('c');
gm.f(gm);
}
} /* Output:
java.lang.String
java.lang.Integer
java.lang.Double
java.lang.Float
java.lang.Character
GenericMethods
*///:~
GenericMethods並不是參數化的,儘管這個類和其內部的方法可以被同時參數化,但是在這個例子中,只有方法f()擁有類型參數。這是由該方法的返回類型前面的類型參數列表指明的。
注意,當使用泛型類時,必須在創建對象的時候指定類型參數的值,而使用泛型方法的時候,通常不必指明參數類型,因爲編譯器會爲我們找出具體的類型。這稱爲類型參數推斷(type argumeut inference)。因此,我們可以像調用普通方法一樣調用f(),而且就好像是f()被無限次地重載過。它甚至可以接受GenericMethods作爲其類型參數。
如果調用f()時傳人基本類型,自動打包機制就會介入其中,將基本類型的值包裝爲對應的對象。事實上,泛型方法與自動打包避免了許多以前我們不得不自己編寫出來的代碼。
15.4.1槓桿利用類型參數推斷
人們對泛型有一個抱怨,使用泛型有時候需要向程序中加人更多的代碼。如果要創建一個持有List的Map,就要像下面這樣:
Map<Person, List<? extends Pet>> petPeople =
new HashMap<Person, List<? extends Pet>>}();
(本章稍後會介紹表達式中問號與extends的用法。)看到了吧,你在重複自己做過的事情,編譯器本來應該能夠從泛型參數列表中的一個參數推斷出另一個參數。唉,可惜的是,編譯器暫時還做不到。然而,在泛型方法中,類型參數推斷可以爲我們簡化一部分工作。例如,我們可以編寫一個工目類,它包含各種各樣的static方法,專門用來創建各種常用的容器對象:
public class New {
public static <K,V> Map<K,V> map() {
return new HashMap<K,V>();
}
public static <T> List<T> list() {
return new ArrayList<T>();
}
public static <T> LinkedList<T> lList() {
return new LinkedList<T>();
}
public static <T> Set<T> set() {
return new HashSet<T>();
}
public static <T> Queue<T> queue() {
return new LinkedList<T>();
}
// Examples:
public static void main(String[] args) {
Map<String, List<String>> sls = New.map();
List<String> ls = New.list();
LinkedList<String> lls = New.lList();
Set<String> ss = New.set();
Queue<String> qs = New.queue();
}
} ///:~
main()方法演示瞭如何使用這個工具類,類型參數推斷避免了重複的泛型參數列表。它同樣可以應用於holding/MapOfList.java:
//: generics/SimplerPets.java
import typeinfo.pets.*;
import java.util.*;
import net.mindview.util.*;
public class SimplerPets {
public static void main(String[] args) {
Map<Person, List<? extends Pet>> petPeople = New.map();
// Rest of the code is the same...
}
} ///:~
對於類型參數推斷而言,這是一個有趣的例子。不過,很難說它爲我們帶來了多少好處。
類型推斷只對賦值操作有效,其他時候並不起作用。如果你將一個泛型方法調用的結果(例如New.map())作爲參數,傳遞給另一個方法,這時編譯器井不會執行類型推斷。在這種情況下,編譯器認爲:調用泛型方法後,其返回值被賦給一個Object類型的變量。下面的例子證明了這一點:
public class LimitsOfInference {
static void
f(Map<Person, List<? extends Pet>> petPeople) {}
public static void main(String[] args) {
// f(New.map()); // Does not compile
}
} ///:~
- 顯式的類型說明
在泛型方法中,可以顯式地指明類型,不過這種語法很少使用。要顯式地指明類型,必須在點操作符與方法名之間擂人尖括號,然後把A型置於尖括號內。如果是在定義該方法的類的內部,必須在點操作符之前使用this關鍵字,如果是使用static的方法,必須在點操作符之前加上類名。使用這種語法,可以解決LimitsOfInference.java中的問題:
public class ExplicitTypeSpecification {
static void f(Map<Person, List<Pet>> petPeople) {}
public static void main(String[] args) {
f(New.<Person, List<Pet>>map());
}
} ///:~
當然,這種語法抵消了New類爲我們帶來的好處(即省去了大量的類型說明),不過,只有在編寫非賦值語句時,我們才需要這樣的額外說明。
15.4.2 可變參數與泛型方法
泛型方法與可變參數列表能夠很好地共存:
public class GenericVarargs {
public static <T> List<T> makeList(T... args) {
List<T> result = new ArrayList<T>();
for(T item : args)
result.add(item);
return result;
}
public static void main(String[] args) {
List<String> ls = makeList("A");
System.out.println(ls);
ls = makeList("A", "B", "C");
System.out.println(ls);
ls = makeList("ABCDEFFHIJKLMNOPQRSTUVWXYZ".split(""));
System.out.println(ls);
}
} /* Output:
[A]
[A, B, C]
[, A, B, C, D, E, F, F, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z]
*///:~
makeList()方法展示了與標淮類庫中java.util.Arrays.asList()方法相同的功能。
15.4.3 用於Generator的泛型方法
利用生成器,我們可以很方便地填充一個Collection,而泛型化這種操作是具有實際意義的:
public class Generators {
public static <T> Collection<T>
fill(Collection<T> coll, Generator<T> gen, int n) {
for(int i = 0; i < n; i++)
coll.add(gen.next());
return coll;
}
public static void main(String[] args) {
Collection<Coffee> coffee = fill(
new ArrayList<Coffee>(), new CoffeeGenerator(), 4);
for(Coffee c : coffee)
System.out.println(c);
Collection<Integer> fnumbers = fill(
new ArrayList<Integer>(), new Fibonacci(), 12);
for(int i : fnumbers)
System.out.print(i + ", ");
}
} /* Output:
Americano 0
Latte 1
Americano 2
Mocha 3
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144,
*///:~
請注意,fill()方法是如何透明地應用於Coffee和lnteger的容器和生成器。
15.4.4 一個通用的Generator
下面的程序可以爲任何類構造一個Generator,只要該類具有默認的構造器。爲了減少類型聲明,它提供了一個泛型方法。用以生成BasicGenerator:
public class BasicGenerator<T> implements Generator<T> {
private Class<T> type;
public BasicGenerator(Class<T> type){ this.type = type; }
public T next() {
try {
// Assumes type is a public class:
return type.newInstance();
} catch(Exception e) {
throw new RuntimeException(e);
}
}
// Produce a Default generator given a type token:
public static <T> Generator<T> create(Class<T> type) {
return new BasicGenerator<T>(type);
}
}
這個類提供了一個基本實現,用以生成某個類的對象。這個類必需具備兩個特點:
(1)它必須聲明爲public,(因爲BasicGenerator與要處理的類在不同的包中,所以該類必須聲明爲public,並且不只具有包內訪問權限.)
(2)它必須具備默認的構造器(無參數的構造器)。要創建這樣的BasicGenerator對象,只需調用create()方法,並傳入想要生成的類型。泛型化的create()方法允許執行BasicGenerator.create(MyType.class),而不必執行麻煩的new BasicGenerator(MyType.class)。
例如,下面是一個具有默認構造器的簡單的類:
//: generics/CountedObject.java
public class CountedObject {
private static long counter = 0;
private final long id = counter++;
public long id() { return id; }
public String toString() { return "CountedObject " + id;}
} ///:~
使用BasicGenerator,你可以很容易地爲CountedObject創建一個Generator:
//: generics/BasicGeneratorDemo.java
import net.mindview.util.*;
public class BasicGeneratorDemo {
public static void main(String[] args) {
Generator<CountedObject> gen =
BasicGenerator.create(CountedObject.class);
for(int i = 0; i < 5; i++)
System.out.println(gen.next());
}
} /* Output:
CountedObject 0
CountedObject 1
CountedObject 2
CountedObject 3
CountedObject 4
*///:~
可以看到,使用泛型方法創建Generator對象,大大減少了我們要編寫的代碼。Java泛型要求傳人Class對象,以便也可以在create()方法中用它進行類型推斷。
CountedObject類能夠記錄下它創建了多少個CountedObject實例,井通過tostring()方法告訴我們其編號。
15.4.5 簡化元組的使用
有了類型參數推斷。再加上static方法.我們可以重新編寫之前看到的元組工具,使其成爲更通用的工具類庫。在這個類中,我們通過重載static方法創建元組:
//: net/mindview/util/Tuple.java
// Tuple library using type argument inference.
package net.mindview.util;
public class Tuple {
public static <A,B> TwoTuple<A,B> tuple(A a, B b) {
return new TwoTuple<A,B>(a, b);
}
public static <A,B,C> ThreeTuple<A,B,C>
tuple(A a, B b, C c) {
return new ThreeTuple<A,B,C>(a, b, c);
}
public static <A,B,C,D> FourTuple<A,B,C,D>
tuple(A a, B b, C c, D d) {
return new FourTuple<A,B,C,D>(a, b, c, d);
}
public static <A,B,C,D,E>
FiveTuple<A,B,C,D,E> tuple(A a, B b, C c, D d, E e) {
return new FiveTuple<A,B,C,D,E>(a, b, c, d, e);
}
} ///:~
下面是修改後的TupleTest.java,用來測試Tuple.java:
//: generics/TupleTest2.java
import net.mindview.util.*;
import static net.mindview.util.Tuple.*;
public class TupleTest2 {
static TwoTuple<String,Integer> f() {
return tuple("hi", 47);
}
static TwoTuple f2() { return tuple("hi", 47); }
static ThreeTuple<Amphibian,String,Integer> g() {
return tuple(new Amphibian(), "hi", 47);
}
static
FourTuple<Vehicle,Amphibian,String,Integer> h() {
return tuple(new Vehicle(), new Amphibian(), "hi", 47);
}
static
FiveTuple<Vehicle,Amphibian,String,Integer,Double> k() {
return tuple(new Vehicle(), new Amphibian(),
"hi", 47, 11.1);
}
public static void main(String[] args) {
TwoTuple<String,Integer> ttsi = f();
System.out.println(ttsi);
System.out.println(f2());
System.out.println(g());
System.out.println(h());
System.out.println(k());
}
} /* Output: (80% match)
(hi, 47)
(hi, 47)
(Amphibian@7d772e, hi, 47)
(Vehicle@757aef, Amphibian@d9f9c3, hi, 47)
(Vehicle@1a46e30, Amphibian@3e25a5, hi, 47, 11.1)
*///:~
注意,方法f()返回一個參數化的TwoTuple對象,而f2()返回的是非參數化的TwoTuple對象。在這個例子中,編譯器井沒有關於f2()的警告信息,因爲我們井沒有將其返回值作爲參數化對象使用。在某種意義上,它被“向上轉型’爲一個非參數化的TwoTuple.然而,如果試圖將f2()的返回值轉型爲參數化的TwoTuple,編譯器就會發出警告。
15.4.6 一個Set實用工具
作爲泛型方法的另一個示例,我們看看如何用Set來表達數學中的關係式。通過使用泛型方法,可以很方便地做到這一點,而且可以應用於多種類型:
//: net/mindview/util/Sets.java
package net.mindview.util;
import java.util.*;
public class Sets {
public static <T> Set<T> union(Set<T> a, Set<T> b) {
Set<T> result = new HashSet<T>(a);
result.addAll(b);
return result;
}
public static <T>
Set<T> intersection(Set<T> a, Set<T> b) {
Set<T> result = new HashSet<T>(a);
result.retainAll(b);
return result;
}
// Subtract subset from superset:
public static <T> Set<T>
difference(Set<T> superset, Set<T> subset) {
Set<T> result = new HashSet<T>(superset);
result.removeAll(subset);
return result;
}
// Reflexive--everything not in the intersection:
public static <T> Set<T> complement(Set<T> a, Set<T> b) {
return difference(union(a, b), intersection(a, b));
}
} ///:~
在前三個方法中,都將第一個參數Set複製了一份,將Set中的所有引用都存人一個新的HashSet對象中,因此,我們並未直接修改參數中的Set,返回的值是一個全新的set對象。
這四個方法表達瞭如下的數學集合操作:union()返回一個Set,它將兩個參數合併在一起;intersection()返回的Set只包含兩個參數共有的部分。difference()方法從superset中移除subset包含的元素;complement()返回的Set包含除了交集之外的所有元素。下面提供了一個enum,它包
含各種水彩畫的顏色。我們將用它來演示以上這些方法的功能和效果。
//: generics/watercolors/Watercolors.java
package generics.watercolors;
public enum Watercolors {
ZINC, LEMON_YELLOW, MEDIUM_YELLOW, DEEP_YELLOW, ORANGE,
BRILLIANT_RED, CRIMSON, MAGENTA, ROSE_MADDER, VIOLET,
CERULEAN_BLUE_HUE, PHTHALO_BLUE, ULTRAMARINE,
COBALT_BLUE_HUE, PERMANENT_GREEN, VIRIDIAN_HUE,
SAP_GREEN, YELLOW_OCHRE, BURNT_SIENNA, RAW_UMBER,
BURNT_UMBER, PAYNES_GRAY, IVORY_BLACK
} ///:~
爲了方便起見(可以直接使用enum中的元素名),下面的示例以static的方式引人Watercolors,這是Java SE5中的新工具,用來從enum直接創建Set。在這裏,我們向static方法EnumSet.range()傳入某個範圍的第一個元素與最後一個元素,然後它將返回一個Set,其中包含該範圍內的所有元素:
//: generics/WatercolorSets.java
import generics.watercolors.*;
import java.util.*;
import static net.mindview.util.Print.*;
import static net.mindview.util.Sets.*;
import static generics.watercolors.Watercolors.*;
public class WatercolorSets {
public static void main(String[] args) {
Set<Watercolors> set1 =
EnumSet.range(BRILLIANT_RED, VIRIDIAN_HUE);
Set<Watercolors> set2 =
EnumSet.range(CERULEAN_BLUE_HUE, BURNT_UMBER);
print("set1: " + set1);
print("set2: " + set2);
print("union(set1, set2): " + union(set1, set2));
Set<Watercolors> subset = intersection(set1, set2);
print("intersection(set1, set2): " + subset);
print("difference(set1, subset): " +
difference(set1, subset));
print("difference(set2, subset): " +
difference(set2, subset));
print("complement(set1, set2): " +
complement(set1, set2));
}
} /* Output: (Sample)
set1: [BRILLIANT_RED, CRIMSON, MAGENTA, ROSE_MADDER, VIOLET, CERULEAN_BLUE_HUE, PHTHALO_BLUE, ULTRAMARINE, COBALT_BLUE_HUE, PERMANENT_GREEN, VIRIDIAN_HUE]
set2: [CERULEAN_BLUE_HUE, PHTHALO_BLUE, ULTRAMARINE, COBALT_BLUE_HUE, PERMANENT_GREEN, VIRIDIAN_HUE, SAP_GREEN, YELLOW_OCHRE, BURNT_SIENNA, RAW_UMBER, BURNT_UMBER]
union(set1, set2): [SAP_GREEN, ROSE_MADDER, YELLOW_OCHRE, PERMANENT_GREEN, BURNT_UMBER, COBALT_BLUE_HUE, VIOLET, BRILLIANT_RED, RAW_UMBER, ULTRAMARINE, BURNT_SIENNA, CRIMSON, CERULEAN_BLUE_HUE, PHTHALO_BLUE, MAGENTA, VIRIDIAN_HUE]
intersection(set1, set2): [ULTRAMARINE, PERMANENT_GREEN, COBALT_BLUE_HUE, PHTHALO_BLUE, CERULEAN_BLUE_HUE, VIRIDIAN_HUE]
difference(set1, subset): [ROSE_MADDER, CRIMSON, VIOLET, MAGENTA, BRILLIANT_RED]
difference(set2, subset): [RAW_UMBER, SAP_GREEN, YELLOW_OCHRE, BURNT_SIENNA, BURNT_UMBER]
complement(set1, set2): [SAP_GREEN, ROSE_MADDER, YELLOW_OCHRE, BURNT_UMBER, VIOLET, BRILLIANT_RED, RAW_UMBER, BURNT_SIENNA, CRIMSON, MAGENTA]
*///:~
我們可以從輸出中看到各種關係運算的結果。
下面的示例使用Sets.difference()打印出java.util包中各種Collection類與Map類之間的方法差異:
//: net/mindview/util/ContainerMethodDifferences.java
package net.mindview.util;
import java.lang.reflect.*;
import java.util.*;
public class ContainerMethodDifferences {
static Set<String> methodSet(Class<?> type) {
Set<String> result = new TreeSet<String>();
for(Method m : type.getMethods())
result.add(m.getName());
return result;
}
static void interfaces(Class<?> type) {
System.out.print("Interfaces in " +
type.getSimpleName() + ": ");
List<String> result = new ArrayList<String>();
for(Class<?> c : type.getInterfaces())
result.add(c.getSimpleName());
System.out.println(result);
}
static Set<String> object = methodSet(Object.class);
static { object.add("clone"); }
static void
difference(Class<?> superset, Class<?> subset) {
System.out.print(superset.getSimpleName() +
" extends " + subset.getSimpleName() + ", adds: ");
Set<String> comp = Sets.difference(
methodSet(superset), methodSet(subset));
comp.removeAll(object); // Don't show 'Object' methods
System.out.println(comp);
interfaces(superset);
}
public static void main(String[] args) {
System.out.println("Collection: " +
methodSet(Collection.class));
interfaces(Collection.class);
difference(Set.class, Collection.class);
difference(HashSet.class, Set.class);
difference(LinkedHashSet.class, HashSet.class);
difference(TreeSet.class, Set.class);
difference(List.class, Collection.class);
difference(ArrayList.class, List.class);
difference(LinkedList.class, List.class);
difference(Queue.class, Collection.class);
difference(PriorityQueue.class, Queue.class);
System.out.println("Map: " + methodSet(Map.class));
difference(HashMap.class, Map.class);
difference(LinkedHashMap.class, HashMap.class);
difference(SortedMap.class, Map.class);
difference(TreeMap.class, Map.class);
}
} ///:~
在第11章的“總結”中,我們使用了這個程序的輸出結果。