Java 泛型進階

擦除


在泛型代碼內(nèi)部,無法獲得任何有關(guān)泛型參數(shù)類型的信息。

例子1:

//這個例子表明編譯過程中并沒有根據(jù)參數(shù)生成新的類型
public class Main2 {
    public static void main(String[] args) {
        Class c1 = new ArrayList<Integer>().getClass();
        Class c2 = new ArrayList<String>().getClass();
        System.out.print(c1 == c2);
    }
}
/* output
true
*/

List<String> 中添加 Integer 將不會通過編譯,但是List<Sring>List<Integer>在運行時的確是同一種類型。

例子2:

//例子, 這個例子表明類的參數(shù)類型跟傳進去的類型沒有關(guān)系,泛型參數(shù)只是`占位符`
public class Table {
}
public class Room {
}
public class House<Q> {
}
public class Particle<POSITION, MOMENTUM> {
}
public class Main {
    public static void main(String[] args) {
        List<Table> tableList = new ArrayList<Table>();
        Map<Room, Table> maps = new HashMap<Room, Table>();
        House<Room> house = new House<Room>();
        Particle<Long, Double> particle = new Particle<Long, Double>();
        System.out.println(Arrays.toString(tableList.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(maps.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(house.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(particle.getClass().getTypeParameters()));
    }
}
/** output
[E]
[K, V]
[Q]
[POSITION, MOMENTUM]
*/

我們在運行期試圖獲取一個已經(jīng)聲明的類的類型參數(shù),發(fā)現(xiàn)這些參數(shù)依舊是‘形參’,并沒有隨聲明改變。也就是說在運行期,我們是拿不到已經(jīng)聲明的類型的任何信息。

編譯器會雖然在編譯過程中移除參數(shù)的類型信息,但是會保證類或方法內(nèi)部參數(shù)類型的一致性。

例子:

List<String> stringList=new ArrayList<String>();
//可以通過編譯
stringList.add("wakaka");
//編譯不通過
//stringList.add(new Integer(0));

//List.java
public interface List<E> extends Collection<E> {
//...
boolean add(E e);
//...
}

List的參數(shù)類型是E,add方法的參數(shù)類型也是E,他們在類的內(nèi)部是一致的,所以添加Integer類型的對象到stringList違反了內(nèi)部類型一致,不能通過編譯。

重用 extends 關(guān)鍵字。通過它能給與參數(shù)類型添加一個邊界。

泛型參數(shù)將會被擦除到它的第一個邊界(邊界可以有多個)。編譯器事實上會把類型參數(shù)替換為它的第一個邊界的類型。如果沒有指明邊界,那么類型參數(shù)將被擦除到Object。下面的例子中,可以把泛型參數(shù)T當作HasF類型來使用。

例子:

/** * Created by yxf on 16-5-28. */
// HasF.java
public interface HasF {
    void f();
}

//Manipulator.java
public class Manipulator<T extends HasF> {
    T obj;
    public T getObj() {
        return obj;
    }
    public void setObj(T obj) {
        this.obj = obj;
    }
}

extend關(guān)鍵字后后面的類型信息決定了泛型參數(shù)能保留的信息。

Java中擦除的基本原理

剛看到這里可能有些困惑,一個泛型類型沒有保留具體聲明的類型的信息,那它是怎么工作的呢?在把《Java編程思想》書中這里的邊界與上文的邊界區(qū)分開來之后,終于想通了。Java的泛型類的確只有一份字節(jié)碼,但是在使用泛型類的時候編譯器做了特殊的處理。

這里根據(jù)作者的思路,自己動手寫了兩個類SimpleHolderGenericHolder,然后編譯拿到兩個類的字節(jié)碼,直接貼在這里:

// SimpleHolder.java
public class SimpleHolder {
    private Object obj;
    public Object getObj() {
        return obj;
    }
    public void setObj(Object obj) {
        this.obj = obj;
    }
    public static void main(String[] args) {
        SimpleHolder holder = new SimpleHolder();
        holder.setObj("Item");
        String s = (String) holder.getObj();
    }
}
// SimpleHolder.class
public class SimpleHolder {
  public SimpleHolder();
    Code:
       0: aload_0       
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return        

  public java.lang.Object getObj();
    Code:
       0: aload_0       
       1: getfield      #2                  // Field obj:Ljava/lang/Object;
       4: areturn       

  public void setObj(java.lang.Object);
    Code:
       0: aload_0       
       1: aload_1       
       2: putfield      #2                  // Field obj:Ljava/lang/Object;
       5: return        

  public static void main(java.lang.String[]);
    Code:
       0: new           #3                  // class SimpleHolder
       3: dup           
       4: invokespecial #4                  // Method "<init>":()V
       7: astore_1      
       8: aload_1       
       9: ldc           #5                  // String Item
      11: invokevirtual #6                  // Method setObj:(Ljava/lang/Object;)V
      14: aload_1       
      15: invokevirtual #7                  // Method getObj:()Ljava/lang/Object;
      18: checkcast     #8                  // class java/lang/String
      21: astore_2      
      22: return        
}
//GenericHolder.java
public class GenericHolder<T> {
    T obj;
    public T getObj() {
        return obj;
    }
    public void setObj(T obj) {
        this.obj = obj;
    }
    public static void main(String[] args) {
        GenericHolder<String> holder = new GenericHolder<>();
        holder.setObj("Item");
        String s = holder.getObj();
    }
}

//GenericHolder.class
public class GenericHolder<T> {
  T obj;

  public GenericHolder();
    Code:
       0: aload_0       
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return        

  public T getObj();
    Code:
       0: aload_0       
       1: getfield      #2                  // Field obj:Ljava/lang/Object;
       4: areturn       

  public void setObj(T);
    Code:
       0: aload_0       
       1: aload_1       
       2: putfield      #2                  // Field obj:Ljava/lang/Object;
       5: return        

  public static void main(java.lang.String[]);
    Code:
       0: new           #3                  // class GenericHolder
       3: dup           
       4: invokespecial #4                  // Method "<init>":()V
       7: astore_1      
       8: aload_1       
       9: ldc           #5                  // String Item
      11: invokevirtual #6                  // Method setObj:(Ljava/lang/Object;)V
      14: aload_1       
      15: invokevirtual #7                  // Method getObj:()Ljava/lang/Object;
      18: checkcast     #8                  // class java/lang/String
      21: astore_2      
      22: return        
}

經(jīng)過一番比較之后,發(fā)現(xiàn)兩分源碼雖然不同,但是對應(yīng)的字節(jié)碼邏輯部分確是完全相同的。

在編譯過程中,類型變量的信息是能拿到的。所以,set方法在編譯器可以做類型檢查,非法類型不能通過編譯。但是對于get方法,由于擦除機制,運行時的實際引用類型為Object類型。為了‘還原’返回結(jié)果的類型,編譯器在get之后添加了類型轉(zhuǎn)換。所以,在GenericHolder.class文件main方法主體第18行有一處類型轉(zhuǎn)換的邏輯。它是編譯器自動幫我們加進去的。

所以在泛型類對象讀取和寫入的位置為我們做了處理,為代碼添加約束。

擦除的缺陷

泛型類型不能顯式地運用在運行時類型的操作當中,例如:轉(zhuǎn)型、instanceofnew。因為在運行時,所有參數(shù)的類型信息都丟失了。

public class Erased<T> {
    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];
    }
}

擦除的補償

1. 類型判斷問題

例子:

class Building {}
class House extends Building {}
public class ClassTypeCapture<T> {
    Class<T> kind;
    public ClassTypeCapture(Class<T> kind) {
        this.kind = kind;
    }
    public boolean f(Object arg) {
        return kind.isInstance(arg);
    }
    public static void main(String[] args) {
        ClassTypeCapture<Building> ctt1 = new ClassTypeCapture<Building>(Building.class);
        System.out.println(ctt1.f(new Building()));
        System.out.println(ctt1.f(new House()));
        ClassTypeCapture<House> ctt2 = new ClassTypeCapture<House>(House.class);
        System.out.println(ctt2.f(new Building()));
        System.out.print(ctt2.f(new House()));
    }
}
//output
//true
//true
//false
//true

泛型參數(shù)的類型無法用instanceof關(guān)鍵字來做判斷。所以我們使用類類型來構(gòu)造一個類型判斷器,判斷一個實例是否為特定的類型。

2. 創(chuàng)建類型實例

Erased.java中不能new T()的原因有兩個,一是因為擦除,不能確定類型;而是無法確定T是否包含無參構(gòu)造函數(shù)。

為了避免這兩個問題,我們使用顯式的工廠模式:

例子:

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

class Foo2<T> {
    private T x;

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

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

class Widget {
    public static class Factory implements IFactory<Widget> {
        @Override
        public Widget create() {
            return new Widget();
        }
    }
}

public class FactoryConstraint {
    public static void main(String[] args) {
        new Foo2<Integer>(new IntegerFactory());
        new Foo2<Widget>(new Widget.Factory());
    }
}

通過特定的工廠類實現(xiàn)特定的類型能夠解決實例化類型參數(shù)的需求。

3. 創(chuàng)建泛型數(shù)組

一般不建議創(chuàng)建泛型數(shù)組。盡量使用ArrayList來代替泛型數(shù)組。但是在這里還是給出一種創(chuàng)建泛型數(shù)組的方法。

public class GenericArrayWithTypeToken<T> {
    private T[] array;

