Java 泛型進階


擦除


在泛型代碼內部,無法獲得任何有關泛型參數類型的信息。

例子1:

//這個例子表明編譯過程中並沒有根據參數生成新的類型
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:

//例子, 這個例子表明類的參數類型跟傳進去的類型沒有關系,泛型參數只是`占位符`
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]
*/

我們在運行期試圖獲取一個已經聲明的類的類型參數,發現這些參數依舊是‘形參’,並沒有隨聲明改變。也就是說在運行期,我們是拿不到已經聲明的類型的任何信息。

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

例子:

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的參數類型是Eadd方法的參數類型也是E,他們在類的內部是一致的,所以添加Integer類型的對象到stringList違反了內部類型一致,不能通過編譯。

重用 extends 關鍵字。通過它能給與參數類型添加一個邊界。

泛型參數將會被擦除到它的第一個邊界(邊界可以有多個)。編譯器事實上會把類型參數替換為它的第一個邊界的類型。如果沒有指明邊界,那么類型參數將被擦除到Object。下面的例子中,可以把泛型參數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關鍵字后后面的類型信息決定了泛型參數能保留的信息。

Java中擦除的基本原理

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

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

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

經過一番比較之后,發現兩分源碼雖然不同,但是對應的字節碼邏輯部分確是完全相同的。

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

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

擦除的缺陷

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

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

泛型參數的類型無法用instanceof關鍵字來做判斷。所以我們使用類類型來構造一個類型判斷器,判斷一個實例是否為特定的類型。

2. 創建類型實例

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

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

例子:

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

通過特定的工廠類實現特定的類型能夠解決實例化類型參數的需求。

3. 創建泛型數組

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

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

這里我們使用的還是傳參數類型,利用類型的newInstance方法創建實例的方式。

邊界


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

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關鍵字聲明中,有兩個要注意的地方:

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

通配符


協變:

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

        // 不能自動協變
        // Holder<Fruit> fruitHolder=appleHolder;

        // 借助 ? 通配符和 extends 關鍵字可以實現協變
        Holder<? extends Fruit> fruitHolder = appleHolder;

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

        //Fruit以及Fruit的父類,就不需要轉型
        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作為參數,並不是 ? extends Fruit
        System.out.print(fruitHolder.equals(d));
    }
}

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

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

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

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

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

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

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

無界通配符

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

一些需要注意的問題


1. 任何基本類型都不能作為類型參數

2. 實現參數化接口

例子:

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

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

3. 轉型和警告

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

4. 重載

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

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

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

父類中我們為Comparable確定了ComparablePet參數,那么其它任何類型都不能再與ComparablePet之外的對象再比較。子類中不能對同一個接口用不同的參數實現兩次。這有點類似於第四點中的重載。
但是我們可以在子類中覆寫父類中的方法。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM