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
*///:~