Java基礎系列(三十六):泛型中需要注意的地方

weixin_33913332發表於2018-11-12

一、不能用型別引數代替基本型別

因為型別擦除之後,原本的型別會被替代為Object型別的域,而Object不能儲存基本型別的值。就是說沒有Pair<double>,取而代之的是該基本型別的包裝器型別Pair<Double>

二、執行時型別查詢之適用於原始型別

這句話怎麼理解呢?也就是說

a instanceof Pair<String> //錯誤
a instanceof Pair<T>    //同樣是錯誤

而使用getClass

Pair<String> stringPair = ...;
Pair<Employee> employeePair = ...;
if (stringPair.getClass() == employeePair.getClass())   //true

這裡比較的結果為什麼為true呢?因為兩次呼叫getClass都會返回Pair.class,即該泛型類的原始型別,這就是型別擦除所起到的作用。

三、不能建立引數化型別的陣列

因為在經過了型別擦除之後,我們所規定的型別被型別擦除之後無效,不僅可以存入我們所規定的型別,同樣的也可以存入其他型別的值。

Pair<String>[] table = new Pair<String>[10];
//擦除之後,table的型別是Pair[], 可以把它轉換為Object[]
Object[] objArr = table;
//這裡可以通過陣列型別儲存檢查,但是仍會導致一個型別錯誤
objArr[0] = new Pair<Employee>();

所以,我們不能建立引數化型別的陣列,這裡需要注意的是,我們僅僅是不能建立,但是宣告型別為Pair<String>[]的變數仍是合法的。不過不能去初始化這個變數。

如果我們想要收集引數化型別物件,只有一種安全且有效的方法:

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

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

為了呼叫這個方法,Java虛擬機器必須建立一個Pair<String>陣列,但是這樣就會違反了前面上一條的規則。但是對於這種情況,規則有所放鬆,我們只會得到一個警告,而不是錯誤。
我們可以使用@SuppressWarnings("unchecked")@SafeVarargs標註這個方法。
但是這裡會存在安全隱患:

static <E> array(E...  array) {
    return array;
}
//呼叫這個方法:
Pair<String>[] table = array(pair1, pair2);
Object objArr = table;
objArr[0] = new Pair<Employee>();

這裡可以順利執行而不出出現ArrayStoreException,因為陣列儲存只會檢查擦除的型別,但是當我們處理table[0]時會在別處得到一個異常。所以當我們需要想引數個數可變的方法傳一個泛型型別的例項的時候,一定要注意,而不是無腦的去加上註釋抑制這個警告

五、不能例項化型別變數

不能使用像new T(...)new T[...]T.class這樣的表示式中的型別變數。下面是一個錯誤的例子:

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

因為型別擦除會將T改變成Object,而且,本意肯定不希望呼叫new Object()。在Java SE 8之後,最好解決方法是讓呼叫者提供一個構造器表示式。例如:

Pair<String> p = Pair.makePair(String::new);

makePair方法接收一個Supplier<T>,這是一個函式式介面,表示一個無引數而且返回型別為T的函式:

public static <T> Pair<T> makePair(Supplier<T> constr) {
    return new Pair<>(constr.get(), constr.get());
}

或者通過Class.newInstance方法來構造泛型物件。

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

這個方法可以按照下列方式呼叫:

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

注意,Class類本身是泛型,String.class就是一個Class<String>的例項,因此makePair方法能夠推斷出pair的型別。

六、不能構造泛型陣列

public static <T extends Comparable> T[] minmax(T[] a) {
    T[] mm = new T[2];
}

型別擦除會讓這個方法永遠構造Comparable[2]陣列。

如果陣列僅僅作為一個類的私有例項域,就可以將這個陣列宣告為Object[],並且在獲取元素的時候進行型別轉換。例如,ArrayList類可以這麼實現:

public class ArrayList<E> {
    private Object[] elements;
    
    @SuppressWarnings("unchecked") 
    public E get(int n) {
        return (E)elements[n];
    }
    public void set(int n, E e) {
        elements[n] = e;
    }
}

