effectiveJava學習筆記:泛型(二)
優先考慮泛型
使用泛型比使用需要在客戶端程式碼中進行裝換的型別來的更加安全,也更加容易。在設計新型別的時候,要確保他們不需要這種裝換就可以使用。這通常意味著要不類做出泛型的,只要時間允許,就把現有的型別都泛型化。這對於型別的新使用者來說會變得更加輕鬆,更不會破壞現有的客戶端。
編寫自己的泛型會比較困難一些,但是值得花些時間去學習如何編寫。
一般來說,將集合宣告引數化,以及使用JDK所提供的泛型和泛型方法,這些都不太困難,編寫自己的泛型會比較困難一些,但是值得花些時間去學習如何編寫。
考慮第6條中這個簡單的堆疊實現:
// Object -based Collection - a prime candidate for generics public class Stack{ private Object[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; public Stack() { elements = new E[DEFAULT_INITIAL_CAPACITY]; } public void push(Object e) { ensureCapacity(); elements[size++] = e; } public Objcet pop() { if (size == 0) throw new EmptyStackException(); Objcet result = elements[--size]; elements[size] = null; return result; } public boolean isEmpty() { return size == 0; } private void ensureCapacity() { if (elements.length == size) elements = Arrays.copyOf(elements, 2 * size + 1); } }
這個類是泛型化的主要備選物件,換句話說,可以適當的強化這個類來利用泛型。根據例項情況來看,必須轉換從堆疊裡彈出的物件,以及可能執行時失敗的那些轉換。
將類泛型化的第一個步驟是給他的宣告新增一或者多個型別引數,在這個例子中有一個型別引數,他表示堆疊的元素型別,這個引數的名稱通常為E。
下一步用相應的型別引數替換所有的Object型別,讓後試著編譯最終的程式:
// Initial attempt to generify Stack = won't compile! public class Stack<E> { private E[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; public Stack() { elements = new E[DEFAULT_INITIAL_CAPACITY]; } public void push(E e) { ensureCapacity(); elements[size++] = e; } public E pop() { if (size == 0) throw new EmptyStackException(); E result = elements[--size]; elements[size] = null; return result; } ...// no changes is isEmpty or ensureCapacity }
通常,你將至少得到一個錯誤或警告,這個類也不例外。幸運的是,這個類只產生一個錯誤,如下:
Stack.java 8: generic array creation
elements =new E[DEFAULT_INITIAL_CAPACITY];
如25條條所述,你不能建立不可具體化的型別的陣列,如E。沒當編寫用陣列支援的泛型時,都會出現這個問題。
解決這個問題有兩種方法:
1.直接繞過建立泛型陣列的禁令:建立一個Object的陣列,並將它裝換泛型陣列型別。
錯誤是消除了,但是編譯器會產生一條警告。這種用法是合法的。但不是型別安全。
Stack.java:8 warning:[unchecked] unchecked cast
found : Object[].required:E[]
elments =(E()) new Objcet[DEFAULT_INITIAL_CAPACITY]
編譯器不可能證明你的程式是型別安全的,但是你可以證明。你自己必須確保未受檢的轉換不會危及到程式的型別安全性。相關的資料儲存一個在私有的域中,永遠不會被返回到客戶端,或者傳給任何其他方法。這個陣列中儲存的唯一元素,是傳給push方法的那些元素,它們的型別為E,因此未受檢的轉換不會有任何危害。
一旦你證明了未受檢的轉換時安全的,就要在儘可能小的範圍禁止警告,在這種情況下,構造器只包含未受檢的陣列建立,因此可以在整個構造器中禁止這條警告。通過增加一條註解來完成禁止,Stack能夠正確無誤的進行編譯,你就可以使用它了,無需顯式的轉換,也無需擔心會出現ClassCastException異常:
//The elements array will contain only E instances from push(E).
//This is sufficient to ensure type safety,but the runtime
//type of the array won't be E[];it will always be Object[]!
@SuppressWarnings("unchecked")
public Stack(){
elements = (E[])new Object[DEFAULT_INITIAL_CAPACITY];
2.將elements域的型別從E[]改為Object[]。這麼做會得到一條不同的錯誤:
Stack.java 8: incompatible types
found : Object, required:ECField
E result=elements[--size];
通過把從陣列中獲取到的元素由Object轉換成E,可以將這條錯誤變成一條警告:
Stack.java 19: warning : [unchecked] unchecked cast
E result=elements[--size];
由於E是一個不可具體化的型別,編譯器無法再執行時檢驗轉換。你是可以自己證實未受檢的轉換是安全的,因此可以禁止該警告。根據第24條的建議,我們只要在包含未受檢轉換的任務上禁止警告,而不是在整個pop方法上就可以了,如下:
//Appropriate suppression of unchecked warning
public E pop(){
if(size==0)
throw new EmptyStackException();
//push requires elements to be of type E,so cast is correct
@SuppressWarnings("unchecked")
E result=(E) elements[--size];
elements[size]=null;//Eliminate obsolete reference
return result;
具體選擇哪一種方法來處理泛型陣列建立錯誤,則看個人的偏好。但是禁止陣列型別的未受檢比禁止標量型別的更加危險,所以採用第二種方案。但是在比Stac更實際的泛型類中,或許程式碼中會有多個地方需要從陣列中讀取元素,因此選擇第二種方案需要多次裝換成E,而不是隻裝換E[],這也是第一種方案之所以更常用的原因。
下面的程式示範了泛型Stack類的使用,程式以相反的順序打印出他的命令列引數,並裝換成大寫字母。如果要在從堆疊中彈出的元素上呼叫String的toUpperCase,並不需要顯示的轉換,並且會確保自動生成的轉換會成功:
//Little program to exercise our generic Stack
public static void main(String[] args){
Stack<String> stact=new Stack<String>();
for(String arg:args)
stack.push(arg);
while(!stack.isEmpty())
System.out.println(stack.pop.toUpperCase());
}
上述示例與第25條相矛盾,第25條鼓勵優先使用列表而非陣列。實際上並不可能或者總想在泛型中使用列表。java並不是生來就支援列表,因此有些泛型如ArrayLis,則必須在陣列上實現。為了提升效能,其他泛型如HashMap也在陣列上實現。
絕大多數泛型就像我夢Stack示例一樣,因為它們的型別引數沒有限制:你可以建立Stack< Object>、Stack< int[]>、Stack< List< String>>,或者任何其他物件引用型別的Stack。注意不能建立基本型別的Stack:企圖建立Stack< int>或者Stack< double>會產生一個編譯器錯誤。這是Java泛型系統根本的侷限性。你可以通過使用基本包裝型別來避開這條限制。
有一下泛型限制了可允許的型別引數值。例如,考慮javautil.concurrent.DelayQueue,其宣告如下:
class DelayQueue<E extends Delayed> implements BlockingQueue<E>;
型別引數列表要求實際的型別引數E必須是java,util.concurrent.Delayed的一個子型別,它允許DelayQueue實現及其客戶端在DelayQueue元素上利用Delayed方法,無需顯示的轉換,也沒有出現ClassCastException的風險。型別引數E被稱作有限制的型別引數。注意,子型別關係確認了每個型別都是都是它自身的子型別,因此建立DelayQueue< Delayed>是合法的。
優先考慮泛型方法
泛型方法就像泛型一樣,使用起來比要求客戶端轉換輸入引數並返回值的方法來的更加安全,也更加容易。就像型別一樣,你應該確保新的方法可以不用轉換就能使用,這通常意味著要將它們泛型化。並且就像型別一樣,還應該將現有的方法泛型化,使新使用者使用起來更加輕鬆,且不會破壞現有的客戶端。
就如類可以從泛型中受益一般,方法也一樣。靜態工具方法尤其適合於泛型化。
編寫泛型方法與編寫型別型別相類似。
例:他返回兩個集合的聯合:
// Users raw types - unaccepable!
public static Set union(Set s1, Set s2) {
Set result = new HashSet(s1);
result.addAll(s2);
return result;
}
這個方法可以編譯,但是有兩條警告:
Unioc.java:5:warning:[unchecked] unchecked call to
HastSet(Collection< ? extends E> as a member fo raw type HastSet
Set result = new HashSet(s1);
Unioc.java:5:warning:[unchecked] unchecked call to
addAll(Collection< ? extends E> as a member fo raw type HastSet
result.addAll(s2);
為了修正這些警告,使方法變成型別安全的,要將方法聲名修改為宣告一個型別引數,表示這三個元素型別(兩個引數及一個返回值),並在方法中使用型別引數。聲名型別引數的型別引數列表,處在方法的修飾符及其返回型別之間。在這個例項中,型別引數列表為< E>,返回型別為Set< E>。型別引數的命名慣例與泛型方法以及泛型的相同。
// Generic method
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<E>(s1);
result.addAll(s2);
return result;
}
至少對於簡單的泛型方法而言,就是這麼回事了。現在改方法編譯時不會產生任何警告,並提供了型別安全性,也更容易使用。以下是一個執行該方法的簡單程式。程式不包含裝換,編譯時不會有錯誤或者警告:
//Simple program to exercise generic method
public static void main(String[] args){
Set<String> guys =new HashSet<String>{
Array.asList("Tom","Dick","Harry"));
Set<String> stooges =new HashSet<String>{
Array.asList("Larry","Moe","Curly"));
Set<String> aflCio=unioc(guys,stooges);
System.out.printle(aflCio);
}
}
執行這段程式是,會列印 [Moe,Harry,Tom,Curly,Larry,Dick]。 元素的順序是依賴於實現的。
union方法侷限在於,三個集合的型別(兩個輸入引數及一個返回值)必須全部相同。利用有限制的萬用字元型別可以使這個方法變得更回靈活。
泛型方法的一個顯著特徵是,無需明確指定型別引數的值,不像呼叫泛型構造器的時候是必須指定的。對於上述程式而言,編譯器發現uniond的兩個引數都是Set< String>型別,因此知道型別引數E必須為String,這個過程稱作為型別推導。
如第一條所述,可以利用泛型方法呼叫所提供的型別推導,是建立引數化型別例項的過程變得更加輕鬆。提醒一下:在呼叫泛型構造器的時候,要明確傳遞型別引數的值可能有點麻煩。型別引數出現在了變數的宣告的左右兩邊,顯得冗餘:
// Parameterized type instance creation with constructor
Map<String, List<String>> anagrams =
new HashMap<String, List<String>>();
為了消除冗餘,可以編寫一個泛型靜態工廠方法,與想要使用的每個構造器相對應。例如,下面是一個無參的HashMap構造器相對應的泛型靜態工廠方法:
// Generic static factory method
public static <K, V> HashMap<K, V> newHashMap() {
return new HashMap<K, V>();
}
通過這個泛型靜態工廠方法,可以用下面這段簡潔的大碼來取代上面那個重複的宣告:
//Parameterized type instance creation with static factory
Map<String,List<String>> anagrans=newHashMap();
相關的模式泛型單例工廠。有時,會需要建立不可變但又適合於許多不同型別的物件。由於泛型是通過擦除來實現的,可以給所有的必要的型別引數使用同一個單個物件,但是需要編寫一個靜態的工廠方法,重複地給每個必要的型別引數分發物件。這種模式叫做“泛型單例工廠”,這種模式最常用於函式物件。如Collections.reverseOrder,但也適用於像Collections.emptySet這樣的集合。
假設有一個介面,描述了一個方法,該方法接受和返回某個型別T的值:
public interface UnaryFunction<T> {
T apply(T arg);
}
現在假設要提供一個恆等函式。如果 在每次需要的時候都重新建立一個,這樣會很浪費,因為它是無狀態的。如果泛型被具體化,每個型別都需要一個桓等函式,但是它們被擦除以後,就只需要一個泛型單例。請看以下示例:
// Generic singleton factory pattern
private static UnaryFunction<Object> INDENTITY_FUNCTION =
new UnaryFunction<Object> {
public Object apply(Object arg) { return arg; }
};
// IDENTITY_FUNCTION is stateless and its type parameter is
// unbounded so it's safe to share one instance across all types.
@SuppressWarnings("unchecked")
public static <T> UnaryFunction<T> identityFunction() {
return (UnaryFunction<T>)INDENTITY_FUNCTION;
}
IDENTITY_FUNCTION裝換成(UnaryFunction< T>),產生一條為受檢的裝換警告。因為UnaryFunction< Object>對於每個T來說並非額每個都是UnaryFunction< T>。但是恆等函式很特殊;他返回未被修改的引數,因此我們知道無論T的值是什麼,用它作為UnaryFunction< T>都是型別安全的。因此:我們可以放心地禁止由這個裝換所產生的未受檢轉換警告。一旦禁止,程式碼在編譯時就不會出現任何錯誤或者警告。
以下是一個範例程式,利用泛型單例作為UnaryFunction< String>和UnaryFunction< Number>。像往常一樣,它不包含,編譯時沒有出現錯誤或者警告:
//Simple program to exercise generic singleton
public static void main(String[] args){
String[] strings ={"jute","hemp","nylon"};
UnaryFunction<String> sameString=identityFunction();
for(String s:strings)
System.out.printle(sameString.addly(s);
Number[] numbers={1,2.0,3L};
UnaryFunction<Number> sameNumber=identityFunction();
for(Number n:numbers)
System.out.printle(sameNumber.addly(n);
}
}
雖然相對少見,但是通過某個包含該型別引數本身的表示式來限制類型引數是允許的。這就是遞迴型別限制。遞迴型別限制最普遍的用途與Comparable介面有關,它定義型別的自然順序:
public interface Comparable<T>{
int compareTo(T o);
}
型別引數T定義的型別,可以與實現Comparable< T> 的型別的元素進行比較。實際上,幾乎所有型別都只能與他們自身的型別的元素相比較。因此,例如String實現Comparable< String>,Integer實現Comparable< Integer>,等等。
許多方法都帶有一個實現Comparable介面的元素列表,並在其中進行搜尋,計算出它的最小值或者最大值,等等。要完成這其中的任何一項工作,要求列表中的每個元素都能夠與列表中的其他元素相比較,換句話說,列表的元素可以相互比較。下面是如何表達這種約束條件的一個示例:
// Using a recursive type bound to express mutual comparability
public static <T extends Comparable<T>> T max(List<T> list) {
...
型別限制< T extends Comparable>,可以讀作“針對可以與自身進行比較的每個型別T”,這與互比性的概念或多或少有一些一致。
下面的方法就帶有上述宣告。它根據元素的自然順序計算列表的最大值,編譯時沒有出現錯誤或者警告:
//Returns the maximun value in a list - uses recursive type bound
public static <T extends Comparable<T>> T max(List<T> list){
Iterator<T> i=list.iterator();
T result=i.next();
while(i.hasNext){
T t=i.next();
if(t.compareTo(result)>0)
result=t;
}
return result;
}
遞迴型別限制可能比這個要複雜得多,但幸運的是,這種情況並不經常發生。如果你理解這種習慣用法以及其萬用字元變數,就能夠處理在實踐中遇到的許多遞迴型別限制了。
利用有限制萬用字元來提升API的靈活性
如果引數化型別表示一個T生產者,就使用<? extends T>,如果表示一個T的消費者,就使用<? super T>。
為什麼要這些?
上面的Stack類
現在需要增加一個方法:
public void pushAll(Iterable<E> src){
src.forEach(e->push(e));
}
這個方法在編譯的時候正確無誤,但是也不能總是正確,比如:
public static void main(String[] args) {
Stack<Number> s = new Stack<>();
List<Integer> list = new ArrayList<>();
s.pushAll(list);
}
在這種情況下,由於泛型的不可變性,導致不能新增,編譯無法通過,但是從理解層面上來說,這應該是被允許的。number是可以接受integer型別的
增加方法的靈活性,可以這樣編寫:
// Wildcard type for parameter that serves as an E producer
public void pushAll(Iterable<? extends E> src) {
for (E e: src)
push(e);
}
與pushAll相對應的,我們在新增一個popAll 方法:
public void popAll(Collection<E> dst){
while(!isEmpty())
dst.add(pop());
}
和上面方法相似,這個方法初一看並沒有什麼不妥。但是並不總是正確:
比如我想傳遞一個List<Object>進去接收,就像這樣:
public static void main(String[] args) {
Stack<Number> s = new Stack<>();
List<Object> list = new ArrayList<>();
s.popAll(list);
}
同樣編譯無法通過,Collection<Number> c = new ArrayList<Object>()這是錯誤的:
但是從實際角度出發,這應該是被允許的,List<Object> 列表是可以新增Number型別的,所以這個方法依然有漏洞:
這個時候可以修改如下:
public void popAll(Collection<? super E> dst){
while(!isEmpty())
dst.add(pop());
}
這樣的話,編譯可以通過,而且型別也是安全的:
結論很明顯。為了獲得最大限度的靈活性,要在表示生產者或者消費者的輸入引數上使用統配符型別。如果某個輸入引數既是生產者也是消費者,那麼統配符就不在適用了。
對 union進行修改:
public static <E> Set<E> union(Set<E> s1,Set s2){
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
由於s1,s2,對於整個類來說是屬於生產者,所以應該用extends:
public static <E> Set<E> union(Set<? extends E > s1,Set<? extends E> s2){
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
注意返回型別依然是set<E>.不要用萬用字元型別作為返回型別。除了為使用者提供額外額靈活性外,它也會要求使用者必須使用萬用字元型別。
統配符型別對於使用者來說應該是無形的,如果使用者必須考慮萬用字元型別,類的API或許就會出錯。
有這樣一個方法:
public static <T extends Comparable<T>> T max(List<T> list){
Iterator<T> i = list.iterator();
T result = i.next();
while(i.hasNext()){
T t = i.next();
if(t.compareTo(result) > 0)
result = t;
}
return result;
}
根據原則該如何修改呢?
public static <T extends Comparable<? super T>> T max(List<? extends T> list){
Iterator<? extends T> i = list.iterator();
T result = i.next();
while(i.hasNext()){
T t = i.next();
if(t.compareTo(result) > 0)
result = t;
}
return result;
}
我們來分析一下:
list中的泛型,對於類來說,無疑是生產者,生產出最大值,所以應該是extends
comparable中的泛型,對於整個類來說,是用來消費產生順序關係的,所以應該用super
針對於一下方法:
public static <E> void swap(List<E> list,int i,int j){};
public static void swap(List<?> list,int i,int j);
這兩種方法,那種方法更好呢?第二種會更好一些,因為它更加簡單,在整個靜態方法中,泛型其實只出現了一次,,這種情況下,是可以用萬用字元取代它的。但是第二個方法有一個問題:
public static void swap(List<?> list,int i,int j){
list.set(i,list.set(j,list.get(i)));
};
無法編譯,這裡可以使用一個輔助方法來捕捉萬用字元型別:
public static void swap(List<?> list,int i,int j){
swapHelper(list,i,j);
};
private static <E> void swapHelper(List<E> list,int i,int j){
list.set(i,list.set(j,list.get(i)));
}
這樣既能提供簡潔的api,也能達到捕獲萬用字元的目的
優先考慮型別安全的異構容器
集合API說明泛型的用法:限制容器只能由固定數目的型別引數。
可以通過將型別引數放在鍵上而不是容器上來避開這一限制
對於型別安全的異構容器,可以用Class物件作為鍵,以這種方式使用的Class物件稱作型別令牌
我們建立一個Favorite類來模擬這種情況
public class Favorites {
private Map<Class<?>, Object> favoties = new HashMap<Class<?>, Object>();
public <T> void putFavorite(Class<T> type, T instance) {
if (type == null) {
throw new NullPointerException("Type is Null");
}
favoties.put(type, instance);
}
public <T> T getFavorite(Class<T> type) {
return type.cast(favoties.get(type));
}
}
class方法返回的是一個class<T>的形式。Favorites是型別安全的,它總是按照鍵返回正確的值,同時它也是異構的,因為它的容器的鍵不是同一種類型,這有別於傳統的Map,因此,將Favorites稱作型別安全的異構容器。異構來自哪?答案是無限制萬用字元的鍵Class<?>,在這裡它僅代表是某種class,因此允許將不同類的class放入同一個Map,這就是異構的原因。注意,因為我們使用的值型別是Object,因此Map並無法保證鍵一對能對應正確的值,它只知道值是一個Object就可以了,這種對應的關係是實現者自己來確保的。手動重新建立型別與值之間的關係是在getFavorite方法中進行的,利用Class的cast方法,將物件動態地轉換成Class物件所表示的型別。cast只是檢驗它的引數是不是為Class物件所表示的型別的例項。
Favorites類有兩種侷限:一是惡意使用者可以通過使用原生態形式的Class來破壞年Favorites例項的型別安全。這種方式可以通知在putFavorite中進行型別檢查來確保例項物件進行檢查。
public <T> void putFavorite(Class<T> type, T instance) {
if (type == null)
throw new NullPointerException("Type is null");
favorites.put(type, type.cast(instance);
}
第二個侷限性在於它不能用在不可具體化的型別中。比如說可以儲存喜愛的String,String[],但是不能儲存List<String>。因為 List<String>.class是語法錯誤。因為在執行時他們的型別會被擦除,所在List<String>與List<Integer>實際上是共用一個Class。如果需要限制些可以傳遞給方法的型別,則可以使用有限制的萬用字元型別。
public <T extends Annotation>
T getAnnotation(Class<T> annotationType);
在這面這段程式碼裡,如果想把一個Class<?>傳遞給getAnnotation方法,那麼按照要求,可能想到可以將其轉換成Class<? extends Annotation>,但是這種行為是非受檢的,會收到編譯器的警告,但是,可以利用Class類提供的一個安全且動態地執行這種轉換的例項方法,asSubclass,它將呼叫它的Class物件轉換成用其引數表示的類的一個子型別,如果轉換成功,該方法就返回它的引數,如果失敗則丟擲ClassCastException。見如下例子:
public <T extends Annotation>
T getAnnotation(Class<T> annotationType);
// Use of asSubclass to safely cast to a bounded type token
static Annotation getAnnocation(AnnocationElement element,
String annotationTypeName) {
Class<?> annotationType = null;// Unbounded type token
try {
annotationType = Class.forName(annotationTypeName);
} catch (Exception ex) {
throw new IllegalArgumentException(ex);
}
return element.getAnnotation {
annotationType.asSubclass(Annotation.class);
}
}