第15章 泛型
一般的類和方法。只能使用具體的類型:要麼是基本類型。要麼是自定義的類。如果要編寫可以應用於多種類型的代碼。這種刻板的限制對代碼的束縛就會很大。
在面向對象編程語言中,多態算是一種泛化機制。例如,你可以將方法的參數類型設爲基類,那麼該方法就可以接受從這個基類中導出的任何類作爲參數。這樣的方法更加通用一些,可應用的地方也多一些。在類的內部也是如此,凡是需要說明類型的地方,如果都使用基類,確實能夠良備更好的靈活性。但是,考慮到除了final類不能擴展,其他任何類都可以被擴展,所以這種靈活性大多數時候也會有一些性能損耗。
有時候,拘泥於單繼承體系,也會使程序受限太多。如果方法的參數是一個接口,而不是一個類,這種限制就放鬆了許多。因爲任何實現了該接口的類都能夠滿足該方法,這也包括暫時還不存在的類。這就給予客戶端程序員一種選擇,他可以通過實現一個接口來滿足類或方法。因此,接口允許我們快捷地實現類繼承,也使我們有機會創建一個新類來做到這一點。
可是有的時候,即便使用了接口,對程序的約束也還是太強了。因爲一旦指明瞭接口,它就要求你的代碼必須使用特定的接口。而我們希望達到的目的是編寫更通用的代碼,要使代碼能夠應用於“某種不具體的類型”,而不是一個具體的接口或類。
這就是Java SE5的重大變化之一:泛型的概念。泛型實現了參數化類型的概念,使代碼可以應用於多種類型。“泛型“這個術語的意思是:“適用於許多許多的類型’。泛型在編程語言中出現時,其最初的目的是希望類或方法能夠具備最廣泛的表達能力。如何做到這一點呢,正是通過解耦類或方法與所使用的類型之間的約束。稍後你將看到,Java中的泛型並沒有這麼高的追求,實際上,你可能會質疑,Java中的術語“泛型”是否適合用來描述這一功能。
如果你從未接觸過參數化類型機制,那麼,在學習了Java中的泛型之後,你會發現,對這門語言而言,泛型確實是一個很有益的補充。在你創建參數化類型的一個實例時,編譯器會爲你負責轉型操作,並且保證類型的正確性。這應該是一個進步。
然而,如果你瞭解其他語言(例如C++)中的參數化類型機制,你就會發現。有些以前能做到的事情,使用Java的泛型機制卻無法做到。使用別人已經構建好的泛型類型會相當容易,但是如果你要自己創建一個泛型實例,就會遇到許多令你喫驚的事情。在本章中,我的任務之一就是向你解釋,Java中的泛型是怎樣發展成現在這樣的。
這並非是說Java的泛型毫無用處。在很多情況下,它們可以使代碼更直接更優雅。不過,如果你具備其他語言的經驗,而那種語言實現了更純粹的泛型,那麼,Java可能令你失望了。在本章中,我們會介紹Java泛型的優點與侷限,希望這能夠幫助你更有效地使用Java的這個新功能。
15.1 與C++的比較
Java的設計者曾說過,設計這門語言的靈感主要來自C++。儘管如此,學習Java時,基本上可以不用參考C++。我也是盡力這樣做的,除非,與C++的比較能夠加深你的理解。
Java中的泛型就需要與C++進行一番比較,理由有二:
- 首先,丁解C++模板的某些方面,有助於你理解泛型的基礎。同時,非常重要的一點是,你可以瞭解Java泛型的侷限是什麼,以及爲什麼會有這些限制。最終的目的是幫助你理解,Java泛型的邊界在哪裏。理解了邊界所在,你才能成爲程序高手。因爲只有知道了某個技術不能做到什麼,你才能更好地做到所能做的(部分原因是,不必浪費時間在死衚衕裏亂轉)。
- 第二個原因是,在Java社區中,人們普遍時C++摸板有一種誤解,而這種誤解可能會誤導你令你在理解泛型的意圖時產生偏差。
因此,在本章中會介紹一些C++模板的例子,不過我也會盡量控制它們的篇幅。
15.2 簡單泛型
有許多原因促成了泛型的出現,而最引人往目的一個原因,就是爲了創造容器類。容器,就是存放要使用的對象的地方.數組也是如此,不過與簡單的數組相比,容器類更加靈活,具備更多不同的功能。事實上,所有的程序,在運行時都要求你持有一大堆對象,所以,容器類算得上最具重用性的類庫之一。
我們先來看看一個只能持有單個對象的類。當然了,這個類可以明確指定其持有的對象的類型:
//: generics/Holder1.java
class Automobile {}
public class Holder1 {
private Automobile a;
public Holder1(Automobile a) { this.a = a; }
Automobile get() { return a; }
} ///:~
不過,這個類的可重用性就不怎麼樣了,它無法特有其他類型的任何對象。我們可不希望爲碰到的每個類型都編寫一個新的類。
在Javaa SE5之前,我們可以讓這個類直接持有Object類型的對象:
//: generics/Holder2.java
public class Holder2 {
private Object a;
public Holder2(Object a) { this.a = a; }
public void set(Object a) { this.a = a; }
public Object get() { return a; }
public static void main(String[] args) {
Holder2 h2 = new Holder2(new Automobile());
Automobile a = (Automobile)h2.get();
h2.set("Not an Automobile");
String s = (String)h2.get();
h2.set(1); // Autoboxes to Integer
Integer x = (Integer)h2.get();
}
} ///:~
現在,Holder2可以存儲任何類型的對象,在這個例子中,只用了一個Hlolder2對象,卻先後三次存儲了三種不同類型的對象。
有些情況下,我們確實希望容器能夠同時持有多種類型的對象。但是,通常而言,我們只會使用容器來存儲一種類型的對象。泛型的主要目的之一就是用來指定容器要持有什麼類型的對象。而且由編譯器來保證類型的正確性。
因此,與其使用Object,我們更喜歡暫時不指定類型,而是稍後再決定具體使用什麼類型。要達到這個目的,需要使用類型參數,用尖括號括住,放在類名後面。然後在使用這個類的時候,再用實際的類型替換此類型參數。在下面的例子中,T就是類型參數:
//: generics/Holder3.java
public class Holder3<T> {
private T a;
public Holder3(T a) { this.a = a; }
public void set(T a) { this.a = a; }
public T get() { return a; }
public static void main(String[] args) {
Holder3<Automobile> h3 =
new Holder3<Automobile>(new Automobile());
Automobile a = h3.get(); // No cast needed
// h3.set("Not an Automobile"); // Error
// h3.set(1); // Error
}
} ///:~
現在,當你創建Holder3對象時,必須指明想持有什麼類型的對象,將其置於尖括號內。就像main()中那樣。然後,你就只能在Holder3中存入該類型(或其子類,因爲多態與泛型不衝突)的對象了。並且,在你從Holder3中取出它持有的對象時,自動地就是正確的類型。
這就是Java泛型的核心概念:告訴編譯器想使用什麼類型,然後編譯器幫你處理一切細節。
一般而言,你可以認爲泛型與其他的類型差不多,只不過它們碰巧有類型參數罷了。稍後我們會看到,在使用泛型時,我們只需指定它們的名稱以及類型參數列表即可。
15.2.1 一個元組類庫
僅一次方法調用就能返回多個對象,你應該經常需要這樣的功能吧。可是return語句只允許返回單個對象,因此,解決辦法就是創建一個對象,用它來持有想要返回的多個對象。當然,可以在每次需要的時候,專門創建一個類來完成這樣的工作。可是有了泛型。我們就能夠一次性地解決該問題,以後再也不用在這個問題上浪費時間了。同時,我們在編譯期就能確保類型安全。
這個概念稱爲元組(tuple),它是將一組對象直接打包存儲幹其中的一個單一對象。這個容器對象允許談取其中元素,但是不允許向其中存放新的對象。(這個概念也稱爲數據傳這對象或信使。)
通常,元組可以具有任意長度,同時,元組中的對象可以是任意不同的類型。不過,我們希望能夠爲每一個對象指明其類型,並且從容器中讀取出來時,能夠得到正確的類型。要處理不同長度的問題,我們需要創建多個不同的元組。下面的程序是一個2維元組,它能夠持有兩個對象:
//: net/mindview/util/TwoTuple.java
package net.mindview.util;
public class TwoTuple<A,B> {
public final A first;
public final B second;
public TwoTuple(A a, B b) { first = a; second = b; }
public String toString() {
return "(" + first + ", " + second + ")";
}
} ///:~
構造器捕獲了要存儲的對象,而toString()是一個便利函數,用來顯示列表中的值。注意,元組隱含地保持了其中元素的次序。
第一次閱讀上面的代碼時,你也許會想,這不是違反了Java編程的安全性原則嗎?first和second應該聲明爲private,然後提供getFirst()和getSecond()之類的訪問方法纔對呀?讓我們仔細看看這個例子中的安全性:客戶端程序可以讀取first和second對象,然後可以隨心所欲地使用這兩個對象。但是,它們卻無法將其他值賦予first或second。因爲final聲明爲你買了相同的安全保險,而且這種格式更簡潔明瞭。
還有另一種設計考慮,即你確實希望允許客戶端程序員改變first辦second所引用的對象。然而,採用以上的形式無疑是更安全的做法,這樣的話,如果程序員想要使用具有不同元素的元組,就強制要求他們另外創建一個新的TwoTuple對象。
我們可以利用繼承機制實現長度更長的元組。從下面的例子中可以看到,增加類型參數是件很簡單的事情:
//: net/mindview/util/ThreeTuple.java
package net.mindview.util;
public class ThreeTuple<A,B,C> extends TwoTuple<A,B> {
public final C third;
public ThreeTuple(A a, B b, C c) {
super(a, b);
third = c;
}
public String toString() {
return "(" + first + ", " + second + ", " + third +")";
}
} ///:~
爲了使用元組,你只需定義一個長度適合的元組,將其作爲方法的返回值,然後在return語句中創建該元組,井返回即可。
//: generics/TupleTest.java
import net.mindview.util.*;
class Amphibian {}
class Vehicle {}
public class TupleTest {
static TwoTuple<String,Integer> f() {
// Autoboxing converts the int to Integer:
return new TwoTuple<String,Integer>("hi", 47);
}
static ThreeTuple<Amphibian,String,Integer> g() {
return new ThreeTuple<Amphibian, String, Integer>(
new Amphibian(), "hi", 47);
}
static
FourTuple<Vehicle,Amphibian,String,Integer> h() {
return
new FourTuple<Vehicle,Amphibian,String,Integer>(
new Vehicle(), new Amphibian(), "hi", 47);
}
static
FiveTuple<Vehicle,Amphibian,String,Integer,Double> k() {
return new
FiveTuple<Vehicle,Amphibian,String,Integer,Double>(
new Vehicle(), new Amphibian(), "hi", 47, 11.1);
}
public static void main(String[] args) {
TwoTuple<String,Integer> ttsi = f();
System.out.println(ttsi);
// ttsi.first = "there"; // Compile error: final
System.out.println(g());
System.out.println(h());
System.out.println(k());
}
} /* Output: (80% match)
(hi, 47)
(Amphibian@1f6a7b9, hi, 47)
(Vehicle@35ce36, Amphibian@757aef, hi, 47)
(Vehicle@9cab16, Amphibian@1a46e30, hi, 47, 11.1)
*///:~
由於有了泛型,你可以很容易地創建元組,令其返回一組任意類型的對象。而你所要做的,只是編寫表達式而已。
通過ttsi.first=“there”語句的錯誤,我們可以看出,final明確實能夠保護public元素,在對象被構造出來之後,聲明爲final的元素便不能被再賦予其他值了。
在上面的程序中,new表達式確實有點羅嗦。本章稍後會介紹,如何利用泛型方法簡化這樣的表達式。
15.2.2 一個堆棧類
接下來我們看一個稍微複雜一點的例子:傳統的下推堆棧。在第11章中,我們看到,這個堆棧是作爲net.mindview.util.Stack類,用一個LinkedList實現的。在那個例子中,LinkedList本身已經具備了創建堆棧所必需的方法,而Stack也可以通過兩個泛型的類Stack<T>
和LinkedList<T>
的組合來創建。在那個示例中,我們可以看出,泛型類型也就是另一種類型罷了(稍候我們會一些例外的情況)。
現在我們不用LinkedList,來實現自己的內部鏈式存儲機制:
public class LinkedStack<T> {
private static class Node<U> {
U item;
Node<U> next;
Node() { item = null; next = null; }
Node(U item, Node<U> next) {
this.item = item;
this.next = next;
}
boolean end() { return item == null && next == null; }
}
private Node<T> top = new Node<T>(); // End sentinel
public void push(T item) {
top = new Node<T>(item, top);
}
public T pop() {
T result = top.item;
if(!top.end())
top = top.next;
return result;
}
public static void main(String[] args) {
LinkedStack<String> lss = new LinkedStack<String>();
for(String s : "Phasers on stun!".split(" "))
lss.push(s);
String s;
while((s = lss.pop()) != null)
System.out.println(s);
}
}
/* Output:
stun!
on
Phasers
*/
內部類Node也是一個泛型,它擁有自己的類型參數。
這個例子使用了一個末端哨兵(end sentinel)來判斷堆棧何時爲空。這個末端哨兵是在構造LinkedStack時創建的。然後,每調用一次push()方法,就會創建一個
Node<T>
對象,並將其鏈接到前一個Node<T>
對象。當你調用pop()方法時,總是返top.item,然後丟棄當前top所指的Node<T>
,並將top轉移到下一個Node<T>
,除非你已經碰到了末端哨兵,這時候就不再移動top了。如果已經到了末端,客戶端程序還繼續調用pop()方法,它只能得到null,說明堆棧已經空了。
15.2.3 RandomList
作爲容器的另一個例子,假設我們需要一個持有特定類型對象的列表,每次調用其上的select()方法時,它可以隨機地選取一個元素。如果我們希望以此構建一個可以應用幹各種類型的對象的工具,就需要使用泛型:
//: generics/RandomList.java
import java.util.*;
public class RandomList<T> {
private ArrayList<T> storage = new ArrayList<T>();
private Random rand = new Random(47);
public void add(T item) { storage.add(item); }
public T select() {
return storage.get(rand.nextInt(storage.size()));
}
public static void main(String[] args) {
RandomList<String> rs = new RandomList<String>();
for(String s: ("The quick brown fox jumped over " +
"the lazy brown dog").split(" "))
rs.add(s);
for(int i = 0; i < 11; i++)
System.out.print(rs.select() + " ");
}
} /* Output:
brown over fox quick quick dog brown The brown lazy brown
*///:~
15.3 泛型接口
泛型也可以應用於接口。例如生成器(generator),這是一種專門負責創建對象的類。實際上,這是工廠方法設計模式的一種應用。不過,當使用生成器創建新的對象時,它不需要任何參數,而工廠方法一般需要參數。也鼓是說,生成器無需額外的信息就知道如何創建新對象。
一般而言,一個生成器只定義一個方法,該方法用以產生新的對象.在這裏,就是next方法。
// A generic interface.
public interface Generator<T> { T next(); }
方法next()的返回類型是參數化的T。正如你所見到的,接口使用泛型與類使用泛型沒什麼區別。
爲了演示如何實現Generator接口,我們還需要一些別的類。例如,Coffee類層次結構如下:
public class Coffee {
private static long counter = 0;
private final long id = counter++;
public String toString() {
return getClass().getSimpleName() + " " + id;
}
}
public class Latte extends Coffee {} // 拿鐵
public class Mocha extends Coffee {} // 摩卡
public class Cappuccino extends Coffee {} // 卡布奇諾
public class Americano extends Coffee {} // 美式咖啡
public class Breve extends Coffee {}
現在,我們可以編寫一個類,實現Generator<Coffee>
接口,它能夠隨機生成不同類型的Coffee對象。
public class CoffeeGenerator implements Generator<Coffee>, Iterable<Coffee> {
private Class[] types = { Latte.class, Mocha.class,
Cappuccino.class, Americano.class, Breve.class, };
private static Random rand = new Random(47);
public CoffeeGenerator() {}
// For iteration:
private int size = 0;
public CoffeeGenerator(int sz) { size = sz; }
public Coffee next() {
try {
return (Coffee)
types[rand.nextInt(types.length)].newInstance();
// Report programmer errors at run time:
} catch(Exception e) {
throw new RuntimeException(e);
}
}
class CoffeeIterator implements Iterator<Coffee> {
int count = size;
public boolean hasNext() { return count > 0; }
public Coffee next() {
count--;
return CoffeeGenerator.this.next();
}
public void remove() { // Not implemented
throw new UnsupportedOperationException();
}
};
public Iterator<Coffee> iterator() {
return new CoffeeIterator();
}
public static void main(String[] args) {
CoffeeGenerator gen = new CoffeeGenerator();
for(int i = 0; i < 5; i++)
System.out.println(gen.next());
for(Coffee c : new CoffeeGenerator(5))
System.out.println(c);
}
} /* Output:
Americano 0
Latte 1
Americano 2
Mocha 3
Mocha 4
Breve 5
Americano 6
Latte 7
Cappuccino 8
Cappuccino 9
*///:~
參數化的Generator接口確保next()的返回值是參數的類型。CofeeGenerator同時還實現了Iterable接口,所以它可以在循環語句中使用。不過,它還需要一個“末端哨兵”來判斷何時停止,這正是第二個構造器的功能。
下面的類是Generetore<T>
接口的另一個實現,它負責生成Fibonacci(斐波那契)數列:
public class Fibonacci implements Generator<Integer> {
private int count = 0;
public Integer next() { return fib(count++); }
private int fib(int n) {
if(n < 2) return 1;
return fib(n-2) + fib(n-1);
}
public static void main(String[] args) {
Fibonacci gen = new Fibonacci();
for(int i = 0; i < 18; i++)
System.out.print(gen.next() + " ");
}
}
/* Output:
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584
*///:~
雖然我們在Fibonacci類的裏裏外外使用的都是int類型,但是其類型參數卻是Integer,這個例子引出了Java泛型的一個侷限性:基本類型無法作爲類型參數。不過,Java 5具備了自動打包和自動拆包的功能,可以很方便地在基本類型和其相應的包裝器類型之間進行轉換。通過這個例子中Fibonacci類對int的使用,我們已經看到了這種效果。
如果還想更進一步,編寫一個實現了Iterable的Fibonacci生成器。我們的一個選擇是重寫這個類,令其實現Iterable接口。不過,你並不是總能擁有源代碼的控制權,井且,除非必須這麼做,否則,我們也不願意重寫一個類。而且我們還有另一種選擇,就是創建個過配器(adapter)來實現所需的接口,我們在前面介紹過這個設計模式。
有多種方法可以實現適配器。例如,可以通過繼承來創建適配器類:
public class IterableFibonacci extends Fibonacci implements Iterable<Integer> {
private int n;
public IterableFibonacci(int count) { n = count; }
public Iterator<Integer> iterator() {
return new Iterator<Integer>() {
public boolean hasNext() { return n > 0; }
public Integer next() {
n--;
return IterableFibonacci.this.next();
}
public void remove() { // Not implemented
throw new UnsupportedOperationException();
}
};
}
public static void main(String[] args) {
for(int i : new IterableFibonacci(18))
System.out.print(i + " ");
}
}
/* Output:
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584
*///:~
如果要在循環語句中使用IterableFibonacci,必須向IterableFibonacci的構造器提供一個邊界值,然後hasNext()方法才能知道何時應該返回false。
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章的“總結”中,我們使用了這個程序的輸出結果。
15.5匿名內部類
泛型還可以應用於內部類以及匿名內部類,下面的示例使用匿名內部類實現了Generator接口。
//: generics/BankTeller.java
// A very simple bank teller simulation.
import java.util.*;
import net.mindview.util.*;
class Customer {
private static long counter = 1;
private final long id = counter++;
private Customer() {}
public String toString() { return "Customer " + id; }
// A method to produce Generator objects:
public static Generator<Customer> generator() {
return new Generator<Customer>() {
public Customer next() { return new Customer(); }
};
}
}
class Teller {
private static long counter = 1;
private final long id = counter++;
private Teller() {}
public String toString() { return "Teller " + id; }
// A single Generator object:
public static Generator<Teller> generator =
new Generator<Teller>() {
public Teller next() { return new Teller(); }
};
}
public class BankTeller {
public static void serve(Teller t, Customer c) {
System.out.println(t + " serves " + c);
}
public static void main(String[] args) {
Random rand = new Random(47);
Queue<Customer> line = new LinkedList<Customer>();
Generators.fill(line, Customer.generator(), 15);
List<Teller> tellers = new ArrayList<Teller>();
Generators.fill(tellers, Teller.generator, 4);
for(Customer c : line)
serve(tellers.get(rand.nextInt(tellers.size())), c);
}
} /* Output:
Teller 3 serves Customer 1
Teller 2 serves Customer 2
Teller 3 serves Customer 3
Teller 1 serves Customer 4
Teller 1 serves Customer 5
Teller 3 serves Customer 6
Teller 1 serves Customer 7
Teller 2 serves Customer 8
Teller 3 serves Customer 9
Teller 3 serves Customer 10
Teller 2 serves Customer 11
Teller 4 serves Customer 12
Teller 2 serves Customer 13
Teller 1 serves Customer 14
Teller 1 serves Customer 15
*///:~
Customer和Teller類都只有private的構造器,這可以強制你必須使用Generator對象。Customer有一個generator()方法,每次執行它都會生成一個新的Generate對象。我們其實不需要多個Generator對象,Teller就只創建了一個public的generator時象。在main()方法中可以看到,這兩種創建Generator的方式都在fill()中用到了。
由於Customer中的generator()方法,以及Teller中的Generator對象都聲明成了static的,所以它們無法作爲接口的一部分,因此無法用接口這種特定的慣用法來泛化這二者。儘管如此,它們在fill()方法中都工作得很好。
在第21章中,我們還會看到關幹這個排隊問題的另一個版本。
泛型的一個重要好處是能夠簡單而安全地創建複雜的模型。例如,我們可以很容易地創建List元組:
//: generics/TupleList.java
// Combining generic types to make complex generic types.
import java.util.*;
import net.mindview.util.*;
public class TupleList<A,B,C,D>
extends ArrayList<FourTuple<A,B,C,D>> {
public static void main(String[] args) {
TupleList<Vehicle, Amphibian, String, Integer> tl =
new TupleList<Vehicle, Amphibian, String, Integer>();
tl.add(TupleTest.h());
tl.add(TupleTest.h());
for(FourTuple<Vehicle,Amphibian,String,Integer> i: tl)
System.out.println(i);
}
} /* Output: (75% match)
(Vehicle@11b86e7, Amphibian@35ce36, hi, 47)
(Vehicle@757aef, Amphibian@d9f9c3, hi, 47)
*///:~
儘管這看上去有些冗長(特別是迭代器的創建),但最終還是沒有用過多的代碼就得到了一個相當強大的數據結構。
下面是另一個示例,它展示了使用泛型類型來構建複雜模型是多麼的簡單。即使每個類部是作爲一個構建塊創建的,但是其整個還是包含許多部分。在本例中,構建的模型是一個零售店,它包含走廊、貨架和商品:
//: generics/Store.java
// Building up a complex model using generic containers.
import java.util.*;
import net.mindview.util.*;
class Product {
private final int id;
private String description;
private double price;
public Product(int IDnumber, String descr, double price){
id = IDnumber;
description = descr;
this.price = price;
System.out.println(toString());
}
public String toString() {
return id + ": " + description + ", price: $" + price;
}
public void priceChange(double change) {
price += change;
}
public static Generator<Product> generator =
new Generator<Product>() {
private Random rand = new Random(47);
public Product next() {
return new Product(rand.nextInt(1000), "Test",
Math.round(rand.nextDouble() * 1000.0) + 0.99);
}
};
}
class Shelf extends ArrayList<Product> {
public Shelf(int nProducts) {
Generators.fill(this, Product.generator, nProducts);
}
}
class Aisle extends ArrayList<Shelf> {
public Aisle(int nShelves, int nProducts) {
for(int i = 0; i < nShelves; i++)
add(new Shelf(nProducts));
}
}
class CheckoutStand {}
class Office {}
public class Store extends ArrayList<Aisle> {
private ArrayList<CheckoutStand> checkouts =
new ArrayList<CheckoutStand>();
private Office office = new Office();
public Store(int nAisles, int nShelves, int nProducts) {
for(int i = 0; i < nAisles; i++)
add(new Aisle(nShelves, nProducts));
}
public String toString() {
StringBuilder result = new StringBuilder();
for(Aisle a : this)
for(Shelf s : a)
for(Product p : s) {
result.append(p);
result.append("\\n");
}
return result.toString();
}
public static void main(String[] args) {
System.out.println(new Store(14, 5, 10));
}
} /* Output:
258: Test, price: $400.99
861: Test, price: $160.99
868: Test, price: $417.99
207: Test, price: $268.99
551: Test, price: $114.99
278: Test, price: $804.99
520: Test, price: $554.99
140: Test, price: $530.99
...
*///:~
正如我們在Store.tostring()中看到的,其結果是許多層容器,但是它們是類型安全且可管理的。令人印象深刻之處是組裝這個的模型十分容易,並不會成爲智力挑戰。
15.7 擦除的神祕之處
當你開始更深人地鑽研泛型時,會發現有大量的東西初看起來是沒有意義的。例如,儘管可以聲明ArrayList.class,但是不能聲明ArrayList<Integer>.class
,請考慮下面的情況:
//: generics/ErasedTypeEquivalence.java
import java.util.*;
public class ErasedTypeEquivalence {
public static void main(String[] args) {
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2);
}
} /* Output:
true
*///:~
ArrayList<String>
和ArrayList<Integer>
很容易被認爲是不同的類型。不同的類型在行爲方面肯定不同,例如,如果嘗試着將一個Integer放入ArrayList<String>
,所得到的行爲(將失敗)與把一個Integer放入ArrayList<Integer>
(將成功)所得到的行爲完全不同。但是上面的程序會認爲它們是相同的類型。
下面是的示例是對這個謎題的一個補充:
//: generics/LostInformation.java
import java.util.*;
class Frob {}
class Fnorkle {}
class Quark<Q> {}
class Particle<POSITION,MOMENTUM> {}
public class LostInformation {
public static void main(String[] args) {
List<Frob> list = new ArrayList<Frob>();
Map<Frob,Fnorkle> map = new HashMap<Frob,Fnorkle>();
Quark<Fnorkle> quark = new Quark<Fnorkle>();
Particle<Long,Double> p = new Particle<Long,Double>();
System.out.println(Arrays.toString(
list.getClass().getTypeParameters()));
System.out.println(Arrays.toString(
map.getClass().getTypeParameters()));
System.out.println(Arrays.toString(
quark.getClass().getTypeParameters()));
System.out.println(Arrays.toString(
p.getClass().getTypeParameters()));
}
} /* Output:
[E]
[K, V]
[Q]
[POSITION, MOMENTUM]
*///:~
根據JDK文檔的描述,Class.getTypeParameters()將“返回一個TypeVariable對象數組,表示有泛型聲明所聲明的類型參數…”。這好像是在暗示你可能發現參數類型的信息,但是,正如你從輸出中所看到的,你能夠發現的只是用作參數佔位符的標識符,這並非有用的信息。
因此,殘酷的現實是:
在泛型代碼內部,無法獲得任何有關泛型參數類型的信息。
因此,你可以知道諸如類型參數標識符和泛型類型邊界這類的信息——你卻無法知道用來創建某個特定實例的實際的類型參數。如果你曾經是C++程序員,那麼這個事實肯定讓你覺得很沮喪,在使用Java泛型工作時它是必須處理的最基本的間題。
Java泛型是使用擦除來實現的,這意味着當你在使用泛型時,任何具體的類型信息都被擦除了,你唯一知遒的就是你在使用一個對象。因此List<String>
和List<interger>
在運行時事實上是相同的類型。這兩種形式都被擦除成它們的“原生”類型,即List。理解擦除以及應該如何處理它,是你在學習Java泛型時面臨的最大障礙。這也是我們在本節將要探討的內容。
15.7.1 C++的方式
下面是使用模版的C++示例,你將注意到用於參數化類型的語法十分相似,因爲Java是受C++的啓發:
//: generics/Templates.cpp
#include <iostream>
using namespace std;
template<class T> class Manipulator {
T obj;
public:
Manipulator(T x) { obj = x; }
void manipulate() { obj.f(); }
};
class HasF {
public:
void f() { cout << "HasF::f()" << endl; }
};
int main() {
HasF hf;
Manipulator<HasF> manipulator(hf);
manipulator.manipulate();
} /* Output:
HasF::f()
///:~
Manipulator類存儲了一個類型T的對象,有意思的地方是manipulate()方法,它在obj上調用方f()。它怎麼能知道f()方法是爲類型參數T而存在的呢?當你實例化這個模版時,C++編譯器將進行檢查,因此在Manipulalor<HasF>
被實例化的這一刻,它看到HasF擁有一個方法f()。如果情況井非如此,就會得到一個編譯期錯誤,這樣類型安全就得到了保障。
用C++編寫這種代碼很簡單,因爲當模版被實例化時,模版代碼知道其模版參數的類型。Java泛型就不同了。下面是HasF的Java版本:
//: generics/HasF.java
public class HasF {
public void f() { System.out.println("HasF.f()"); }
} ///:~
如果我們將這個示例的其餘代碼都翻譯成Java,那麼這些代碼將不能編譯:
//: generics/Manipulation.java
// {CompileTimeError} (Won't compile)
class Manipulator<T> {
private T obj;
public Manipulator(T x) { obj = x; }
// Error: cannot find symbol: method f():
public void manipulate() { obj.f(); }
}
public class Manipulation {
public static void main(String[] args) {
HasF hf = new HasF();
Manipulator<HasF> manipulator =
new Manipulator<HasF>(hf);
manipulator.manipulate();
}
} ///:~
由於有了擦除,Java編譯器無法將maaipulate()必須能夠在obj上調用f()這一需求映射到HasF有f()這一事實上。爲了調用f(),我們必須協助泛型類,給定泛型類的邊界,以此告知編譯器只能接受遵循這個邊界的類型。這裏重用了extends關鍵字。由於有了邊界,下面的代碼就可以編譯了:
//: generics/Manipulator2.java
class Manipulator2<T extends HasF> {
private T obj;
public Manipulator2(T x) { obj = x; }
public void manipulate() { obj.f(); }
} ///:~
邊界<T extends HasF>
聲明T必須具有類型HasF或者從HasF導出的類型。如果情況確實如此,那麼就可以安全地在obj上調用f()了。
我們說泛型類型參數將擦除到它的第一個邊界(它可能會有多個邊界,稍候你就會看到),我們還捉到了類型參數
的擦除。編譯器實際上會把類型參數替換爲它的擦除,就像上面的示例一樣。T擦除到了HasF,就好像在類的聲明中用HasF替換了T一樣。
你可能已經正確地觀察到,在Manipulation2.java中,泛型沒有貢獻任何好處。只需很容易地自己去執行擦除,就可以創建出沒有泛型的類:
//: generics/Manipulator3.java
class Manipulator3 {
private HasF obj;
public Manipulator3(HasF x) { obj = x; }
public void manipulate() { obj.f(); }
} ///:~
這提出了很重要的一點:只有當你希望使用的類型參數比某個具體類型(以及它的所有子類型)更加“泛化”時——也就是說,當你希望代碼能夠跨多個類工作時,使用泛型纔有所幫助。因此,類型參數和它們在有用的泛型代碼中的應用,通常比簡單的類替換要更復雜。但是,不能因此而認爲<T extends HasF>
形式的任何東西而都是有缺陷的。例如,如果某個類有一個返回T的方法,那麼泛型就有所幫助,因爲它們之後將返回確切的類型:
//: generics/ReturnGenericType.java
class ReturnGenericType<T extends HasF> {
private T obj;
public ReturnGenericType(T x) { obj = x; }
public T get() { return obj; }
} ///:~
必須查看所有的代碼,並確定它是否“足夠複雜”到必須作用泛型的程度。
我們將在本章稍後介紹有關邊界的更多細節。
15.7.2 遷移兼容性
爲了減少潛在的關於擦除的混淆,你必須清楚地認識到這不是一個語言特性。它是Java的泛型實現中的一種折中,因爲泛型不是Java語言出現時就有的組成部分,所以這種折中是必需的。這種折中會使你痛苦,因此你需要習慣它並瞭解爲什麼它會是這樣。
如果泛型在Java 1.0中就已經是其一部分了,那麼這個特性將不會使用擦除來實現——它將使用縣體化,使類型參數保持爲第一類實體,因此你就能夠在類型參數上執行基於類型的語言操作和反射操作。你將在本章稍後看到,擦除減少了泛型的泛化性。泛型在Java中仍歸是有用的,只是不如它們本來設想的那麼有用,而原因就是擦除。
在基於擦除的實現中,泛型類型被當作第二類類型處理,即不能在某些重要的上下文環境中使用的類型。泛型類型只有在靜態類型檢查期間纔出現,在此之後,程序中的所有泛型類型都將被擦除,替換爲它們的非泛型上界。例如,諸如List<T>
這樣的類型註解將被擦除爲List,而普通的類型變量在未指定邊界的情況下將被擦除爲Object。
擦除的核心動機是它使得泛化的客戶端可以用非泛化的類庫來使用,反之亦然,這經常被稱爲“遷移兼容性”。在理想情況下,當所有事物都可以同時被泛化時,我們就可以專注於此。在現實中,即使程序員只編寫泛型代碼,他們也必須處理在Java SE5之前編寫的非泛型類庫。那些類庫的作者可能從沒有想過要泛化它們的代碼,或者可能剛剛開始接觸泛型。
因此Java泛型不僅必須支特向後兼容性,即現有的代碼和類文件仍舊合法,井且繼續保持其之前的含義而且還要支持遷移兼容性,使得類庫接照它們自己的步調變爲泛型的,並且當某個類庫變爲泛型時,不會破壞依賴於它的代碼和應用程序。在決定這就是目標之後,Java設計者們和從事此問題相關工作的各個團隊決策認爲擦除是唯一可行的解決方案。通過允許非泛型代碼與泛型代碼共存,擦除使得這種向着泛型的遷移成爲可能。
例如,假設某個應用程序具有兩個類庫X和Y,並且Y還要使用類庫Z。隨着Java SE5的出現,這個應用程序和這些類庫的創建者最終可能希望遷移到泛型上。但是,當進行這種遷移時,他們有着不同動機和限制。爲了實現遷移兼容性,每個類庫和應用程序都必須與其他所有的部分是否使用了泛型無關。這樣,它們必須不具備探測其他類庫是否使用了泛型的能力。因此,某個特定的類庫使用了泛型這樣的證據必須被“擦除”。
如果沒有某種類型的遷移途徑,所有已經構建了很長時間的類庫就需要與希望遷移到Java泛型上的開發者們說再見了。但是,類庫是編程語言無可爭議的一部分,它們對生產效率會產生最重要的影響,因此這不是一種可以接受的代價。擦除是否是最佳的或者唯一的遷移途徑,還需要時間來證明。
15.7.3 擦除的問題
因此,擦除主要的正當理由是從非泛化代碼到泛化代碼的轉變過程,以及在不破壞現有類庫的情況下,將泛型融入Java語言。擦除使得現有的非泛型客戶端代碼能夠在不改變的情況下繼續使用,直至客戶端準備好用泛型重寫這些代碼。這是一個崇高的動機,因爲它不會突然間破壞所有現有的代碼。
擦除的代價是顯著的。泛型不能用於顯式地引用運行時類型的操作之中,例如轉型、instanceof操作和new表達式。因爲所有關於參數的類型信息都丟失了。無論何時,當你在編寫泛型代碼時,必須時刻提醒自己,你只是看起來好像
擁有有關參數的類型信息而已。因此,如果你編寫了下面這樣的代碼段:
class Foo<T> {
T var;
}
那麼,看起來當你在創建Foo實例時:
Foo<Cat> f = new Foo<Cat>();
class Foo中的代碼應該知道現在工作於Cat之上,而泛型語法也在強烈暗示:在整個類中的各個地方,類型T都在被替換。但是事實並非如此,無論何時,當你在編寫這個類的代碼時,必須提醒自己:“不,它只是一個Object。”
另外,擦除和遷移兼容性意味着,使用泛型並不是強制的,儘管你可能希望這樣:
//: generics/ErasureAndInheritance.java
class GenericBase<T> {
private T element;
public void set(T arg) { arg = element; }
public T get() { return element; }
}
class Derived1<T> extends GenericBase<T> {}
class Derived2 extends GenericBase {} // No warning
// class Derived3 extends GenericBase<?> {}
// Strange error:
// unexpected type found : ?
// required: class or interface without bounds
public class ErasureAndInheritance {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
Derived2 d2 = new Derived2();
Object obj = d2.get();
d2.set(obj); // Warning here!
}
} ///:~
Derived2繼承自GenericBase,但是沒有任何泛型參數,而編譯器不會發出任何警告。警告在Set()被調用時纔會出現。
爲了關閉警告,Java提供了一個註解,我們可以在列表中看到它(這個註解在Java SE5之前的版本中不支持):
@suppressWarnings("unchecked")
注意,這個往解被放置在可以產生這類警告的方法之上,而不是整個類上。當你要關閉警告時,最好是儘量地“聚焦”,這樣就不會因爲過於寬泛地關閉警告,而導致意外地遮蔽掉真正的問題。
可以推斷,Derived3產生的錯誤意味着編譯器期望得到一個原生基類。
當你希望將類型參數不要僅僅當作Object處理時,就需要付出額外努力來管理邊界,並且與在C++, Ada和Eiffel這樣的語言中獲得參數化類型相比,你需要付出多得多的努力來獲得少得多的回報。這並不是說,對於大多數的編程問題而言,這些語言通常都會比Java更得心應手,這只是說,它們的參數化類型機制比Java的更靈活、更強大。
15.7.4 邊界處的動作
正是因爲有了擦除,我發現泛型最令人困惑的方面源自這樣一個事實,即可以表示沒有任何意義的事物。例如:
//: generics/ArrayMaker.java
import java.lang.reflect.*;
import java.util.*;
public class ArrayMaker<T> {
private Class<T> kind;
public ArrayMaker(Class<T> kind) { this.kind = kind; }
@SuppressWarnings("unchecked")
T[] create(int size) {
return (T[])Array.newInstance(kind, size);
}
public static void main(String[] args) {
ArrayMaker<String> stringMaker =
new ArrayMaker<String>(String.class);
String[] stringArray = stringMaker.create(9);
System.out.println(Arrays.toString(stringArray));
}
} /* Output:
[null, null, null, null, null, null, null, null, null]
*///:~
即使kind被存儲爲Class<T>
,擦除也意味着它實際將被存儲爲Class,沒有任何參數。因此,當你在使用它時,例如在創建數組時,Array.newInstance()實際上並未擁有kind所蘊含的類型信息,因此這不會產生具體的結果。所以必須轉型,這將產生一條令你無法滿意的警告。
注意,對於在泛型中創建數組,使用Array.newInstance()是推薦的方式。
如果我們要創建一個容器而不是數組,情況就有些不同了:
//: generics/ListMaker.java
import java.util.*;
public class ListMaker<T> {
List<T> create() { return new ArrayList<T>(); }
public static void main(String[] args) {
ListMaker<String> stringMaker= new ListMaker<String>();
List<String> stringList = stringMaker.create();
}
} ///:~
編譯器不會給出任何警告,儘管我們(從擦除中)知道在create()內部的new ArrayList<T>
中的<T>
被移除了——在運行時,這個類的內部沒有任何<T>
,因此這看起來毫無意義。但是如果你遵從這種思路,並將這個表達式改爲new ArrayList(),編譯器就會給出警告。
在本例中,這是否真的毫無意義呢?如果返回list之前,將某些對象放入其中。就像下面這樣,情況又會如何呢?
//: generics/FilledListMaker.java
import java.util.*;
public class FilledListMaker<T> {
List<T> create(T t, int n) {
List<T> result = new ArrayList<T>();
for(int i = 0; i < n; i++)
result.add(t);
return result;
}
public static void main(String[] args) {
FilledListMaker<String> stringMaker =
new FilledListMaker<String>();
List<String> list = stringMaker.create("Hello", 4);
System.out.println(list);
}
} /* Output:
[Hello, Hello, Hello, Hello]
*///:~
即使編譯器無法知道有關create()中的T的任何信息,但是它仍舊可以在編譯期確保你放置到result中的對象具有T類型,使共適合ArrayList<T>
。因此,即使擦除在方法或類內部移除了有關實際類型的信息,編譯器仍舊可以確保在方法或類中使用的類型的內部一致性。
因爲擦除在方法體中移除了類型信息,所以在運行時的問題就是邊界:即對象進入和離開方法的地點。這些正是編譯器在編譯期執行類型檢查並插入轉型代碼的地點。請考慮下面的非泛型示例:
//: generics/SimpleHolder.java
public class SimpleHolder {
private Object obj;
public void set(Object obj) { this.obj = obj; }
public Object get() { return obj; }
public static void main(String[] args) {
SimpleHolder holder = new SimpleHolder();
holder.set("Item");
String s = (String)holder.get();
}
} ///:~
如果用Javap -c SimpleHolder反編譯這個類,就可以得到下面的(經過編輯的)內容:
public void set (java.lang.Object):
0: aload_0
1: aload_1
2: putfield #2; //Field obj:Object
5: return
public java.lang.Object get():
0: aload_0
1: getfield #2; //Field obj:Object
4: areturn
public static void main(java.lang.String[]):
0: new #3; //class SimpleHolder
3: dup
4: invokespecial #4; //Method "<init>:()v
7: a-store_1
8: aload_ 1
9: ldc #5; //String Item;
11: invokevitrual #6; //Method set:(Object;)V
14: aload_1
15: invokevirtual #7: //Method get:()Object;
18: checkcast #8; //class java/lang/String
21: astore_2
22: return
get()和set()方法將直接存儲和產生值,而轉型是在調用get()的時候接受檢查的。
現在將泛型合併到上面的代碼中:
//: generics/GenericHolder.java
public class GenericHolder<T> {
private T obj;
public void set(T obj) { this.obj = obj; }
public T get() { return obj; }
public static void main(String[] args) {
GenericHolder<String> holder =
new GenericHolder<String>();
holder.set("Item");
String s = holder.get();
}
} ///:~
從get()返回之後的轉型消失了,但是我們還知道傳遞給set()的值在編譯期會接受檢查。下面是相關的字節碼:
public void set (java.lang.Object):
0: aload_0
1: aload_1
2: putfield #2; //Field obj:Object
5: return
public java.lang.Object get():
0: aload_0
1: getfield #2; //Field obj:Object
4: areturn
public static void main(java.lang.String[]):
0: new #3; //class GenericHolder
3: dup
4: invokespecial #4; //Method "<init>:()v
7: a-store_1
8: aload_ 1
9: ldc #5; //String Item;
11: invokevitrual #6; //Method set:(Object;)V
14: aload_1
15: invokevirtual #7: //Method get:()Object;
18: checkcast #8; //class java/lang/String
21: astore_2
22: return
所產生的字節碼是相同的。對進入set()的類型進行檢查是不需要的,因爲這將由編譯器執行。而對從get()返回的值進行轉型仍舊是需要的,但這與你自己必須執行的操作是一樣的——此處它將由編譯器自動插入,因此你寫人(和讀取)的代碼的噪聲將更小,
由於所產生的get()和set()的字節碼相同,所以在泛型中的所有動作都發生在邊界處——對傳遞進來的值進行額外的編譯期檢查,並插入對傳遞出去的值的轉型。這有助於澄清對擦除的混淆,記住,“邊界就是發生動作的地方”。
15.8 擦除的補償
正如我們看到的,擦除丟失了在泛型代碼中執行某些操作的能力。任何在運行時需要知道確切類型信息的操作都將無法工作:
//: generics/Erased.java
// {CompileTimeError} (Won't compile)
public class Erased<T> {
private final int SIZE = 100;
public static void f(Object arg) {
if(arg instanceof T) {} // Error
T var = new T(); // Error
T[] array = new T[SIZE]; // Error
T[] array = (T)new Object[SIZE]; // Unchecked warning
}
} ///:~
偶爾可以繞過這些問題來編程,但是有時必須通過引入類型標籤來對擦除進行補償。這意味着你需要顯式地傳遞你的類型的Class對象,以便你可以在類型表達式中使用它。
例如,在前面示例中對使用instanceof的嘗試最終失敗了,因爲其類型信息已經被擦除了。如果引入類型標籤,就可以轉而使用動態的isInstance():
//: generics/ClassTypeCapture.java
class Building {}
class House extends Building {}
public class ClassTypeCapture<T> {
Class<T> kind;
public ClassTypeCapture(Class<T> kind) {
this.kind = kind;
}
public boolean f(Object arg) {
return kind.isInstance(arg);
}
public static void main(String[] args) {
ClassTypeCapture<Building> ctt1 =
new ClassTypeCapture<Building>(Building.class);
System.out.println(ctt1.f(new Building()));
System.out.println(ctt1.f(new House()));
ClassTypeCapture<House> ctt2 =
new ClassTypeCapture<House>(House.class);
System.out.println(ctt2.f(new Building()));
System.out.println(ctt2.f(new House()));
}
} /* Output:
true
true
false
true
*///:~
編譯器將確保類型標籤可以匹配泛型參數。
15.8.1 創建類型實例
在Erased.java中對創建一個new T()的嘗試將無法實現,部分原因是因爲擦除,而另一部分原因是因爲編譯器不能驗證T具有默認(無參)構造器。但是在C++中,這種操作很自然、很直觀,並且很安全(它是在編譯期受到檢查的):
//: generics/InstantiateGenericType.cpp
// C++, not Java!
template<class T> class Foo {
T x; // Create a field of type T
T* y; // Pointer to T
public:
// Initialize the pointer:
Foo() { y = new T(); }
};
class Bar {};
int main() {
Foo<Bar> fb;
Foo<int> fi; // ... and it works with primitives
} ///:~
Jaya中的解決方案是傳遞一個工廠對象,並使用它來創建新的實例。最便利的工廠對象就是Class對象,因此如果使用類型標籤,那麼你就可以使用newInstance來創建這個類型的新對象:
//: generics/InstantiateGenericType.java
import static net.mindview.util.Print.*;
class ClassAsFactory<T> {
T x;
public ClassAsFactory(Class<T> kind) {
try {
x = kind.newInstance();
} catch(Exception e) {
throw new RuntimeException(e);
}
}
}
class Employee {}
public class InstantiateGenericType {
public static void main(String[] args) {
ClassAsFactory<Employee> fe =
new ClassAsFactory<Employee>(Employee.class);
print("ClassAsFactory<Employee> succeeded");
try {
ClassAsFactory<Integer> fi =
new ClassAsFactory<Integer>(Integer.class);
} catch(Exception e) {
print("ClassAsFactory<Integer> failed");
}
}
} /* Output:
ClassAsFactory<Employee> succeeded
ClassAsFactory<Integer> failed
*///:~
這可以編譯,但是會因ClassAsFactory<Integer>
而失敗,因爲Integer沒有任何默認的構造器。因爲這個錯誤不是在編譯期捕獲的,所以Sun的夥計們對這種方式井不贊成,他們建議使用顯式的工廠,井將限制其類型,使得只能接受實現了這個工廠的類:
//: generics/FactoryConstraint.java
interface FactoryI<T> {
T create();
}
class Foo2<T> {
private T x;
public <F extends FactoryI<T>> Foo2(F factory) {
x = factory.create();
}
// ...
}
class IntegerFactory implements FactoryI<Integer> {
public Integer create() {
return new Integer(0);
}
}
class Widget {
public static class Factory implements FactoryI<Widget> {
public Widget create() {
return new Widget();
}
}
}
public class FactoryConstraint {
public static void main(String[] args) {
new Foo2<Integer>(new IntegerFactory());
new Foo2<Widget>(new Widget.Factory());
}
} ///:~
注意,這確實只是傳遞Class<T>
的一種變體。兩種方式都傳遞了工廠對象,Class<T>
碰巧是內建的工廠對象,而上面的方式創建了一個顯式的工廠對象,但是你卻獲得了編譯期檢查。
另一種方式是模板方法設計模式。在下面的示例中,get()是模板方法,而create()是在子類中定義的、用來產生子類類型的對象:
//: generics/CreatorGeneric.java
abstract class GenericWithCreate<T> {
final T element;
GenericWithCreate() { element = create(); }
abstract T create();
}
class X {}
class Creator extends GenericWithCreate<X> {
X create() { return new X(); }
void f() {
System.out.println(element.getClass().getSimpleName());
}
}
public class CreatorGeneric {
public static void main(String[] args) {
Creator c = new Creator();
c.f();
}
} /* Output:
X
*///:~
15.8.2泛型數組
正如你在Erased.java中所見,不能創建泛型數組。一般的解決方案是在任何想要創建泛型數組的地方都使用ArrayList:
//: generics/ListOfGenerics.java
import java.util.*;
public class ListOfGenerics<T> {
private List<T> array = new ArrayList<T>();
public void add(T item) { array.add(item); }
public T get(int index) { return array.get(index); }
} ///:~
這裏你將獲得數組的行爲,以及由泛型提供的編譯期的類型安全。
有時,你仍舊希璧創建泛型類型的數組(例如,ArrayList內部使用的是數組)。有趣的是可以按照編譯器喜歡的方式來定義一個引用,例如:
//: generics/ArrayOfGenericReference.java
class Generic<T> {}
public class ArrayOfGenericReference {
static Generic<Integer>[] gia;
} ///:~
編譯器將接受這個程序,而不會產生任何警告。但是,永遠都不能創建這個確切類型的數組(包括類型參數),因此這有一點令人困惑。既然所有數組無論它們持有的類型如何,都具有相同的結構(每個數組槽位的尺寸和數組的佈局),那麼看起來你應該能夠創建一個Object數組,並將其轉型爲所希望的數組類型。事實上這可以編譯,但是不能運行,它將產生ClassCaseException:
//: generics/ArrayOfGeneric.java
public class ArrayOfGeneric {
static final int SIZE = 100;
static Generic<Integer>[] gia;
@SuppressWarnings("unchecked")
public static void main(String[] args) {
// Compiles; produces ClassCastException:
//! gia = (Generic<Integer>[])new Object[SIZE];
// Runtime type is the raw (erased) type:
gia = (Generic<Integer>[])new Generic[SIZE];
System.out.println(gia.getClass().getSimpleName());
gia[0] = new Generic<Integer>();
//! gia[1] = new Object(); // Compile-time error
// Discovers type mismatch at compile time:
//! gia[2] = new Generic<Double>();
}
} /* Output:
Generic[]
*///:~
問題在於數組將跟蹤它們的實際類型,而這個類型是在數組被創建時確定的,因此,即使gia已經被轉型爲Generic<Integer>[]
,但是這個信息只存在幹編譯期(如果沒有@Suppress Warnings註解,你將得到有關這個轉型的警告)。在運行時,它仍將引發問題。成功創建泛型數組的唯一方式就是創建一個被擦除類型的新數組,然後對其轉型。
讓我們看一個更復雜的示例。考慮一個簡單的泛型數組包裝器:
//: generics/GenericArray.java
public class GenericArray<T> {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArray(int sz) {
array = (T[])new Object[sz];
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) { return array[index]; }
// Method that exposes the underlying representation:
public T[] rep() { return array; }
public static void main(String[] args) {
GenericArray<Integer> gai =
new GenericArray<Integer>(10);
// This causes a ClassCastException:
//! Integer[] ia = gai.rep();
// This is OK:
Object[] oa = gai.rep();
}
} ///:~
與前面相同,我們井不能聲明T[] array=new T[sz],因此我們創建了一個對象數組,然後將其轉型。
rep()方法將返回T[],它在main()中將用於gai,因此應該是Integer[],但是如果調用它,並嘗試着將結果作爲Integer[]引用來捕獲,就會得到ClassCastException,這還是因爲實際的運行時類型是Objet[]。
如果在註釋掉@Suppresswarniugs註解之後再編譯GenericArray.java ,編譯器就會產生警告:
Note: GenericArray.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
在這種情況下,我們將只獲得單個的警告,並且相信這事關轉型。但是如果真的想要確定是否是這麼回事,就應該用.Xlint:unchecked來編譯:
GenerieArray.java:7: warning:[unchecked] unchecked cast
found : java.lang.Object[]
required: T[]
array = (T[])new Object[sz];
^
1 warning
這確實是對轉型的抱怨。因爲警告會變得令人迷惑,所以一旦我們驗證某個特定警告是可預期的,那麼我們的上策就是用@SuppressWarnings關閉它。通過這種方式,當警告確實出現時,我們就可以真正地展開對它的調查了。
因爲有了擦除,數組的運行時類型就只能是Object[]。如果我們立即將其轉型爲T[],那麼在編譯期該數組的實際類型就將丟失,而編譯器可能會錯過某些潛在的錯誤檢查。正因爲這樣,最好是在集合內部使用Object[],然後當你使用數組元素時,添加一個對T的轉型。讓我們看着這是如何作用於GenericArray.java示例的:
//: generics/GenericArray2.java
public class GenericArray2<T> {
private Object[] array;
public GenericArray2(int sz) {
array = new Object[sz];
}
public void put(int index, T item) {
array[index] = item;
}
@SuppressWarnings("unchecked")
public T get(int index) { return (T)array[index]; }
@SuppressWarnings("unchecked")
public T[] rep() {
return (T[])array; // Warning: unchecked cast
}
public static void main(String[] args) {
GenericArray2<Integer> gai =
new GenericArray2<Integer>(10);
for(int i = 0; i < 10; i ++)
gai.put(i, i);
for(int i = 0; i < 10; i ++)
System.out.print(gai.get(i) + " ");
System.out.println();
try {
Integer[] ia = gai.rep();
} catch(Exception e) { System.out.println(e); }
}
} /* Output: (Sample)
0 1 2 3 4 5 6 7 8 9
java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
*///:~
初看起來,這好像沒多大變化,只是轉型挪了地方。如果沒有@Suppresswarnings註解,你仍舊會得到unchecked告。但是,現在的內部表示是Object[]而不是T[]。當get()被調用時,它將對象轉型爲T,這實際上是正確的類型,因此這是安全的。然而,如果你調用rep() ,它還是嘗試着將Object[]轉型爲T[],這仍舊是不正確的,將在編譯期產生警告,在運行時產生異常。因此,沒有任何方式可以推翻底層的數組類型,它只能是Object[]。在內部將array當作Object[]而不是T[]處理的優勢是:我們不太可能忘記這個數組的運行時類型,從而意外地引入缺陷(儘管大多數也可能是所有這類缺陷都可以在運行時快速地探測到)。
對於新代碼,應該傳遞一個類型標記。在這種情況下,GenericArray看起來會像下面這樣:
//: generics/GenericArrayWithTypeToken.java
import java.lang.reflect.*;
public class GenericArrayWithTypeToken<T> {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArrayWithTypeToken(Class<T> type, int sz) {
array = (T[])Array.newInstance(type, sz);
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) { return array[index]; }
// Expose the underlying representation:
public T[] rep() { return array; }
public static void main(String[] args) {
GenericArrayWithTypeToken<Integer> gai =
new GenericArrayWithTypeToken<Integer>(
Integer.class, 10);
// This now works:
Integer[] ia = gai.rep();
}
} ///:~
類型標記Class<T>
被傳遞到構造器中,以便從擦除中恢復,使得我們可以創建需要的實際類型的數組,儘管從轉型中產生的警告必須用@Suppresswarnings壓制住。一旦我們獲得了實際類型。就可以返回它,並獲得想要的結果,就像在main()中看到的那樣。該數組的運行時類型是確切類型T[]。
遺憾的是,如果查看java SE5標準類庫中的源代碼,你就會看到從Object數組到參數化類型的轉型遍及各處。例如,下面是經過整理和簡化之後的從Collection中複製ArrayList的構造器:
public ArrayList(Collection c) {
size = c.size();
elementData = (E[])new Object[size];
c.toArray(elementData);
}
如果你通讀ArrayList.java,就會發現它充滿了這種轉型。如果我們編譯它,又會發生什麼呢?
Note: ArrayList.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
可以十分肯定,標準類庫會產生大量的警告。如果你曾經用過C++,特別是ANSI C之前的版本,你就會記得警告的特殊效果:當你發現可以忽略它們時,你就可以忽略。正是因爲這個原因,最好是從編譯器中不要發出任何消息,除非程序員必須對其進行響應。
Neal Gafter (Java SE5的領導開發者之一)在他的博客中指出,在重寫Java庫時,他十分懶散,而我們不應該像他那樣。Neal還指出,在不破壞現有接口的情況下,他將無法修改某些Java類庫代碼。因此,即使在Java類庫源代碼中出現了某些慣用法,也不能表示這就是正確的解決之道。當查看類庫代碼時,你不能認爲它就是應該在自己的代碼中遵循的示例。
15.9 邊界
本章前面簡單地介紹過邊界。邊界使得你可以在用於泛型的參數類型上設置限制條件。儘管這使得你可以強制規定泛型可以應用的類型,但是其潛在的一個更重要的效果是你可以按照自己的邊界類型來調用方法。
因爲擦出移除了類型信息,所以,可以用無界泛型參數調用的方法只是哪些可以用Object調用的方法。但是,如果能夠將這個參數限制爲某個類型子集,那麼你就可以用這些類型子集來調用方法。爲了執行這種限制,Java泛型重用了extends關鍵字。對你來說有一點很重要,即要理解extends關鍵字在泛型邊界上下文環境中和在普通情況下所具有的意義是完全不同的。下面的示例展示了邊界的基本要素:
//: generics/BasicBounds.java
interface HasColor { java.awt.Color getColor(); }
class Colored<T extends HasColor> {
T item;
Colored(T item) { this.item = item; }
T getItem() { return item; }
// The bound allows you to call a method:
java.awt.Color color() { return item.getColor(); }
}
class Dimension { public int x, y, z; }
// This won't work -- class must be first, then interfaces:
// class ColoredDimension<T extends HasColor & Dimension> {
// Multiple bounds:
class ColoredDimension<T extends Dimension & HasColor> {
T item;
ColoredDimension(T item) { this.item = item; }
T getItem() { return item; }
java.awt.Color color() { return item.getColor(); }
int getX() { return item.x; }
int getY() { return item.y; }
int getZ() { return item.z; }
}
interface Weight { int weight(); }
// As with inheritance, you can have only one
// concrete class but multiple interfaces:
class Solid<T extends Dimension & HasColor & Weight> {
T item;
Solid(T item) { this.item = item; }
T getItem() { return item; }
java.awt.Color color() { return item.getColor(); }
int getX() { return item.x; }
int getY() { return item.y; }
int getZ() { return item.z; }
int weight() { return item.weight(); }
}
class Bounded
extends Dimension implements HasColor, Weight {
public java.awt.Color getColor() { return null; }
public int weight() { return 0; }
}
public class BasicBounds {
public static void main(String[] args) {
Solid<Bounded> solid =
new Solid<Bounded>(new Bounded());
solid.color();
solid.getY();
solid.weight();
}
} ///:~
你可能已經觀察到了,BasiicBounds.java看上去包含可以通過繼承消除的冗餘。下面,可以看到如何在繼承的每個層次上添加邊界限制:
//: generics/InheritBounds.java
class HoldItem<T> {
T item;
HoldItem(T item) { this.item = item; }
T getItem() { return item; }
}
class Colored2<T extends HasColor> extends HoldItem<T> {
Colored2(T item) { super(item); }
java.awt.Color color() { return item.getColor(); }
}
class ColoredDimension2<T extends Dimension & HasColor>
extends Colored2<T> {
ColoredDimension2(T item) { super(item); }
int getX() { return item.x; }
int getY() { return item.y; }
int getZ() { return item.z; }
}
class Solid2<T extends Dimension & HasColor & Weight>
extends ColoredDimension2<T> {
Solid2(T item) { super(item); }
int weight() { return item.weight(); }
}
public class InheritBounds {
public static void main(String[] args) {
Solid2<Bounded> solid2 =
new Solid2<Bounded>(new Bounded());
solid2.color();
solid2.getY();
solid2.weight();
}
} ///:~
HoldItem直接持有一個對象,因此這種行爲被繼承到了Colored2中,它也要求其參數與HasColor一致。ColoredDimension2和Solid2進一步擴展了這個層次結構,並在每個層次上都添加了邊界。現在這些方法被繼承,因而不必在每個類中重複。
下面是具有更多層次的示例:
//: generics/EpicBattle.java
// Demonstrating bounds in Java generics.
import java.util.*;
interface SuperPower {}
interface XRayVision extends SuperPower {
void seeThroughWalls();
}
interface SuperHearing extends SuperPower {
void hearSubtleNoises();
}
interface SuperSmell extends SuperPower {
void trackBySmell();
}
class SuperHero<POWER extends SuperPower> {
POWER power;
SuperHero(POWER power) { this.power = power; }
POWER getPower() { return power; }
}
class SuperSleuth<POWER extends XRayVision>
extends SuperHero<POWER> {
SuperSleuth(POWER power) { super(power); }
void see() { power.seeThroughWalls(); }
}
class CanineHero<POWER extends SuperHearing & SuperSmell>
extends SuperHero<POWER> {
CanineHero(POWER power) { super(power); }
void hear() { power.hearSubtleNoises(); }
void smell() { power.trackBySmell(); }
}
class SuperHearSmell implements SuperHearing, SuperSmell {
public void hearSubtleNoises() {}
public void trackBySmell() {}
}
class DogBoy extends CanineHero<SuperHearSmell> {
DogBoy() { super(new SuperHearSmell()); }
}
public class EpicBattle {
// Bounds in generic methods:
static <POWER extends SuperHearing>
void useSuperHearing(SuperHero<POWER> hero) {
hero.getPower().hearSubtleNoises();
}
static <POWER extends SuperHearing & SuperSmell>
void superFind(SuperHero<POWER> hero) {
hero.getPower().hearSubtleNoises();
hero.getPower().trackBySmell();
}
public static void main(String[] args) {
DogBoy dogBoy = new DogBoy();
useSuperHearing(dogBoy);
superFind(dogBoy);
// You can do this:
List<? extends SuperHearing> audioBoys;
// But you can't do this:
// List<? extends SuperHearing & SuperSmell> dogBoys;
}
} ///:~
注意,通配符〔我們下面將要學習)被限制爲單一邊界。
15.10通配符
你已經在第11章中看到了一些使用通配符的示例——在泛型參數表達式中的問號,在第14章中這種示例更多。本節將更深人地探討這個問題。
我們開始入手的示例要展示數組的一種特殊行爲:可以嚮導出類型的數組賦予基類型的數組引用:
//: generics/CovariantArrays.java
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}
public class CovariantArrays {
public static void main(String[] args) {
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple(); // OK
fruit[1] = new Jonathan(); // OK
// Runtime type is Apple[], not Fruit[] or Orange[]:
try {
// Compiler allows you to add Fruit:
fruit[0] = new Fruit(); // ArrayStoreException
} catch(Exception e) { System.out.println(e); }
try {
// Compiler allows you to add Oranges:
fruit[0] = new Orange(); // ArrayStoreException
} catch(Exception e) { System.out.println(e); }
}
} /* Output:
java.lang.ArrayStoreException: Fruit
java.lang.ArrayStoreException: Orange
*///:~
main()中的第一行創建了一個Apple數組,井將其賦值給一個Fruit數組引用。這是有意義的,因爲Apple也是一種Fruit,因此Apple數組應該也是一個Fruit數組。
但是,如果實際的數組類型是Apple[],你應該只能在其中放置Apple或Apple的子類型,這在編譯期和運行時都可以工作。但是請注意,編譯器允許你將Fruit放置到這個數組中,這對於編譯器來說是有意義的,因爲它有一個Fruit[]引用——它有什麼理由不允許將Fruit對象或名任何從Fruit繼承出來的對象(例如Orange),放置到這個數組中呢?因此,在編譯期,這是允許的。但是,運行時的數組機制知道它處理的是Apple[],因此會在向數組中放置異構類型時拋出異常。
實際上,向上轉型不合適用在這裏。你真正做的是將一個數組賦值給另一個數組。數組的行爲應該是它可以持有其他對象,這裏只是因爲我們能夠向上轉型而已,所以很明顯,數組對象可以保留有關它們包含的對象類型的規則。就好像數組對它們持有的對象是有意識的,因此在編譯期檢查和運行時檢查之間,你不能濫用它們。
對數組的這種賦值並不是那麼可怕,因爲在運行時可以發現你已經插入了不正確的類型。但是泛型的主要目標之一是將這種錯誤檢測移入到編譯期。因此當我們試圖使用泛型容器來代替數組時,會發生什麼呢?
//: generics/NonCovariantGenerics.java
// {CompileTimeError} (Won't compile)
import java.util.*;
public class NonCovariantGenerics {
// Compile Error: incompatible types:
List<Fruit> flist = new ArrayList<Apple>();
} ///:~
儘管你在第一次閱讀這段代碼時會認爲:“不能將一個Apple容器賦值給一個Fruit容器”。別忘了,泛型不僅和容器相關。正確的說法是:“不能把一個涉及Apple的泛型賦給一個涉及Fruit的泛型”。如果就像在數組的情況中一樣,編譯器對代碼的瞭解足夠多,可以確定所涉及到的容器,那麼它可能會留下一些餘地。但是它不知透任何有關這方面的信息,因此它拒絕向上轉型。然而實際上這根本不是向上轉型——Apple的List不是Fruit的List。Apple的List將持有Apple和Apple的子類型,而Fruit的List將持有任何類型的Fruit,誠然,這包括Apple在內,但是它不是一個Apple的List,它仍舊是Fruit的List。Apple的List在類型上不等價於Fruit的List,即使Apple是一種Fruit類型。
真正的問題是我們在談論容器的類型。而不是容器持有的類型。與數組不同,泛型沒有內建的協變類型。這是因爲數組在語言中是完全定義的,因此可以內建了編譯期和運行時的檢查,但是在使用泛型時,編譯器和運行時系統都不知道你想用類型做些什麼,以及該採用什麼樣的規則。
但是,有時你想要在兩個類型之間建立某種類型的向上轉型關係,這正是通配符所允許的:
//: generics/GenericsAndCovariance.java
import java.util.*;
public class GenericsAndCovariance {
public static void main(String[] args) {
// Wildcards allow covariance:
List<? extends Fruit> flist = new ArrayList<Apple>();
// Compile Error: can't add any type of object:
// flist.add(new Apple());
// flist.add(new Fruit());
// flist.add(new Object());
flist.add(null); // Legal but uninteresting
// We know that it returns at least Fruit:
Fruit f = flist.get(0);
}
} ///:~
flist類型現在是List<? extends Fruit>
,你可以將其讀作“具有任何從Fruit繼承的類型的列表”。但是,這實際上並不意味着這個List將持有任何類型的Fruit。通配符引用的是明確的類型,因此它意味着“某種first引用沒有指定的具體類型”。因此這個被賦值的List必須持有諸如Fruit或Apple這樣的某種指定類型,但是爲了向上轉型爲flist,這個類型是什麼並沒有人關心。
如果唯一的限制是這個List要持有某種具體的Fruit或Fruit的子類型,但是你實際上並不關心它是什麼,那麼你能用這樣的List做什麼呢?如果不知道List持有什麼類型,那麼你怎樣才能安全地向其中添加對象呢?就像在CovariantArrays.java中向上轉型數組一樣,你不能,除非編譯器而不是運行時系統可以阻止這種操作的發生。你很快就會發現這一問題。
你可能會認爲,事情變得有點走極端了,因爲現在你甚至不能向剛剛聲明過將持有Apple對象的List中放置一個Apple對象了。是的,但是編譯器並不知道這一點。List<? extends Fruit>
可以合法地指向一個List<Orange>
。一旦執行這種類型的向上轉型,你就將丟失掉向其中傳遞任何對象的能力,甚至是傳遞Object也不行。
另一方面,如果你調用一個返回Fruit的方法,則是安全的,因爲你知道在這個List中的任何對象至少具有Fruit類型,因此編譯器將允許這麼做。
15.10.1 編譯器有多聰明
現在,你可能會猜想自己被阻止去調用任何接受參數的方法,但是請考慮下面的程序:
//: generics/CompilerIntelligence.java
import java.util.*;
public class CompilerIntelligence {
public static void main(String[] args) {
List<? extends Fruit> flist =
Arrays.asList(new Apple());
Apple a = (Apple)flist.get(0); // No warning
flist.contains(new Apple()); // Argument is 'Object'
flist.indexOf(new Apple()); // Argument is 'Object'
}
} ///:~
你可以看到,對contains()和indexOf()的調用,這兩個方法都接受Apple對象作爲參數,而這些調用都可以正常執行。這是否意味着編譯器實際上將檢查代碼,以查着是否有某個特定的方法修改了它的對象?
通過查看ArrsyList的文檔,我們可以發現,編譯器並沒有這麼聰明。儘管add()將接受一個具有泛型參數類型的參數,但是contains()和indexOf()將接受Object類型的參數。因此當你指定一個ArrayList<? extends Fruit>
時,add()的參數就變成了“? Extends Fruit”。從這個描述中,編譯器並不能瞭解這裏需要Fruit的哪個具體子類型,因此它不會接受任何類型的Fruit。如果先將Apple向上轉型爲Fruit,也無關緊要——編譯器將直接拒絕對參數列表中涉及通配符的方法(例如add())的調用。
在使用contains()和indexOf()時,參數類型是Object,因此不涉及任何通配符,而編譯器也將允許這個調用。這意味着將由泛型類的設計者來決定哪些調用是“安全的”,並使用Object類型作爲其參數類型。爲了在類型中使用了通配符的情況下禁止這類調用,我們需要在參數列表中使用類型參數。
可以在一個非常簡單的Holder類中看到這一點:
//: generics/Holder.java
public class Holder<T> {
private T value;
public Holder() {}
public Holder(T val) { value = val; }
public void set(T val) { value = val; }
public T get() { return value; }
public boolean equals(Object obj) {
return value.equals(obj);
}
public static void main(String[] args) {
Holder<Apple> Apple = new Holder<Apple>(new Apple());
Apple d = Apple.get();
Apple.set(d);
// Holder<Fruit> Fruit = Apple; // Cannot upcast
Holder<? extends Fruit> fruit = Apple; // OK
Fruit p = fruit.get();
d = (Apple)fruit.get(); // Returns 'Object'
try {
Orange c = (Orange)fruit.get(); // No warning
} catch(Exception e) { System.out.println(e); }
// fruit.set(new Apple()); // Cannot call set()
// fruit.set(new Fruit()); // Cannot call set()
System.out.println(fruit.equals(d)); // OK
}
} /* Output: (Sample)
java.lang.ClassCastException: Apple cannot be cast to Orange
true
*///:~
Holder有一個接受T類型對象的set()方法,一個get()方法,以及一個接受Object對象的equals()方法。正如你已經看到的,如果創建了一個Holder<Apple>
,不能將其向上轉型爲Holder<Fruit>
,但是可以將其向上轉型爲Holder<? extends Fruit>
,如果調用get(),它只會返回一個Fruit——這就是在給定“任何擴展自Fruit的對象”這一邊界之後,它所能知道的一切了。
如果能夠了解更多的信息,那麼你可以轉型到某種具體的Fruit類型,而這不會導致任何警告,但是你存在着得到ClassCastException的風險。set()方法不能工作於Apple或Fruit,因爲set()的參數也是”? Extends Fruit”,這意味着它可以是任何事物,而編譯器無法驗證“任何事物”的類型安全性。
但是,equals()方法工作良好,因爲它將接受Object類型而井非T類型的參數。因此,編譯器只關注傳遞進來和要返回的對象類型,它井不會分析代碼,以查看是否執行了任何實際的寫入和讀取操作。
15.10.2 逆變
還可以走另外一條路,即使用超類型通配符
。這裏,可以聲明通配符是由某個特定類的任何基類來界定的,方法是指定<? super MyClass>
,甚至或者使用類型參數:<? super T>
(儘管你不能對泛型參數給出一個超類型邊界;即不能聲明<T super MyClass>
)。這使得你可以安全地傳遞一個類型對象到泛型類型中。因此,有了超類型通配符,就可以向Collection寫入了:
//: generics/SuperTypeWildcards.java
import java.util.*;
public class SuperTypeWildcards {
static void writeTo(List<? super Apple> apples) {
apples.add(new Apple());
apples.add(new Jonathan());
// apples.add(new Fruit()); // Error
}
} ///:~
參數Apple是Apple的某種基類型的List,這樣你就知道向其中添加Apple或Apple的子類型是安全的。但是,既然Apple是下界,那麼你可以知道向這樣的List中添加Fruit是不安全的,因爲這將使這個List敞開口子,從而可以向其中添加非Apple類型的對象,而這是違反靜態類型安全的。
因此你可能會根據如何能夠向一個泛型類型“寫人”(傳遞給一個方法),以及如何能夠從一個泛型類型中“讀取”(從一個方法中返回),來着手思考子類型和超類型邊界。
超類型邊界放鬆了在可以向方法傳遞的參數上所作的限制:
//: generics/GenericWriting.java
import java.util.*;
public class GenericWriting {
static <T> void writeExact(List<T> list, T item) {
list.add(item);
}
static List<Apple> apples = new ArrayList<Apple>();
static List<Fruit> fruit = new ArrayList<Fruit>();
static void f1() {
writeExact(apples, new Apple());
// writeExact(fruit, new Apple()); // Error:
// Incompatible types: found Fruit, required Apple
}
static <T> void
writeWithWildcard(List<? super T> list, T item) {
list.add(item);
}
static void f2() {
writeWithWildcard(apples, new Apple());
writeWithWildcard(fruit, new Apple());
}
public static void main(String[] args) { f1(); f2(); }
} ///:~
writeExact()方法使用了一個確切參數類型(無通配符)。在f1()中,可以看到這工作良好——只要你只向List<Apple>
中放置Apple。但是,writeExact()不允許將Apple放置到List<Fruit>
中,即使知道這應該是可以的。
在writeWithWildcard()中,其參數現在是List<? super T>
,因此這個List將持有從T導出的某種具體類型,這樣就可以安全地將一個T類型的對象或者從T導出的任何對象作爲參數傳遞給List的方法。在f2()中可以看到這一點,在這個方法中我們仍舊可以像前面那樣,將Apple放置到List<Apple>
中,但是現在我們還可以如你所期望的那樣,將Apple放置到List<Fruit>
中。
我們可以執行下面這個相同的類型分析。作爲對協變和通配符的一個複習:
//: generics/GenericReading.java
import java.util.*;
public class GenericReading {
static <T> T readExact(List<T> list) {
return list.get(0);
}
static List<Apple> apples = Arrays.asList(new Apple());
static List<Fruit> fruit = Arrays.asList(new Fruit());
// A static method adapts to each call:
static void f1() {
Apple a = readExact(apples);
Fruit f = readExact(fruit);
f = readExact(apples);
}
// If, however, you have a class, then its type is
// established when the class is instantiated:
static class Reader<T> {
T readExact(List<T> list) { return list.get(0); }
}
static void f2() {
Reader<Fruit> fruitReader = new Reader<Fruit>();
Fruit f = fruitReader.readExact(fruit);
// Fruit a = fruitReader.readExact(apples); // Error:
// readExact(List<Fruit>) cannot be
// applied to (List<Apple>).
}
static class CovariantReader<T> {
T readCovariant(List<? extends T> list) {
return list.get(0);
}
}
static void f3() {
CovariantReader<Fruit> fruitReader =
new CovariantReader<Fruit>();
Fruit f = fruitReader.readCovariant(fruit);
Fruit a = fruitReader.readCovariant(apples);
}
public static void main(String[] args) {
f1(); f2(); f3();
}
} ///:~
與前面一樣,第一個方法readExact()使用了精確的類型。因此如果使用這個沒有任何通配符的精確類型,就可以向List中寫人和讀取這個精確類型。另外,對於返回值,靜態的泛型方法readExact()可以有效地“適應”每個方法調用,並能夠從List<Apple>
中返回一個Apple,從List<Fruit>
中返回一個Fruit,就像在f1()中看到的那樣。因此,如果可以擺脫靜態泛型方法,那麼當只是讀取時,就不需要協變類型了。
但是,如果有一個泛型類,那麼當你創建這個類的實例時,要爲這個類確定參數。就像在f2()中看到的,fruitReader實例可以從List<Fruit>
中讀取一個Fruit,因爲這就是它的確切類型。但是List<Apple>
還應該產生Fruit對象,而fruitReader不允許這麼做。
爲了修正這個問題,CovariantReader.readCovcariant()方法將接受List<? extends T>
,因此,從這個列表中讀取一個T是安全的(你知道在這個列表中的所有對象至少是一個T,並且可能是從T導出的某種對象)。在f3()中,你可以看到現在可以從List<Apple>
中讀取Fruit了。
15.10.3 無界通配符
無界通配符<?>
看起來意味着“任何事物”,因此使用無界通配符好像等價於使用原生類型。
事實上,編譯器初看起來是支持這種判斷的:
//: generics/UnboundedWildcards1.java
import java.util.*;
public class UnboundedWildcards1 {
static List list1;
static List<?> list2;
static List<? extends Object> list3;
static void assign1(List list) {
list1 = list;
list2 = list;
// list3 = list; // Warning: unchecked conversion
// Found: List, Required: List<? extends Object>
}
static void assign2(List<?> list) {
list1 = list;
list2 = list;
list3 = list;
}
static void assign3(List<? extends Object> list) {
list1 = list;
list2 = list;
list3 = list;
}
public static void main(String[] args) {
assign1(new ArrayList());
assign2(new ArrayList());
// assign3(new ArrayList()); // Warning:
// Unchecked conversion. Found: ArrayList
// Required: List<? extends Object>
assign1(new ArrayList<String>());
assign2(new ArrayList<String>());
assign3(new ArrayList<String>());
// Both forms are acceptable as List<?>:
List<?> wildList = new ArrayList();
wildList = new ArrayList<String>();
assign1(wildList);
assign2(wildList);
assign3(wildList);
}
} ///:~
有很多情況都和你在這裏看到的情況類似,即編譯器很少關心使用的是原生類型還是<?>
。在這些情況中,<?>
可以被認爲是一種裝飾,但是它仍舊是很有價值的,因爲,實際上,它是在聲明:“我是想用Java的泛型來編寫這段代碼,我在這裏並不是要用原生類型,但是在當前這種情況下,泛型參數可以持有任何類型。”
第二個示例展示了無界通配符的一個重要應用。當你在處理多個泛型參數時,有時允許一個參數可以是任何類型,同時爲其他參數確定某種特定類型的這種能力會顯得很重要:
//: generics/UnboundedWildcards2.java
import java.util.*;
public class UnboundedWildcards2 {
static Map map1;
static Map<?,?> map2;
static Map<String,?> map3;
static void assign1(Map map) { map1 = map; }
static void assign2(Map<?,?> map) { map2 = map; }
static void assign3(Map<String,?> map) { map3 = map; }
public static void main(String[] args) {
assign1(new HashMap());
assign2(new HashMap());
// assign3(new HashMap()); // Warning:
// Unchecked conversion. Found: HashMap
// Required: Map<String,?>
assign1(new HashMap<String,Integer>());
assign2(new HashMap<String,Integer>());
assign3(new HashMap<String,Integer>());
}
} ///:~
但是,當你擁有的全都是無界通配符時,就像在Map<?,?>
中看到的那樣,編譯器看起來就無法將其與原生Map區分開了。另外,UnboundedWildcards.java展示了編譯器處理List<?>
和List<? extends Object>
時是不同的。
令人困惑的是,編譯器並非總是關注像List和List<?>
之間的這種差異,因此它們看起來就像是相同的事物。因爲,事實上,由幹泛型參數將擦除到它的第一個邊界,因此List<?>
看起來等價於List<Object>
,而List實際上也是List<Object>
——除非這些語句都不爲真。List實際上表示“持有任何Object類型的原生List”,而List<?>
表示“具有某種特定類型
的非原生List,只是我們不知道那種類型是什麼。”
編譯器何時纔會關注原生類型和涉及無界通配符的類型之間的差異呢?下面的示例使用了前面定義的Holder<T>
類,它包含接受Holder作爲參數的各種方法,但是它們具有不同的形式作爲原生類型,具有具體的類型參數以及具有無界通配符參數:
//: generics/Wildcards.java
// Exploring the meaning of wildcards.
public class Wildcards {
// Raw argument:
static void rawArgs(Holder holder, Object arg) {
// holder.set(arg); // Warning:
// Unchecked call to set(T) as a
// member of the raw type Holder
// holder.set(new Wildcards()); // Same warning
// Can't do this; don't have any 'T':
// T t = holder.get();
// OK, but type information has been lost:
Object obj = holder.get();
}
// Similar to rawArgs(), but errors instead of warnings:
static void unboundedArg(Holder<?> holder, Object arg) {
// holder.set(arg); // Error:
// set(capture of ?) in Holder<capture of ?>
// cannot be applied to (Object)
// holder.set(new Wildcards()); // Same error
// Can't do this; don't have any 'T':
// T t = holder.get();
// OK, but type information has been lost:
Object obj = holder.get();
}
static <T> T exact1(Holder<T> holder) {
T t = holder.get();
return t;
}
static <T> T exact2(Holder<T> holder, T arg) {
holder.set(arg);
T t = holder.get();
return t;
}
static <T>
T wildSubtype(Holder<? extends T> holder, T arg) {
// holder.set(arg); // Error:
// set(capture of ? extends T) in
// Holder<capture of ? extends T>
// cannot be applied to (T)
T t = holder.get();
return t;
}
static <T>
void wildSupertype(Holder<? super T> holder, T arg) {
holder.set(arg);
// T t = holder.get(); // Error:
// Incompatible types: found Object, required T
// OK, but type information has been lost:
Object obj = holder.get();
}
public static void main(String[] args) {
Holder raw = new Holder<Long>();
// Or:
raw = new Holder();
Holder<Long> qualified = new Holder<Long>();
Holder<?> unbounded = new Holder<Long>();
Holder<? extends Long> bounded = new Holder<Long>();
Long lng = 1L;
rawArgs(raw, lng);
rawArgs(qualified, lng);
rawArgs(unbounded, lng);
rawArgs(bounded, lng);
unboundedArg(raw, lng);
unboundedArg(qualified, lng);
unboundedArg(unbounded, lng);
unboundedArg(bounded, lng);
// Object r1 = exact1(raw); // Warnings:
// Unchecked conversion from Holder to Holder<T>
// Unchecked method invocation: exact1(Holder<T>)
// is applied to (Holder)
Long r2 = exact1(qualified);
Object r3 = exact1(unbounded); // Must return Object
Long r4 = exact1(bounded);
// Long r5 = exact2(raw, lng); // Warnings:
// Unchecked conversion from Holder to Holder<Long>
// Unchecked method invocation: exact2(Holder<T>,T)
// is applied to (Holder,Long)
Long r6 = exact2(qualified, lng);
// Long r7 = exact2(unbounded, lng); // Error:
// exact2(Holder<T>,T) cannot be applied to
// (Holder<capture of ?>,Long)
// Long r8 = exact2(bounded, lng); // Error:
// exact2(Holder<T>,T) cannot be applied
// to (Holder<capture of ? extends Long>,Long)
// Long r9 = wildSubtype(raw, lng); // Warnings:
// Unchecked conversion from Holder
// to Holder<? extends Long>
// Unchecked method invocation:
// wildSubtype(Holder<? extends T>,T) is
// applied to (Holder,Long)
Long r10 = wildSubtype(qualified, lng);
// OK, but can only return Object:
Object r11 = wildSubtype(unbounded, lng);
Long r12 = wildSubtype(bounded, lng);
// wildSupertype(raw, lng); // Warnings:
// Unchecked conversion from Holder
// to Holder<? super Long>
// Unchecked method invocation:
// wildSupertype(Holder<? super T>,T)
// is applied to (Holder,Long)
wildSupertype(qualified, lng);
// wildSupertype(unbounded, lng); // Error:
// wildSupertype(Holder<? super T>,T) cannot be
// applied to (Holder<capture of ?>,Long)
// wildSupertype(bounded, lng); // Error:
// wildSupertype(Holder<? super T>,T) cannot be
// applied to (Holder<capture of ? extends Long>,Long)
}
} ///:~
在rawArgs()中,編譯器知道Holder是一個泛型類型,因此即使它在這裏被表示成一個原生類型,編譯器仍舊知道向set()傳遞一個Object是不安全的。由於它是原生類型,你可以將任何類型的對象傳遞給set(),而這個對象將被向上轉型爲Object。因此,無論何時,只要使用了原生類型,都會放棄編譯期檢查。對get()的調用說明了相同的問題:沒有任何T類型的對象,因此結果只能是一個Object。
人們很自然地會開始考慮原生Holder與Holder<?>
是大致相同的事物。但是unboundedArg()強調它們是不同的——它揭示了相同的問題,但是它將這些問題作爲錯誤而不是警告報告,因爲原生Holder將持有任何類型的組合,而Holder<?>
將持有具有某種具體類型的同構集合,因此不能只是向其中傳遞Object。
在exact1()和exact2()中,你可以看到使用了確切的泛型參數沒有任何通配符。你將看到,exact2()與exact1()具有不同的限制,因爲它有額外的參數。
在wildSubtype()中,在Holder類型上的限制被放鬆爲包括持有任何擴展自T的對象的Holder。這還是意味着如果T是Fruit,那麼Holder可以是Holder<Apple>
,這是合法的。爲了防止將Orange放置到Holder<Apple>
中,對set()的調用(或者對任何接受這個類型參數爲參數的方法的調用)都是不允許的。但是,你仍舊知道任何來自Holder<? extends Fruit>
的對象至少是Fruit,因此get()(或者任何將產生具有這個類型參數的返回值的方法)都是允許的。
wildSupertype()展示了超類型通配符,這個方法展示了與wildSubtype()相反的行爲:holder可以是持有任何T的基類型的容器。因此,set()可以接受T,因爲任何可以工作於基類的對象都可以多態地作用於導出類(這裏就是T)。但是,嘗試着調用get()是沒有用的,因爲由holde持有的類型可以是任何超類型,因此唯一安全的類型就是Object。
這個示例還展示了對於在unbounded()中使用無界通配符能夠做什麼不能做什麼所做出的限制。對於遷移兼容性,rawArgs()將接受所有Holder的不同變體,而不會產生警告。unbounded-Args()方法也可以接受相同的所有類型,儘管如前所述,它在方法體內部處理這些類型的方式並不相同。
如果向接受“確切”泛型類型(沒有通配符)的方法傳遞一個原生Holder引用,就會得到一個警告,因爲確切的參數期望得到在原生類型中井不存在的信息。如果向exact1()傳遞一個無界引用,就不會有任何可以確定返回類型的類型信息。
可以看到,exact2()具有最多的限制,因爲它希望精確地得到一個Holder<T>
,以及一個具有類型T的參數,正由於此,它將產生錯誤或警告,除非提供確切的參數。有時,這樣做很好,但是如果它過於受限,那麼就可以使用通配符,這取決於是否想要從泛型參數中返回類型確定的返回值(就像在wildSubtype()中看到的那樣),或者是否想要向泛型參數傳遞類型確定的參數(就像在wildSupertype()中看到的那樣)。
因此,使用確切類型來替代通配符類型的好處是,可以用泛型參數來做更多的事,但是使用通配符使得你必須接受範圍更寬的參數化類型作爲參數。因此,必須逐個情況地權衡利弊,找到更適合你的需求的方法。
15.10.4 捕獲轉換
有一種情況特別需要使用<?>
而不是原生類型。如果向一個使用<?>
的方法傳遞原生類型,那麼對編譯器來說,可能會推斷出實際的類型參數,使得這個方法可以迴轉並調用另一個使用這個確切類型的方法。下面的示例演示了這種技術,它被稱爲捕獲轉換,因爲未指定的通配符類型被捕獲,並被轉換爲確切類型。這裏,有關警告的註釋只有在@SuppressWarnings註解被移除之後才能起作用:
//: generics/CaptureConversion.java
public class CaptureConversion {
static <T> void f1(Holder<T> holder) {
T t = holder.get();
System.out.println(t.getClass().getSimpleName());
}
static void f2(Holder<?> holder) {
f1(holder); // Call with captured type
}
@SuppressWarnings("unchecked")
public static void main(String[] args) {
Holder raw = new Holder<Integer>(1);
// f1(raw); // Produces warnings
f2(raw); // No warnings
Holder rawBasic = new Holder();
rawBasic.set(new Object()); // Warning
f2(rawBasic); // No warnings
// Upcast to Holder<?>, still figures it out:
Holder<?> wildcarded = new Holder<Double>(1.0);
f2(wildcarded);
}
} /* Output:
Integer
Object
Double
*///:~
f1()中的類型參數都是確切的,沒有通配符或邊界。在f2()中,Holder()參數是一個無界通配符,因此它看起來是未知的。但是,在f2()中,f1()被調用,而f1()需要一個已知參數。這裏所發生的是:參數類型在調用f2()的過程中被捕獲,因此它可以在對f1()的調用中被使用。
你可能想知道,這項技術是否可以用於寫入,但是這要求要在傳遞Holder<?>
時同時傳遞一個具體類型。捕獲轉換隻有在這樣的情況下可以工作:即在方法內部,你需要使用確切的類型。注意,不能從f2()中返回T,因爲T對於f2()來說是未知的。捕獲轉換十分有趣,但是非常受限。
15.11問題
本節將闡述在使用Java泛型時會出現的各類問題。
15.11.1任何基本類型都不能作爲類型參數
正如本章早先提到過的,你將在Java泛型中發現的限制之一是,不能將基本類型用作類型參數。因此不能創建ArrayList<int>
之類的東西。
解決之道是使用甚本類型的包裝器類以及Java SE5的自動包裝機制。如果創建一個ArrayList<Integer>
,並將基本類型int應用於這個容器,那麼你將發現自動包裝機制將自動地實現int到Integer的雙向轉換——因此,這幾乎就像是有一個ArrayList<int>
一樣:
//: generics/ListOfInt.java
// Autoboxing compensates for the inability to use
// primitives in generics.
import java.util.*;
public class ListOfInt {
public static void main(String[] args) {
List<Integer> li = new ArrayList<Integer>();
for(int i = 0; i < 5; i++)
li.add(i);
for(int i : li)
System.out.print(i + " ");
}
} /* Output:
0 1 2 3 4
*///:~
注意,自動包裝機制甚至允許用foreach語法來產生int。
通常,這種解決方案工作得很好—能夠成功地存儲和讀取int,有一些轉換碰巧在發生的同時會對你屏蔽掉。但是,如果性能成爲了問題,就需要使用專門適配基本類型的容器版本。
//: generics/ByteSet.java
import java.util.*;
public class ByteSet {
Byte[] possibles = { 1,2,3,4,5,6,7,8,9 };
Set<Byte> mySet =
new HashSet<Byte>(Arrays.asList(possibles));
// But you can't do this:
// Set<Byte> mySet2 = new HashSet<Byte>(
// Arrays.<Byte>asList(1,2,3,4,5,6,7,8,9));
} ///:~
注意,自動包裝機制解決了一些問題,但並不是解決了所有問題。下面的示例展示了一個泛型的Generator接口,它指定next()方法返回一個具有其參數類型的對象。FArray類包含一個泛型方法,它通過使用生成器在數組中填充對象(這使得類泛型在本例中無法工作,因爲這個方法是靜態的)。Generator實現來自第16章,並且在main()中,可以看到FArray.fill()使用它在數組中填充對象:
//: generics/PrimitiveGenericTest.java
import net.mindview.util.*;
// Fill an array using a generator:
class FArray {
public static <T> T[] fill(T[] a, Generator<T> gen) {
for(int i = 0; i < a.length; i++)
a[i] = gen.next();
return a;
}
}
public class PrimitiveGenericTest {
public static void main(String[] args) {
String[] strings = FArray.fill(
new String[7], new RandomGenerator.String(10));
for(String s : strings)
System.out.println(s);
Integer[] integers = FArray.fill(
new Integer[7], new RandomGenerator.Integer());
for(int i: integers)
System.out.println(i);
// Autoboxing won't save you here. This won't compile:
// int[] b =
// FArray.fill(new int[7], new RandIntGenerator());
}
} /* Output:
YNzbrnyGcF
OWZnTcQrGs
eGZMmJMRoE
suEcUOneOE
dLsmwHLGEa
hKcxrEqUCB
bkInaMesbt
7052
6665
2654
3909
5202
2209
5458
*///:~
由於RandomGenerator.Integer實現了Generator<Integer>
,所以我的希望是自動包裝機制可以自動地將next()的值從Integer轉換爲int。但是,自動包裝機制不能應用於數組,因此這無法工作。
15.11.2實現參數化接口
一個類不能實現同一個泛型接口的兩種變體,由於擦除的原因,這兩個變體會成爲相同的接口。下面是產生這種衝突的情況:
//: generics/MultipleInterfaceVariants.java
// {CompileTimeError} (Won't compile)
interface Payable<T> {}
class Employee implements Payable<Employee> {}
class Hourly extends Employee
implements Payable<Hourly> {} ///:~
Hourly不能編譯,因爲擦除會將Payable<Employee>
和Payable<Hourly>
簡化爲相同的類Payable,這樣,上面的代碼就意味着在重複兩次地實現相同的接口。十分有趣的是,如果從Payable的兩種用法中都移除掉泛型參數〔就像編譯器在擦出階段所做的那樣)這段代碼就可以編譯。
在使用某些更基本的Java接口,例如Comparable時,這個問題可能會變得十分令人惱火,就像你在本節稍後就會看到的那樣。
15.11.3轉型和警告
使用帶有泛型類型參數的轉型或instanceof不會有任何效果。下面的容器在內部將各個值存儲爲Object,並在獲取這些值時,耳將它們轉型回T:
//: generics/GenericCast.java
class FixedSizeStack<T> {
private int index = 0;
private Object[] storage;
public FixedSizeStack(int size) {
storage = new Object[size];
}
public void push(T item) { storage[index++] = item; }
@SuppressWarnings("unchecked")
public T pop() { return (T)storage[--index]; }
}
public class GenericCast {
public static final int SIZE = 10;
public static void main(String[] args) {
FixedSizeStack<String> strings =
new FixedSizeStack<String>(SIZE);
for(String s : "A B C D E F G H I J".split(" "))
strings.push(s);
for(int i = 0; i < SIZE; i++) {
String s = strings.pop();
System.out.print(s + " ");
}
}
} /* Output:
J I H G F E D C B A
*///:~
如果沒有@SuppressWarnings註解,編譯器將對pop()產生“unchecked cast’警告。由於擦除的原因,編譯器無法知道這個轉型是否是安全的,並且pop()方法實際上並沒有執行任何轉型。這是因爲,T被擦除到它的第一個邊界,默認情況下是Object, 因此pop()實際上只是將Object轉型爲Object。
有時,泛型沒有消除對轉型的需要,這就會由編譯器產生警告,而這個警告是不恰當的。例如:
//: generics/NeedCasting.java
import java.io.*;
import java.util.*;
public class NeedCasting {
@SuppressWarnings("unchecked")
public void f(String[] args) throws Exception {
ObjectInputStream in = new ObjectInputStream(
new FileInputStream(args[0]));
List<Widget> shapes = (List<Widget>)in.readObject();
}
} ///:~
正如你將在下一章學到的那樣,readObject()無法知道它正在讀取的是什麼,因此它返的是必須轉型的對象。但是當註釋掉@SuppressWarnings註解,並編譯這個程序時,就會得到下面的警告:
Note: NeedCasting.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
如果遵循這條指示,使用-Xliint: unchecked來重新編譯:
NeedCasting.java:12: warning: [unchecked] unchecked cast found:java.lang.Object
required: java.util.List<Widget>
List<Shape> shapes = (List<Widget>)in.readObjlect():
你會被強制要求轉型,但是又被告知不應該轉型。爲了解決這個問題,必須使用在Java SE5中引入的新的轉型形式,既通過泛型類來轉型:
//: generics/ClassCasting.java
import java.io.*;
import java.util.*;
public class ClassCasting {
@SuppressWarnings("unchecked")
public void f(String[] args) throws Exception {
ObjectInputStream in = new ObjectInputStream(
new FileInputStream(args[0]));
// Won't Compile:
// List<Widget> lw1 =
// List<Widget>.class.cast(in.readObject());
List<Widget> lw2 = List.class.cast(in.readObject());
}
} ///:~
但是,不能轉型到實際類型(List<Widget>)
。也就是說,不能聲明:
List<Widget>.class.cast(in.readObject())
甚至當你添加一個像下面這樣的另一個轉型時:
(List<Widget>)List.class.cast(in.readObject())
仍舊會得到一個警告。
15.11.4重裁
下面的程序是不能編譯的,即使編譯它是一種合理的嘗試:
//: generics/UseList.java
// {CompileTimeError} (Won't compile)
import java.util.*;
public class UseList<W,T> {
void f(List<T> v) {}
void f(List<W> v) {}
} ///:~
由於擦除的原因,重載方法將產生相同的類型簽名。
與此不同的是,當被擦除的參數不能產生唯一的參數列表時。必須提供明顯有區別的方法名:
//: generics/UseList2.java
import java.util.*;
public class UseList2<W,T> {
void f1(List<T> v) {}
void f2(List<W> v) {}
} ///:~
幸運的是,這類問題可以由編譯器探測到。
15.11.5 基類劫持了接口
假設你有一個Pet類,它可以與其他的Pet對象進行比較(實現了Comparable接口):
//: generics/ComparablePet.java
public class ComparablePet
implements Comparable<ComparablePet> {
public int compareTo(ComparablePet arg) { return 0; }
} ///:~
對可以與ComparablePet的子類比較的類型進行窄化是有意義的,例如,一個Cat對象就只能與其他的Cat對象比較:
//: generics/HijackedInterface.java
// {CompileTimeError} (Won't compile)
class Cat extends ComparablePet implements Comparable<Cat>{
// Error: Comparable cannot be inherited with
// different arguments: <Cat> and <Pet>
public int compareTo(Cat arg) { return 0; }
} ///:~
遺憾的是,這不能工作。一旦爲Comparable確定了ComparablePet參數,那麼其他任何實現類都不能與ComparablePet之外的任何對象比較:
//: generics/RestrictedComparablePets.java
class Hamster extends ComparablePet
implements Comparable<ComparablePet> {
public int compareTo(ComparablePet arg) { return 0; }
}
// Or just:
class Gecko extends ComparablePet {
public int compareTo(ComparablePet arg) { return 0; }
} ///:~
Hamster說明再次實現ComparablePet中的相同接口是可能的,只要她們精確地相同,包括參數類型在內。但是,這只是與覆蓋基類中的方法相同,就像在Geeko中看到的那樣。