Java是一種強類型語言,平常開發中,不管是類的屬性仍是方法的參數和返回值都須要明確指定具體數據類型。這其實是把類和方法與數據類型綁定了,這種理所固然的思想影響了編程的抽象性和靈活性。事實上,代碼與它們可以操做的數據類型不是必須綁定的,同一套代碼能夠用於多種數據類型。這樣,不只能夠複用代碼,下降耦合,並且能夠提升代碼的可讀性和安全性。算法
先來回顧一下,在Java開發中咱們是如何一步一步突破數據類型限制的。咱們以Fan類爲例:編程
第一種狀況,這個類只處理一種數據類型Person,那直接聲明參數爲此類型便可:數組
public class Fan { Person first; public Fan(Person first) { this.first = first; } public Person getFirst() { return first; } }
第二種狀況,若是這個類能夠處理多個數據類型,好比,除了Person還有Book和Article,咱們通常會讓這三種數據類型繼承同一父類或實現統一接口(Namable),而後以父類或接口做爲參數類型:安全
public class Fan { Namable first; public Fan(Namable first) { this.first = first; } public Namable getFirst() { return first; } }
第三種狀況,若是繼續擴展,這個類能夠處理全部數據類型,按照上面的思路,咱們去找全部數據類型的父類,Object!因而咱們能夠這樣寫:數據結構
public class Fan { Object first; public Fan(Object first) { this.first = first; } public Object getFirst() { return first; } }
第三種狀況雖然已是泛型的雛形,並且泛型的底層原理也確實是這樣的,但我認爲這樣寫依然沒有跳出數據類型的思惟方式,並且缺少安全性和可讀性。數據結構和算法
正真的泛型就是類型參數化,處理的數據類型不是固定的,而是能夠做爲參數傳入。以下,T就是類型參數:函數
public class Fan <T> { T first; public Fan(T first) { this.first = first; } public T getFirst() { return first; } }
若是Object也能夠解決一樣的問題,爲何還要引入泛型?泛型有兩個好處:this
要解釋安全性須要瞭解Java編譯:Java有編譯器和Java虛擬機,編譯器將Java源代碼轉換成.class文件,虛擬機加載並運行.class文件。對於泛型類,Java編譯器會將泛型代碼轉換爲普通代碼,這個過程 會將類型參數T擦除,替換爲Object,插入必要的強制類型轉換。也就是說:spa
泛型是經過類型擦除來實現的,Java虛擬機運行時對泛型基本一無所知!code
泛型的安全性體如今代碼調用上,以下泛型類調用和Object代碼調用:
/*Object用法*/ Fan obj = new Fan("lilei"); Integer age = (Integer)obj.getFirst(); //不會有編譯錯誤,但運行時出錯 /*泛型用法*/ Fan<String> f = new Fan<>("lilei"); Integer age2 = f.getFirst(); //開發環境或編譯器都會直接報錯
使用Object方法沒法在編譯時發現錯誤,只有運行時纔會報錯;而使用泛型,因爲輸入了類型參數,在預編譯或編譯時就能及時發現錯誤。
泛型在可讀性上也是優於Object的,好比Fan構造函數有多個參數時,所有寫成Object形成閱讀困難,而泛型能夠按以下方式寫:
public class Fan { /*都是Object類型,形成閱讀困難*/ Object first; Object second; public Fan(Object first, Object second) { this.first = first; this.second = second; } } public class Fan <T,U> { /*泛型能夠用不一樣通配符表示,更好閱讀*/ T first; U second; public Fan(T first, U second) { this.first = first; this.second = second; } }
老是,泛型是計算機程序中一種重要的思惟方式,它將數據結構和算法與數據類型想分離,使得同一套數據結構和算法可以應用於各類數據類型,並且能夠保證類型安全,提升可讀性。
泛型能夠在三個地方使用:
(1)泛型類
public class Fan <T,U> { T first; U second; public Fan(T first, U second) { this.first = first; this.second = second; } }
(2)泛型方法
除了泛型類,方法也能夠是泛型的,並且,一個方法是否是泛型的,與它所在類是否是泛型沒有什麼關係。
public static <T> int getIndex(T[] arr, T elm) { for (int i = 0; i < arr.length; i++) { if (arr[i].equals(elm)) { return i; } } return -1; }
(3)泛型接口
實現泛型接口時,應該指定具體的類型。
public interface Comparable<T> { int compareTo(T to); }
除了像上面那樣聲明類型參數外,泛型還能夠用通配符 ? 來表示,通配符有三種寫法:
下面解釋以前,咱們先設定一個場景:有三個類A,B,C,其中,B和C都繼承於A
public class A{} public class B extends A{} public class C extends A{}
若是,咱們定義兩個List:
List<A> listA = new ArrayList<A>(); List<B> listB = new ArrayList<B>();
這兩個List是不能互相賦值的,即下面語句不合法:
listA = listB; //非法,由於listA指向listB後,能夠經過listA向其中添加C元素,形成listB含有C元素,不合法 listB = listA; //非法,由於listA中可能含有C元素,會形成listB中包含C元素,不合法
那如今咱們能夠得出結論:若是一個方法接收的參數是List<A>類型,咱們沒法傳入List<B>或List<C>類型實參;一樣若是接收的參數是List<B>,更不能傳入List<A>或List<C>。這樣就形成一種不便:即便方法算法是同樣的,由於參數不一樣,咱們須要寫多個方法。有沒有辦法只寫一個方法,就能夠指定接收特定類型範圍的集合呢?泛型通配符就能夠簡化這種寫法:
通配符機制的目的是:讓一個持有特定類型(好比A類型)的集合可以強制轉換爲持有A的子類或父類型的集合。
泛型通配符主要針對如下兩種需求:
除了這兩種狀況,通常狀況下,咱們依然使用類型參數來表達泛型。
下面分別介紹三種通配符的用法:
(1)無限定通配符 <?>
List<?> 的意思是這個集合是一個能夠持有任意類型的集合,它能夠是List<A>,也能夠是List<B>,或者List<C>等等。由於不知道集合是哪一種類型,因此只可以對集合進行讀操做。而且你只能把讀取到的元素當成 Object 實例來對待:
public void processElements(List<?> elements){ for(Object o : elements){ Sysout.out.println(o); } }
(2)上限定通配符 <? extends A>
List<? extends A> 表明的是一個能夠持有 A及其子類(如B和C)的實例的List集合。依然不能寫,可是讀取後不用轉Object了,能夠轉換成A:
public void processElements(List<? extends A> elements){ for(A a : elements){ System.out.println(a.getValue()); } }
(3)下限定通配符 <? super A>
List<? super A> 的意思是List集合 list,它能夠持有 A 及其父類的實例,當你知道集合裏所持有的元素類型都是A及其父類的時候,此時往list集合裏面插入A及其子類(B或C)是安全的,但讀取時依然只能轉換成Object:
public static void insertElements(List<? super A> list){ list.add(new A()); list.add(new B()); list.add(new C()); }
咱們已經知道泛型會在編譯階段擦除類型參數,替換爲Object,這會對泛型的使用形成一些限制:
(1)類型參數不能使用基本數據類型:
Fan<int> f = new Fan<int>(10); // type params cannot be of primitive type
(2)不能像下面這樣重載方法,由於類型擦除後,兩個方法是徹底同樣的:
public static void test(Fan<Integer> arr){} public static void test(Fan<String> arr){}
(3)不能直接類型參數實例化對象,由於會形成誤解,用戶覺得是實例化的T類型,實際上類型擦除後是Object類型,因此乾脆被Java禁止
T elm = new T(); //不合法,Java禁止
若是必定要實例化,只能使用反射方法
public static <T> T create(Class<T> type) { try { return type.newInstance(); } catch (Exception e) { return null; } }
(4)對於泛型類聲明的類型參數,實例變量和方法可使用,但靜態變量和方法不可使用,由於靜態變量和方法只有一份,沒法知足全部實例化類型:
public class Singleton<T> { private static T instance; //不合法,T不能在靜態變量中使用 public synchronized static T getInstance() { //不合法,T不能在靜態方法中使用 } }
可是,靜態方法能夠單獨聲明本身的類型參數,即泛型方法
public class Singleton { public synchronized static T getInstance() { //合法,該方法爲泛型方法 } }
(5)Java不容許建立泛型數組,由於數組是Java直接支持的類型,自己就會因賦值不當形成運行時錯誤,泛型數組一樣會引發延時類運行錯誤,比較複雜,再也不詳解。