前陣子給公司新人培訓Java 基礎相關的一些點,系統整理了一下泛型相關的知識點。特來分享一下。但願能讓一些對泛型不熟悉的同窗徹底掌握Java 泛型的相關知識點。java
開始以前,先給你們來一道測試題。程序員
List<String> strList = new ArrayList<String>(); List<Integer> integerList = new ArrayList<Integer>(); System.out.println(strList.getClass() == integerList.getClass());
請問,上面代碼最終結果輸出的是什麼?熟悉泛型的同窗應該可以答出來,而對泛型有所瞭解,可是瞭解不深刻的同窗可能會答錯。編程
泛型概述數組
泛型的定義和使用安全
通配符 ?app
List<String>
傳遞給一個接受List<Object>
參數的方法嗎?最先的「泛型編程」的概念起源於C++的模板類(Template),Java 借鑑了這種模板理念,只是二者的實現方式不一樣。C++ 會根據模板類生成不一樣的類,Java 使用的是類型擦除的方式。
Java1.5 發行版本中增長了泛型(Generic)。jvm
有不少緣由促成了泛型的出現,而最引人注意的一個緣由,就是爲了建立容器類。-- 《Java 編程思想》ide
容器就是要存放要使用的對象的地方。數組也是如此,只是相比較的話,容器類更加的靈活,具備更多的功能。全部的程序,在運行的時候都要求你持有一大堆的對象,因此容器類算得上最須要具備重用性的類庫之一了。測試
看下面這個例子,ui
public class AutoMobile { } /** * 重用性很差的容器類 */ public class Holder1 { private AutoMobile a; public Holder1(AutoMobile a) { this.a = a; } //~~ } /** * 想要在java5 以前實現可重用性的容器類 * @author Richard_yyf * @version 1.0 2019/8/29 */ public class Holder2 { private Object a; public Holder2(Object a) { this.a = a; } public Object getA() { return a; } public void setA(Object a) { this.a = a; } public static void main(String[] args) { Holder2 h2 = new Holder2(new AutoMobile()); AutoMobile a = (AutoMobile) h2.getA(); h2.setA("Not an AutoMobile"); String s = (String) h2.getA(); h2.setA(1); Integer x = (Integer) h2.getA(); } } /** * 經過泛型來實現可重用性 * 泛型的主要目的是指定容器要持有什麼類型的對象 * 並且由編譯器來保證類型的正確性 * * @author Richard_yyf * @version 1.0 2019/8/29 */ public class Holder3WithGeneric<T> { private T a; public Holder3WithGeneric(T a) { this.a = a; } public T getA() { return a; } public void setA(T a) { this.a = a; } public static void main(String[] args) { Holder3WithGeneric<AutoMobile> h3 = new Holder3WithGeneric<>(new AutoMobile()); // No class cast needed AutoMobile a = h3.getA(); } }
經過上述對比,咱們應該能夠理解類型參數化具體是什麼個意思。
在沒有泛型以前,從集合中讀取到的每個對象都須要進行轉換。若是有人不當心插入了類型錯誤的對象,在運行時的轉換處理就會出錯。這顯然是不可忍受的。
泛型的出現,給Java帶來了不同的編程體驗。
Holder<AutoMobile>
這個類型顯化的效果,程序員可以一目瞭然猜想出這個容器類持有的數據類型。泛型按照使用狀況能夠分爲 3 種。
定義格式:
public class 類名 <泛型類型1,...> { ... }
尖括號 <>
中的 字母 被稱做是類型參數,用於指代任何類型。咱們常看到<T>
的寫法,事實上,T 只是一種習慣性寫法,若是你願意。你能夠這樣寫。
public class Test<Hello> { Hello field1; }
但出於規範和可讀性的目的,Java 仍是建議咱們用單個大寫字母來表明類型參數。常見的如:
定義格式:
public <泛型類型> 返回類型 方法名(泛型類型 變量名) { ... }
注意事項:
<T>
中的T
被稱爲類型參數,而方法中的 T
被稱爲參數化類型,它不是運行時真正的參數。/** * 泛型類與泛型方法的共存現象 * @author Richard_yyf * @version 1.0 2019/8/29 */ public class GenericDemo2<T> { public void testMethod(T t){ System.out.println(t.getClass().getName()); } public <T> T testMethod1(T t){ return t; } public static void main(String[] args) { GenericDemo2<String> t = new GenericDemo2<>(); t.testMethod("generic"); Integer integer = 1; Integer i = t.testMethod1(integer); } }
泛型方法始終以本身定義的類型參數爲準
固然,現實場景下千萬不要去做死寫出這麼難以閱讀的代碼。
泛型接口和泛型類差很少。
定義格式:
public interface 接口名<泛型類型> { ... }
Demo
public interface GenericInterface<T> { void show(T t); } public class GenericInterfaceImpl<String> implements GenericInterface<String>{ @Override public void show(String o) { } }
除了用 <T>
表示泛型外,還有 <?>
這種形式。? 被稱爲通配符。
爲何要引進這個概念呢?先來看下下面的Demo.
public class GenericDemo2 { class Base{} class Sub extends Base{} public void test() { // 繼承關係 Sub sub = new Sub(); Base base = sub; List<Sub> lsub = new ArrayList<>(); // 編譯器是不會讓下面這行代碼經過的, // 由於 Sub 是 Base 的子類,不表明 List<Sub>和 List<Base>有繼承關係。 List<Base> lbase = lsub; } }
在現實編碼中,確實有這樣的需求,但願泛型可以處理某一範圍內的數據類型,好比某個類和它的子類,對此 Java 引入了通配符這個概念。
因此,通配符的出現是爲了指定泛型中的類型範圍。
通配符有 3 種形式。
<?>
被稱做無限定的通配符<? extends T>
被稱做有上限的通配符<? super T>
被稱做有下限的通配符<?>
無限定通配符常常與容器類配合使用,它其中的 ? 其實表明的是未知類型,因此涉及到 ? 時的操做,必定與具體類型無關。
// Collection.java public interface Collection<E> extends Iterable<E> { boolean add(E e); } public class GenericDemo3 { /** * 測試 無限定通配符 <?> * @param collection c */ public void testUnBoundedGeneric(Collection<?> collection) { collection.add(123); collection.add("123"); collection.add(new Object()); // 你只能調用 Collection 中與類型無關的方法 collection.iterator().next(); collection.size(); } }
無需關注 Collection 中的真實類型,由於它是未知的。因此,你只能調用 Collection 中與類型無關的方法。
有同窗可能會想,<?>
既然做用這麼眇小,那麼爲何還要引用它呢?
我的認爲,提升了代碼的可讀性,程序員看到這段代碼時,就可以迅速對此創建極簡潔的印象,可以快速推斷源碼做者的意圖。
(用的不多,可是要理解)
爲了接下去的說明方便,先定義一下幾個類。
class Food {} class Fruit extends Food {} class Apple extends Fruit {} class Banana extends Fruit {} // 容器類 class Plate<T> { private T item; public Plate(T item) { this.item = item; } public T getItem() { return item; } public void setItem(T item) { this.item = item; } }
<?>
表明着類型未知,可是咱們的確須要對於類型的描述再精確一點,咱們但願在一個範圍內肯定類別,好比類型 T 及 類型 T 的子類均可以放入這個容器中。
在這個體系中,上限通配符 Plate<? extends Fruit>
覆蓋下圖中藍色的區域。
邊界讓Java不一樣泛型之間的轉換更容易了。但不要忘記,這樣的轉換也有必定的反作用。那就是容器的部分功能可能失效。
public void testUpperBoundedBoundedGeneric() { Plate<? extends Fruit> p = new Plate<>(new Apple()); // 不能存入任何元素 p.setItem(new Fruit()); // error p.setItem(new Apple()); // error // 讀出來的元素須要是 Fruit或者Fruit的基類 Fruit fruit = p.getItem(); Food food = p.getItem(); // Apple apple = p.getItem(); }
<? extends Fruit>會使往盤子裏放東西的set( )方法失效。但取東西get( )方法還有效。好比下面例子裏兩個set()方法,插入Apple和Fruit都報錯。
緣由是編譯器只知道容器內是Fruit或者它的派生類,但具體是什麼類型不知道。多是Fruit?多是Apple?也多是Banana,RedApple,GreenApple?
若是你須要一個只讀容器,用它來produce T,那麼使用<? extends T> 。
相對應的,還有下限通配符 <? super T>
對應剛纔那個例子,Plate<? super Fruit>
覆蓋下圖中紅色的區域。
public void testLowerBoundedBoundedGeneric() { // Plate<? super Fruit> p = new Plate<>(new Food()); Plate<? super Fruit> p = new Plate<>(new Fruit()); // 存入元素正常 p.setItem(new Fruit()); p.setItem(new Apple()); // 讀取出來的東西,只能放在Object中 Apple apple = p.getItem(); // error Object o = p.getItem(); }
由於下界規定了元素的最小粒度的下限,其實是放鬆了容器元素的類型控制。既然元素是Fruit的基類,往裏面存比Fruit粒度小的類均可以。可是往外讀取的話就費勁了,只有全部類的基類Object能夠裝下。但這樣一來元素類型信息就都丟失了。
PECS - Producer Extends Consumer Super
泛型是 Java 1.5 版本才引進的概念,在這以前是沒有泛型的概念的,但顯然,泛型代碼可以很好地和以前版本的代碼很好地兼容。
這是由於,泛型信息只存在於代碼編譯階段,在進入 JVM 以前,與泛型相關的信息會被擦除掉
專業術語叫作 類型擦除。
List<String> strList = new ArrayList<String>(); List<Integer> integerList = new ArrayList<Integer>(); System.out.println(strList.getClass() == integerList.getClass());
==== output ===== true =================
打印的結果爲 true 是由於 List<String>
和 List<Integer>
在 jvm 中的 Class 都是 List.class。
泛型信息被擦除了。
/** * 類型擦除 相關類 * * @author Richard_yyf * @version 1.0 2019/8/29 */ public class EraseHolder<T> { T data; public EraseHolder(T data) { this.data = data; } public static void main(String[] args) { EraseHolder<String> holder = new EraseHolder<>("hello"); Class clazz = holder.getClass(); System.out.println("erasure class is:" + clazz.getName()); Field[] fs = clazz.getDeclaredFields(); for ( Field f:fs) { // 那咱們可不能夠說,泛型類被類型擦除後,相應的類型就被替換成 Object 類型呢? System.out.println("Field name "+f.getName()+" type:"+f.getType().getName()); } EraseHolder2<String> holder2 = new EraseHolder2<>("hello"); clazz = holder2.getClass(); fs = clazz.getDeclaredFields(); for ( Field f:fs) { System.out.println("Field name "+f.getName()+" type:"+f.getType().getName()); } } static class EraseHolder2<T extends String> { T data; public EraseHolder2(T data) { this.data = data; } } }
利用類型擦除的原理,用反射的手段就繞過了正常開發中編譯器不容許的操做限制。
public class EraseReflectDemo { public static void main(String[] args) { List<Integer> list = new ArrayList<>(); list.add(23); // can't add here // 由於泛型的限制 boolean add(E e); list.add("123"); // error // 利用反射能夠繞過編譯器去調用add方法 // 又由於類型擦除時 boolean add(E e); 等同於 boolean add(Object e); try { Method method = list.getClass().getDeclaredMethod("add", Object.class); method.invoke(list, "test"); method.invoke(list, 42.9f); } catch (Exception e) { e.printStackTrace(); } for (Object o : list) { System.out.println(o); } } }
==== output ===== 23 test 42.9 =================