Java-Java5.0泛型解读
- 概述
- 泛型類
- 泛型方法
- 泛型接口
- 邊界符
- 通配符
- PECS原則
- 類型擦除
概述
Java 泛型(generics)是 JDK 5 中引入的一個新特性, 泛型提供了編譯時類型安全檢測機制,該機制允許程序員在編譯時檢測到非法的類型。
泛型的本質是參數(shù)化類型,也就是說所操作的數(shù)據(jù)類型被指定為一個參數(shù)。
泛型類
我們先看一個簡單的類的定義
package com.xgj.master.java.generics;public class GenericClass {private String str;public String getStr() {return str;}public void setStr(String str) {this.str = str;}}這是我們最常見的做法,但是這樣做有一個壞處 GenericClass類中只能裝入String類型的元素,如果我們以后要擴展該類,比如轉入Integer類型的元素,就不想要從重寫,代碼得不到復用,我們使用泛型可以很好地解決這個問題。
如下代碼所示:
package com.xgj.master.java.generics; /*** * @ClassName: GenericClass* @Description: 泛型類* @author: Mr.Yang* @date: 2017年8月31日 下午3:35:38* @param <T>*/ public class GenericClass<T> {// T stands for "Type"private T t;public T getT() {return t;}public void setT(T t) {this.t = t;}}這樣GenericClass類便可以得到復用,我們可以將T替換成任何我們想要的類型:
假設我們有一個User類
GenericClass<String> string = new GenericClass<String>(); GenericClass<User> user = new GenericClass<User>();另外一個示例:
package com.xgj.master.java.generics;import org.junit.Test;public class NormalClass {@Testpublic void test(){Point point = new Point();point.setX(100); // int -> Integer -> Objectpoint.setY(20);int x = (Integer) point.getX(); // 必須向下轉型int y = (Integer) point.getY();System.out.println("This point is:" + x + ", " + y);point.setX(25.4); // double -> Integer -> Objectpoint.setY("字符串");// 必須向下轉型double m = (Double) point.getX();// 運行期間拋出異常 java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Doubledouble n = (Double) point.getY(); System.out.println("This point is:" + m + ", " + n);}/*** * @ClassName: Point* @Description: 一般內部類* @author: Mr.Yang* @date: 2017年8月31日 下午8:23:31*/class Point {Object x = 0;Object y = 0;public Object getX() {return x;}public void setX(Object x) {this.x = x;}public Object getY() {return y;}public void setY(Object y) {this.y = y;}} }上面的代碼中,設置值的時候不會有任何問題,但是取值時,要向下轉型,因向下轉型存在著風險,而且編譯期間不容易發(fā)現(xiàn),只有在運行期間才會拋出異常,所以要盡量避免使用向下轉型。
那么,有沒有更好的辦法,既可以不使用重載(有重復代碼),又能把風險降到最低呢?
可以使用泛型類(Java Class),它可以接受任意類型的數(shù)據(jù)。所謂“泛型”,就是“寬泛的數(shù)據(jù)類型”,任意的數(shù)據(jù)類型。
package com.xgj.master.java.generics;import org.junit.Test;public class GenericClass2 {@Testpublic void test() {Point<Integer, Integer> point = new Point<Integer, Integer>();point.setX(200);point.setY(400);Integer x = point.getX();Integer y = point.getY();System.out.println("This point is:" + x + ", " + y);Point<Double, String> point2 = new Point<Double, String>();point2.setX(25.4);point2.setY("字符串");double m = point2.getX();String n = point2.getY();System.out.println("This point is:" + m + ", " + n);}/*** * * @ClassName: Point* * @Description: 泛型類, T1 T2僅表示類型* * @author: Mr.Yang* * @date: 2017年8月31日 下午8:20:26* * @param <T1>* @param <T2>*/class Point<T1, T2> {T1 x;T2 y;public T1 getX() {return x;}public void setX(T1 x) {this.x = x;}public T2 getY() {return y;}public void setY(T2 y) {this.y = y;}} }與普通類的定義相比,上面的代碼在類名后面多出了 <T1, T2>,T1, T2 是自定義的標識符,也是參數(shù),用來傳遞數(shù)據(jù)的類型,而不是數(shù)據(jù)的值,我們稱之為類型參數(shù)。在泛型中,不但數(shù)據(jù)的值可以通過參數(shù)傳遞,數(shù)據(jù)的類型也可以通過參數(shù)傳遞。T1, T2 只是數(shù)據(jù)類型的占位符,運行時會被替換為真正的數(shù)據(jù)類型。
傳值參數(shù)(我們通常所說的參數(shù))由小括號包圍,如 (int x, double y),類型參數(shù)(泛型參數(shù))由尖括號包圍,多個參數(shù)由逗號分隔,如 <T> 或 <T, E>。
類型參數(shù)需要在類名后面給出。一旦給出了類型參數(shù),就可以在類中使用了。類型參數(shù)必須是一個合法的標識符,習慣上使用單個大寫字母,通常情況下,K 表示鍵,V 表示值,E 表示異常或錯誤,T 表示一般意義上的數(shù)據(jù)類型。
泛型類在實例化時必須指出具體的類型,也就是向類型參數(shù)傳值,格式為:
className variable<dataType1, dataType2> = new className<dataType1, dataType2>();也可以省略等號右邊的數(shù)據(jù)類型,但是會產生警告,即:
className variable<dataType1, dataType2> = new className();因為在使用泛型類時指明了數(shù)據(jù)類型,賦給其他類型的值會拋出異常,既不需要向下轉型,也沒有潛在的風險,比上個例子的自動裝箱和向上轉型要更加實用。
泛型方法
我們可以編寫一個泛型方法,該方法在調用時可以接收不同類型的參數(shù)。根據(jù)傳遞給泛型方法的參數(shù)類型,編譯器適當?shù)靥幚砻恳粋€方法調用。
定義泛型方法的規(guī)則如下:
所有泛型方法聲明都有一個類型參數(shù)聲明部分(由尖括號分隔),該類型參數(shù)聲明部分在方法返回類型之前, 比如 public static <E> void printArray( E[] inputArray ){}
每一個類型參數(shù)聲明部分包含一個或多個類型參數(shù),參數(shù)間用逗號隔開。一個泛型參數(shù),也被稱為一個類型變量,是用于指定一個泛型類型名稱的標識符。
類型參數(shù)能被用來聲明返回值類型,并且能作為泛型方法得到的實際參數(shù)類型的占位符。
泛型方法體的聲明和其他方法一樣。注意類型參數(shù)只能代表引用型類型,不能是short, int, double, long, float, byte, char等原始類型。但是傳遞基本類型不會報錯,因為它們會自動裝箱成對應的包裝類。
那如何聲明一個泛型方法呢? 聲明一個泛型方法很簡單,只要在返回類型前面加上一個類似<K, V>的形式就可以啦。
比如:
package com.xgj.master.java.generics;/*** * @ClassName: Compare* @Description: 內部類,泛型類* @author: Mr.Yang* @date: 2017年8月31日 下午4:01:39* @param <K>* @param <V>*/ public class ComPare<K,V> {private K key;private V value;/*** * @Title:ComPare* @Description:構造函數(shù)* @param key* @param value* @return */public ComPare(K key, V value) {this.key = key;this.value = value;}public K getKey() {return key;}public void setKey(K key) {this.key = key;}public V getValue() {return value;}public void setValue(V value) {this.value = value;} } package com.xgj.master.java.generics;/*** * @ClassName: GenericMethod* @Description: 泛型方法演示* @author: Mr.Yang* @date: 2017年8月31日 下午3:54:23*/ public class GenericMethod {/*** * @Title: cofer* @Description: 泛型方法* @param c1* @param c2* @return* @return: boolean*/public static <K,V> boolean cofer(ComPare<K,V> c1, ComPare<K,V> c2){return c1.getKey().equals(c2.getKey()) && c1.getValue().equals(c2.getValue());}// 泛型方法的調用public static void main(String[] args) {ComPare<Integer,String> c1 = new ComPare<>(1, "dog");ComPare<Integer, String> c2 = new ComPare<>(2, "cat");boolean different = GenericMethod.<Integer,String>cofer(c1, c2);System.out.println("c1 compares c2,and the result is " + different);// 在Java1.7/1.8可以利用type inference,讓Java自動推導出相應的類型參數(shù)boolean different2 = GenericMethod.cofer(c1, c2);System.out.println("自動推導 c1 compares c2,and the result is " + different2);} }運行結果:
c1 compares c2,and the result is false 自動推導 c1 compares c2,and the result is false第二個示例:
package com.xgj.master.java.generics;public class GenericMethod2 {public static void main(String[] args) {// 實例化泛型類Point<Integer, Integer> p1 = new Point<Integer, Integer>();p1.setX(10);p1.setY(20);p1.printPoint(p1.getX(), p1.getY());Point<Double, String> p2 = new Point<Double, String>();p2.setX(25.4);p2.setY("字符串");p2.printPoint(p2.getX(), p2.getY());} }/*** * * @ClassName: Point* * @Description: 定義泛型類* * @author: Mr.Yang* * @date: 2017年8月31日 下午7:59:47* * @param <T1>* @param <T2>*/ class Point<T1, T2> {T1 x;T2 y;public T1 getX() {return x;}public void setX(T1 x) {this.x = x;}public T2 getY() {return y;}public void setY(T2 y) {this.y = y;}/*** * * @Title: printPoint* * @Description: 定義泛型方法* * @param x* @param y* * @return: void*/public <T1, T2> void printPoint(T1 x, T2 y) {T1 m = x;T2 n = y;System.out.println("This point is:" + m + ", " + n);} }上面的代碼中定義了一個泛型方法 printPoint(),既有普通參數(shù),也有類型參數(shù),類型參數(shù)需要放在修飾符后面、返回值類型前面。一旦定義了類型參數(shù),就可以在參數(shù)列表、方法體和返回值類型中使用了。
與使用泛型類不同,使用泛型方法時不必指明參數(shù)類型,編譯器會根據(jù)傳遞的參數(shù)自動查找出具體的類型。泛型方法除了定義不同,調用就像普通方法一樣。
注意:泛型方法與泛型類沒有必然的聯(lián)系,泛型方法有自己的類型參數(shù),在普通類中也可以定義泛型方法。
泛型方法 printPoint() 中的類型參數(shù) T1, T2 與泛型類 Point 中的 T1, T2 沒有必然的聯(lián)系,也可以使用其他的標識符代替:
public static <V1, V2> void printPoint(V1 x, V2 y){V1 m = x;V2 n = y;System.out.println("This point is:" + m + ", " + n); }泛型接口
在Java中也可以定義泛型接口, 示例如下:
package com.xgj.master.java.generics;public class GenericInterfaceDemo {public static void main(String arsg[]) {Info<String> obj = new InfoImp<String>("xiaogongjiang");System.out.println("Length Of String: " + obj.getVar().length());} }/*** * * @ClassName: Info* * @Description: 定義泛型接口* * @author: Mr.Yang* * @date: 2017年8月31日 下午8:06:06* * @param <T>*/ interface Info<T> {public T getVar(); }/*** * * @ClassName: InfoImp* * @Description: 泛型接口實現(xiàn)類* * @author: Mr.Yang* * @date: 2017年8月31日 下午8:06:13* * @param <T>*/ class InfoImp<T> implements Info<T> {private T var;/*** * * @Title:InfoImp* * 定義泛型構造方法* * @param var*/public InfoImp(T var) {this.setVar(var);}public void setVar(T var) {this.var = var;}public T getVar() {return this.var;} }邊界符
在上面的代碼中 , 類型參數(shù)可以接受任意的數(shù)據(jù)類型,只要它是被定義過的。但是,很多時候我們只需要一部分數(shù)據(jù)類型就夠了,用戶傳遞其他數(shù)據(jù)類型可能會引起錯誤。例如,編寫一個泛型函數(shù)用于返回不同類型數(shù)組(Integer 數(shù)組、Double 數(shù)組、Character 數(shù)組等)中的最大值
package com.xgj.master.java.generics;public class CountGreater {public static <T> int countGreaterThan(T[] array, T element){int count = 0 ;// 遍歷數(shù)組for (T t : array) {if (t > element) { // 編譯報錯++count;}}return count;} }但是這樣很明顯是錯誤的,因為除了short, int, double, long, float, byte, char等原始類型,其他的類并不一定能使用操作符>,所以編譯器報錯,那怎么解決這個問題呢?答案是使用邊界符, 通過 extends 關鍵字可以限制泛型的類型.
public interface Comparable<T> {public int comparable(T t); }做個類似下面這樣的聲明,這樣就等于告訴編譯器類型參數(shù)T代表的都是實現(xiàn)了Comparable接口的類,這樣等于告訴編譯器它們都至少實現(xiàn)了compareTo方法。
public class CountGreater {public static <T extends Comparable<T>> int countGreaterThan(T[] array, T element){int count = 0 ;// 遍歷數(shù)組for (T t : array) {if (t.comparable(element) > 0) { ++count;}}return count;}}extends 后面可以是類也可以是接口。但這里的 extends 已經不是繼承的含義了,應該理解為 T 是繼承自 Comparable類的類型,或者 T 是實現(xiàn)了 XX 接口的類型。
通配符
在了解通配符之前,我們首先必須要澄清一個概念,還是借用我們上面定義的GenericClass類,假設我們添加一個這樣的方法:
public void test(GenericClass<Number> n){/***/}那么現(xiàn)在GenericClass<Number> n允許接受什么類型的參數(shù)?
我們是否能夠傳入GenericClass<Integer>或者GenericClass<Double>呢?
答案是否定的,雖然Integer和Double是Number的子類,但是在泛型中GenericClass<Integer>或者GenericClass<Double>與GenericClass<Number>之間并沒有任何的關系。
這一點非常重要,接下來我們通過一個完整的例子來加深一下理解。
首先我們先定義幾個簡單的類,下面我們將用到它:
class Fruit {} class Apple extends Fruit {} class Orange extends Fruit {}我們創(chuàng)建了一個泛型類Reader,然后在f1()中當我們嘗試Fruit f = fruitReader.readExact(apples);編譯器會報錯,因為List<Fruit>與List<Apple>之間并沒有任何的關系。
如下:
package com.xgj.master.java.generics;import java.util.Arrays; import java.util.List;public class GenericReading {static List<Apple> apples = Arrays.asList(new Apple());static List<Orange> oranges = Arrays.asList(new Orange());static class Reader<T> {T readExact(List<T> list) {return list.get(0);}}static void f1() {Reader<Fruit> fruitReader = new Reader<Fruit>();// The method readExact(List<Fruit>) in the type// GenericReading.Reader<Fruit> is not applicable for the arguments (List<Apple>)Fruit f = fruitReader.readExact(apples); // 編譯報錯}public static void main(String[] args) {f1();} }但是按照我們通常的思維習慣,Apple和Fruit之間肯定是存在聯(lián)系,然而編譯器卻無法識別,那怎么在泛型代碼中解決這個問題呢?我們可以通過使用通配符來解決這個問題:
package com.xgj.master.java.generics;import java.util.Arrays; import java.util.List;public class GenericReading {static List<Apple> apples = Arrays.asList(new Apple());static List<Orange> oranges = Arrays.asList(new Orange());static class CovariantReader<T> {T readCovariant(List<? extends T> list) {return list.get(0);}}static void f2() {CovariantReader<Fruit> fruitReader = new CovariantReader<Fruit>();Fruit f = fruitReader.readCovariant(oranges);Fruit a = fruitReader.readCovariant(apples);}public static void main(String[] args) {f2();} }這樣就相當與告訴編譯器, fruitReader的readCovariant方法接受的參數(shù)只要是滿足Fruit的子類就行(包括Fruit自身),這樣子類和父類之間的關系也就關聯(lián)上了。
PECS原則
上面我們看到了類似<? extends T>的用法,利用它我們可以從list里面get元素,那么我們可不可以往list里面add元素呢?
比如
package com.xgj.master.java.generics;import java.util.ArrayList; import java.util.List;public class GenericsAndCovariance {public static void main(String[] args) {// Wildcards allow covariance:List<? extends Fruit> flist = new ArrayList<Apple>();// The method add(capture#1-of ? extends Fruit) in the type List<capture#1-of ? extends Fruit> // is not applicable for the arguments (Apple)// flist.add(new Apple()); 編譯報錯// flist.add(new Orange());編譯報錯// flist.add(new Fruit());編譯報錯// flist.add(new Object());編譯報錯flist.add(null); // 雖然可以添加null ,但是沒有意義// We Know that it returns at least Fruit:Fruit f = flist.get(0);} }答案是否定,Java編譯器不允許我們這樣做,為什么呢?對于這個問題我們不妨從編譯器的角度去考慮。因為List
List<? extends Fruit> flist = new ArrayList<Fruit>(); List<? extends Fruit> flist = new ArrayList<Apple>(); List<? extends Fruit> flist = new ArrayList<Orange>();- 當我們嘗試add一個Apple的時候,flist可能指向new ArrayList<Orange>();
- 當我們嘗試add一個Orange的時候,flist可能指向new ArrayList<Apple>();
- 當我們嘗試add一個Fruit的時候,這個Fruit可以是任何類型的Fruit,而flist可能只想某種特定類型的Fruit,編譯器無法識別所以會報錯。
所以對于實現(xiàn)了<? extends T>的集合類只能將它視為Producer向外提供(get)元素,而不能作為Consumer來對外獲取(add)元素。
如果我們要add元素應該怎么做呢?可以使用<? super T>:
package com.xgj.master.java.generics;import java.util.ArrayList; import java.util.List;public class GenericWriting {static List<Apple> apples = new ArrayList<Apple>();static List<Fruit> fruit = new ArrayList<Fruit>();static <T> void writeExact(List<T> list, T item) {list.add(item);}static void f1() {writeExact(apples, new Apple());writeExact(fruit, new Apple());}static <T> void writeWithWildcard(List<? super T> list, T item) {list.add(item);}static void f2() {writeWithWildcard(apples, new Apple());writeWithWildcard(fruit, new Apple());}public static void main(String[] args) {f1();f2();} }這樣我們可以往容器里面添加元素了,但是使用super的壞處是以后不能get容器里面的元素了,原因很簡單,我們繼續(xù)從編譯器的角度考慮這個問題,對于List<? super Apple> list,它可以有下面幾種含義:
List<? super Apple> list = new ArrayList<Apple>(); List<? super Apple> list = new ArrayList<Fruit>(); List<? super Apple> list = new ArrayList<Object>();根據(jù)上面的例子,我們可以總結出一條規(guī)律,”Producer Extends, Consumer Super”:
“Producer Extends” – 如果你需要一個只讀List,用它來produce
T,那么使用? extends T。“Consumer Super” – 如果你需要一個只寫List,用它來consume T,那么使用? super T。
如果需要同時讀取以及寫入,那么我們就不能使用通配符了。
如何閱讀過一些Java集合類的源碼,可以發(fā)現(xiàn)通常我們會將兩者結合起來一起用,比如像下面這樣:
public class Collections {public static <T> void copy(List<? super T> dest, List<? extends T> src) {for (int i=0; i<src.size(); i++)dest.set(i, src.get(i));} }類型擦除
Java泛型中最令人苦惱的地方或許就是類型擦除了. 如果在使用泛型時沒有指明數(shù)據(jù)類型,那么就會擦除泛型類型.
因為在使用泛型時沒有指明數(shù)據(jù)類型,為了不出現(xiàn)錯誤,編譯器會將所有數(shù)據(jù)向上轉型為 Object,所以在取出坐標使用時要向下轉型.
比如
public class Demo {public static void main(String[] args){Point p = new Point(); // 類型擦除p.setX(10);p.setY(20.8);int x = (Integer)p.getX(); // 向下轉型double y = (Double)p.getY();System.out.println("This point is:" + x + ", " + y);} } class Point<T1, T2>{T1 x;T2 y;public T1 getX() {return x;}public void setX(T1 x) {this.x = x;}public T2 getY() {return y;}public void setY(T2 y) {this.y = y;} }因為在使用泛型時沒有指明數(shù)據(jù)類型,為了不出現(xiàn)錯誤,編譯器會將所有數(shù)據(jù)向上轉型為 Object,所以在取出坐標使用時要向下轉型,和不使用泛型沒什么兩樣。
另外一個例子
public class Node<T> {private T data;private Node<T> next;public Node(T data, Node<T> next) {this.data = data;this.next = next;}public T getData() { return data; }// ... }編譯器做完相應的類型檢查之后,實際上到了運行期間上面這段代碼實際上將轉換成:
public class Node {private Object data;private Node next;public Node(Object data, Node next) {this.data = data;this.next = next;}public Object getData() { return data; }// ... }這意味著不管我們聲明Node<String>還是Node<Integer>,到了運行期間,JVM統(tǒng)統(tǒng)視為Node<Object>。有沒有什么辦法可以解決這個問題呢?這就需要我們自己重新設置bounds了,將上面的代碼修改成下面這樣:
public class Node<T extends Comparable<T>> {private T data;private Node<T> next;public Node(T data, Node<T> next) {this.data = data;this.next = next;}public T getData() { return data; }// ... }這樣編譯器就會將T出現(xiàn)的地方替換成Comparable而不再是默認的Object了:
public class Node {private Comparable data;private Node next;public Node(Comparable data, Node next) {this.data = data;this.next = next;}public Comparable getData() { return data; }// ... }上面的概念或許還是比較好理解,但其實泛型擦除帶來的問題遠遠不止這些,接下來我們系統(tǒng)地來看一下類型擦除所帶來的一些問題。
在Java中不允許創(chuàng)建泛型數(shù)組
Java泛型很大程度上只能提供靜態(tài)類型檢查,然后類型的信息就會被擦除,所以像下面這樣利用類型參數(shù)創(chuàng)建實例的做法編譯器不會通過
但是如果某些場景我們想要需要利用類型參數(shù)創(chuàng)建實例,我們應該怎么做呢?可以利用反射解決這個問題:
public static <E> void append(List<E> list, Class<E> cls) throws Exception {E elem = cls.newInstance(); // OKlist.add(elem); }實際上對于這個問題,還可以采用Factory和Template兩種設計模式解決.
- 我們無法對泛型代碼直接使用instanceof關鍵字,因為Java編譯器在生成代碼的時候會擦除所有相關泛型的類型信息.
總結
以上是生活随笔為你收集整理的Java-Java5.0泛型解读的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spring-AOP @AspectJ切
- 下一篇: Spring-AOP @AspectJ切