Java泛型

泛型概述


由來

泛型是JDK 1.5的一項新特性,在Java語言處于還沒有出現泛型的版本時,只能通過Object是所有類型的父類和類型強制轉換兩個特點的配合來實現類型泛化。例如在哈希表的存取中,JDK 1.5之前使用HashMap的get()方法,返回值就是一個Object對象,由于Java語言里面所有的類型都繼承于java.lang.Object,那Object轉型為任何對象成都是有可能的。但是也因為有無限的可能性,就只有程序員和運行期的虛擬機才知道這個Object到底是個什么類型的對象。在編譯期間,編譯器無法檢查這個Object的強制轉型是否成功,如果僅僅依賴程序員去保障這項操作的正確性,許多ClassCastException的風險就會被轉嫁到程序運行期之中。

偽泛型

許多人都認為c++模板template和java泛型generic這兩個概念是等價的,不過,各種語言是怎么實現該功能,以及為什么這么做,卻千差萬別.

在C++中,模板本質上就是一套宏指令集,只是換了個名頭,編譯器會針對每種類型創建一份模板代碼的副本。有個證據可以證明這一點:MyClass<Foo>不會與MyClass<Bar>共享靜態變量。然而,兩個MyClass<Foo>實例則會共享靜態變量。但在Java中,MyClass類的靜態變量會由所有MyClass實例共享,無論類型參數相與否。

Java語言中的泛型則不一樣,它只在程序源碼中存在,在編譯后的字節碼文件中,就已經被替換為原來的原始類型(Raw Type,也稱為裸類型)了,并且在相應的地方插入了強制轉型代碼,因此對于運行期的Java語言來說,ArrayList<int>與ArrayList<String>就是同一個類。所以說泛型技術實際上是Java語言的一顆語法糖,Java語言中的泛型實現方法稱為類型擦除,基于這種方法實現的泛型被稱為偽泛型

泛型使用


泛型分類

  1. 泛型類
/**
 * 泛型類
 * @param <T> 此處T可以隨便寫為任意標識,常見的如T、E、K、V等形式的參數常用于表示泛型 在實例化泛型類時,必須指定T的具體類型
 */
class Generic<T> {
    /**
     * key這個成員變量的類型為T,T的類型由外部指定
     */
    private T key;

    /**
     * @param key 形參key的類型也為T,T的類型由外部指定
     */
    public Generic(T key) {
        this.key = key;
    }

    /**
     * @return 返回值類型為T,T的類型由外部指定
     */
    public T getKey() {
        return key;
    }
}
  1. 泛型接口
/**
 * 泛型接口
 *
 * @param <T>
 */
interface Generator<T> {
    public T next();
}

/**
 * 1. 未傳入泛型實參時,與泛型類的定義相同,在聲明類的時候,需將泛型的聲明也一起加到類中
 * 即:class FruitGenerator<T> implements Generator<T>{
 * 如果不聲明泛型,如:class FruitGenerator implements Generator<T>,編譯器會報錯:"Unknown class"
 */
class Fruit1Generator<T> implements Generator<T> {

    @Override
    public T next() {
        return null;
    }
}

/**
 * 2. 傳入泛型實參時:
 * 定義一個生產器實現這個接口,雖然我們只創建了一個泛型接口Generator<T>
 * 但是我們可以為T傳入無數個實參,形成無數種類型的Generator接口。
 * 在實現類實現泛型接口時,如已將泛型類型傳入實參類型,則所有使用泛型的地方都要替換成傳入的實參類型
 * 即:Generator<T>,public T next();中的的T都要替換成傳入的String類型。
 */
class Fruit2Generator implements Generator<String> {

    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

    @Override
    public String next() {
        Random rand = new Random();
        return fruits[rand.nextInt(3)];
    }
}
  1. 泛型方法
class StaticGenerator<E> {

    /**
     * 泛型方法
     * 需要添加額外的泛型聲明
     */
    public <T> void push(T data) {

    }

