1. 程式人生 > 其它 >Java泛型學習筆記

Java泛型學習筆記

技術標籤:Java學習

泛型

泛型是一種“程式碼模板”,可以用一套程式碼套用各種型別。

什麼是泛型

在講解什麼是泛型之前,我們先觀察Java標準庫提供的ArrayList,它可以看作“可變長度”的陣列,因為用起來比陣列更方便。

實際上ArrayList內部就是一個Object[] 陣列,配合儲存一個當前分配的長度,就可以充當“可變陣列”:

public class ArrayList {
    private Object[] array;
    private int size;
    
    public void add(Object e){
        
    }
public void remove(int index){ } public Object get(int index){ return null; } }

如果用上述ArrayList儲存String型別,需要強制轉型。

ArrayList list = new ArrayList();
list.add("Hello");
String first = (String) list.get(0);
System.out.println(first);

很容易出現ClassCastException,因為容易"誤轉型":

list.add(123);
//error: ClassCastException
String second = (String) list.get(1);

為String單獨編寫ArrayList是一種解決方法。
實際上,還需要為其他所有的class單獨編寫一種ArrayList:
這是不可能的。

為了解決新的問題,我們必須把ArrayList變成一種模板:ArrayList,T可以是任何class。這樣一來,我們就實現了:編寫一次模板,可以建立任意型別的ArrayList:

ArrayList<String> strList = new ArrayList<String>
(); ArratList<Float> floatList = new ArrayList<Float>(); ArrayList<Person> personList = new ArrayList<Person>();

因此,泛型就是定義一種模板,例如ArrayList,然後在程式碼中為用到的類建立對應的ArrayList<型別>:

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

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

strList.add("Hello");//OK
String s = strList.get(0);//OK
strList.add(123);//compile error!
int n = strList.get(0);//compile error!

這樣一來,既實現了編寫一次,萬能匹配,又通過編譯器保證了型別安全:這就是泛型。

向上轉型

在Java標準庫中的ArrayList實現了List介面,它可以向上轉型為LIst:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

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

即型別ArrayList可以向上轉型為List。

要特別注意:不能把ArrayList向上轉型為ArrayList或List。

error:

java: 不相容的型別: java.util.ArrayList<java.lang.Integer>無法轉換為java.util.ArrayList<java.lang.Number>

這是為什麼呢?

因為ArrayList改為ArrayList後,將可以被賦予Float,Double等是Number的子類,但不是Integer的物件,從ArrayList獲取Integer時有可能讀到非Integer物件,產生ClassCastException。

實際上,編譯器為了避免這種錯誤,根本不允許大ArrayList轉型為ArrayList。

ArrayList和ArrayList兩者完全沒有繼承關係。

使用泛型

使用ArrayList時,如果不定義泛型型別時,泛型型別實際上就是Object:

List list = new ArrayList();
list.add("Hello");
list.add("World");
list.add(123);
String first = (String) list.get(0);
String second = (String) list.get(1);
int third = (Integer)list.get(2);

此時,只能把當做Object使用,沒有發揮泛型的優勢。
當我們定義泛型型別後,List的泛型介面變為強型別List:

List<String> list1 = new ArrayList<>();//可以省略後面的String,編譯器可以自動推斷泛型型別。
list1.add("Hello");
list1.add("World");
String s1 = list1.get(0);
String s2 = list1.get(1);

泛型介面

除了ArrayList使用了泛型,還可以在介面中使用泛型.例如,Arrays.sort(Object[])可以對任意陣列進行排序,但待排序的元素必須實現Comparable這個泛型介面:

public interface Comparable<T> {
	/**
	* 返回負數:當前例項比引數o小
	* 返回0: 當前例項與引數o相等
	* 返回正數: 當前例項比引數o大
	*/
    public int compareTo(T o);
}

編寫泛型

編寫泛型類比普通類要複雜.通常來說,泛型類一般用在集合類中,例如ArrayList,我們很少需要編寫泛型類.

靜態方法

編寫泛型類時,要特別注意,泛型型別不能用於靜態方法.例如:

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 static Pair<T> create(T first,T last){
        return new Pair<T>(first, last);
    }
}

上述程式碼會導致編譯錯誤,我們無法在靜態方法create()的方法引數和返回型別上使用泛型型別T.

