JAVA基礎複習(三)

    本着重新學習(看到什麼複習什麼)的原則,這一篇講的是JAVA的泛型。看了諸位大神的解釋後詳細的查了一些東西,記錄下來,也感謝各位在網絡上的分享!!!

    上一篇中複習了很多集合類,現在回想起來還都是各種<E>,而這些<E>體現的正是JAVA中的泛型的思想。它接收很多可能被參數化的類型,而不是強制使用某一種類型,將類型的具體定義放在具體實現時,如創建對象,接受返回值或者調用方法等。通過將類型參數化,即將類型作爲參數進行傳遞來達到代碼量的減少和類型的統一。這種參數類型可以用在類、接口和方法中,分別被稱爲泛型類、泛型接口、泛型方法。

    泛型的優勢在於代碼的複用,讓原本僅需要更改參數類型而不需要更改算法實現的類等有了更多的選擇,相當於是將所有類型或者是自己定義的類型橫向鋪開,可供開發人員選擇的餘地也多了。並且由於值類型相同,無需進行拆箱和裝箱,所以減少了性能損耗。泛型的類型參數化的概念將泛型的優點都指向了“類型”二字,參數類型多變,類型安全(一旦指定,不能添加其他類型元素,若不指定類型,編譯不報錯,但是執行時會拋出異常,因爲在首次添加元素的時候會帶類型進入,也會指定類型),避免強制類型轉換(避免因爲要向一種類型參數的實現上靠攏而使用的強制類型轉換)和類型統一帶來的性能優化。

    說過了泛型的優點後就要說但是了,那就是JAVA的泛型是僞泛型。類型擦除。JAVA編譯器把所有的泛型類型<T>都視爲Object類型,編譯器會在需要的時候根據<T>的具體定義實現安全的強制轉型。類型擦除無法獲取帶泛型的Class對象,因爲在獲取時已經經過了擦除,那麼返回的都是不帶<T>的原類型的Class。並且其侷限性還在於我們不能實例化(T)類型,因爲在擦除後的結果與想要的結果不同(new Object())。除此之外,由於類型擦除會導致傳入參數的類型改變爲Object類型,便會導致可能會出現方法重名的問題(即我們或許並不想重寫某些方法,但是類型擦除和方法名稱相同導致了這個問題)。

    在瞭解了類型擦除後,我們來明確這麼幾個詞,型變,協變,逆變和不變。我們在使用時也會看到很多<? extends E>或者<? super E>的泛型使用方法,實際上前者就是協變,後者就是逆變。協變描述的是子類型向父類型的向上轉換,逆變描述的就是父類型向子類型的向下轉換,不變表示不存在型變關係。而型變描述的是類型轉換的繼承關係,實質上就是協變,逆變和不變的總稱。而我們平時會看到“?”的原因是因爲JAVA的泛型引入通配符來解決泛型類型的類型轉換問題,如有上限的類型轉換(<? extends E>),有下限的類型轉換(<? super E>)。來看這麼幾個場景,從而瞭解型變。在TryChange類中我定義了幾個內部類,分別賦予了一個speak方法。類間關係是Puppy繼承自Dog,Dog和Cat繼承自Animal。

