Java核心技術卷1: 泛型程序設計

爲何要使用泛型程序設計

泛型程序設計意味着編寫的代碼能夠被不少不一樣類型的對象所重用.java

在Java中增長泛型類以前, 泛型程序設計是用繼承實現的. 例如ArrayList類只維護一個Object引用的數組:數組

public class ArrayList {
  private Object[] elementData;
  public Object get(int i) {...}
  public void add(Object o) {...}
}

這樣實現有兩個問題.安全

1. 當獲取一個值時必須進行強制類型轉換.函數

ArrayList first = new ArrayList();
...;
String filename = (String) files.get(0);

2. 沒有進行錯誤檢查, 能夠向數組列表中添加任何類的對象.測試

files.add(new file("..."));

而泛型提供了一個更好的解決方案: 類型參數:this

ArrayList<String> files = new ArrayList<String>();

備註:spa

問題1: 爲何針對files.get(0), 強制轉換爲String能夠成功?翻譯

答: 雖然ArrayList存儲的是Object, 但它也會存儲額外的信息, 用來肯定所存儲的元素類型, 因此才能保證強制轉換的正確性.設計

問題2: 爲何是以Object爲引用?code

答: 由於Object是Java中的最原始的類型, 除了基本數據類型外, 全部的對象均派生於Object, 即全部的對象均可向上轉型爲Object.

 

定義簡單泛型類

package pair1;

/**
 * Created by lgt on 16/7/9.
 */
public class Pair<T> {
  private T first;
  private T second;
  
  public Pair() { first = null; second = null;}
  public Pair(T first, T second) {this.first = first; this.second = second;}

  public T getFirst() {return first;}
  public T getSecond() {return second;}

  public void setFirst(T newValue) {first = newValue;}
  public void setSecond(T newValue) {second = newValue;}
}

備註: 在Java中, 使用變量E表示集合的元素類型, K和V分別表示表的關鍵字與值的類型, T, U和S表示任意類型.

 

泛型方法

class ArrayAlg {
  public static <T> T getMiddle(T... a) {
    return a[a.length / 2];
  }
}

而後, 咱們能夠這樣進行調用:

String middle = ArrayAlg.<String>getMiddle("hello", "world", "Java");

備註: 

1. 泛型方法既能夠定義在普通類中, 也能夠定義在泛型類中.

2. 若是泛型方法的參數類型能夠推導出來, 則可省略, 如

String middle = ArrayAlg.getMiddle("John", "Q.", "Public");

可是在沒法推導出來時候, 例如double和int, 則仍是須要類型參數.

// ERROR
double middle = ArrayAlg.getMiddle(3.14, 1729, 0);

 

類型變量的限定

有時, 類或方法須要對類型變量加以約束. 例如咱們計算數組中的最小元素:

class ArrayAlg {
  public static <T> T min(T[] a) {
    if (a == null || a.length == 0) return null;
    T smallest = a[0];
    for (int i = 1; i < a.length; i++) {
      if (smallest.compareTo(a[i]) > 0) smallest = a[i];
    }
    return smallest;
  }
}

這裏存在一個問題在於: 調用compareTo方法的對象必須實現了Comparable接口才行, 而T類型並不肯定是否實現了Comparable接口.

咱們須要擴展T類型:

public static <T extends Comparable> T min(T[] a) {...}

而一個類型變量或通配符能夠有多個限定:

T extends Comparable & Serializable

一個實際的例子:

package pair2;

import pair1.Pair;
/**
 * Created by lgt on 16/7/9.
 */
public class PairTest2 {
  public static void main(String[] args) {
    String[] strs = new String[]{"hello", "world", "i", "love", "coding"};
    Pair<String> mm = ArrayAlg.minmax(strs);
    System.out.println("min=" + mm.getFirst());
    System.out.println("max=" + mm.getSecond());
  }
}

class ArrayAlg {
  public static <T extends Comparable> Pair<T> minmax(T[] a) {
    if (a == null || a.length == 0) return null;
    T min = a[0];
    T max = a[0];
    for (int i = 1; i < a.length; i++) {
      if (min.compareTo(a[i]) > 0) min = a[i];
      if (max.compareTo(a[i]) < 0) max = a[i];
    }
    return new Pair<>(min, max);
  }
}

 

泛型代碼和虛擬機

虛擬機沒有泛型類型對象--全部對象都屬於普通類.

