《Java編程思想》第四版讀書筆記 第十五章 泛型

15.4 泛型方法java

除了將泛型應用於整個類,還能夠在類中包含泛型化方法,而這個方法所在的類能夠是泛型類也能夠不是泛型類。apache

做者推薦的一個基本指導原則:不管什麼時候,只要能作到,就應該儘可能使用泛型方法。也就是說,若是使用泛型方法能夠取代將整個類泛型化,那就應該只使用泛型方法。編程

對於一個static方法沒法訪問泛型類的類型參數,因此若是static方法須要使用泛型能力就必須使其成爲泛型方法。設計模式

要定義泛型方法,只需將泛型參數列表置於返回值以前。數組

當使用泛型類時必須在建立對象的時候指定類型參數的值,而使用泛型方法的時一般沒必要指明參數類型,由於編譯器會爲咱們找出具體的類型,這稱爲類型參數推斷。安全

15.4.1小節做者討論了添加泛型參數後形成代碼增多:app

Map> petPeople = new HashMap>();編程語言

做者使用了泛型方法來消除後面的泛型聲明。函數

在1.7中加入了類型推斷的新特性解決了上面的問題:ui

Map> petPeople = new HashMap<>();

在使用泛型方法時能夠顯示指明類型,在點操做符與方法名之間插入尖括號,而後把類型置於尖括號內。若是是在定義該方法的類的內部,必須在點操做符以前使用this關鍵字,若是是使用static方法,必須在點操做符以前加上類名:

New.>map();

泛型與可變長參數能夠結合使用:

makeList(T ... args);

做者在15.4.5小節例子寫到:

public class Tuple {
    public static  Twopule tuple(A a, B b) {
       return new TwoTuple(a, b);
    }
}

public class TupleTest2 {
    static TwoTuple f() {
       return tuple("hi", 47);
    }
    static TwoTuple f2() {
       return tuple("hi", 47);
    }
}

方法f()返回一個參數化的TwoTuple對象,而f2()返回的是非參數化的TwoTuple對象。在這個例子中,編譯器並無關於f2()的警告信息,由於咱們並無將其返回值做爲參數化對象使用。在某種意義上它被向上轉型爲一個非參數化的TwoTuple。然而,若是試圖將f2()的返回值轉型爲參數化的TwoTuple,編譯器就會發出警告。

對於這段話我不理解,而且通過我本身coding,f2()函數是會被編譯器警告的。有沒有大蝦能解釋一下?

在15.4.6小節的例子中做者使用了EnumSet類,它是1.5新加入的,用來從enum直接建立Set,它的range()靜態方法傳入enum某個範圍的第一個元素和最後一個元素,而後返回一個Set。

15.7 擦除

能夠聲明ArrayList.class,可是不能聲明ArrayList.class;經過coding:

Class c1 = new ArrayList().getClass();
Class c2 = new ArrayList().getClass();
System.out.println(c1 == c2);

輸出的結果是true,代表編譯器認爲ArrayList和ArrayList是相同的類型。

對一個帶有泛型參數的對象使用getClass().getTypeParameters()看到返回的只是用做參數佔位符的標識符。

由此做者得出結論:在泛型代碼內部,沒法得到任何有關泛型參數類型的信息。

Java泛型是使用 擦除來實現的,這意味着當你在使用泛型時任何具體的類型信息都被擦除了。

咱們能夠給定泛型的邊界,以此告訴編譯器只能接受遵循這個邊界的類型,可使用extends關鍵字。邊界聲明T必須具備類型HasF或者HasF導出的類型。

泛型類型參數將擦除到它的第一個邊界(它可能會有多個邊界)。

這種邊界方式有時沒有給代碼帶來任何好處。因此做者提出:只有當你但願使用的類型參數比某個具體類型(以及它的全部子類型)更加泛化時,也就是說,當你但願代碼可以跨多個類工做時,使用泛型纔有所幫助。

練習20提醒了我,在給泛型參數指定邊界時,邊界也能夠是接口,而且依然使用extends關鍵字而不是implements。

在15.7.4小節中做者經過對比了一個普通類和一個泛型類的字節碼,發現二者的字節碼是相同的(由於泛型擦除,運行時根本不知道泛型參數的具體類型)。在get()方法取值處普通類須要咱們手動轉型爲String,字節碼隨之有了類型檢查一行,而泛型了編譯器替咱們添加了這段代碼。