package com.day_3.excercise_1;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class TryChange {
	static class Animal {
		public static void speak(){
			System.out.println("總!"); 
		}
	}
	static class Cat extends Animal {
		public static void speak(){
			System.out.println("喵!");
		}
	}
	static class Dog extends Animal {
		public static void speak(){
			System.out.println("汪!");
		}
	}
	static class Puppy extends Dog {
		public static void speak(){
			System.out.println("小汪!");
		}
	}

	public static void testList(){
		// 1.編譯錯誤
//		List<Animal> flist = new ArrayList<Dog>();
		// 2.可以編譯,但是有限制
		List<? extends Animal> flist = new ArrayList<Dog>();
//		flist.add(new Animal());
//		flist.add(new Dog());
//		flist.add(new Cat());
//		flist.add(new Puppy());
        // 3.可以編譯,父類型能接收對象,子類型不能接收該對象
        List<? extends Animal> alllist = clist;
		Animal animal = alllist.get(0);
        // 編譯錯誤
//		Cat cat = alllist.get(0);
        // class com.day_3.excercise_1.TryChange$Cat
		System.out.println(animal.getClass());
        // class com.day_3.excercise_1.TryChange$Cat
		System.out.println(alllist.get(0).getClass());
	}
	public static void main(String args[]){
		testList();
	}
}

    針對場景1中的語句,代碼直接編譯錯誤的原因在於泛型沒有內建的協變,也就是說雖然Dog類是Animal類的子類,但是仍然無法將二者聯繫在一起。針對場景2中的語句,代碼是可以通過編譯的。雖然指向的類型是實現了泛型的協變(向上轉換),但是可以發現卻無法添加元素了,無論該元素對象是否是Animal類本身或者是Animal類的子類。這是因爲在類型匹配時,編譯器只能知道所有添加的元素對象都是符合要求的類型,但是具體使用什麼類型卻反而模糊了。原本(List<Cat> clist = new ArrayList<Cat>();)中的元素是可以使用Cat類中的所有定義方法,但是現在可能存在的類型包括Cat,Dog,Puppy甚至Animal,這無疑打破了泛型的類型統一。所以編譯器就拒絕了所有的類型匹配。針對場景3中的語句,可以看到泛型指定類型爲<? extends Animal>,即上界爲Animal類,故可以用Animal接收任一定義對象,畢竟父類型可以自動接收子類型實例化的對象。但是我們也可以看到第0個元素的對象類型明明是Cat類型,但是無法使用原本的Cat類定義對象接收。通過觀察錯誤改正提示可見,它要求我將類型強行轉換成Cat類型或者使用Animal類型接收,也就是說在協變中只能用父類型進行對象的接收。

    另外,來測試一下協變和逆變。我定義了一個實體類Pet,此後定義一個Owner的內部類作爲買家,而後進行Pet的購買,使用Integer類型等包裝類和他們的父類Number類作爲實驗。

package com.day_3.excercise_1;

public class Pet<T> {
	private T age;
	private T price;

	public Pet(T age) {
		this.age = age;
	}
	public T getAge() {
		return age;
	}
	public void setAge(T age) {
		this.age = age;
	}
	public T getPrice() {
		return price;
	}
	public void setPrice(T price) {
		this.price = price;
	}
	public String toString() {
		return "Pet(" + age + "," + price + ")";
	}
	static <K> Pet<K> buy(K age) {
		return new Pet<K>(age);
	}
}
package com.day_3.excercise_1;

import java.util.ArrayList;
import java.util.List;

