使用泛型的目的
當我們第一次接觸泛型時狡孔,第一個問題肯定會是撩匕,為什么要使用泛型害捕?最直接的答案是為了避免轉型绿淋,使得編譯器能夠在編譯期就發(fā)現(xiàn)轉型錯誤而不用等到運行時。
比如說尝盼,當我們聲明了一個泛型為Integer的列表吞滞,那么該列表的元素就只能是Integer,當我們往里面放非Integer的元素時东涡,編譯器就能夠發(fā)現(xiàn)冯吓,
List<Integer> intList = new ArrayList<>();
intList.add("abc"); //編譯期錯誤,不能存放String對象到intList中
同理疮跑,當我們從列表中獲取元素時组贺,我們也不再需要使用強制轉型將列表中的元素轉型成Integer
Integer i = intList.get(0);
Integer i = (Integer)intList.get(0);//不再需要進行強制轉型
然而,泛型卻有一些容易令人困惑的地方祖娘,第一個就是關于泛型的帶有通配符的上下界失尖。
帶有通配符的上下界
Java泛型現(xiàn)在支持兩種帶有通配符的上下界的表達方式啊奄,
- ? extends T - 這里的?表示類型T的任意子類型,包括類型T本身掀潮。
- ? super T - 這里的?表示類型T的任意父類型菇夸,包括類型T本身。
這兩者的含義都很容易理解和區(qū)分仪吧,難點在于我們什么時候該用 ? 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呢羞芍? 這里有一條簡單的規(guī)則 (Get and Put Rule):
當你需要從一個數(shù)據結構中獲取數(shù)據時(get), 那么就使用? extends T. 如果你需要存儲數(shù)據(put)到一個數(shù)據結構時,那么就使用? super T. 如果你又想存儲數(shù)據郊艘,又想獲取數(shù)據荷科,那么就不要使用通配符? . 即直接使用具體泛型T。
所以纱注,根據Get&Put規(guī)則畏浆,在上面的例子中,我們是需要往數(shù)據結構里面存儲數(shù)據的奈附,所以需要使用? super T. 修改上面的例子全度,你可以發(fā)現(xiàn)程序可以工作了煮剧。
List<? super Number> intList = new ArrayList<>();
intList.add(1); //it works
intList.add(3.14); //it works
第二個比較讓人困惑的是關于泛型的類型系統(tǒng)斥滤。
泛型的類型系統(tǒng)
泛型的類型系統(tǒng)中最令人困惑的地方就是,Integer類是Number類的子類勉盅,而List<Integer>卻不是List<Number>的子類佑颇。也就是說,你不能將List<Integer>實例對象直接賦值給List<Number>實例對象草娜。 List<Integer>不是List<Number>子類的原因我們只能通過反證法推理出來:
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<Integer>是List<Number>的子類,那么List<Integer>實例對象ints就可以賦值給List<Number>實例對象nums宰闰。接著我們可以往nums實例添加非Integer對象茬贵,如float對象3.14。但是這個是不允許的移袍,因為nums實例實際上是List<Integer>實例對象解藻,它不能接受任何非Integer對象。由此可證葡盗,List<Integer>并不是List<Number>的子類螟左。但是,List<Integer>是Collection<Integer>的子類。
所以對于泛型的類型系統(tǒng)來講胶背,它應當遵循以下一些規(guī)則巷嚣,摘抄自Java深度歷險-Java泛型
引入泛型之后的類型系統(tǒng)增加了兩個維度:一個是類型參數(shù)自身的繼承體系結構,另外一個是泛型類或接口自身的繼承體系結構钳吟。第一個指的是對于List<String>和List<Object>這樣的情況廷粒,類型參數(shù)String是繼承自Object的。而第二種指的是List接口繼承自Collection接口红且。對于這個類型系統(tǒng)评雌,有如下的一些規(guī)則:
- 相同類型參數(shù)的泛型類的關系取決于泛型類自身的繼承體系結構。即List<String>是Collection<String> 的子類型直焙,List<String>可以替換Collection<String>景东。這種情況也適用于帶有上下界的類型聲明。
- 當泛型類的類型聲明中使用了通配符的時候奔誓,其子類型可以在兩個維度上分別展開斤吐。如對Collection<? extends Number>來說,其子類型可以在Collection這個維度上展開厨喂,即List<? extends Number>和Set<? extends Number>等和措;也可以在Number這個層次上展開,即Collection<Double>和 Collection<Integer>等蜕煌。如此循環(huán)下去派阱,ArrayList<Long>和 HashSet<Double>等也都算是Collection<? extends Number>的子類型。
- 如果泛型類中包含多個類型參數(shù)斜纪,則對于每個類型參數(shù)分別應用上面的規(guī)則贫母。
最后我們來看看在Java泛型參數(shù)是如何進行推斷的。
泛型參數(shù)的自動推斷
有兩種方式可以指定泛型參數(shù)的類型盒刚,一種是顯示的指定腺劣,比如對于下面的方法,
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;
}
}
我們可以顯示的指定泛型參數(shù)的類型因块,
System.out.println(GenericUtil.<String>asList("a","b","c"));
除了顯示的指定之外橘原,我們還可以隱式的指定, 實際上就是讓編譯器自己去推斷涡上。
System.out.println(GenericUtil.asList("a","b","c"));
編譯器可以通過asList方法接收的參數(shù)類型來推斷出T實際上就是String趾断。 如果從方法接收的參數(shù)類型推斷不出來的話,那么編譯器還會從方法賦值的目標參數(shù)來推斷吩愧,比如說下面的newObject方法芋酌,
public class GenericUtil {
public static <T> T newObject(String className) throws Exception {
Class clz = Class.forName(className);
return (T) clz.newInstance();
}
}
從方法接收參數(shù)上編譯器并不能推斷出T是什么類型。這個時候可以從方法賦值的目標參數(shù)進行推斷耻警,
Person p = GenericUtil.newObject("com.kevin.hibernate01.Person");
從方法賦值的目標參數(shù)p可以得知隔嫡,newObject方法中定義的泛型參數(shù)T類型應該為Person類型甸怕。