Java泛型解析

目錄

泛型之前

泛型

1、Java中的泛型是什麼 ? 使用泛型的好處和意義是什麼

2、編譯器是如何處理泛型的

3、什麼是類型擦除

4、什麼是泛型中的限定通配符和非限定通配符 

5、通配符

6、泛型帶來的問題

7、泛型中跨界問題

8、總結

9、代碼膨脹和數據類型對齊補白


泛型之前

在面向對象編程語言中,多態算是一種泛化機制。例如,你可以將方法的參數類型設置爲基類,那麼該方法就可以接受從這個基類中導出的任何類作爲參數,這樣的方法將會更具有通用性。此外,如果將方法參數聲明爲接口,將會更加靈活。

在Java增加泛型類型之前,通用程序的設計就是利用繼承實現的,例如,ArrayList類只維護一個Object引用的數組,Object爲所有類基類。

public class BeforeGeneric {
	static class ArrayList{//泛型之前的通用程序設計
		private Object[] elements=new Object[0];
		public Object get(int i){
			return elements[i];
		}
		public void add(Object o){
			//這裏的實現,只是爲了演示,不具有任何參考價值
			int length=elements.length;
			Object[] newElments=new Object[length+1];
			for(int i=0;i<length;i++){
				newElments[i]=elements[i];
			}
			newElments[length]=o;
			elements=newElments;
		}
	}
	public static void main(String[] args) {
		ArrayList stringValues=new ArrayList();
		stringValues.add(1);//可以向數組中添加任何類型的對象
		//問題1——獲取值時必須強制轉換	
		String str=(String) stringValues.get(0); 
		//問題2——上述強制轉型編譯時不會出錯,而運行時報異常java.lang.ClassCastException
	}
}

這樣的實現面臨兩個問題:

1、當我們獲取一個值的時候,必須進行強制類型轉換。

2、假定我們預想的是利用stringValues來存放String集合,因爲ArrayList只是維護一個Object引用的數組,我們無法阻止將Integer類型(Object子類)的數據加入stringValues。然而,當我們使用數據的時候,需要將獲取的Object對象轉換爲我們期望的類型(String),如果向集合中添加了非預期的類型(如Integer),編譯時我們不會收到任何的錯誤提示。但當我們運行程序時卻會報異常:

public static void main(String[] args) {
        ArrayList list = new ArrayList<>();
        list.add(2);
        list.add("23");
        System.out.println((String) list.get(0));
    }
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at generic.BeforeGeneric.main(BeforeGeneric.java:24)

這顯然不是我們所期望的,如果程序有潛在的錯誤,我們更期望在編譯時被告知錯誤,而不是在運行時報異常。

泛型

1、Java中的泛型是什麼 ? 使用泛型的好處和意義是什麼

泛型是一種參數化類型的機制。它可以使得代碼適用於各種類型,從而編寫更加通用的代碼,例如集合框架。

泛型是一種編譯時類型確認機制。它提供了編譯期的類型安全,確保在泛型類型(通常爲泛型集合)上只能使用正確類型的對象,避免了在運行時出現ClassCastException。簡單來說就是:(1)在沒有泛型時,一個集合List中可以存入任何類型,取出時需要做強制轉換成存儲的類型,非常的不方便。有了泛型就可以只存儲定義好的參數化類型,方便存取(不用強制轉換);(2)適用於多種數據類型執行相同的代碼(代碼複用)

2、編譯器是如何處理泛型的

通常情況下,一個編譯器處理泛型有兩種方式:

1.Code specialization。在實例化一個泛型類或泛型方法時都產生一份新的目標代碼(字節碼or二進制代碼)。例如,針對一個泛型list,可能需要 針對string,integer,float產生三份目標代碼

2.Code sharing。對每個泛型類只生成唯一的一份目標代碼;該泛型類的所有實例都映射到這份目標代碼上,在需要的時候執行類型檢查和類型轉換。例如,針對一個泛型list,只產生一份目標代碼,只在使用的地方會有安全檢查機制。

C++和C#是使用Code specialization的處理機制,前面提到,他有一個缺點,那就是 會導致代碼膨脹 。另外一個弊端是在引用類型系統中,浪費空間,因爲引用類型集合中元素本質上都是一個指針。沒必要爲每個類型都產生一份執行代碼。而這也是Java編譯器中採用Code sharing方式處理泛型的主要原因。

Java編譯器通過Code sharing方式爲每個泛型類型創建唯一的字節碼錶示,並且將該泛型類型的實例都映射到這個唯一的字節碼錶示上。將多種泛型類形實例映射到唯一的字節碼錶示是通過 類型擦除 (type erasue)實現的。