    /**
     * 泛型方法 靜態方法使用泛型必須為泛型方法,如果定義
     * 如:public static void show(E t){..},此時編譯器會提示錯誤信息:
     * "'StaticGenerator.this' cannot be referenced from a static context"
     */
    public static <T> void show(T t) {

    }


    /**
     * 泛型類中的包含泛型的方法,不是泛型方法
     */
    public E get() {
        return null;
    }
}

泛型通配符

看到Generic<Integer>不能被看作為Generic<Number>的子類,故無法使用多態,但可以使用如下形式接收不同泛型類型的Generic對象

public void showKeyValue1(Generic<?> obj){
    Log.d("泛型測試","key value is " + obj.getKey());
}

類型通配符一般是使用?代替具體的類型實參,注意了,此處’?’是類型實參,而不是類型形參 。再直白點的意思就是,此處的?和Number、String、Integer一樣都是一種實際的類型,可以把?看成所有類型的父類。是一種真實的類型。

泛型限定

  1. ? extends SomeClass 這種限定,說明的是只能接收SomeClass及其子類類型,所謂的“上限”
  2. ? super SomeClass 這種限定,說明只能接收SomeClass及其父類類型,所謂的“下限”

限定有以下規則

  • 不管該限定是類還是接口,統一都使用關鍵字extends
  • 可以使用&符號給出多個限定
  • 如果限定既有接口也有類,那么類必須只有一個,并且放在首位置

原理


類型擦除

在JAVA的虛擬機中并不存在泛型,泛型只是為了完善java體系,增加程序員編程的便捷性以及安全性而創建的一種機制,在JAVA虛擬機中對應泛型的都是確定的類型,在編寫泛型代碼后,java虛擬中會把這些泛型參數類型都擦除,用相應的確定類型來代替,代替的這一動作叫做類型擦除,而用于替代的類型稱為原始類型,在類型擦除過程中,一般使用第一個限定的類型來替換,若無限定則使用Object

class Test<? extends Comparable>
{
    private T t;
    public void show(T t)
    {

    }
}

虛擬機進行翻譯后的原始類型:

class Test
{
    private Comparable t;
    public void show(Comparable t)
    {
        
    }
}

用反射來看泛型的機制(甚至可以破壞)

public static void main(String[] args) {
    ArrayList<Integer> list = new ArrayList<>();
    list.add(1);

    try {
        list.getClass().getMethod("add", Object.class).invoke(list, "hello");
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    }

    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
}

在程序中定義了一個ArrayList泛型類型實例化為Integer的對象,如果直接調用add方法,那么只能存儲整形的數據。不過當我們利用反射調用add方法的時候,卻可以存儲字符串。這說明了Integer泛型實例在編譯之后被擦除了,只保留了原始類型。

原始類型(raw type)就是擦除去了泛型信息,最后在字節碼中的類型變量的真正類型。

類型擦除引起的問題及解決辦法


先檢查、再編譯

public static void main(String[] args) {
    ArrayList<Integer> list = new ArrayList<>();
    list.add(1);
    list.add("hello"); // 編譯報錯

}

因為類型擦除是在編譯期完成的,在運行的時候就會忽略泛型,為了保證在運行的時候不出現類型錯誤,就需要在編譯時檢查是否滿足泛型要求(類型檢查)。

類型檢查的依據

public static void main(String[] args) {
    // 1. 方式1
    ArrayList<Integer> list1 = new ArrayList<>();
    // 2. 方式2
    ArrayList list2 = new ArrayList<Integer>();
    list1.add(1);
    list1.add("hello"); // 該句編譯報錯
    list2.add(1);
    list2.add("hello"); // 該句編譯正常
}

注釋1和2都沒有編譯錯誤:第一種情況,在使用list1時與完全使用泛型參數一樣的效果,因為new ArrayList()只是在內存中新開辟一個存儲空間,它并不能判斷類型,而真正涉及類型檢查的是它的引用,所以在調用list1的時候會進行類型檢查。同理,第二種情況,就不會進行類型檢查。

泛型參數化類型沒有繼承關系