不管什麼時候定義一個泛型類型, 都自動提供了一個相應的原始類型. 原始類型的名字就是刪去類型參數後的泛型類型名. 擦除類型變量, 並替換爲限定類型.

如Pair<T>的原始類型以下:

public class Pair {
  private Object first;
  private Object second;
  ......
}

而假定對T進行了擴展, 則爲擴展的類型:

public class Interval<T extends Comparable & Serializable> implements Serializable {}

其中原始類型爲: Comparable

但若是某些變量的類型爲Serializable, 則編譯器在必要時候進行強制轉換(Comparable --> Serializable)

翻譯泛型表達式

Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();

擦除getFirst的返回類型後將返回Object類型. 編譯器自動插入Employee的強制類型轉換. 即編譯器把這個方法調用翻譯爲兩條虛擬機指令:

1. 對原始方法Pair.getFirst的調用.

2. 將返回的Object類型強制轉換爲Employee類型.

因爲對象buddies會存儲實際類型的信息(Employee), 因此能夠保證強制類型轉換成功.

翻譯泛型方法

類型擦除也會出如今泛型方法中:

public static <T extends Comparable> T min(T[] a);

通過類型擦除後變成:

public static Comparable min(Comparable[] a);

但方法的擦除帶來兩個複雜的問題, 例如:

class DateInterval extends Pair<Date> {
  public void setSecond(Date second) {...}
}

這裏因爲Pair也有setSecond(Date d)方法, 因此它們爲一樣的方法, 動態運行時候能夠綁定變量的類型, 決定調用哪一個方法(多態).

可是因爲類型擦除後:

class DateInterval extends Pair {
  public void setSecond(Date second) {...}
}

Pair中的setSecond爲: public void setSecond(Object second) {...}, 因此沒法進行動態綁定(Date和Object爲不一樣的類型, 即此時兩個setSecond爲不一樣的方法).

因爲類型擦除致使多態失效. 因此咱們須要用橋方法將兩個setSecond方法"多態"起來:

class DateInterval extends Pair {
  public void setSecond(Date second) {...}
  public void setSecond(Object second) {
    setSecond((Date) second);
  }
}

編譯器生成了第二個setSecond方法, 從而解決了擦除致使多態失效的問題.

總結以下:

1. 虛擬機中沒有泛型, 只有普通的類和方法.

2. 全部的類型參數都用它們的限定類型替換.

3. 橋方法被合成保持多態.

4. 爲保持類型安全性, 必要時插入強制類型轉換.

 

約束與侷限性

不能用基本類型實例化類型參數

例如沒有Pair<double>, 只有Pair<Double>, 由於擦除後只有Object, 而Object不能存儲double類型.

運行時類型查詢只適用於原始類型

因爲存在類型擦除, 因此泛型類型實際上存儲的是原始類型. 因此:

if (a instanceof Pair<String>)

是語法錯誤的.

if (a instanceof Pair<Object>)

也是語法錯誤的. 由於a被當作Pair類型, 而元素類型被擦除爲Object而已, 它自己爲一個普通的類, 不存在任何的泛型信息.

因此

if (a instanceof Pair)

是正確的.

同理, 任何Pair的getClass確定都等於Pair.class:

Pair<String> strPair = ...;
Pair<Employee> empPair = ...;
strPair.getClass() == empPair.getClass();

不能建立參數化類型的數組

之因此不能實例化參數類型的數組, 是由於數組會記住它元素的類型, 例如字符串的數組是不能存儲浮點數的.

而若是對泛型數組進行實例化, 因爲擦除的存在, 致使數組的類型爲Object, 則能夠存儲任何的類型, 這跟數組的語法相沖突, 因此在語法層面上, 參數化類型的數組自己是不容許的. 如:

Pair<String>[] table = new Pair<String>[10]; //ERROR

Varargs警告

假設咱們編寫以下的代碼:

public static <T> void addAll(Collection<T> coll, T... ts) {
  for (t: ts) coll.add(t);
}
Collection<Pair<String>> table = ...;
Pair<String> pair1 = ...;
Pair<String> pair2 = ...;
addAll(table, pair1, pair2);

這在語法層面是沒有問題的, 運行起來是存在警告的, 是由於虛擬機會創建一個Pair<String>數組, 而這違反了"不能建立參數化類型的數組".

這裏之因此正確是由於: 1. 數組的存儲空間在編譯時期肯定的, 因此須要肯定數組元素的類型. 2. 而針對集合Collection來講, 它的存儲空間是動態遞增的, 因此無需考慮元素的類型. 

