Java中泛型

==《Thinking in Java》 第15章筆記==

即使使用了接口,就要求代碼必須使用特定的接口,對程序的約束也還是太強了。我們希望達到的目的是編寫更通用的代碼,要使代碼能夠應用與“某種不具體的類型”,而不是一個具體的接口或類。泛型這個術語的意思就是適用於許多許多的類型。

  • 15.1 與C++的比較
public class Holder<T>{
    private T a;
    ...
}

T就是類型參數,Java泛型的核心概念:告訴編譯器想使用什麼類型,然後編譯器幫你處理一切細節。

  • 15.2.1 一個元祖類庫

元祖:將一組對象直接打包存儲於其中的一個單一對象。

public class TwoTuple<A,B>{
    public final A first;
    public fianl B second;
    public TwoTuple(A a, B b){
        first = a;
        second = b;
    }
}

通過final關鍵字保證安全性,可以隨心所欲的使用這兩個對象,卻無法改變這兩個對象。

  • 15.3 泛型接口

例如生成器,是工廠方法設計模式的一種應用。

public interface Generator<T> {
    T next();
}

public class CoffeeGenerator implements Generator<Coffee>, Iterable<Coffee> {
    private Class[] types = {Latte.class, Mocha.class, Cappuccino.class, Americano.class, Breve.class};
    private static Random random = new Random(44);
    public CoffeeGenerator() {}
    private int size = 0;
    public CoffeeGenerator(int sz) {size = sz;}
    @Override
    public Coffee next() {
        try{
            return (Coffee) types[random.nextInt(types.length)].newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    class CoffeeIterator implements Iterator<Coffee>{
        int count = size;

        @Override
        public boolean hasNext() {
            return count > 0;
        }

        @Override
        public Coffee next() {
            count --;
            return CoffeeGenerator.this.next();
        }

        @Override
        public void remove() {
            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);
    }
}

注意:基本類型無法作爲類型參數,不過Java SE5具備了自動打包和自動拆包的功能,可以很方便地在基本類型和其相應的包裝器類型之間進行轉換。

  • 15.4 泛型方法

是否擁有泛型方法,與其所在的類是否是泛型沒有關係,可以是泛型類,也可以不是泛型類。

如果使用泛型方法可以取代將整個類泛型化,那麼就應該只使用泛型方法,因爲它可以使事情更加清楚明白。另外,對於一個static的方法而言,無法訪問泛型類的類型參數。

public <T> void f(T x){
    System.out.println(x.getClass().getName());
}

調用:

gm.f("");
gm.f(1);
gm.f(1.0);
...

使用泛型方法的時候,通常不必指明參數類型,因爲編譯器會爲我們找出具體的類型。這稱爲類型參數判斷。這就好像是f()被無限次的重載過。如果f()調用時傳入了基本類型,那麼自動打包機制就會接入其中,將基本類型的值包裝爲對應的對象。

  • 15.4.3 用於Generator的泛型方法
public class Generators {
    public static <T> Collection<T> fill(Collection<T> coll, Generator<T> generator, int n){
        for(int i =0; i < n; i++)
            coll.add(generator.next());
        return coll;
    }
}

//調用
fill(new ArrayList<Coffee>(), new CoffeeGenerator(), 4);
  • 15.5 匿名內部類
public static Generator<Teller> generator = 
   new Generator<Teller>(){
       public Teller next(){ return new Teller(); }
   };
  • 15.7 擦除的神祕之處
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1==c2);

輸出爲true。

Java泛型是使用擦除來實現的,在泛型代碼內部,無法獲得任何有關泛型參數類型的信息。因此List和List在運行時事實上是相同的類型。

因爲泛型內部是沒有類型信息的,所以要調用t.f()方法時,也是不可以的,解決方法是給定泛型類的邊界:,如此,就可以調用t.f().

public Test<T extends HasF>{
    private T t;
    ...
    public void test(){ t.f(); }
}

編譯器會把類型參數替換爲它的擦除,上述的例子T擦除到了HasF.

如果自己去執行擦除,那麼前一個例子可以簡單的創建出一個沒有泛型的類:

public Test(){
    private HasF t;
    ...
    public void test(){ t.f(); }
}

但是泛型可以返回確切的類型,而用Object的話必須強轉:

public T get() { return t; }
  • 15.7.2 遷移兼容性

泛型類型只有在靜態類型檢查期間纔出現,在此之後,程序中的所有泛型類型都將被擦除,替換爲它們的非泛型上屆,例如,List這樣的類型註解將被擦除爲List,而普通的類型變量在未指定邊界的情況下將被擦除爲Object。

遷移兼容性:擦除的核心動機是它使得泛化的客戶端可以用非泛化的類庫來使用,反之亦然。