public static void main(String[] args) {
    ArrayList<String> list1 = new ArrayList<>();
    push(list1); // 編譯報錯

}
public static void push(ArrayList<Object> list) {

}

可以通過泛型通配符解決

public static void main(String[] args) {
    ArrayList<String> list1 = new ArrayList<>();
    push(list1);

}

public static void push(ArrayList<?> list) {

}

類型擦除與多態的沖突和解決方法


class Pair<T> {
    private T value;

    public T getValue() {
        return value;
    }

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

class DateInter extends Pair<Date> {
    @Override
    public void setValue(Date value) {
        super.setValue(value);
    }

    @Override
    public Date getValue() {
        return super.getValue();
    }
}

可以看到,父類和子類的方法中參數類型不同,如果是在普通的繼承關系中,這完全不是重寫,而是重載;但是如果在泛型中呢?

public static void main(String[] args) {
    DateInter dateInter = new DateInter();
    dateInter.setValue(new Date());
    dateInter.setValue(new Object()); // 編譯報錯
}

無法接收Object類型參數,可見在泛型中確實是重寫了,而不是重載。具體原理如下,編譯class后再通過jad工具反編譯出代碼如下

class Pair
{

    public Pair()
    {
    }

    public Object getValue()
    {
        return value;
    }

    public void setValue(Object obj)
    {
        value = obj;
    }

    private Object value;
}

class DateInter extends Pair
{

    DateInter()
    {
    }

    public void setValue(Date date)
    {
        super.setValue(date);
    }

    public Date getValue()
    {
        return (Date)super.getValue();
    }

    public volatile void setValue(Object obj)
    {
        setValue((Date)obj);
    }

    public volatile Object getValue()
    {
        return getValue();
    }
}

由于DateInter繼承Pair<Date>,但是Pair在類型擦除后還有一個public volatile void setValue(Object obj)方法,這和那個public void setValue(Date date)出現重載,但是程序本意卻是不需要public volatile void setValue(Object obj)的,故通過橋方法調用了public void setValue(Date date)方法,達到了重寫的效果。

泛型數組


看到了很多文章中都會提起泛型數組,經過查看sun的說明文檔,在java中是”不能創建一個確切的泛型類型的數組”的。

也就是說下面的這個例子是不可以的:

List<String>[] ls = new ArrayList<String>[10];  

而使用通配符創建泛型數組是可以的,如下面這個例子:

List<?>[] ls = new ArrayList<?>[10]; 

這樣也是可以的,但是仍存在ClassCastException問題

List<String>[] ls = new ArrayList[10];

假如泛型數組允許創建,代碼如下

// Not really allowed.
List<String>[] lsa = new List<String>[10];
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
// Unsound, but passes run time store check
oa[1] = li;

// Run-time error: ClassCastException.
String s = lsa[1].get(0);

這種情況下,由于JVM泛型的擦除機制,在運行時JVM是不知道泛型信息的,所以可以給oa[1]賦上一個ArrayList而不會出現異常,但是在取出數據的時候卻要做一次類型轉換,所以就會出現ClassCastException,如果可以進行泛型數組的聲明,上面說的這種情況在編譯期將不會出現任何的警告和錯誤,只有在運行時才會出錯。而對泛型數組的聲明進行限制,對于這樣的情況,可以在編譯期提示代碼有類型安全問題,比沒有任何提示要強很多。

解決辦法:

  1. 采用通配符方式
    下面采用通配符的方式是被允許的,對于通配符的方式,最后取出數據是要做顯式的類型轉換的
// OK, array of unbounded wildcard type.
List<?>[] lsa = new List<?>[10];
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
// Correct.
oa[1] = li;
// Run time error, but cast is explicit.
String s = (String) lsa[1].get(0);
  1. 采用List方式
List<List<String>> lists = new ArrayList<>();

錯誤方式

// 編譯不會報錯,但存在潛在的運行時ClassCastException
List<String>[] ls = new ArrayList[10];

參考


  1. java 泛型詳解-絕對是對泛型方法講解最詳細的,沒有之一
  2. 關于Java泛型深入理解小總結
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容