Java泛型使用的簡單介紹
- 2019 年 10 月 3 日
- 筆記
目錄
一. 泛型是什麼
「泛型」,顧名思義,「泛指的類型」。我們提供了泛指的概念,但具體執行的時候卻可以有具體的規則來約束,比如我們用的非常多的ArrayList就是個泛型類,ArrayList作為集合可以存放各種元素,如Integer, String,自定義的各種類型等,但在我們使用的時候通過具體的規則來約束,如我們可以約束集合中只存放Integer類型的元素,如List<Integer> iniData = new ArrayList<>()
。
二. 使用泛型有什麼好處
以集合來舉例,使用泛型的好處是我們不必因為添加元素類型的不同而定義不同類型的集合,如整型集合類,浮點型集合類,字元串集合類,我們可以定義一個集合來存放整型、浮點型,字元串型數據,而這並不是最重要的,因為我們只要把底層存儲設置了Object即可,添加的數據全部都可向上轉型為Object。 更重要的是我們可以通過規則按照自己的想法控制存儲的數據類型。
我們以ArrayList為例,假如我們要將本月截至今天的日期放到ArrayList中,如果我們不使用泛型,此時我們定義一個ArrayList.
List monthDays = new ArrayList();
我們向其中加入1號到4號日期
public static List addMonthDays(){ List monthDays = new ArrayList(); monthDays.add(LocalDate.now().withDayOfMonth(1)); monthDays.add(LocalDate.now().withDayOfMonth(2)); monthDays.add(LocalDate.now().withDayOfMonth(3)); monthDays.add(new Date()); return monthDays; }
這樣有沒有問題?大家也看出來了,當然有,雖然都可以表示日期,但卻用了Date,LocalDate,我們調用方法直接列印出來,就是這樣
public static void main(String[] args) { List monthDays = addMonthDays(); for(Object day : monthDays){ System.out.println(day); } }
2019-08-01 2019-08-02 2019-08-03 Sun Aug 04 10:27:10 CST 2019
我們肯定不想要這樣的結果,我們想要的是
2019-08-01 2019-08-02 2019-08-03 2019-08-04
如果存儲的元素類型只是這兩種(假如我們知道),這個時候我們就手動判斷一下
public static void main(String[] args) { List monthDays = addMonthDays(); for(Object day : monthDays){ if (day instanceof Date){ Date date = (Date) day; System.out.println(LocalDate.of(date.getYear(), date.getMonth(), date.getDay())); }else { System.out.println(day); } } }
這個時候我們就可以達成上述目的了,但大家也知道,這種寫法問題問題很大
- 我們無法控制存儲的元素到底是否和日期相關,如我們存儲了「1」,65536等非日期,定義的方法也不會報錯,但在調用進行類型轉換的時候必然會報錯;
- 我們不知道集合中到底存儲了哪些類型的元素,比如還有「2019/08/04」這種日期字元串、java.sql.Date這種類型呢,我們很難保證可以窮盡;
- 程式碼過於複雜,太難維護;
這時泛型就提供了很好的解決方案,從源頭上就制定好規則,只能添加LocalDate類型,那麼上述的問題就都得以解決了。
public static List<LocalDate> addFormatMonthDays(){ List<LocalDate> monthDays = new ArrayList<>(); monthDays.add(LocalDate.now().withDayOfMonth(1)); monthDays.add(LocalDate.now().withDayOfMonth(2)); monthDays.add(LocalDate.now().withDayOfMonth(3)); monthDays.add(new Date());//這個程式碼在編譯期間就無法通過了 return monthDays; }
不僅提高了程式碼的可讀性,一眼即可看出我們存儲的是LocalDate類型。同時編譯器也可很好的利用該資訊,編譯期間就可進行類型檢查,保證了安全性,在get的時候,無需進行強制類型轉換。
三. 泛型類
為使類適應更多的情況,具備更好的擴展性,我們可以將其設置為泛型類,即具有一個或多個類型變數的類,寫法如下:
public class ClassName<泛型標識,可以是字母、中文字元等,不過一般用大寫英文字元> { private 泛型標識 property; }
對於泛型標識符,一般有一些約定俗稱的寫法,如果是表示集合的元素類型,一般用字母E,如我們常用的ArrayList,public class ArrayList<E>
,我們自定義如下:
//車庫 public class Garage<E> { //向車庫中添加車 public void add(E car){ //... } }
我們還可以用字元K和V表示關鍵字和值的類型,如我們常用的HashMap,public class HashMap<K,V>
,我們也可以自定義如下:
//映射關係 //K,V: 蔬菜,是否有機; 水果,產地; 服裝,類型; 汽車,品牌 public class Mapping<K, V> { private K key; private V value; }
我們還經常用一個字元T表示類型
public class Person<T> { private T t; public Person(T t){ this.t = t; } public void run(){ System.out.println(t); } }
如何使用泛型類,類型如何實例化,我們只需要保證傳入的實參類型和泛型參數類型相同即可。
//正常用法 Person<String> s1 = new Person<String>("張三"); //jdk7.0後可以省略後面的參數類型 Person<String> s2 = new Person<>("張三"); s2.run(); //當然泛型的定義是可以幫助我們按照某種規則去做事,如果不做限制,也不會編譯錯誤,但泛型就毫無意義了 Person t = new Person(111); t.run(); //泛型的類型不能是八大基本類型,下面會編譯出錯 Person<int> s = new Person<>(1);
四. 泛型介面
泛型介面和泛型類的定義基本一致,定義如下:
public interface Person<T> { public T parent(); public String eat(); }
當我們定義了一個類要實現該介面時,那麼該類的泛型類型必須和介面類的泛型類型一致,未傳遞實參的情況下,繼續使用泛型類型T,傳遞了實參的情況下,泛型類型必須使用實參類型
public class Teacher<T> implements Person<T> { @Override public T parent() { return null; } @Override public String eat() { return null; } }
//Teacher不必再定義類型了,因為泛型類型在Person處已經定義好了 public class Teacher implements Person<Integer> { //這裡的返回類型必須為Integer,否則必須出錯 @Override public Integer parent() { return null; } @Override public String eat() { return null; } }
五. 泛型方法
泛型方法可以定義在普通類和泛型類中,比如泛型類更為常用,一般能用泛型方法解決的問題優先使用泛型方法而不使用泛型類,類型變數放在修飾符的後面,如public static ,public final等的後面。
public class Teacher { public static <T> T println(T t){ System.out.println(t); return t; } }
調用很簡單,很一般方法調用是一樣的,更方便的是類型不像一般方法做了限定。
String s = Teancher.println("str");
另外需要說明的是,定義在泛型類中的泛型方法的泛型變數之間是沒有關係的,如這樣的程式碼
public class Teacher<T> { T teacher; public Teacher(T t){ this.teacher = t; } public <T> T println(T t){ System.out.println(t); return t; } }
Teacher<String> teacher = new Teacher<>("張三"); Integer in = teacher.println(123456);
類泛型類型為String,方法的泛型類型為Integer,雖然都是用T來表示的。
同時關於泛型方法需要說明的是:
在修飾符public xx與方法名之間非常重要,有< T >這樣的才算是泛型方法;僅僅使用了泛型變數並不算是泛型方法。
六. 限定類型變數
不論是泛型類還是泛型方法,目前來說其實都是沒有做類型限定,無論我們傳遞什麼樣類型的變數進去都可以,因為我們在處理邏輯中並沒有使用到該類型特有的東西(成員變數、方法等)。假如我們想傳遞的參數類型僅僅是某個大類(父類)下面的一些小類(子類),那麼怎麼做呢?
public class ArrayFlag { public static <T> T getMax(T[] array){ if(array == null || array.length == 0){ return null; } T maxValue = array[0]; for(int i = 0; i < array.length; i++){ if(array[i].compareTo(maxValue) > 0){ maxValue = array[i]; } } return maxValue; } }
大家也看到了我們在getMax方法中使用了compareTo方法進行比較,但如果我們傳入的類型T沒有compareTo方法呢,豈不是要報錯,因此我們需要做限定,只要限定了是Comparable介面的必然具備compareTo方法,那麼改造後就成了這樣
public class ArrayFlag { public static <T extends Comparable> T getMax(T[] array){ if(array == null || array.length == 0){ return null; } T maxValue = array[0]; for(int i = 0; i < array.length; i++){ if(array[i].compareTo(maxValue) > 0){ maxValue = array[i]; } } return maxValue; } }
同時需要說明的是,此處用的是extends關鍵字,extends在這裡是表示的是綁定了Comparable介面及其子類型,是「綁定、限定」的意思,非「繼承」的意思,後面也可以是介面或者類,如果有多個限制,可以使用&分隔,如:
public static <T extends Comparable & Serializable> T getMax(T[] array)
七. 泛型通配符
舉個例子,定義了一個書籍類和一個小說類
//定義了一個書籍類 public class Book {} //定義了一個小說書籍類繼承書籍類 public class Novel extends Book {}
我們再定義一個書櫃類用來裝書籍以及小說
//定義了一個書櫃類用來裝書 public class Bookcase<T> { T b; public Bookcase(T t){ this.b = t; } public void set(T t) { b=t; } public T get() { System.out.println(b.getClass()); return b; } }
下面我們就用書櫃來裝小說
//以前的寫法,無法編譯通過,提示Incompatible types, Required Book Found Novel Bookcase<Book> bc = new Bookcase<Novel>(new Novel());
但在jdk7.0後,new Bookcase的時候是可以不用給出泛型類型的,省略的類型可以從變數的類型推斷得出,因此如果下面這種寫法
Bookcase<Book> bc = new Bookcase<>(new Novel()); System.out.println(bc.getClass()); bc.get();
此時可以編譯通過,我們執行後得出的結果是:
class generic.Bookcase class generic.Novel
當然我們還可以通過通配符來解決該問題,通配符包括以下幾種:
上界通配符、下界通配符、無限定通配符
7.1 上界通配符
上界通配符定義方式如下:用extends 關鍵字,含義是該書櫃只能放置小說類書籍(如什麼都市小說、愛情小說、玄幻小說都可以),但不能放置父類書籍、其他類如史書、職場類書籍、財經類書籍等,是在使用的時候進行限定,如:
Bookcase<? extends Book> bc = new Bookcase<Novel>(new Novel());
這種定義方式就不會編譯錯誤了。另外關於上界通配符的特點,對上有限制,根據java多態向上造型的原則,不適合頻繁插入數據,適合頻繁讀取數據的場景。
7.2 下界通配符
下界通配符定義方式如下:用super關鍵字,含義就是書櫃放置設置了下限,我們只能放置Book書籍以及Novel書籍,卻無法再將細分的都市小說、愛情小說類書籍放進去
Bookcase<? super Novel> bc = new Bookcase<Novel>(new Novel());
另外關於下界通配符的特點,和上界通配符正好相反,不適合頻繁讀取數據,適合頻繁插入數據的場景。
7.3 無限定通配符
無限定通配符意味著可以使用任何對象,因此使用它類似於使用原生類型。但它是有作用的,原生類型可以持有任何類型,而無限定通配符修飾的容器持有的是某種具體的類型。
舉個例子:
List<?> list = new ArrayList<>(); //無法編譯通過 list.add(new Object()); //下面這樣的卻可以添加任何類型 List<Object> list = new ArrayList<>(); list.add(new Object());
再說一下< T > 和< ? >之間的區別,初看好像他們都可以表示泛型變數,都可以extends,但它們確實有不同的使用場景
- 類型參數< T >聲明一個泛型類或泛型方法
- 無限定通配符< ? >使用泛型類或泛型方法
八. 總結
泛型在java中可以說很常用,我們前面提到的集合類,如ArrayList,HashSet,以及Map都使用到了泛型,泛型也是也是我們再進行一些組件封裝經常用到的,本文主要介紹了泛型基本概念,使用泛型的好處,泛型類、介面、方法、通配符的簡單介紹以及使用方法,最後泛型一般和反射集合使用,通過泛型可以進行類型的靈活傳遞,通過反射可獲取到實體以及類的數據資訊,從而實現一些框架、組件的封裝,若有不對之處,請批評指正,望共同進步,謝謝!