淺談Java泛型

使用泛型的目的

當咱們第一次接觸泛型時,第一個問題確定會是,爲何要使用泛型?最直接的答案是爲了避免轉型,使得編譯器可以在編譯期就發現轉型錯誤而不用等到運行時。java

好比說,當咱們聲明瞭一個泛型爲Integer的列表,那麼該列表的元素就只能是Integer,當咱們往裏面放非Integer的元素時,編譯器就可以發現,數據結構

List<Integer> intList = new ArrayList<>();
intList.add("abc"); //編譯期錯誤,不能存放String對象到intList中

同理,當咱們從列表中獲取元素時,咱們也再也不須要使用強制轉型將列表中的元素轉型成Integerhibernate

Integer i = intList.get(0);
Integer i = (Integer)intList.get(0);//再也不須要進行強制轉型

然而,泛型卻有一些容易使人困惑的地方,第一個就是關於泛型的帶有通配符的上下界。code

帶有通配符的上下界

Java泛型如今支持兩種帶有通配符的上下界的表達方式,對象

  1. ? extends T - 這裏的?表示類型T的任意子類型,包括類型T自己。
  2. ? super T - 這裏的?表示類型T的任意父類型,包括類型T自己。

這二者的含義都很容易理解和區分,難點在於咱們何時該用 ? extends T, 何時改用? super T.繼承

好比說,因爲? extends T表示類型T和它的任意子類型,那麼咱們能夠說List<? extends Number> 實例能夠添加任意爲Number子類的元素嗎?接口

List<? extends Number> intList = new ArrayList<>();
    intList.add(1); //complier error
    intList.add(3.14); //compiler error

從上述的代碼來看,它並不像咱們所理解的那樣工做。那麼,咱們在何時須要使用? extends T和? super T呢? 這裏有一條簡單的規則 (Get and Put Rule):get

當你須要從一個數據結構中獲取數據時(get), 那麼就使用? extends T. 若是你須要存儲數據(put)到一個數據結構時,那麼就使用? super T. 若是你又想存儲數據,又想獲取數據,那麼就不要使用通配符? . 即直接使用具體泛型T。編譯器

因此,根據Get&Put規則,在上面的例子中,咱們是須要往數據結構裏面存儲數據的,因此須要使用? super T. 修改上面的例子,你能夠發現程序能夠工做了。string

List<? super Number> intList = new ArrayList<>();
    intList.add(1); //it works
    intList.add(3.14); //it works

第二個比較讓人困惑的是關於泛型的類型系統。

泛型的類型系統

泛型的類型系統中最使人困惑的地方就是,Integer類是Number類的子類,而List 卻不是List 的子類。也就是說,你不能將List 實例對象直接賦值給List 實例對象。 List 不是List 子類的緣由咱們只能經過反證法推理出來:

List<Integer> ints = new ArrayList<Integer>();
ints.add(1);
ints.add(2);
List<Number> nums = ints; // compile-time error
nums.add(3.14);
assert ints.toString().equals("[1, 2, 3.14]"); // uh oh!

從上面的例子中,假設List 是List 的子類,那麼List 實例對象ints就能夠賦值給List 實例對象nums。接着咱們能夠往nums實例添加非Integer對象,如float對象3.14。可是這個是不容許的,由於nums實例其實是List 實例對象,它不能接受任何非Integer對象。由此可證,List 並非List 的子類。可是,List 是Collection 的子類。

因此對於泛型的類型系統來說,它應當遵循如下一些規則,摘抄自Java深度歷險-Java泛型

引入泛型以後的類型系統增長了兩個維度:一個是類型參數自身的繼承體系結構,另一個是泛型類或接口自身的繼承體系結構。第一個指的是對於List 和List<Object>這樣的狀況,類型參數String是繼承自Object的。而第二種指的是List接口繼承自Collection接口。對於這個類型系統,有以下的一些規則:

  1. 相同類型參數的泛型類的關係取決於泛型類自身的繼承體系結構。即List 是Collection 的子類型,List 能夠替換Collection 。這種狀況也適用於帶有上下界的類型聲明。
  2. 當泛型類的類型聲明中使用了通配符的時候,其子類型能夠在兩個維度上分別展開。如對Collection<? extends Number>來講,其子類型能夠在Collection這個維度上展開,即List<? extends Number>和Set<? extends Number>等;也能夠在Number這個層次上展開,即Collection 和 Collection 等。如此循環下去,ArrayList 和 HashSet 等也都算是Collection<? extends Number>的子類型。
  3. 若是泛型類中包含多個類型參數,則對於每一個類型參數分別應用上面的規則。

最後咱們來看看在Java泛型參數是如何進行推斷的。

泛型參數的自動推斷

有兩種方式能夠指定泛型參數的類型,一種是顯示的指定,好比對於下面的方法,

public class GenericUtil {
    public static <T> List<T> asList(T... array) {
        List<T> tlist = new ArrayList<>();
        for (T t : array) {
            tlist.add(t);
        }
        return tlist;
    }
}

咱們能夠顯示的指定泛型參數的類型,

System.out.println(GenericUtil.<String>asList("a","b","c"));

除了顯示的指定以外,咱們還能夠隱式的指定, 實際上就是讓編譯器本身去推斷。

System.out.println(GenericUtil.asList("a","b","c"));

編譯器能夠經過asList方法接收的參數類型來推斷出T實際上就是String。 若是從方法接收的參數類型推斷不出來的話,那麼編譯器還會從方法賦值的目標參數來推斷,好比說下面的newObject方法,

public class GenericUtil {

    public static <T> T newObject(String className) throws Exception {
        Class clz = Class.forName(className);
        return (T) clz.newInstance();
    }
}

從方法接收參數上編譯器並不能推斷出T是什麼類型。這個時候能夠從方法賦值的目標參數進行推斷,

Person p = GenericUtil.newObject("com.kevin.hibernate01.Person");

從方法賦值的目標參數p能夠得知,newObject方法中定義的泛型參數T類型應該爲Person類型。

相關文章
相關標籤/搜索