15.8 擦除的補償

因爲擦除丟失了在泛型代碼中執行某些操做的能力,任何在運行時須要知道確切類型信息的操做都沒法完成:

public class Erased {
  private final int SIZE = 100;
  public static void f(Object arg){
    if(arg instanceof T) {      錯誤
       T var = new T();         錯誤
       T[] array = new T[SIZE]; 錯誤
       T[] array = (T)new Object[SIZE]; 正確但有警告
    }
}

若是想實現上述錯誤代碼的功能能夠顯示傳遞類型的Class對象:

public Erased {
  
  public Class kind;
 
  public Erased(Class kind) {
    this.kind = kind;
  }

  public static void f(Object arg) {
    if(kind.isInstance(arg){
 
    }
  }
}

一、建立對象

在錯誤的例子中使用泛型參數:new T() 建立對象是沒法實現的,部分緣由是由於擦除,而另外一部分緣由是由於編譯器不能驗證T具備無參構造函數。解決方案是傳遞一個工廠對象,並使用它來建立新的實例。最便利的工廠對象是Class對象,使用newInstance來建立這個類型的對象。可是有些沒有無參構造函數的類沒法使用這種方法來建立,而且這個錯誤沒法再編譯期被捕獲,因此Java的建立者不同意這種方式,他們建議使用顯示的工廠,並將其限制類型。

interface Factory {
	T create();
}

class IntegerFactory implements Factory {
	public Integer create() {
		return new Integer(0);
	}
}

class Widget {
	public static class WidgetFactory implements Factory {
		public Widget create() {
			return new Widget();
		}
	}
}

class Foo2 {
	private T x;
	public > Foo2(F factory) {
		x = factory.create();
	}
}

另外一種方式使模板方法設計模式,在下面的例子中get()時模板方法,而create()是在子類中定義的、用來產生子類類型的對象:

abstract class GenericWithCreate {
	final T element;
	GenericWithCreate() {
		element = create();
	}
	abstract T create();
}

class X {
	
}

class Creator extends GenericWithCreate {
	X create() {
		return new X();
	}
}

練習24翻譯有誤,應該翻譯成修改練習21,使得Map中持有的是工廠對象而不是Class。

二、泛型數組

能夠聲明一個泛型數組的引用:

class Generic {
	
}

public class ArrayOfGeneric {
	static Generic[] gia;
}

可是卻不能建立確切泛型類型的數組:gia = new Generic[10],這種寫法是錯誤的。若是想把Object[]轉型爲Generic運行時會拋出ClassCastException。這個問題在於數組將跟蹤它們的實際類型,而這個類型是在數組被建立時肯定的,所以即便gia已經被轉型爲Generic[],可是這個信息只存在於編譯期。在運行時,它仍舊是Object數組,而這將引起問題。

成功建立泛型數組的惟一方式就是建立一個被擦除類型的新數組,而後對其轉型。

接下來是一個更加複雜的例子:

public class GenericArray {
	private T[] array;
	
	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];
	}
	
	public T[] rep() {
		return array;
	}
	
	public static void main(String[] args){
		GenericArray gai = new GenericArray(10);
		Integer[] ia = gai.rep();
	}
}

當程序運行到Integer[] ia = gai.rep();語句時會拋出ClassCastException。由於泛型擦除,在實際運行時數組array的類型是Object[],而要將其轉型爲Integer[]就會產生異常。因此在上個例子中,較好的方式是將array引用的類型聲明爲Object[],由於在運行時T[]和Object[]是相同的,而Object[]不會引發誤會。固然調用Integer[] ia = gai.rep();依舊會出錯的。

建立泛型數組的最好方法是持有一個泛型類型標識(Class類的對象),而後使用(T[])Array.newInstance(Class, size);建立數組。

15.9 邊界

Java泛型重用了extends關鍵字,它在泛型邊界上下文環境中和在普通狀況下所具備的意義是徹底不一樣的。

它要點是上界能夠指定單獨的類或者方法,也能夠指定一個類和(一個或者多個)方法,用&符號分隔,此時類必須在最前面:

 

15.10 通配符