    @SuppressWarnings("unchecked")
    public GenericArrayWithTypeToken(Class<T> type, int sz) {
        array = (T[]) Array.newInstance(type, sz);
    }

    public void put(int index, T item) {
        array[index] = item;
    }

    public T[] rep() {
        return array;
    }

    public static void main(String[] args) {
        GenericArrayWithTypeToken<Integer> gai = new GenericArrayWithTypeToken<Integer>(Integer.class, 10);
        Integer[] ia = gai.rep();
    }
}

這里我們使用的還是傳參數(shù)類型,利用類型的newInstance方法創(chuàng)建實例的方式。

邊界


這里Java重用了 extend關(guān)鍵字。邊界可以將類型參數(shù)的范圍限制到一個子集當中。

interface HasColor {
    Color getColor();
}

class Colored<T extends HasColor> {
    T item;

    public Colored(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }

    public Color color() {
        return item.getColor();
    }
}

class Dimension {
    public int x, y, z;
}

class ColoredDemension<T extends HasColor & Dimension> {
    T item;

    public ColoredDemension(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }

    Color color() {
        return item.getColor();
    }

    int getX() {
        return item.x;
    }

    int getY() {
        return item.y;
    }

    int getZ() {
        return item.z;
    }

}

interface Weight {
    int weight();
}

class Solid<T extends Dimension & HasColor & Weight> {
    T item;

