Java基礎(chǔ)-泛型

??面試被問到:泛型是怎么被擦除的?愣是說不出個所以然來...
其實了解泛型的擦拭法才能更好地使用和編寫泛型。基礎(chǔ)永遠不能忘~!

  • 1.使用泛型
  • 2.編寫泛型
  • 3.擦拭法
  • 4.extends通配符
  • 5.super通配符
  • 6.泛型和反射

1.使用泛型

泛型是一種“代碼模板”,可以用一套代碼套用各種類型。例如ArrayList<T>,然后在代碼中為用到的類創(chuàng)建對應(yīng)的ArrayList<類型>

ArrayList<String> strList = new ArrayList<可省略>();

由編譯器針對類型作檢查:

strList.add("hello"); // OK
String s = strList.get(0); // OK
strList.add(new Integer(123)); // compile error!
Integer n = strList.get(0); // compile error!

向上轉(zhuǎn)型
在Java標(biāo)準庫中的ArrayList<T>實現(xiàn)了List<T>接口,它可以向上轉(zhuǎn)型為List<T>

public class ArrayList<T> implements List<T> {
    ...
}

List<String> list = new ArrayList<String>();

要特別注意:不能把ArrayList<Integer>向上轉(zhuǎn)型為ArrayList<Number>List<Number>。
???這是為什么呢?假設(shè)ArrayList<Integer>可以向上轉(zhuǎn)型為ArrayList<Number>,觀察一下代碼:

// 創(chuàng)建ArrayList<Integer>類型:
ArrayList<Integer> integerList = new ArrayList<Integer>();
// 添加一個Integer:
integerList.add(new Integer(123));
// “向上轉(zhuǎn)型”為ArrayList<Number>:
ArrayList<Number> numberList = integerList;
// 添加一個Float,因為Float也是Number:
numberList.add(new Float(12.34));
// 從ArrayList<Integer>獲取索引為1的元素(即添加的Float):
Integer n = integerList.get(1); // ClassCastException!

實際上,編譯器為了避免這種錯誤,根本就不允許??。

??泛型的好處是:使用時不必對類型進行強制轉(zhuǎn)換,它通過編譯器對類型進行檢查。

2.編寫泛型

編寫泛型類比普通類要復(fù)雜。通常來說,泛型類一般用在集合類中,例如ArrayList<T>,我們很少需要編寫泛型類。
可我們一定要編寫的話,首先,按照某種類型,例如:String,來編寫類:

public class Pair {
    private String first;
    private String last;
    public Pair(String first, String last) {
        this.first = first;
        this.last = last;
    }
    public String getFirst() {
        return first;
    }
    public String getLast() {
        return last;
    }
}

然后,把特定類型String替換為T,并申明<T>

public class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
}

??????編寫泛型類時,要特別注意,泛型類型<T>不能用于靜態(tài)方法。
對于靜態(tài)方法,我們可以單獨改寫為“泛型”方法,只需要使用另一個類型即可。對于上面的create()靜態(tài)方法,我們應(yīng)該把它改為另一種泛型類型,例如,<K>

public class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() { ... }
    public T getLast() { ... }

    // 靜態(tài)泛型方法應(yīng)該使用其他類型區(qū)分:
    public static <K> Pair<K> create(K first, K last) {
        return new Pair<K>(first, last);
    }
}

這樣才能清楚地將靜態(tài)方法的泛型類型和實例類型的泛型類型區(qū)分開。

多個泛型類型
泛型還可以定義多種類型。例如,我們希望Pair不總是存儲兩個類型一樣的對象,就可以使用類型<T, K>

public class Pair<T, K> {
    private T first;
    private K last;
    public Pair(T first, K last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() { ... }
    public K getLast() { ... }
}
// 使用的時候,需要指出兩種類型:
Pair<String, Integer> p = new Pair<>("test", 123);

Java標(biāo)準庫的Map<K, V>就是使用兩種泛型類型的例子。它對Key使用一種類型,對Value使用另一種類型。

3.擦拭法

泛型是一種類似”模板代碼“的技術(shù),不同語言的泛型實現(xiàn)方式不一定相同。