首先建立幾個用於例子的類,注意它們的繼承體系。

class Fruit {

}

class Apple extends Fruit {

}

class Jonathan extends Apple {

}

class Orange extends Fruit{

}

對於數組,能夠將導出類型的數組賦予基類型的數組引用。

class CovariantArrays {
  public static void main(String[] args) {
    Fruit[] fruit = new Apple[10];
    fruit[0] = new Apple();
    fruit[1] = new Jonathan();
    fruit[2] = new Fruit(); //會拋出ArrayStoreException
    fruit[3] = new Orange(); //會拋出ArrayStoreException
  }
}

第一行建立了一個Apple數組,並將其賦值給一個Fruit數組引用。這是有意義的,由於Apple也是一種Fruit,所以Apple數組應該也是一個Fruit數組。可是這裏實際的數組類型是Apple[],因此只能在其中放置Apple或Apple的子類型,這在編譯器和運行時能夠工做。可是,編譯器容許將Fruit放置到這個數組中,而運行時的數組機制知道它處理的是Apple[],所以會在向數組中放置異構類型時拋出異常。

實際上向上轉型不適合在這裏。數組的行爲應該是它能夠持有其餘對象。

相對於數組,泛型容器不容許這樣的向上轉型:

List flist = new ArrayList(); //這樣作編譯器是不容許的!!

因爲泛型不知道類型信息,所以它拒絕向上轉型。與數組不一樣,泛型沒有內建的協變類型。數組在語言中徹底定義的,所以能夠內建了編譯期和運行時的檢查,可是在使用泛型時,編譯器和運行時系統都不知道你想用類型作什麼,以及應該採用什麼樣的規則。

若是想要在兩個類型之間創建某種類型的向上轉型關係,可使用通配符:

List flist = new ArrayList();
flist.add(new Apple()); //編譯錯誤
flist.add(new Fruit()); //編譯錯誤
flist.add(new Object());//編譯錯誤
flist.add(null);        //正確

引用的類型是List能夠將其視爲「具備任何從Fruit繼承的類型的列表」。可是這實際上並不意味着這個List將 持有任何類型的List。通配符引用的是明確的類型,所以它意味着」flist引用持有某種沒有指定具體的類型「。

能夠看到一旦執行了上例子中的向上轉型,就會丟失傳遞任何對象的能力,甚至是Object也不行,只能傳入null。

做者提到對一個泛型類,若是使用了通配符的向上轉型,對於那些帶有Object類型參數的方法是能夠正常工做的。

二、超類型通配符

能夠聲明通配符是由某個特定類的任何基類來界定的,方法是指定,也可使用類型參數,但不能對泛型參數給出一個超類型 邊界,即不能聲明(個人理解,超類型通配符是在引用類型中使用的,不能再定義中使用)。這使得能夠安全的傳遞一個類型對象到泛型類型中。有了超類型通配符,能夠向Collection寫入對象了:

class SuperTypeWildcards {
  static void writeTo(List apples) {
    apples.add(new Apple());
    apples.add(new Jonathan());
    //apples.add(new Fruit()); 編譯錯誤
  }
}

上面這個例子有些繞,須要好好理解。參數apples是Apple的某種基類型的List,這樣就能夠知道向其中添加Apple或Apple的子類型是 安全的。可是,Apple是下界,那麼能夠知道向這樣的List中添加Fruit是不安全的。

能夠根據若是可以像一個泛型類型「寫入」(傳遞給一個方法),以及如何可以從一個泛型類型中「讀取」(從一個方法中返回),來思考子類型和超類型邊界。

下面這個例子表現出超類型邊界放鬆了在能夠向方法傳遞的參數上所作的限制:

static void writeExact(List list, T item) {
  list.add(item);
}

static List apples = new ArrayList<>();
static List fruits = new ArrayList<>();

static void f1() {
  writeExact(apples, new Apple());
  //writeExact(fruits, new Apple()); 編譯錯誤
}

static void writeWithWildcard(List list, T item) {
  list.add(item);
}

static void f2(){
  writeWithWildcard(apples, new Apple());
  writeWithWildcard(fruite, new Apple());
}

