10 java泛型

泛型是JDK 5.0中引入的一個新特性。這個特性允許在類和接口的定義中使用類型參數。該類型參數在程序運行時被具體的類型替換。泛型就是類型參數的佔位符。我們在學習API的類集框架時就使用過泛型來表示元素的類型。下面簡單總結一下泛型的基礎知識。

1. 使用泛型

最常見的使用帶泛型的集合類:
import java.util.*;

public class Test
{
    public static void main(String[] args)
    {
        ArrayList<Integer> array = new ArrayList<Integer>();
        System.out.println(array.getClass().getName());
    }
}
//運行結果:
//java.util.ArrayList
因爲泛型是在Java編譯器中實現的,僅僅用作編譯時類型檢查,在編譯得到的字節碼中會被擦除。因此運行時的反射命令得到的array類型是ArrayList類而非ArrayList<Integer>類。利用這個特點,我們可以繞過編譯時的泛型匹配檢查,在運行時給集合類賦予與類型參數不符的元素。這同樣要用到反射:
import java.util.*;
import java.lang.reflect.*;

public class Test
{
    public static void main(String[] args)  throws Exception
    {
        ArrayList<Integer> array = new ArrayList<Integer>();
        array.getClass().getMethod("add", Object.class).invoke(array, "test");
        System.out.println(array.get(0));
    }
}
//運行結果
//test
類型擦除不止適用於集合類,所有使用泛型的類,接口,方法和構造器都是一樣的道理(泛型作爲方法的形參除外)。

2. 泛型的兼容性和繼承

類型參數非空的泛型類引用可以指向類型參數爲空的泛型類實例,反之亦可。還是拿集合類來舉例,比如:
ArrayList<Integer> array = new ArrayList();
ArrayList array2 = new ArrayList<Integer>();
這樣的語句編譯可以通過,只是會有警告。
但是類型參數同樣非空的引用和實例,若要求該引用指向該實例,需先保證兩者類型參數嚴格一致:
//以下語句都報錯:不兼容的類型
ArrayList<Integer> array = new ArrayList<String>();
ArrayList<Object> array2 = new ArrayList<Integer>();
也即:類型參數的繼承關係與泛型兼容性無關。另外,數組的定義中不能出現泛型:
//編譯通過
ArrayList[] array = new ArrayList[3];
//報錯:創建泛型數組
ArrayList<Integer>[] array2 = new ArrayList<Integer>[3];
上面就是對泛型兼容性的簡單總結。特別要注意的是,這裏不涉及泛型通配符。使用通配符時的兼容規則更爲複雜。

3. 泛型通配符

3.1 形參的通配符

不考慮泛型的時候,一個方法的形參的類是傳入對象的類或者其父類即可。如果不知道傳入對象的具體類型,形參可以設置成Object類的對象。但若考慮泛型,則需要考慮前面說到的兼容性問題。如果方法在運行前只知道要傳入一個泛型對象,但不知道具體的類型參數,這時由於類型參數沒有繼承關係,爲了保證兼容性,可以使用泛型通配符"?"來表示此處有一個待定的類型參數(也可以不用,後面會提到):
import java.util.*;
import java.lang.reflect.*;

public class Test
{
    public static void main(String[] args)  throws Exception
    {
        show(new ArrayList<Integer>());
    }
    public static void show(ArrayList<?> obj)
    {
        //打印“0”
        System.out.println(obj.size());
        //報錯:找不到add(int)方法
        obj.add(1);
        //報錯:找不到add(java.lang.Integer)方法
        obj.add(new Integer(1));
        //編譯通過
        obj = new ArrayList<String>();
    }
}
上面的例子可以看出,通配符只是表示類型參數的存在,但不能當成任何具體的類型參數來用。如果一個泛型對象是用通配符傳入的,則該泛型對象中涉及類型參數的方法都不能使用。不過,不涉及類型參數的方法是正常使用的,比如ArrayList泛型類的不涉及泛型的size()方法。筆者實驗得知,如果想正常使用涉及類型參數的方法,一是將該形參指向一個新的泛型對象,如上面例子所示,二是去掉形參的通配符,讓其成爲原始類型:
import java.util.*;
import java.lang.reflect.*;