  • Java語言的泛型實現(xiàn)方式是擦拭法(Type Erasure)。
    虛擬機對泛型其實一無所知,所有的工作都是編譯器做的。

例如,我們編寫了一個泛型類Pair<T>,這是編譯器看到的代碼:

public class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
}
// 使用:
Pair<String> p = new Pair<>("Hello", "world");
String first = p.getFirst();
String last = p.getLast();

而虛擬機根本不知道泛型。這才是虛擬機執(zhí)行的代碼:

public class Pair {
    private Object first;
    private Object last;
    public Pair(Object first, Object last) {
        this.first = first;
        this.last = last;
    }
    public Object getFirst() {
        return first;
    }
    public Object getLast() {
        return last;
    }
}
// 使用:
Pair p = new Pair("Hello", "world");
String first = (String) p.getFirst();
String last = (String) p.getLast();

因此,Java使用擦拭法實現(xiàn)泛型,導(dǎo)致了:

  • 編譯器把類型<T>視為Object
  • 編譯器根據(jù)<T>實現(xiàn)安全的強制轉(zhuǎn)型

??所以,重點來了??????Java的泛型是由編譯器在編譯時實行的,編譯器內(nèi)部永遠把所有類型T視為Object處理,但是,在需要轉(zhuǎn)型的時候,編譯器會根據(jù)T的類型自動為我們實行安全地強制轉(zhuǎn)型。

??了解了Java泛型的實現(xiàn)方式——擦拭法,我們就知道了Java泛型的局限:

  • 局限一:<T>不能是基本類型,例如int,因為實際類型是Object,Object類型無法持有基本類型
  • 局限二:無法取得帶泛型的Class。觀察以下代碼:
public class Main {
    public static void main(String[] args) {
        Pair<String> p1 = new Pair<>("Hello", "world");
        Pair<Integer> p2 = new Pair<>(123, 456);
        Class c1 = p1.getClass();
        Class c2 = p2.getClass();
        System.out.println(c1==c2); // true
        System.out.println(c1==Pair.class); // true
    }
}
class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
}
// 判斷類型時編譯失敗
Pair<Integer> p = new Pair<>(123, 456);
// Compile error:
if (p instanceof Pair<String>.class) {
}

無論T的類型是什么,getClass()返回同一個Class實例,因為編譯后它們?nèi)慷际?code>Pair<Object>,因此也無法判斷帶泛型的Class的類型:。

  • 局限四:不能實例化T類型:
public class Pair<T> {
    private T first;
    private T last;
    public Pair() {
        // Compile error:
        first = new T();
        last = new T();
    }
}

這樣一來,創(chuàng)建new Pair<String>()和創(chuàng)建new Pair<Integer>()就全部成了Object,顯然編譯器要阻止這種類型不對的代碼。
要實例化T類型,我們必須借助額外的Class<T>參數(shù):

public class Pair<T> {
    private T first;
    private T last;
    public Pair(Class<T> clazz) {
        first = clazz.newInstance();
        last = clazz.newInstance();
    }
}

上述代碼借助Class<T>參數(shù)并通過反射來實例化T類型,使用的時候,也必須傳入Class<T>。例如:

Pair<String> pair = new Pair<>(String.class);

因為傳入了Class<String>的實例,所以我們借助String.class就可以實例化String類型。

泛型繼承

在繼承了泛型類型的情況下,子類可以獲取父類的泛型類型。例如:IntPair可以獲取到父類的泛型類型Integer。獲取父類的泛型類型代碼比較復(fù)雜:

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