3、什麼是類型擦除

類型擦除指的是通過類型參數合併,將泛型類型實例關聯到同一份字節碼上。編譯器只爲泛型類型生成一份字節碼,並將其實例關聯到這份字節碼上。類型擦除的關鍵在於從泛型類型中清除類型參數的相關信息,並且再必要的時候添加類型檢查和類型轉換的方法。 類型擦除可以簡單的理解爲將泛型java代碼轉換爲普通java代碼,只不過編譯器更直接點,將泛型java代碼直接轉換成普通java字節碼。 類型擦除的主要過程如下: 1.將所有的泛型參數用其最左邊界(最頂級的父類型)類型替換。(這部分內容可以看:Java泛型中extends和super的理解) 2.移除所有的類型參數。

// 編譯前的代碼:

public static void main(String[] args) {

Map<String, String> map = new HashMap<String, String>();

map.put("name", "hollis");

map.put("age", "22");

System.out.println(map.get("name"));

System.out.println(map.get("age"));

}

// 反編譯後的代碼:

public static void main(String[] args) {

Map map = new HashMap();

map.put("name", "hollis");

map.put("age", "22");

System.out.println((String) map.get("name"));

System.out.println((String) map.get("age"));

}

 

// 原始代碼

interface Comparable<A> {
    public int compareTo(A that);
}

public final class NumericValue implements Comparable<NumericValue> {

    private byte value;

    public NumericValue(byte value) {
        this.value = value;
    }

    public byte getValue() {
        return value;
    }

    public int compareTo(NumericValue that) {
        return this.value - that.value;
    }

}

// 反編譯後的代碼

interface Comparable {
    public int compareTo( Object that);
}

public final class NumericValue implements Comparable {

    public NumericValue(byte value) {
        this.value = value;
    }

    public byte getValue() {
        return value;
    }

    public int compareTo(NumericValue that) {
        return value - that.value;
    }

    public volatile int compareTo(Object obj) {
        return compareTo((NumericValue)obj);
    }

    private byte value;
}

從以上代碼發現,反編譯後代碼的泛型類型都不見了,代碼變成了普通代碼

4、什麼是泛型中的限定通配符和非限定通配符 

限定通配符對類型進行了限制。有兩種限定通配符,一種是<? extends T>它通過確保類型必須是T的子類來設定類型的上界,另一種是<? super T>它通過確保類型必須是T的父類來設定類型的下界。泛型類型必須用限定內的類型來進行初始化,否則會導致編譯錯誤。另一方面<?>表示了非限定通配符,因爲<?>可以用任意類型來替代。

5、通配符

(1)通配符上界

public class Test {
	public static void printIntValue(List<? extends Number> list) {
		for (Number number : list) {
			System.out.print(number.intValue()+" "); 
		}
		System.out.println();
	}
	public static void main(String[] args) {
		List<Integer> integerList=new ArrayList<Integer>();
		integerList.add(2);
		integerList.add(2);
		printIntValue(integerList);
		List<Float> floatList=new ArrayList<Float>();
		floatList.add((float) 3.3);
		floatList.add((float) 0.3);
		printIntValue(floatList);
	}
}

輸出:

2 2 
3 0 

非法使用

public class Test {
	public static void fillNumberList(List<? extends Number> list) {
		list.add(new Integer(0));//編譯錯誤
		list.add(new Float(1.0));//編譯錯誤
	}
	public static void main(String[] args) {
		List<? extends Number> list=new ArrayList();
		list.add(new Integer(1));//編譯錯誤
		list.add(new Float(1.0));//編譯錯誤
	}
}

(2)通配符下界

public class Test {
	public static void fillNumberList(List<? super Number> list) {
		list.add(new Integer(0));
		list.add(new Float(1.0));
	}
	public static void main(String[] args) {
		List<? super Number> list=new ArrayList(); 
		list.add(new Integer(1));
		list.add(new Float(1.1));
	}
}

可以添加Number的任何子類,爲什麼呢?
List<? super Number>可以代表List<T>,其中T爲Number父類,(雖然Number沒有父類)。如果說,T爲Number的父類,我們想List<T>中加入Number的子類肯定是可以的。
非法使用
對List<? superT>進行迭代是不允許的。爲什麼呢?你知道用哪種接口去迭代List嗎?只有用Object類的接口才能保證集合中的元素都擁有該接口,顯然這個意義不大。其應用場景略。

6、泛型帶來的問題

(1)當泛型遇到重載

public class GenericTypes {