  • 15.8 擦除的補償

擦除丟失了在泛型代碼中執行某些操作的能力,任何在運行時需要知道確切類型信息的操作都將無法工作:

T var = new T();//Error
T[] arr = new T[size];//Error
if(arg instanceof T){}//Error
T[] arr = (T)new Object[size];//Unchecked warning
  • 15.8.1 創建類型實例

C++中可以直接創建,Java中的解決方法是傳遞一個工廠對象,並用它創建新的實例,最便利的工廠對象就是Class對象:

class ClassAsFactory<T>{
    T x;
    public ClssAsFactory(Class<T> kind){
        try{
            x = kind.newInstance();
        }catch(Exception e){
            ...
        }
    }
}

...

ClassAsFactory<Coffee> fe = 
    new ClassAsFactory<>(Coffee.class);

但是如果傳入的是Integer.class,可以編譯但是最終卻catch到了異常,因爲Integer沒有任何默認的構造器。因此Sun建議使用顯示的工廠。

interface Factory<T>{
    T create();
}

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

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

//調用
new Foo2<Integer>(new IntegerFactory());

或者是模板方法設計模式,下面的示例中,create()就是在子類中定義的、用來產生子類類型的對象:

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(){
        ...
    }
}

//調用
Creator c = new Creator();
c.f();
  • 15.8.2 泛型數組
    不能直接創建泛型數組,一般的解決方案是在任何想要創建泛型數組的地方都使用ArrayList:
private List<T> array = new ArrayList<T>();
public void add(T item) {}
public T get() {}

既然所有數組無論它們持有的類型如何,都具有相同的結構(每個數組槽位的尺寸和數組的佈局),那麼看起來應該能創建一個Object數組,並將其轉型爲所希望的數組類型,事實上這可以編譯,但是不能運行:

public class ArrayOfGeneric {
    static final int SIZE = 100;
    static Generic<Integer>[] gia;
    static Generic[] giaNormal;
    public static void main(String[] args){
        //java.lang.Object; cannot be cast to [Lcom.whu.fly.Chapter15.GenericInterface.Generic;
//        gia = (Generic<Integer>[]) new Object[SIZE];
        //Error:創建泛型數組
//        gia = new Generic<Integer>[SIZE];
        gia = new Generic[SIZE];
        System.out.println(gia.getClass().getSimpleName());
        giaNormal = new Generic[SIZE];
        giaNormal[0] = new Generic<Double>();
        gia[0] = new Generic<Integer>();

    }
}

數組將跟蹤它們的實際類型,而這個類型是在數組被創建的時候確定的,因此,即使gia已經被轉型爲Generic[],但這個信息只存在於編譯器,在運行時,它仍舊是Object數組。

gia = (Generic[]) new Object[SIZE]將報java.lang.Object; cannot be cast to [Lcom.whu.fly.Chapter15.GenericInterface.Generic的錯誤。而gia = new Generic[SIZE]將報Error:創建泛型數組的錯誤。

第一個錯誤原因顯而易見,第二個錯誤原因==我覺得==是無法建立泛型數組,所以用了通用的Generic[]類型,但是gia中並不能放其他類型的,gia[1] = new Generic()會報錯無法將Double插入到Integer中。

成功創建泛型數組的唯一方式就是創建一個被擦除類型的新數組,然後對其轉型:

public class GenericArray<T> {
    private T[] array;
    public GenericArray(int size){
        array = (T[]) new Object[size];
    }
    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<Integer> gai = new GenericArray<>(10);
//        java.lang.Object; cannot be cast to [Ljava.lang.Integer
//        Integer[] ia = gai.rep();

        Object[] oa = gai.rep();
    }
}

rep()返回的是T[],但是嘗試左右Integer[]引用來捕獲,依然會報錯,這是因爲實際的運行時類型是Object[]。

