1. 程式人生 > >[瘋狂Java]泛型:泛型的定義(類、介面、物件)、使用、繼承

[瘋狂Java]泛型:泛型的定義(類、介面、物件)、使用、繼承

1. 設計泛型的初衷:

    1) 主要是為了解決Java容器無法記憶元素型別的問題:

         i. 由於Java設計之初並不知道會往容器中存放什麼型別的元素,因此元素型別都設定為Object,這樣就什麼東西都能放了!

         ii. 但是這樣設計有明顯的缺點:

             a. 取出元素的時候必須進行強制型別轉換(儘管集合在執行時裡面元素的“執行時型別”不變,即元素的getClass返回的還是最初自己的型別而不是Object);

             b. 如果不小心往集合里加了不相同型別的元素可能會導致型別異常(進行equals、compare比較的時候尤為明顯);

             c. 由於沒有型別就需要在很多地方進行強制型別轉換,但是這樣做增加了程式設計的複雜度,並且程式碼也不美觀(臃腫),維護起來也更加困難;

    2) 泛型的概念定義:

         i. 從Java 5開始,引入了引數化型別(Parameterized Type)的概念,改造了所有的Java集合,使之都實現泛型,允許程式在建立集合時就可以指定集合元素的型別,比如List<String>就表名這是一個只能存放String型別的List;

         ii. 泛型(Generic):就是指引數化型別,上面的List<String>就是引數化型別,因此就是泛型,而String就是該List<String>泛型的型別引數;

    3) 泛型的好處:

         i. 使集合可以記住元素型別,即取出元素的時候無需進行強制型別轉化了,可以直接用原型別的引用接收;

         ii. 一旦指定了性引數那麼集合中元素的型別就確定了,不能新增其他型別的元素,否則會直接編譯儲存,這就可以避免了“不小心放入其他型別元素”的可能;

         iii. 上述保證瞭如果在編譯時沒有發出警告,則在執行時就一定不會產生型別轉化異常(ClassCastException);

         iv. 顯然,泛型使程式設計更加通用,並且程式碼也更加簡潔,程式碼更加容易維護;

2. 建立泛型物件——自動型別推斷的菱形語法:

    1) 首先,定義泛型引用一定要使用尖括號指定型別引數,例如:List<String> list、Map<String, Integer>等,其中的String、Integer之類的就是型別引數;

    2) 其次,使用構造器構造泛型物件的時候可以指定型別引數也可以不指定,例如:

         i. List<String> list = new List<String>();  // 這當然是對的

         ii. List<String> list = new List<>();  // 這樣對,因為List的型別引數可以從引用推斷出!

!!但是引用的型別引數是一定要加的,否則無法推斷;

    3) 由於<>很像菱形,因此上面的語法也叫做菱形語法;

    4) 錯誤提示:引用無型別引數但構造器有型別引數的寫法是不對的!例如,List list = new List<String>();

!!至於為什麼不對,這會在泛型原理的章節中詳細介紹,這裡先記住這樣寫不對就行了!

!反正就是一個原則,泛型引用是一定要指定型別引數的!!

    5) 示例:

public class Test {
	
	public static void main(String[] args) {
		ArrayList<String> list = new ArrayList<>();
		list.add("lala");
		list.add("haha");
		// list.add(5); // 型別不符,直接報錯!!
		list.forEach(ele -> System.out.println(ele)); // 可以看到取出的ele無需強制型別轉換,直接就是String型別的
		// 說明泛型集合能記住元素的型別,程式碼簡潔了很多
		
		HashMap<String, Integer> map = new HashMap<>();
		map.put("abc", 15);
		map.put("def", 88);
		map.forEach((key, value) -> System.out.println(key + " : " + value)); // 可以看到key、value同樣無需強制型別轉化
	}
}
3. 定義泛型類、介面:

    1) 不僅Java的集合都定義成了泛型,使用者自己也可以定義任意泛型的類、介面,只要在定義它們時用<>來指定型別引數即可;

    2) 例如:public class Fruit<T> { ... },其中<T>指定了該泛型的型別引數,這個T是一個型別引數名,使用者可以任意命名(就像方法引數的形參名一樣),只有在定義該泛型的物件時將T替換成指定的具體型別從而產生一個例項化的泛型物件,例如:Fruit<String> fruit = new Fruit<>(...);

    3) 型別形參可以在整個介面、類體內當成普通型別使用,集合所有可使用普通型別的地方都可以使用型別形參,例如:

public interface MyGneric<E> {
    E add(E val);
    Set<E> makeSet();
    ...
}
!!可以看到,在介面內/類體內甚至還可以使用該型別形參運用泛型!例如上面makeSet方法返回一個泛型Set;

    4) 定義泛型構造器:泛型的構造器還是類名本身,不用使用菱形語法,例如

public class MyGenric<T> {
    MyGeneric(...) { ... }
    ...
}
!定義構造器無需MyGeneric<T>(...) { ... }了,只有在new的時候需要用到菱形語法;