對於靜態方法,我們可以單獨改寫為"泛型"方法,只需要使用另一個型別即可.

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 static <K> Pair<K> create(K first,K last){
        return new Pair<K>(first, last);
    }
}

這樣才能清楚地將靜態方法的泛型型別和例項型別的泛型型別區分開.

多個泛型型別

Java標準庫的Map<K,V>就是使用兩種泛型型別的例子.它對key使用一種型別,對Value使用另一種型別.

擦拭法

Java語言的泛型實現方式是擦拭法(Type Erasure).
所謂擦除法是指,虛擬機器對泛型一無所知,所有的工作都是編譯器做的.

Java使用擦拭法實現泛型,導致了:

  • 編譯器把型別視為Obejct;
  • 編譯器根據實現安全的強制轉型.

編譯器看到的程式碼

Pair<String> p = new Pair<>("Hello","World");
String first = p.getFirst();
String last = p.getLast();

而虛擬機器的程式碼並沒有泛型:

Pair p = new Pair("Hello","World");
String first = (String) p.getFirst();
String last = (String) p.getLast();

瞭解了Java泛型的實現方式,我們就知道了Java泛型的侷限:

  1. 不能是基本型別,因為實際型別是Object,Object型別無法持有基本型別;
  2. 無法取得帶泛型的Class,觀察一下程式碼:
public class TypeErasure {

    public static void main(String[] args) {
        Pair2<String> p1 = new Pair2<>("Hello","World");
        Pair2<Integer> p2 = new Pair2<>(123,456);
        Class c1 = p1.getClass();
        Class c2 = p2.getClass();
        System.out.println(c1==c2);
        System.out.println(c1==Pair2.class);
    }

    

}

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

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

    public T getFirst() {
        return first;
    }

    public void setFirst(T first) {
        this.first = first;
    }

    public T getLast() {
        return last;
    }

    public void setLast(T last) {
        this.last = last;
    }
}
因為T是Object,我們對Pair<String>和Pair<Integer>型別獲取Class時,獲取到的是同一個Class,也就是Pair類的Class.
  1. 無法判斷帶泛型的型別:
    只有唯一的Pair.class
  2. 不能例項化T型別
    因為T是Object,任何new T()都相當於new Object();沒有意義,禁止.
    要例項化T型別,我們必須藉助額外的Class引數:
    public Pair2(Class<T> clazz) throws IllegalAccessException, InstantiationException {
        first = clazz.newInstance();
        last = clazz.newInstance();
    }
    
    上述程式碼藉助Class引數並通過反射來例項化T型別,使用的時候,也必須傳入Class.
Pair<String> pair = new Pair<>(String.class);

不恰當的覆寫方法

看似正確定義的方法無法編譯
equals(T t)方法實際上會被擦拭成equals(Object t),而這個方法是繼承自Object的,編譯器會阻止一個實際上會變為覆寫的泛型方法定義.

換個方法名,避開與Object的方法同名就可以.
在這裡插入圖片描述

泛型繼承

一個類可以繼承自一個泛型類.
在這裡插入圖片描述
使用的時候,因為子類IntPair並沒有泛型型別,正常使用即可.
在這裡插入圖片描述
前面講了,我們無法獲取Pair的T型別,即給定一個變數Pair p,無法從p中獲取到Integer型別.

但是,在父類是泛型型別的情況下,編譯器就必須把型別T儲存到子類的class檔案中,不然編譯器就不知道IntPair只能存取Integer這種型別.

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

public class TypeErasure {

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

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

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

    public Pair2(Class<T> clazz) throws IllegalAccessException, InstantiationException {
        first = clazz.newInstance();
        last = clazz.newInstance();
    }

    public boolean same(T t){
        return this == t;
    }

    public T getFirst() {
        return first;
    }

    public void setFirst(T first) {
        this.first = first;
    }

    public T getLast() {
        return last;
    }

    public void setLast(T last) {
        this.last = last;
    }
}

class IntPair extends Pair2<Integer>{

    public IntPair(Integer first, Integer last) {
        super(first, last);
    }

    public IntPair(Class<Integer> clazz) throws IllegalAccessException, InstantiationException {
        super(clazz);
    }
}

因為Java引入了泛型,所以,只用Class來標記型別已經不夠了.Java的型別系統結構如下:

在這裡插入圖片描述

extends萬用字元