因爲有了擦除,數組的運行時類型就只能是Object[],如果我們立即將其轉型爲T[],那麼在編譯器該數組的實際類型就將丟失,而編譯器可能會錯誤某些潛在的錯誤檢查。正因爲這樣,最好是在集合內部使用Object[]:

public class GenericArray2<T> {
    private Object[] array;
    public GenericArray2(int size){
        array = new Object[size];
    }
    public void put(int index, T item){
        array[index] = item;
    }
    public T get(int index){
        return (T)array[index];
    }
    public T[] rep(){
        return (T[])array;
    }
    public static void main(String[] args){
        GenericArray2<Integer> gai = new GenericArray2<>(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();
        //java.lang.Object; cannot be cast to [Ljava.lang.Integer
//        Integer[] ia = gai.rep();

    }
}

依然不能將gai.rep()轉爲Integer[],它只能是Object[]。

如果確實需要具體的類型,可以傳遞一個類型標記:

public class GenericArrayWithTypeToken<T> {
    private T[] array;
    public GenericArrayWithTypeToken(Class<T> type, int size){
        array = (T[]) Array.newInstance(type, size);
    }
    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){
        GenericArrayWithTypeToken<Integer> gai = new GenericArrayWithTypeToken<>(Integer.class, 10);
        Integer[] ia = gai.rep();
    }
}

類型標記Class被傳到構造器中,以便從擦除中回覆,使得我們可以創建需要的實際類型的數組。
- 15.9 邊界

如15.7所講,邊界使得你可以在用於泛型的參數類型上設置限制條件,可以按照自己的邊界類型來調用方法。

public class ColoredDimension<T extends FruitClass & Inter1 & Inter2> {
}

邊界可以設置多個,但是class必須在前面,然後是接口(接口可以是多個)。
- 15.10 通配符

數組可以由一個父類的引用來持有子類的數組,例如:

Fruit[] fruits = new Apple[10];
fruits[0] = new Apple();
//可以編譯,但是不能運行
fruits[1] = new Fruit();

因爲它有一個Fruit[]的引用,所以它沒有理由不允許將Fruit對象或者任何其子類加入其中,因此,在編譯器這是允許的,但是它的實際類型是Apple[],在運行時,數組機制知道它處理的是Apple[],因此會拋出異常。

在使用泛型容器時,這種“向上轉型”就不允許了:

//編譯期報錯
List<Fruit> fruitList = new ArrayList<Apple>();

這實際上根本不是向上轉型,Apple的List不是Fruit的List。

真正的問題是我們在談論容器的類型,而不是容器持有的類型。與數組不同,泛型沒有內建的協變類型(????)。這是因爲數組在語言中是完全定義的,因此可以內建了編譯期和運行時的檢查,但是在使用泛型時,編譯期和運行時系統都不知道你想用類型做些什麼,以及應採用什麼樣的規則。

有時你想要在兩個類型之間建立某種類型的向上轉型關係,這正是通配符所允許的:

List<? extends Fruit> fList = new ArrayList<Apple>();

但是此時就丟失了向其中傳遞任何對象的能力,甚至是Object:

fList.add(new Apple());//Error
fList.add(new Fruit());//Error
fList.add(new Object());//Error

fList.add(null);//可以,但是沒有意義

List

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;
    }
}

Holder<? extends Fruit> fruit = new Holder<Apple>(new Apple());

創建了一個Holder,可以將其向上轉型爲Holder

static void writeTo(List<? super Apple> apples){
    apples.add(new Apple());
    apples.add(new Jonathan());
    apples.add(new Fruit());//Error
}
  • 15.10.3 無界通配符

無界通配符

ArrayList fList = new ArrayList<Fruit>();
fList.add(new Apple("an apple"));
fList.add(new Fruit());
fList.add(new Object());
//fList.add(null);
for(Object f : fList){
    System.out.println(f.getClass().getSimpleName());
}
Apple apple = (Apple) fList.get(0);
System.out.println(apple.getAppleName());

完全可以正常運行,可以添加,可以強制類型轉換後(get()返回的是Object)調用它的方法。

ArrayList<Fruit> fList = new ArrayList();

可以是任何,限制跟普通的new ArrayList一樣。

public class Wildcards {
    static void rawArgs(Holder holder, Object arg){
//        holder.set(arg);//unchecked warning
//        holder.set(new Wildcards());//same warning

        //OK, 但是類型信息丟失了
        Object obj = holder.get();
    }