4. 實現/繼承泛型介面/泛型類:

    1) 定義泛型和使用泛型的概念:主要區別就是定義和使用

         i. 那Java的方法做類比,Java的方法在定義的時候使用的都是形參(虛擬引數),但是在呼叫方法(使用方法)的時候必須傳入實參;

         ii. 同樣泛型也有這個特點,泛型的型別引數和方法的引數一樣,也是一種引數,只不過是一種特殊的引數,用來表示未知的型別罷了;

         iii. 因此,泛型也是在定義的時候必須使用形參(虛擬引數,使用者自己隨意命名),但是在使用泛型的時候(比如定義泛型引用、繼承泛型)就必須使用實參,而泛型的實參就是具體的型別,像String、Integer等具體的型別(當然也可以是自定義型別);

    2) 泛型定義的時候使用形參,例如:public class MyGeneric<T> { ... }  // T就是一個自己隨意命名的型別形參

    3) 使用泛型的時候必須傳入實參:

         i. 定義引用(物件)的時候毫無疑問,肯定需要傳實參:ArrayList<String> list = ...;   // 必須用具體的型別,像這裡就是String來代替形參,即實參

         ii. 實現/繼承一個泛型介面/類的時候:

!!你在實現/繼承一個介面/類的時候實際上是在使用該介面/類,比如:public class Son extends Father { ... }中Father這個類就是正在被使用,毫無疑問,必定是在使用;

!!因此泛型其實無法繼承/實現,因為在實現/繼承的時候必須為泛型傳入型別實參,給定實參後它就是一個具體的型別了,就不再是泛型了

!!示例:public class MyType extends MyGeneric<String> { ... } // implements、extends的時候必須傳入型別實參,因為實在使用泛型!!

!!原則上,任何程式語言都不允許泛型模板層層繼承!!

    4) 繼承之後,父類/介面中的所有方法中的型別引數都將變成具體的型別,你在子類中覆蓋這些方法的時候一定要用具體的型別,不能繼續使用泛型的型別形參了,例如:

class Father<T> {
	T info;
	public Father(T info) {
		this.info = info;
	}
	public T get() {
		return info;
	}
	public T set(T info) {
		T oldInfo = this.info;
		this.info = info;
		return this.info;
	}
}

class Son extends Father<String> { // 所有從父類繼承來的方法的型別引數全部都確定為String了
	// 因此在覆蓋的時候都要使用具體的型別實參了!
	
	public Son(String info) {
		super(info);
	}

	@Override
	public String get() {
		return "haha";
	}

	@Override
	public String set(String info) {
		return "lala";
	}
	
}

!!這一定能保證,這三個方法都是從父類中繼承來的,只不過型別形參T被例項化成了String;

5. 泛型引數繼承:

    1) 上面派生出來的類不是泛型,是一個實體型別,因為其繼承的泛型是具有型別實參的,而Java還支援一種特殊的語法,可以讓你從泛型繼續派生出泛型,而泛型的型別引數可以繼續傳承下去;

    2) 語法如下:

class Father<T> { ... }

class Son<T> extends Father<T> { ... }
!即子泛型可以傳承父泛型的泛型引數,那麼在子類中泛型引數T就和父類的完全相同,還是照常使用(和父類一樣正常使用);

    3) 注意:

         i. 這裡extends Father<T>了,因此父類泛型Father就是被使用了,而按照之前講的規則,使用給一個泛型是必須要指定型別實參的!因此這裡的這個語法是一種特殊語法,Java專門為這種語法開了後門,這種語法只有在型別引數傳承的時候才會用到(即上面這種應用);

         ii. 一旦使用了這種語法,就表示要進行型別引數的傳承了(即父類的T傳遞給子類繼續使用,因此子類也是一個跟父類一樣的泛型);

         iii. 並且一旦使用了這種語法,那麼子類定義中的Son<T>和extends Father<T>中的型別引數必須和定義父類時的型別引數名完全一樣!!

              a. 以下三種情況全部錯誤(全部發生編譯報錯):

class Father<T> { }
class Son<E> extends Father<T> { }

class Father<T> { }
class Son<T> extends Father<E> { }

class Father<T> { }
class Son<E> extends Father<E> { }
!!必須全部使用和父類定義相同的型別引數名(T)!才行,這是Java語法的特殊規定;

    4) 其實Java容器中很多類/介面都是通過型別引數傳承來定義的:

         i. 最典型的例子就是:public interface List<T> extends Collection<T> { ... }

         ii. 雖然"如果A是B的父類,但Generic<A>不是Generic<B>"的父類,但"如果A是B的父類,那A<T>一定是B<T>的父類"!這是一定的;

         iii. 因為型別引數傳承的定義方式本身就是:Son<T> extends Father<T>,那Father<T>一定是Son<T>的父類咯!


6. 在使用泛型的時候可以不使用菱形語法指定實參,直接裸用型別名:

    1) 例如:

         i. 定義引用(物件)時裸用類名:ArrayList list = new ArrayList(); // 當然也可以寫成ArrayList list = new ArrayList<>();

         ii. 實現/繼承:public class MyType extends MyGeneric { ... }

!!上面使用的型別或者介面在定義的時候都是泛型!!但是使用它們的時候忽略型別引數(都不用加菱形);

    2) Java規定,一個泛型無論如何、在任何地方、不管如何使用,它永遠都是泛型,因此這裡既是你忽略型別實參它底層也是一個泛型,那麼它的型別實參會是什麼呢?既然我們沒有顯式指定,那麼Java肯定會隱式為其指定一個型別實參吧?

    3) 答案是肯定的,如果使用泛型的時候不指定型別實參,那麼Java就會用該泛型的“原生型別“來作為型別實參傳入!

!!那麼“原生型別“是什麼呢?這裡先不介紹,會在下一章的”泛型原理“裡詳細分解;

!!但是我們這裡可以先透露一下,Java集合的原生型別基本都是Object,因此像上面的ArrayList list = new ArrayList();寫法其實傳入的是Object型別實參,即ArrayList<Object>!