    public Solid(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }

    Color color() {
        return item.getColor();
    }

    int getX() {
        return item.x;
    }

    int getY() {
        return item.y;
    }

    int getZ() {
        return item.z;
    }

    int weight() {
        return item.weight();
    }
}

class Bounded extends Dimension implements HasColor, Weight {
    @Override
    public Color getColor() {
        return null;
    }

    @Override
    public int weight() {
        return 0;
    }
}

public class BasicBound {
    public static void main(String[] args) {
        Solid<Bounded> solid = new Solid<Bounded>(new Bounded());
        solid.color();
        solid.weight();
        solid.getZ();
    }
}

extends關(guān)鍵字聲明中,有兩個要注意的地方:

  1. 類必須要寫在接口之前;
  2. 只能設(shè)置一個類做邊界,其它均為接口。

通配符


協(xié)變:

public class Holder<T> {
    private T value;

    public Holder(T apple) {
    }

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }

    @Override
    public boolean equals(Object o) {
        return value != null && value.equals(o);
    }

    public static void main(String[] args) {
        Holder<Apple> appleHolder = new Holder<Apple>(new Apple());
        Apple d = new Apple();
        appleHolder.setValue(d);

        // 不能自動協(xié)變
        // Holder<Fruit> fruitHolder=appleHolder;

        // 借助 ? 通配符和 extends 關(guān)鍵字可以實現(xiàn)協(xié)變
        Holder<? extends Fruit> fruitHolder = appleHolder;

        // 返回一個Fruit,因為添加邊界之后返回的對象是 ? extends Fruit,
        // 可以把它轉(zhuǎn)型為Apple,但是在不知道具體類型的時候存在風險
        d = (Apple) fruitHolder.getValue();

        //Fruit以及Fruit的父類,就不需要轉(zhuǎn)型
        Fruit fruit = fruitHolder.getValue();
        Object obj = fruitHolder.getValue();

        try {
            Orange c = (Orange) fruitHolder.getValue();
        } catch (Exception e) {
            System.out.print(e);
        }

        // 編譯不通過,因為編譯階段根本不知道子類型到底是什么類型
        //        fruitHolder.setValue(new Apple());
        //        fruitHolder.setValue(new Orange());

        //這里是可以的因為equals方法接受的是Object作為參數(shù),并不是 ? extends Fruit
        System.out.print(fruitHolder.equals(d));
    }
}

在Java中父類型可以持有子類型。如果一個父類的容器可以持有子類的容器,那么我們就可以稱為發(fā)生了協(xié)變。在java中,數(shù)組是自帶協(xié)變的,但是泛型的容器沒有自帶協(xié)變。我們可以根據(jù)利用邊界和通配符?來實現(xiàn)近似的協(xié)變。

Holder<? extends Fruit>就是一種協(xié)變的寫法。它表示一個列表,列表持有的類型是Fruit或其子類。

這個Holder<? extends Fruit>運行時持有的類型是未知的,我們只知道它一定是Fruit的子類。正因為如此,所以我們無法向這個holder中放入任何類型的對象,Object類型的對象也不可以。但是,調(diào)用它的返回方法卻是可以的。因為邊界明確定義了它是Fruit類型的子類。

逆變:

package wildcard;

import java.util.ArrayList;
import java.util.List;

public class GenericWriting {
    static <T> void writeExact(List<T> list, T item) {
        list.add(item);
    }

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