    static void unboundedArg(Holder<?> holder, Object arg){
//        holder.set(arg);//Error.Capture of ? cannot be applied to objec
//        holder.set(new Wildcards());//same error

        //OK, 但是類型信息丟失了
        Object object = 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 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();//? super T 不是T

        Object obj = holder.get();
    }

    public static void main(String[] args){
        Holder raw = new Holder<Long>();
        //Or
        raw = new Holder();
        Holder<Long> qualified = new Holder<>();
        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 ri = exact1(raw);//unchecked warning
        Long r2 = exact1(qualified);
        Object r3 = exact1(unbounded);//Must return Object
        Long r4 = exact1(bounded);

        Long r5 = exact2(raw, lng);//unchecked warning,from Holder to Holder<Long>
        Long r6 = exact2(qualified, lng);
//        Long r7 = exact2(unbounded, lng);//Error, (Holder<T>, T) cannot be applied to (Holder<capture of ?>, Long)
//        Long r8 = exact2(bounded, lng);//Error, (Holder<T>, T) cannot be applied to (Holder<capture of ? extends Long>, Long)

        Long r9 = wildSubtype(raw, lng);//unchecked warning
        Long r10 = wildSubtype(qualified, lng);
        //OK, 但是隻能返回Object
        Object r11 = wildSubtype(unbounded, lng);
        Long r12 = wildSubtype(bounded, lng);

        wildSubtype(raw, lng);//unchecked warning,from Holder to Holder<Long>
        wildSupertype(qualified, lng);
//        wildSupertype(unbounded, lng);//Error:(Holder<? super T>, T>) cannot be applied to (Holder<capture of ?>, Long);
//        wildSupertype(bounded, lng);//Error:(Holder<? super T>, T>) cannot be applied to (Holder<capture of ? extends Long>, Long);
    }
}

上面的示例中包含了各種Holder作爲參數的用法,它們都具有不同的形式,包括原生類型,具體的類型參數以及無界通配符參數。

總結如下:
1. 只要使用了原生類型,都會放棄編譯期的檢查,在rawArgs()中,可以將任何類型的對象傳遞給set(),這個對象會被向上轉型爲Object。
2. 在unboundedArg()中可以看出

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);
    }
    public static void main(String[] args){
        Holder raw = new Holder<Integer>(1);
        f1(raw);//有警告
        f2(raw);//無警告
        Holder rawBasic = new Holder();
        rawBasic.set(new Object());//警告
        f2(rawBasic);//無警告
        Holder<?> wildcard = new Holder<>(1.0);
        f2(wildcard);
    }
}

output//
Integer
Integer
Object
Double

最後一個是根據(1.0)判斷的,如果換成了1.0f,那麼就會輸出Float。

此處,f1()中的類型參數都是確切的,沒有通配符或者邊界。在f2()中,Holder參數是一個無界通配符,因此它看起來是未知的。但是,在f2()中,f1()被調用,而f1()需要一個已知參數。這裏所發生的的是:參數類型在調用f2()的過程中被捕獲。

  • 15.11.2 實現參數化接口

一個類不能實現同一個泛型接口的兩種變體,由於擦除的原因,這兩個變體會成爲相同的接口:

interface Payable<t>{}

class Employee implements Payable<Employee> {}

//不能編譯
class Hourly extends Employee implements Payable<Hourly> {}

如果從Payable的兩種用法中都移除掉泛型參數,這段代碼就可以編譯。

  • 15.11.4 重載
public class UseList<W,T>{
    void f(List<T> t){}
    void f(List<W> w){}
}

由於擦除的原因,重載方法將產生相同的類型簽名,因此必須提供明顯有區別的方法名,f1(),f2()。

  • 15.12 自限定的類型
class SelfBounded<T extends SelfBounded<T>>{}

基類用導出類替代其參數,這意味着泛型基類變成了一種其所有導出類的公共的模板。

class A extends SelfBounded<A> {}
class B extends SelfBounded<A> {}//也行
class C extends SelfBounded<B> {}//Error,B不滿足T extends SelfBound<T>(T一致)

自限定的參數的意義:它可以保證類型參數必須與正在被定義的類相同,即這個類所用的類型參數將與使用這個參數的類具有相同的基類型。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章