如果按照這個思路去改造上面的程式碼:

public static <T extends Comparable> T[] minmax(T[] a) {
    Object[] mm = new Object[2];
    return (T[]) mm;    
}

這樣做,雖然在編譯的時候沒有任何警告,但是在執行的時候會將Object[]引用賦給Comparable[]變數的時候,會丟擲一個ClassNotCastException異常。
對於這種情況,我們有兩種解決方案,可以讓使用者提供一個陣列構造器表示式:

String[] ss = ArrayAlg.minmax(String[]::new, "Tom", "Dick", "Harry");

構造器表示式String::new指示一個函式,給定所需的長度,會構造一個指定長度的String陣列。
minmax方法使用這個引數生產一個有正確型別的陣列:

public static <T extends Comparable> T[] minmax(IntFunction<T[]> constr, T... a) {
    T[] mm = constr.apply(2);
    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 mm;
}

或者使用反射也可以達到這個效果:

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

七、泛型類的靜態上下文中型別變數無效

不能在靜態域或方法中引用型別變數,例如下面的程式碼就是錯誤的:

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

八、不能丟擲或捕獲泛型類的例項

既不能丟擲也不能捕獲泛型類物件,實際上,甚至泛型類擴充套件Throwable都是不合法的。例如下面的程式碼:

public class Problem<T> extends Exception {...} 

catch 子句中不能使用型別變數。例如,以下方法將不能通過編譯:

public static <T extends Throwable> void doWork(Class<T> t) {
    try {
        //do work
    }catch (T e) {  //不能catch型別變數
        ...
    }
}

不過,在異常規範中使用型別變數是允許的

public static <T extends Throwable> void doWork(T t) throws T {
    try {
        //do work
    } catch (Throwable realCause) {
        t.initCause(realCause);
        throw t;
    }
}

九、可以消除對受查異常的檢查

Java異常處理的一個基本原則是,必須為所有受查異常提供一個處理器。不過可以利用泛型消除這個限制。

@SuppressWarnings("unchecked")
public static <T extends Throwable> void throwAs(Throwable e) throws T {
    throw (T) e;
}

假如這個方法包含在類Block中,如果呼叫

Block.<RuntimeException>throwAs(t);

編譯器就會認為t是一個非受查異常。以下程式碼就會把所有異常都轉換為編譯器所認為的非受查異常:

try {
    //do work
} catch (Throwable t) {
    Block.<RuntimeExcetion>throwAs(t);
}

下面把這個程式碼包裝到一個抽象類中。使用者可以覆蓋body方法來提供一個具體的動作。呼叫toThread時,會得到Thread類的一個物件,它的run方法不會介意受查異常。

public abstract class Block {
    public abstract void body() throws Exception;
    public Thread toThread() {
        return new Thread() {
            public void run() {
                try{
                    body();
                } catch (Throwable t) {
                    Block.<RuntimeException>throwAs(t);
                }
            }
        }
    }
    
    public static <T extends Throwable> void throwAs(Throwable e) throws T {
        throw (T) e;
    }
}

現在我們寫一個程式來執行一個執行緒,它會丟擲一個受查異常

public class Test {
    public static void main(String[] args) {
        new Block() {
            public void body() throws Exception {
                Scanner in = new Scanner(new File("1231"), "UTF-8");
                while (in.hasNext()) {
                    System.out.println(in.next())
                }
            }
        }.toThread().start();
    }
}

執行這個程式的時候,會得到一個棧軌跡,其中包含一個FileNotFoundException。這就意味著,在正常情況下,我們必須捕獲執行緒run方法中的所有受查異常,把他們包裝到非受查異常中,因為run方法宣告為不丟擲任何受查異常。
但是我們在這裡並沒有進行這種包裝,我們只是丟擲異常,並哄騙編譯器,讓它認為這不是一個受查異常。

原創文章,文筆有限,才疏學淺,文中若有不正之處,萬望告知


公眾號

10641481-0a644730ab366707

相關文章