這能夠增長@SafeVarargs來抑制這個警告.

@SafeVarargs
public static <T> void addAll(Collection<T> coll, T... ts){}

不能實例化類型變量

不能使用像new T(...), new T[...]或T.class這樣的表達式中的類型變量, 例以下例的Pair<T>構造器是非法的:

public Pair() {first = new T(); second = new T();}

由於類型擦除將T改變爲Object, 而new Object()確定不是代碼的本意.

同理, 咱們也不能使用:

first = T.class.newInstance();

由於類型擦除的存在, T.class沒法明確其Class類型. 因此咱們須要顯式的指明其Class類型:

public static <T> Pair<T> makePair(Class<T> c1) {
  try {return new Pair<>(c1.newInstance(), c1.newInstance());}
  catch (Exception ex) {return null;}
}

咱們能夠這樣調用:

Pair<String> p = Pair.makePair(String.class);

而new T[...]着實讓人頭疼, 由於類型擦除的緣由致使沒法確切知道數組的原始類型(語法層面上數組必須知道其元素類型, 才能判斷出String[]存儲double時候會報錯), 則咱們須要反射的機制(在運行時獲取其class的信息, 從而獲取其具體的類型, 則可進行new的操做)動態獲取其數據類型, 來進行new T[...].

public static <T extends Comparable> T[] minmax(T... a) {
  T[] mm = (T[])Array.newInstance(a.getClass().getComponentType(), 2);
}

泛型類的靜態上下文中類型變量無效

靜態的方法或變量是跟具體的類實例無關的, 而泛型的存在自己就跟具體的類實例有關, 二者衝突致使靜態域或方法中引用類型變量是無效的.

public class Singleton<T> {
  private static T singleInstance; //ERROR
  public static T getSingleInstance() {} //ERROR
}

不能拋出或捕獲泛型類的實例

由於一旦類型擦除, 根本就不肯定其具體的異常類型.

public static <T extends Throwable> void doWork(Class<T> t) {
  try {

  } catch (Throwable e) { //OK

  } catch (T e) { //ERROR
    
  }
}

因爲不能拋出或捕獲泛型類, 因此也不能對泛型類進行擴展Exception:

public class Problem<T> extends Exception {} // ERROR

備註: 對"能夠消除已檢查異常的檢查", 不太理解(書章節12.6.7, p543)

擦除後的衝突

例如咱們編寫以下的代碼:

public class Pair<T> {
  public boolean equals(T value) {return first.equals(value) && second.equals(value);}
}

因爲擦除的存在, 致使Pair<String>實際上有兩個equals: 

boolean equals(String) //defined in Pair<T>
boolean equals(Object) //inherited from Objects

要麼使用"橋方法", 要麼重命名函數進行修復.

備註: 泛型規範的原則之一: 要想支持擦除的轉換, 就須要強行限制一個類或類型變量不能同時成爲兩個接口類型的子類, 而這兩個接口是同一個接口的不一樣參數化.

 

泛型類型的繼承規則

不管S與T有什麼聯繫(例如子類和父類的關係), Pair<S>和Pair<T>均沒有任何關係. 由於Pair<S>和Pair<T>的本質類型都是Pair.

package pair1;


/**
 * Created by lgt on 16/7/9.
 */

class A {
  private String s;
  A(String s) {
    this.s = s;
  }
  public String show() {
    return s;
  }
}
class B extends A {
  B(String s) {
    super(s);
  }
}
public class PairTest1 {
  public static void main(String[] args) {
    Pair<B> b = new Pair<>(new B("hello"), new B("world"));
//    Pair<A> a = b; // ERROR, Pair<B>沒法轉換爲Pair<A>
    Pair c = b;
    c.setFirst(new B("java"));
    System.out.println(((B)c.getFirst()).show());
  }
}

 

通配符類型

Pair<? extends Employee>表示任何泛型Pair類型, 它的類型參數是Employee的子類, 如Pair<Manager>, 但不是Pair<String>.

因此, 若是咱們要打印出全部僱員的信息, 不能定義:

public static void printBuddies(Pair<Employee> p);

而應該定義:

public static void printBuddies(Pair<? extends Employee> p);

備註:

1. 針對語法糖extends, 它每每表示擴展某個接口,類型或者繼承了某個類. 例如interface A extends B, 則說明接口A擴展了接口B, class A extends B, 表明A繼承B.

因此** A extends B, 則類型爲B.