public class Main {
    public static void main(String[] args) {
        Class<IntPair> clazz = IntPair.class;
        Type t = clazz.getGenericSuperclass();
        if (t instanceof ParameterizedType) {
            ParameterizedType pt = (ParameterizedType) t;
            Type[] types = pt.getActualTypeArguments(); // 可能有多個泛型類型
            Type firstType = types[0]; // 取第一個泛型類型
            Class<?> typeClass = (Class<?>) firstType;
            System.out.println(typeClass); // Integer
        }
    }
}

class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
}

class IntPair extends Pair<Integer> {
    public IntPair(Integer first, Integer last) {
        super(first, last);
    }
}

因為Java引入了泛型,所以,只用Class來標(biāo)識類型已經(jīng)不夠了。??實際上,Java的類型系統(tǒng)結(jié)構(gòu)如下:

4.extends通配符

我們前面已經(jīng)講到了泛型的繼承關(guān)系:·Pair<Integer>不是Pair<Number>的子類。
??可是我們想傳遞Pair<Integer>這怎么辦呢?
??這時候我們可以extends通配符來限定T的類型:

public class Pair<T extends Number> { ... }

現(xiàn)在,我們可以這樣定義了,因為Number、IntegerDouble都符合<T extends Number>

Pair<Number> p1 = null;
Pair<Integer> p2 = new Pair<>(1, 2);
Pair<Double> p3 = null;

5.super通配符

需求:和extends通配符相反,這次,我們希望接受Pair<Integer>類型,以及Pair<Number>、Pair<Object>,因為NumberObjectInteger的父類,我們這樣定義:

public class Pair<T super Integer> { ... }

現(xiàn)在,我們可以定義了,因為Number、IntegerObject都是父類<T super Integer>

Pair<Number> p1 = null;
Pair<Integer> p2 = new Pair<>(1, 2);
Pair<Object> p3 = null;

好啦,以上使我們必須要掌握的語法。


華麗的分割線


以上是用在定義泛型類型時通配符的使用,其實泛型還可被定義在方法參數(shù)上,以下知識點容易頭疼......
下面代碼說明了get和set時兩種情況:

public class Main {
public static void main(String[] args) {
        Pair<Integer> p = new Pair<>(123, 456);
        int n = getNum(p);
        System.out.println(n);
        Pair<Number> p1 = new Pair<>(12.3, 4.56);
        Pair<Integer> p2 = new Pair<>(123, 456);
        setSame(p1, 100);
        setSame(p2, 200);
        System.out.println(p1.getFirst() + ", " + p1.getLast());
        System.out.println(p2.getFirst() + ", " + p2.getLast());
    }
    static int getNum(Pair<? extends Number> p) {
        Number first = p.getFirst();
        Number last = p.getLast();
        return first.intValue() + last.intValue();
    }
    static void setSame(Pair<? super Integer> p, Integer n) {
        p.setFirst(n);
        p.setLast(n);
    }
}
class Pair<T> {
    private T first;
    private T last;

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

    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
    public void setFirst(T first) {
        this.first = first;
    }
    public void setLast(T last) {
        this.last = last;
    }
}

作為方法參數(shù),<? extends T>類型和<? super T>類型的區(qū)別在于:

  • <? extends T>允許調(diào)用讀方法T get()獲取T的引用,但不允許調(diào)用寫方法set(T)傳入T的引用(傳入null除外);
  • <? super T>允許調(diào)用寫方法set(T)傳入T的引用,但不允許調(diào)用讀方法T get()獲取T的引用(獲取Object除外)。
    一個是允許讀不允許寫,另一個是允許寫不允許讀。
    先記住上面的結(jié)論,我們來看Java標(biāo)準庫的Collections類定義的copy()方法:
public class Collections {
    // 把src的每個元素復(fù)制到dest中:
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        for (int i=0; i<src.size(); i++) {
            T t = src.get(i);
            dest.add(t);
        }
    }
}