public class Test
{
    public static void main(String[] args)  throws Exception
    {
        show(new ArrayList<Integer>());
    }
    //不使用通配符
    public static void show(ArrayList obj)
    {
        //編譯通過,但有警告
        obj.add(1);
        //編譯通過,但有警告
        obj.add("abc");
    }
}
從上面兩個例子看出,在不使用通配符的情況下,泛型形參和泛型實參的關係與同一方法中泛型引用和泛型對象的關係完全一致,類型參數沒有繼承關係且要兼容。在形參使用通配符的情況下,除了涉及類型參數的方法不能使用外,其它規則與無通配符時一致。

3.2 非形參的通配符

如果一個引用不是形參,那可不可以用通配符呢?答案是可以:
//編譯通過
ArrayList<?> array = new ArrayList();
//編譯通過
ArrayList<?> array2 = new ArrayList<Integer>();
//報錯:意外的類型,等號右邊需要不帶限制的類或接口
ArrayList<?> array3 = new ArrayList<?>();
上面的例子說明了,生成泛型對象時new語句不能使用通配符,但是泛型引用可以使用,而且可以指向擁有具體類型參數的泛型對象(這一點與形參通配符的情形一致)。

3.3 通配符的上下界

通配符不僅可以表示一個類型參數,還可以表示一定範圍內的多個類型參數:
ArrayList<? extends Number> array1 = new ArrayList<Integer>();
ArrayList<? extends Number> array2 = new ArrayList<Number>();
ArrayList<? super Integer> array3 = new ArrayList<Number>();
ArrayList<? super Integer> array4 = new ArrayList<Integer>();
上面四種情況都是可以的。? extends A表示該處類型參數是A類或者A的子類,? super
B表示該處類型參數是B類或者B的父類。前面提到類型參數沒有繼承關係指的是不用通配符時,類型參數爲子類的泛型對象不能賦值給類型參數爲父類的泛型引用。這裏使用通配符表示類型參數的上下界,界內的泛型對象就可以賦值給泛型引用了,這也是使用通配符時的兼容性與不使用通配符時的兼容性不同的地方。

4. 自定義泛型

我們可以給類,接口和方法加入類型參數,也就是自定義泛型。

4.1 自定義泛型方法

泛型方法的類型參數的佔位符要在返回值前的<>裏註明,以區別於類的名稱:
public class Test
{
    public static void main(String[] args)  throws Exception
    {
        show(1);
        show("abc");
    }
    //<T>表示這是一個泛型方法,T是類型參數的佔位符
    public static <T> void show(T obj)
    {
        System.out.println(obj);
    }
}
//運行結果:
//1
//abc
在使用自定義的泛型方法的時候要注意,傳入的泛型對象的類型是不確定的,許多操作是不能進行的:
public class Test
{
    public static void main(String[] args)  throws Exception
    {
        //編譯方法時報錯,所以該句不執行
        //int a = add(2, 3);
        //運行時報錯:ClassCastException
        String C = convert("abc", 1);
    }
    //編譯時報錯:" + "不能用於T, T
    //public static <T> T add(T obj1, T obj2)
    //{
    //    return obj1 + obj2;
    //}
    public static <A, B> A convert(A obj1, B obj2)
    {
        return (A) obj2;
    }
}
上面對泛型對象進行加法,編譯時即報錯。而試圖將一個泛型對象轉換成另一個泛型表示的類時,運行時會報錯。
這裏的類型參數佔位符就如同前面的通配符。3.1和3.2中通配符僅出現在一個確定的類的類型參數位置上,比如ArrayList<?> a,表示集合元素的類不明確,但引用a的類仍然明確是ArrayList。上面例子中obj1的類本身都是不明確的。定義泛型時需要佔位符,使用泛型時可以給這個佔位符賦予具體的值, 這個值可以是通配符。
使用泛型進行類型轉換能否成功,要考慮很多因素。
一是A,B兩個類的父子關係:
public class Test
{
    public static void main(String[] args)  throws Exception
    {
        //警告
        Object C = convert(new Object(), "abc");
    }
  
