1. 程式人生 > >Java泛型:泛型的定義(類、接口、對象)、使用、繼承

Java泛型:泛型的定義(類、接口、對象)、使用、繼承

們的 hashmap rgs println 運行 報錯 collect arr param

地址 http://blog.csdn.net/lirx_tech/article/details/51570138

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) 示例:

[java] view plain copy
  1. public class Test {
  2. public static void main(String[] args) {
  3. ArrayList<String> list = new ArrayList<>();
  4. list.add("lala");
  5. list.add("haha");
  6. // list.add(5); // 類型不符,直接報錯!!
  7. list.forEach(ele -> System.out.println(ele)); // 可以看到取出的ele無需強制類型轉換,直接就是String類型的
  8. // 說明泛型集合能記住元素的類型,代碼簡潔了很多
  9. HashMap<String, Integer> map = new HashMap<>();
  10. map.put("abc", 15);
  11. map.put("def", 88);
  12. map.forEach((key, value) -> System.out.println(key + " : " + value)); // 可以看到key、value同樣無需強制類型轉化
  13. }
  14. }

3. 定義泛型類、接口:

1) 不僅Java的集合都定義成了泛型,用戶自己也可以定義任意泛型的類、接口,只要在定義它們時用<>來指定類型參數即可;

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

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

[java] view plain copy
  1. public interface MyGneric<E> {
  2. E add(E val);
  3. Set<E> makeSet();
  4. ...
  5. }

!!可以看到,在接口內/類體內甚至還可以使用該類型形參運用泛型!例如上面makeSet方法返回一個泛型Set;

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

[java] view plain copy
  1. public class MyGenric<T> {
  2. MyGeneric(...) { ... }
  3. ...
  4. }

!定義構造器無需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) 繼承之後,父類/接口中的所有方法中的類型參數都將變成具體的類型,你在子類中覆蓋這些方法的時候一定要用具體的類型,不能繼續使用泛型的類型形參了,例如:

[java] view plain copy
  1. class Father<T> {
  2. T info;
  3. public Father(T info) {
  4. this.info = info;
  5. }
  6. public T get() {
  7. return info;
  8. }
  9. public T set(T info) {
  10. T oldInfo = this.info;
  11. this.info = info;
  12. return this.info;
  13. }
  14. }
  15. class Son extends Father<String> { // 所有從父類繼承來的方法的類型參數全部都確定為String了
  16. // 因此在覆蓋的時候都要使用具體的類型實參了!
  17. public Son(String info) {
  18. super(info);
  19. }
  20. @Override
  21. public String get() {
  22. return "haha";
  23. }
  24. @Override
  25. public String set(String info) {
  26. return "lala";
  27. }
  28. }

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

5. 泛型參數繼承:

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

2) 語法如下:

[java] view plain copy
  1. class Father<T> { ... }
  2. class Son<T> extends Father<T> { ... }

!即子泛型可以傳承父泛型的泛型參數,那麽在子類中泛型參數T就和父類的完全相同,還是照常使用(和父類一樣正常使用);

3) 註意:

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

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

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

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

[java] view plain copy
  1. class Father<T> { }
  2. class Son<E> extends Father<T> { }
  3. class Father<T> { }
  4. class Son<T> extends Father<E> { }
  5. class Father<T> { }
  6. 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>!

Java泛型:泛型的定義(類、接口、對象)、使用、繼承