    public static void method(List<String> list) {
        System.out.println("invoke method(List<String> list)");
    }

    public static void method(List<Integer> list) {
        System.out.println("invoke method(List<Integer> list)");
    }
}

參數List<Integer>和List<String>編譯之後都被擦除了,變成了一樣的原生類型List ,擦除動作導致這兩個方法的特徵簽名變得一模一樣。

(2)當泛型遇到catch

如果我們自定義了一個泛型異常類GenericException ,那麼,不要嘗試用多個catch取匹配不同的異常類型,例如你想要分別捕獲GenericException 、GenericException ,這也是有問題的。

(3)當泛型內包含靜態變量

public static void main(String[] args){

        GT<Integer> gti = new GT<Integer>();
        gti.var=1;
        
        GT<String> gts = new GT<String>();
        gts.var=2;

        System.out.println(gti.var);
}

class GT<T>{

        public static int var=0;

        public void nothing(T x){}

}

答案是2,由於經過類型擦除,所有的泛型類實例都關聯到同一份字節碼上,泛型類的所有靜態變量是共享的。

(4)泛型類型變量不能是基本數據類型
就比如,沒有ArrayList<double>,只有ArrayList<Double>。因爲當類型擦除後,ArrayList的原始類中的類型變量(T)替換爲Object,但Object類型不能存儲double值。

(5)、泛型類型引用傳遞問題

ArrayList<String> arrayList1=new ArrayList<Object>();//編譯錯誤  
ArrayList<Object> arrayList1=new ArrayList<String>();//編譯錯誤 

第一種誰知道Object會存儲什麼類型,所以就會有ClassCastException了;

第二種雖然取值的時候不會出現ClassCastException,因爲是從String轉換爲Object。可是,這樣做有什麼意義呢,泛型出現的原因,就是爲了解決類型轉換的問題。我們使用了泛型,到頭來,還是要自己強轉,違背了泛型設計的初衷。所以java不允許這麼幹。再說,你如果又用arrayList2往裏面add()新的對象,那麼到時候取得時候,我怎麼知道我取出來的到底是String類型的,還是Object類型的呢?
所以,要格外注意泛型中引用傳遞問題。

7、泛型中跨界問題

class Pair<T> {

    private T value;

    public T get() {
        return value;
    }

    public boolean add(T value) {
        this.value= value;
        return true;
    }
}

public static void main(String[] args) throws NoSuchMethodException,     
 InvocationTargetException, IllegalAccessException {
        Pair<Integer> pair = new Pair<>();
        pair.add(2);
        pair.getClass().getMethod("add",Object.class).invoke(pair,"我是字符串");
        System.out.println(pair.get());
}

打印結果:

所以通過反射可以實現跨界存值,但這樣使用好像也沒有什麼意義

8、總結

虛擬機中沒有泛型,只有普通類和普通方法,所有泛型類的類型參數在編譯時都會被擦除,泛型類並沒有自己獨有的Class類對象。比如並不存在List<String>.class或是List<Integer>.class,而只有List.class。

創建泛型對象時請指明類型,讓編譯器儘早的做參數檢查( Effective Java,第23條:請不要在新代碼中使用原生態類型 )

不要忽略編譯器的警告信息,那意味着潛在的ClassCastException等着你。

靜態變量是被泛型類的所有實例所共享的。對於聲明爲MyClass<T>的類,訪問其中的靜態變量的方法仍然是 MyClass.myStaticVar。不管是通過new MyClass<String>還是new MyClass<Integer>創建的對象,都是共享一個靜態變量。

泛型的類型參數不能用在Java異常處理的catch語句中。因爲異常處理是由JVM在運行時刻來進行的。由於類型信息被擦除,JVM是無法區分兩個異常類型MyException<String>和MyException<Integer>的。對於JVM來說,它們都是 MyException類型的。也就無法執行與異常對應的catch語句。

9、代碼膨脹和數據類型對齊補白

:在32位虛擬機中,本地指針佔用4字節,在64位虛擬機中本地指針佔用8字節
數據類型對齊補白:Java原始數據類型的長度是固定的,但是對於不同的平臺本地數據類型長度並不固定,當JVM中調用本地(Native)方法時,它們之間交互的數據,比如一個int,在java中一定爲32位的,但是在本地實現中(比如C/C++)可能是64位的,多出來的那部分就需要補0來對齊。


參考鏈接:https://blog.csdn.net/sunxianghuang/article/details/51982979
參考鏈接:https://www.jianshu.com/p/36356dba3ee9
參考鏈接:https://www.zhihu.com/question/58546494/answer/570585201

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章