    public static <A, B> A convert(A obj1, B obj2)
    {
        return (A) obj2;
    }
}
A是B的父類,所以能成功。
二是多態:
public class Test
{
    public static void main(String[] args)  throws Exception
    {
        //編譯通過,運行警告
        Object obj1 = "abc";
        String D = convert("abc", obj1);
        
        //編譯通過,運行報錯:ClassCastException
        Object obj2 = new Object();
        String E = convert("abc", obj2);
    }
  
    public static <A, B> A convert(A obj1, B obj2)
    {
        return (A) obj2;
    }
}
對象具有多態性。上面的obj1的運行時類型是String, obj2的運行時類型是Object。故obj1轉換成功,obj2不成功。張老師的視頻裏檢驗了obj1的情況,但沒檢驗obj2的情況。
總之,泛型的使用不能使得原本不可行的操作變成可行的,畢竟佔位符也好,通配符也好,都是要替換成具體類型的。不明確某個對象所屬的類,就不能對它進行不確定是否可行的操作。

4.2 自定義泛型類

當一個類中的多個泛型方法共用同樣的類型參數時,可以將佔位符從方法移到類定義處,成爲自定義泛型類:
import java.util.*;
import java.lang.reflect.*;

public class Test
{
    public static void main(String[] args)  throws Exception
    {
        Operate<Integer> op = new Operate<Integer>();
        op.print(1);
        op.reflect(1);
    }
}

class Operate<T>
{
    ArrayList<T> array = new ArrayList<T>();

    public void print(T obj)
    {
        System.out.println(obj);
    }
    public void reflect(T obj)
    {
        System.out.println(obj.getClass().getName());
    }
    public void add(T obj) 
    {
        array.add(obj);
    }
}
//運行結果:
//1
//java.lang.Integer
上面是一個簡單的泛型類,類型參數佔位符寫在類名之後,方法名中無需再寫。

5. 泛型的反射

前面提到過由於類型擦除的緣故,泛型引用的反射是無法得到類型參數的。
但是我們卻可以用一種較爲曲折的方法得到:
(1)建立一個包含該泛型的形參的方法;
(2)反射該方法所在的類得到該方法的類對象;
(3)通過Method類的getGenericParameterTypes()方法得到形參實際類型組成的數組,以及getGenericReturnType()方法得到返回值實際類型;
(4)從類型數組中取出泛型形參對應的類型;
(5)用ParameterizedType類的getActualTypeArguments()方法得到該泛型形參的類型參數:
import java.util.*;
import java.lang.reflect.*;

public class Test
{
    public static void main(String[] args)  throws Exception
    {
        Vector<Integer> op = new Vector<Integer>();
        //反射得到getGenerics()方法的類對象
        Method GenericMethod = Test.class.getMethod("getGenerics", Vector.class);
        //得到getGenerics()方法形參的類型參數
        Type[] parameterType = GenericMethod.getGenericParameterTypes();
        ParameterizedType pType = (ParameterizedType) parameterType[0];
        System.out.println(pType.getActualTypeArguments()[0]);
        //得到getGenerics()方法返回值的類型參數
        Type returnType = GenericMethod.getGenericReturnType();
        ParameterizedType rType = (ParameterizedType) returnType;
        System.out.println(rType.getActualTypeArguments()[0]);
    }

    public Vector<Integer> getGenerics(Vector<Integer> tmp)
    {
        return null;
    }
}
//運行結果:
//class java.lang.Integer
//class java.lang.Integer
上面的例子說明,類型擦除僅針對非形參的泛型的類型參數,如果泛型出現在方法的形參列表中,是一個形參,則內存的方法區中仍然保留了形參的類型參數信息(用作運行時檢驗兼容性)。

6. 總結

泛型的內容抽象且繁雜。筆者感覺要掌握好泛型,一是要能推斷類型參數的實際值,二是能避免對泛型進行不可行的操作。第一個能力的掌握是第二個的前提。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章