public class TryGenericity {
	static class Owner{
		// 1.
//		public static int add(Pet<Number> p){
//			Number age = p.getAge();
//			return age.intValue();
//		}
		// 2.
		public static int add(Pet<? extends Number> p){
			Number age = p.getAge();
			// C.
			Number ageSet = new Float(2.5f);
//			p.setAge(ageSet);
			return age.intValue();
		}
	}
	// 3.
//	public static void set(Pet<Integer> p,Integer price){
//		p.setPrice(price);
//	}
	// 4.
	public static void set(Pet<? super Integer> p,Integer price){
		// F
//		Integer petInteger = p.getPrice();
		p.setPrice(price);
	}
	public static void testExtends(){
		// A.
		Owner.add(new Pet<Number>(2));
		// B.
		Owner.add(new Pet<Integer>(2));
		Owner.add(new Pet<Double>(2.5));
	}
	public static void testSuper(){
		// D.
		set(new Pet<Integer>(1),1);
		// E.
		set(new Pet<Number>(1.5),2);
	}
	public static void main(String args[]){
		testExtends();
		testSuper();
	}
}

    如上所示,分別有幾個場景(數字)和幾個關鍵點(大寫字母)。當使用(1,A,B)時,可以看到我在Owner類中定義了一個add方法,接收的是Pet<Number>,這說明實體類中的類型被指定爲了Number,所有傳入參數都會進行類型檢查。而Integer類型雖然也是Number類的子類,但是仍然無法逃過被攔截的命運。故若使用場景1,A是可以編譯通過的,B是無法編譯通過的。爲了解決這個問題,即明明都是子類但是無法放入該類型的對象,可以使用場景2中的書寫方式,即使用協變。通過上邊的瞭解可以知道,協變是允許泛型接收父類及父類的所有子類的,當使用(2,A,B)時,B就可以編譯通過了,此時可以傳入所有Number類的子類或者Number類本身類型的對象。當我們把方法簽名改變成<? extends Number>時,getAge方法的方法簽名已經改變爲了(? extends Number getAge()),因此調用的時候可以放心進行賦值給Number類型的變量,但是同上方講述的例子3相同的是,雖然明確接受的是傳入的一定是Number類的相關類,但是不能預測傳入的具體參數類型,例如可能會出現定義了一個Integer類型的變量在add方法中,傳入的卻是一個Double或其他類型,所以儘可能使用父類型或者Object類型進行參數的接收。但是當使用(2,C)時,我們使用setAge方法是否可行呢?實際上無法傳遞任何Number類型給setAge(? extends Number)方法。首先可以看到setAge方法的方法簽名也變成了(? extends Nmuber),其次是因爲如果傳入類型時Integer類型,而在add方法內創建一個Number類型並且實際類型是Float類型的ageSet時,Pet<Integer>根本無法接收Float類型的變量。

    所以使用協變(<? extends T>)時,方法內部可以調用獲取父類型(如Number)引用的方法,即可以使用父類型或者Object類型接收傳入參數,但是無法調用傳入父類型引用的方法(null例外)。並且協變規定泛型類型被限定爲可以接受上界爲父類型的包括父類型和所有父類型的子類型。

    可以看到我在main方法中定義了一個set方法去設置Pet的價格,當使用(3,D,E)時,E語句是無法通過編譯的,這是因爲需要傳入參數爲Integer類型的set方法無法接收Number類型。而當使用(4,D,E)時就可以發現,E語句可以通過編譯。通過使用<? super Integer>使得方法可以接收所有泛型類型爲Integer類型或者Intger類型的超類(Number類型)的傳入參數,同理,setPrice方法的方法簽名也已經改變爲了(? super Integer),所以也可以安全的傳入Number類型的變量了。但是當使用(4,F)時,也會發現getPrice的方法簽名改變爲了(? super Integer),但是會出現編譯錯誤,即無法賦值給Integer類型的變量。因爲不能確認方法的返回值就一定是一個Integer類型的。例如我使用E處傳入一個小數,在set方法中的調用的getPrice方法便不能準確接收到傳遞類型,從而導致類型混亂,畢竟方法的返回值只能確認爲是Integer的超類,但不能保證一定是Integer類型。

    所以使用逆變(<? super T>)時,方法內部可以調用傳入類型T及T的超類(如Integer和Number)引用的方法,但是無法調用獲取T引用的方法(Object除外)。並且逆變規定泛型類型被限定爲可以接受下界爲T類型的包括T類型和T類型的超類。

    除上述通配符的使用外,還有無限定通配符(<?>),它同時包括了extends和super的所有限制,即不能使用set方法(null除外)且只能通過get方法獲取Object的引用。

    那麼泛型能否被繼承呢?答案是肯定的,例如父類是Pet<Integer>,子類是intPet,那麼按照繼承原則,子類的參數都應該是Integer類型,並且此時編譯器在處理父類是泛型的情況時只能選擇將類型保存在子類的編譯結果中,因爲如果不進行存儲,就無法得知intPet類只能接收Integer類型。所以子類可以獲取父類的泛型類型

    另外,不只有泛型類,還有泛型接口和泛型方法。在處理泛型方法時還要注意的是static,即靜態方法。編譯器無法在static修飾符修飾下的字段或方法中使用泛型類型,而如果靜態方法的傳入參數確實不確定的情況下,需要將泛型定義在方法上,即使用泛型方法。如我在Pet類中定義的Buy方法一樣的使用方式。

    最後還要注意,JAVA中不允許直接創建泛型數組

    這一篇寫的磕磕絆絆,從在網上無限查詢和在eclipse中無限嘗試到嘗試聽視頻課程,自己看的迷迷糊糊,但是無法否定的是,一直都有很多好的文章在指引着,不過可能有些時候或許文字並不能更加有效地完成講解,或者把所有知識點全都說請,聽聽課程也挺好的,真香。。。在這裏還是要感謝網上的大神們,沒有大神們的文章我也不會有能讓自己更加清晰理解知識的這篇文章。

發佈了19 篇原創文章 · 獲贊 6 · 訪問量 4935
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章