2. 針對A extends B來講, 只適合get的操做, 由於明確知道其基本類型爲B, 但不能執行set操做, 由於不知道具體類型是什麼.

通配符的超類型限定

與"? extends Employee"相反, "? super Manager"限制爲Manager的全部超類型.

void setFirst(? super Manager);
? extends Employee getFirst();

備註: 針對? super Manager, 只適合set的操做, 由於知道具體類型爲Manager, 但不能執行get操做, 由於不知道其基本類型.

無限定通配符

對於Pair<?>的方法:

? getFirst()
void setFirst(?)

getFirst的返回值只能賦給一個Object. setFirst方法不能被調用, 甚至不能用Object調用. Pair<?>和Pair本質的不一樣在於: 能夠用任意Object對象調用原始的Pair類的setObject方法.

備註: 這裏setObject泛指一切set的方法.

因此若是咱們要測試一個Pair是否包含一個null引用, 則能夠這樣定義:

public static boolean hasNulls(Pair<?> p) {
  return p.getFirst() == null || p.getSecond() == null;
}

而無需定義成:

public static <T> boolean hasNulls(Pair<T> p){}

一個總結性的例子:

pair3/PairTest3.java:

package pair3;

import pair1.Pair;

/**
 * Created by lgt on 16/7/10.
 */
public class PairTest3 {
  public static void main(String[] args) {
    Manager ceo = new Manager("Gus Greedy", 800000);
    Manager cfo = new Manager("Sid Sneaky", 600000);
    Pair<Manager> buddies = new Pair<>(ceo, cfo);
    printBuddies(buddies);

    ceo.setBonus(1000000);
    cfo.setBonus(500000);
    Manager[] managers = {ceo, cfo};

    Pair<Employee> result = new Pair<>();
    minmaxBonus(managers, result);
    System.out.println("first:" + result.getFirst().getName() + ", second:" + result.getSecond().getName());
    maxminBonus(managers, result);
    System.out.println("first:" + result.getFirst().getName() + ", second:" + result.getSecond().getName());
  }

  public static void printBuddies(Pair<? extends Employee> p) {
    Employee first = p.getFirst();
    Employee second = p.getSecond();
    System.out.println(first.getName() + " and " + second.getName() + " are buddies.");
  }

  public static void minmaxBonus(Manager[] a, Pair<? super Manager> result) {
    if (a == null || a.length == 0) return;
    Manager min = a[0];
    Manager max = a[0];
    for (int i = 1; i < a.length; i++) {
      if (min.getBonus() > a[i].getBonus()) min = a[i];
      if (max.getBonus() < a[i].getBonus()) max = a[i];
    }
    result.setFirst(min);
    result.setSecond(max);
  }
  public static void maxminBonus(Manager[] a, Pair<? super Manager> result) {
    minmaxBonus(a, result);
    PairAlg.swapHelper(result);
  }
}

class PairAlg {
  public static boolean hasNulls(Pair<?> p) {
    return p.getFirst() == null || p.getSecond() == null;
  }
  public static void swap(Pair<?> p) { swapHelper(p);}
  public static <T> void swapHelper(Pair<T> p) {
    T t = p.getFirst();
    p.setFirst(p.getSecond());
    p.setSecond(t);
  }
}

pair3/Employee.java:

package pair3;

/**
 * Created by lgt on 16/7/10.
 */
public class Employee {
  private String name;
  private double salary;

  Employee(String n, double s) {
    this.name = n;
    this.salary = s;
  }

  public double getSalary() {
    return salary;
  }

  public String getName() {
    return name;
  }
}

pair3/Manager.java:

package pair3;

/**
 * Created by lgt on 16/7/10.
 */
public class Manager extends Employee {
  private double bonus;
  Manager(String n, double s) {
    super(n, s);
    this.bonus = 0;
  }

  public void setBonus(double b) {
    bonus = b;
  }

  public double getBonus() {
    double baseBonus = super.getSalary();
    return baseBonus + bonus;
  }
}

pair1/Pair.java:

package pair1;

/**
 * Created by lgt on 16/7/9.
 */
public class Pair<T> {
  private T first;
  private T second;

  public Pair() { first = null; second = null;}
  public Pair(T first, T second) {this.first = first; this.second = second;}

  public T getFirst() {return first;}
  public T getSecond() {return second;}

  public void setFirst(T newValue) {first = newValue;}
  public void setSecond(T newValue) {second = newValue;}
}
相關文章
相關標籤/搜索