第一方法使用了確切的泛型參數類型(無通配符),它不容許將Apple放置到List中,即便知道這是能夠的。第二個方法泛型參數時? super T,所以這個List將持有從T導出的某種具體類型,這樣就能夠安全的將一個T類型的對象或者從T導出的任何對象做爲參數傳遞給List方法。

三、無界通配符<?>

static List list1;
static List<?> list2;
static List<? extends Object> list3;

static void assign1(List list){
  list1 = list;
  list2 = list;
  list3 = list; //有警告:unchecked conversion
}

static void assign2(List<?> list){
  list1 = list;
  list2 = list;
  list3 = list;
}

static void assign3(List<? exnteds Object list){
  list1 = list;
  list2 = list;
  list3 = list;
}

看似List<?>與List相同,可是從上例子中能夠看出仍是有區別的。實際上它是在聲明:「我是想用Java的泛型來編寫這段代碼,我在這裏並 不是要用原生類型,可是在當前這種狀況下,泛型參數能夠持有任何類型」。

當在處理多個泛型參數時,容許一個參數能夠是任何類型,同時爲其餘參數肯定某種特定類型有時很重要。

15.11泛型的問題

一、泛型不能持有基本類型,但可使用對應的包裝器類,可是這會在必定程度上影響效率。org.apache.commons.collections.primitives是適配基本類型的容器版本。另外自動包裝機制不能用於數組。

二、一個類不能實現同一個泛型接口的兩種變體,下面的例子是產生衝突的狀況:

interface Payable<T> {
}

class Employee implements Payable<Employee> {
}

class Hourly extends  Employee implements Payable<Hourly> {
}

Hourly不能編譯,由於擦除會將Payable<Employee>和Payable<Hourly>簡化爲相同的類Payable。搞笑的是把兩個Payable的泛型參數都去掉Hourly能夠編譯經過。

三、帶泛型的參數類型沒法做爲重載的依據:

public class UseList<W, T> {
  void f(List<T> v){}
  void f(LIst<W> v){}
}

以上這種形式不是重載,編譯器會報錯。

15.12 自限定的類型

常常會出現以下的泛型寫法:

class SelfBounded<T extends SelfBounded<T>>

這種寫法的主要意義是保證類型參數必須與被定義的類相同。

另外,從Java 1.5開始加入了返回類型的協變(繼承類函數的返回類型能夠是基類該函數返回類型的子類),可是沒有方法實現函數參數的協變,運用這種自限定寫法能夠實現函數參數的協變。

15.13 動態類型安全

由於能夠向Java 1.5以前的代碼傳遞帶泛型參數的容器,因此舊代碼會破壞容器的正確性。Collections類中提供了一組靜態方法checkedCollection()、checkedList()、checkedMap()、checkedSet()、checkedSortedMap()和checkedSortedSet()返回受檢查的容器。每一個方法的第一個參數接受容器,第二個參數接受容器的泛型類型。當試圖向受檢查的容器插入不正確的對象時會拋出ClassCastException。

通過coding驗證產生的受檢查容器和傳入的容器不是一個引用。

15.14 異常

帶泛型參數的類不能直接或間接繼承自Throwable。

catch語句中不能捕獲泛型類型的異常。

可是泛型類型參數能夠應用到throws子句中:

interface ProcessRunner<T, E extends Exception> {
  void process(List<T> resultCollector) throws E;
}

15.15 混型

混型最基本的概念是混合多個類的能力,以產生一個能夠表示混型中全部類型的類。混型的價值之一是它們能夠將特性和行爲一致的應用於多個類之上。若是想在混型類中修改某些東西,這些修改將應用於混型所應用的全部類型之上。混型有一點AOP的味道。

因爲參數Java沒法像C++這樣來實現混型class TimeStamped : public T。做者 討論了幾種替代方式:

(1)使用組合(代理)

(2)使用裝飾器模式

(3)使用動態代理

15.16 潛在類型機制

一些編程語言提供一種機制稱爲潛在類型機制或結構化類型機制,還有叫鴨子類型機制,即「若是它走起來像鴨子,而且叫起來像鴨子,那麼就能夠把它當鴨子來對待「。C++ 和 Python提供這種機制,而Java泛型由於擦除並不能提供。

15.17 對缺少潛在類型機制的補償

(1)使用反射能夠實現相似潛在類型機制的功能;

(2)使用適配器模式。

相關文章
相關標籤/搜索