泛型概述
由來
泛型是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語言中的泛型實現方法稱為類型擦除,基于這種方法實現的泛型被稱為偽泛型。
泛型使用
泛型分類
- 泛型類
/**
* 泛型類
* @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;
}
}
- 泛型接口
/**
* 泛型接口
*
* @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)];
}
}
- 泛型方法
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一樣都是一種實際的類型,可以把?看成所有類型的父類。是一種真實的類型。
泛型限定
-
? extends SomeClass
這種限定,說明的是只能接收SomeClass及其子類類型,所謂的“上限” -
? 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,如果可以進行泛型數組的聲明,上面說的這種情況在編譯期將不會出現任何的警告和錯誤,只有在運行時才會出錯。而對泛型數組的聲明進行限制,對于這樣的情況,可以在編譯期提示代碼有類型安全問題,比沒有任何提示要強很多。
解決辦法:
- 采用通配符方式
下面采用通配符的方式是被允許的,對于通配符的方式,最后取出數據是要做顯式的類型轉換的
// 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);
- 采用List方式
List<List<String>> lists = new ArrayList<>();
錯誤方式
// 編譯不會報錯,但存在潛在的運行時ClassCastException
List<String>[] ls = new ArrayList[10];