*它的作用是把一個List的每個元素依次添加到另一個List中。
它的第一個參數(shù)是List<? super T>,表示目標(biāo)List,第二個參數(shù)List<? extends T>,表示要復(fù)制的List。
我們可以簡單地用for循環(huán)實現(xiàn)復(fù)制。在for循環(huán)中,我們可以看到:

  • 對于類型<? extends T>的變量src,我們可以安全地獲取類型T的引用,
  • 而對于類型<? super T>的變量dest,我們可以安全地傳入T的引用。*

這個copy()方法的定義就完美地展示了extendssuper的意圖:

  • copy()方法內(nèi)部不會讀取dest,因為不能調(diào)用dest.get()來獲取T的引用;
  • copy()方法內(nèi)部也不會修改src,因為不能調(diào)用src.add(T)。

這是由編譯器檢查來實現(xiàn)的。如果在方法代碼中意外修改了src,或者意外讀取了dest,就會導(dǎo)致一個編譯錯誤:

public class Collections {
    // 把src的每個元素復(fù)制到dest中:
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        ...
        T t = dest.get(0); // compile error!
        src.add(t); // compile error!
    }
}

這個copy()方法的另一個好處是可以安全地把一個List<Integer>添加到List<Number>,但是無法反過來添加:

// copy List<Integer> to List<Number> ok:
List<Number> numList = ...;
List<Integer> intList = ...;
Collections.copy(numList, intList);

// ERROR: cannot copy List<Number> to List<Integer>:
Collections.copy(intList, numList);

而這些都是通過superextends通配符,并由編譯器強制檢查來實現(xiàn)的。

無限定通配符

我們已經(jīng)討論了<? extends T><? super T>作為方法參數(shù)的作用。實際上,Java的泛型還允許使用無限定通配符(Unbounded Wildcard Type),即只定義一個?:

void sample(Pair<?> p) {
}

因為<?>通配符既沒有extends,也沒有super,因此:

  • 不允許調(diào)用set(T)方法并傳入引用(null除外);
  • 不允許調(diào)用T get()方法并獲取T引用(只能獲取Object引用)。

換句話說,既不能讀,也不能寫,那只能做一些null判斷:

static boolean isNull(Pair<?> p) {
    return p.getFirst() == null || p.getLast() == null;
}

大多數(shù)情況下,可以引入泛型參數(shù)<T>消除<?>通配符:

static <T> boolean isNull(Pair<T> p) {
    return p.getFirst() == null || p.getLast() == null;
}

<?>通配符有一個獨特的特點,就是:Pair<?>是所有Pair<T>的超類:

public class Main {
    public static void main(String[] args) {
        Pair<Integer> p = new Pair<>(123, 456);
        Pair<?> p2 = p; // 安全地向上轉(zhuǎn)型
        System.out.println(p2.getFirst() + ", " + p2.getLast());
    }
}

class Pair<T> {
    private T first;
    private T last;

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

    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
    public void setFirst(T first) {
        this.first = first;
    }
    public void setLast(T last) {
        this.last = last;
    }
}

上述代碼是可以正常編譯運行的,因為Pair<Integer>Pair<?>的子類,可以安全地向上轉(zhuǎn)型。

6.泛型和反射

Java的部分反射API也是泛型。例如:Class<T>就是泛型:

// compile warning:
Class clazz = String.class;
String str = (String) clazz.newInstance();

// no warning:
Class<String> clazz = String.class;
String str = clazz.newInstance();

調(diào)用ClassgetSuperclass()方法返回的Class類型是Class<? super T>

Class<? super String> sup = String.class.getSuperclass();

構(gòu)造方法Constructor<T>也是泛型:

Class<Integer> clazz = Integer.class;
Constructor<Integer> cons = clazz.getConstructor(int.class);
Integer i = cons.newInstance(123);

??使用<T>泛型時,要帶著最終被擦除的思想去設(shè)計代碼,要知道最終是會被改寫成Object的,這樣才能使用好和編寫好泛型!

泛型的使用就到這里,其他語法使用:Java基本功系列之?注解Annotation
(End)

最后編輯于
?著作權(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ù)。

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