    static void f1() {
        writeExact(apples, new Apple());
        //this cannot be compile,said in Thinking in Java
        writeExact(fruits, new Apple());
    }

    static <T> void writeWithWildcard(List<? super T> list, T item) {
        list.add(item);
    }

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

    static <T> readWithWildcard(List<? super T> list, int index) {
        //Compile Error, required T but found Object
        return list.get(index);
    }
    public static void main(String[] args) {
        f1();
        f2();
    }
}

如果一個類的父類型容器可以持有該類的子類型的容器,我們稱這種關(guān)系為逆變。聲明方式List<? super Integer>, List<? super T> list。

不能給泛型參數(shù)給出一個超類型邊界;即不能聲明List<T super MyClass>。

上面的例子中,writeExact(fruits,new Apple());在《Java編程思想》中說是不能通過編譯的,但我試了一下,在Java1.6,Java1.7中是可以編譯的。不知道是不是編譯器比1.5版本升級了。

由于給出了參數(shù)類型的‘下界’,所以我們可以在列表中添加數(shù)據(jù)而不會出現(xiàn)類型錯誤。但是使用get方法獲取返回類型的時候要注意,由于聲明的類型區(qū)間是Object到T具有繼承關(guān)系的類。所以返回的類型為了確保沒有問題,都是以O(shè)bject類型返回回來的。比如過例子中list.get(index)的返回類型就是Object。

無界通配符

無界通配符<?> 意味著可以使用任何對象,因此使用它類似于使用原生類型。但它是有作用的,原生類型可以持有任何類型,而無界通配符修飾的容器持有的是某種具體的類型。舉個例子,在List<?>類型的引用中,不能向其中添加Object, 而List類型的引用就可以添加Object類型的變量。

一些需要注意的問題


1. 任何基本類型都不能作為類型參數(shù)

2. 實現(xiàn)參數(shù)化接口

例子:

interface Payable<T>{}
class Employee implements Payable<Employee> {}
//Compile Error
class Hourly extends Employee implements Payable<Hourly> {}

因為擦除的原因,Payable<Employee>Payable<Hourly>簡化為相同的Payable<Object>,例子中的代碼意味著重復兩次實現(xiàn)相同的接口。但他們的參數(shù)類型卻是不相同的。

3. 轉(zhuǎn)型和警告

使用帶有泛型類型參數(shù)的轉(zhuǎn)型或者instanceof不會有任何效果。因為他們在運行時都會被擦除到上邊界上。所以轉(zhuǎn)型的時候用的類型實際上是上邊解對應(yīng)的類型。

4. 重載

//Compile Error. 編譯不能通過
public class UseList<W,T>{
    void f(List<T> v){}
    void f(List<W> v){}
}

由于擦除的原因,重載方法將產(chǎn)生相同的類型簽名。避免這種問題的方法就是換個方法名。

5. 基類劫持接口

例子:

public class ComparablePet implements Comparable<ComparablePet>{
    public int compareTo(ComparablePet arg) {return 0;}
}
class Cat extends ComparablePet implements Comparable<Cat>{
    // Error: Comparable connot be inherited with
    // different arguments: <Cat> and <ComparablePet>
    public int compareTo(Cat arg);
}

父類中我們?yōu)?code>Comparable確定了ComparablePet參數(shù),那么其它任何類型都不能再與ComparablePet之外的對象再比較。子類中不能對同一個接口用不同的參數(shù)實現(xiàn)兩次。這有點類似于第四點中的重載。
但是我們可以在子類中覆寫父類中的方法。

關(guān)于泛型問題就先了解這么多,有什么不對的地方還請大家指正。也歡迎小伙伴們一起交流。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 開發(fā)人員在使用泛型的時候,很容易根據(jù)自己的直覺而犯一些錯誤。比如一個方法如果接收List作為形式參數(shù),那么如果嘗試...
    時待吾閱讀 1,128評論 0 3
  • 在之前的文章中分析過了多態(tài),可以知道多態(tài)本身是一種泛化機制,它通過基類或者接口來設(shè)計,使程序擁有一定的靈活性,但是...
    _小二_閱讀 764評論 0 0
  • 前面,由于對泛型擦除的思考,引出了對Java-Type體系的學習。本篇,就讓我們繼續(xù)對“泛型”進行研究: JDK1...
    賈博巖閱讀 5,303評論 3 29
  • 文章作者:Tyan博客:noahsnail.com 1. 什么是泛型 Java泛型(Generics)是JDK 5...
    SnailTyan閱讀 844評論 0 3
  • 剛才洗完臉照鏡子,突然發(fā)現(xiàn)自己眼角有褶子了。定了定睛,發(fā)現(xiàn)是看錯了。 石站長也說我長得著急,我笑笑。如果可以,我也...
    sorry_TheWorld閱讀 933評論 0 0

友情鏈接更多精彩內(nèi)容