[參考資料]http://www.reibang.com/p/31b44188b973
??最近項目組在進行泛型代碼編寫時遇到很多困難幸缕,討論下來發(fā)現(xiàn)大家對這個概念都是一知片解,然而在我們的項目開發(fā)過程中,又會有大量需要用到泛型來簡化代碼、增加復用性的場景奖年。因此闲勺,決定用一次share來增強大家對Java泛型的理解旬蟋,提升項目組的代碼質(zhì)量。
1.什么是泛型
??Java在1.5之后加入了泛型的概念敬肚。泛型毕荐,即“參數(shù)化類型”。泛型的本質(zhì)是為了參數(shù)化類型(在不創(chuàng)建新的類型的情況下艳馒,通過泛型指定的不同類型來控制形參具體限制的類型)憎亚。也就是說在泛型使用過程中,操作的數(shù)據(jù)類型被指定為一個參數(shù)弄慰,這種參數(shù)類型可以用在類第美、接口和方法中,分別被稱為泛型類陆爽、泛型接口什往、泛型方法。
舉個例子
List arrayList = new ArrayList();
arrayList.add("aaaa");
arrayList.add(100);
for(int i = 0; i< arrayList.size(); i++){
String item = (String)arrayList.get(i);
System.out.println("泛型測試慌闭,item = " + item);
}
毫無疑問别威,程序運行會以崩潰結(jié)束:
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at Test.main(GenericTest.java:17)
??ArrayList可以存放任意類型,例子中添加了一個String類型驴剔,添加了一個Integer類型省古,再使用時都以String的方式使用,因此程序崩潰了丧失。為了解決類似這樣的問題(在編譯階段就可以解決)豺妓,泛型應運而生。
??將之前第一行聲明list的代碼修改一下,編譯器就會在編譯階段幫我們提前發(fā)現(xiàn)類似的問題琳拭。
List<String> arrayList = new ArrayList<String>();
...
//arrayList.add(100); 在編譯階段载佳,編譯器就會報錯
??如上面所說泛型只在代碼編譯階段有效,來看下面的代碼:
List<String> stringArrayList = new ArrayList<String>();
List<Integer> integerArrayList = new ArrayList<Integer>();
Class classStringArrayList = stringArrayList.getClass();
Class classIntegerArrayList = integerArrayList.getClass();
if(classStringArrayList.equals(classIntegerArrayList)){
System.out.println("泛型測試,類型相同");
}
??我們發(fā)現(xiàn)對于編譯器來說臀栈,stringArrayList和integerArrayList其實是同一類型的對象蔫慧。這是因為代碼在編譯之后采取了類似于去泛型化的措施,也就是泛型的類型擦除权薯,這個概念后面會介紹姑躲。
2.Java為什么要設計泛型
??在Java SE 1.5之前,沒有泛型的情況的下盟蚣,通過對類型Object的引用來實現(xiàn)參數(shù)的“任意化”黍析,“任意化”帶來的缺點是要做顯式的強制類型轉(zhuǎn)換,而這種轉(zhuǎn)換是要求開發(fā)者對實際參數(shù)類型可以預知的情況下進行的屎开。對于強制類型轉(zhuǎn)換錯誤的情況 阐枣,編譯器可能不提示錯誤,在運行的時候才出現(xiàn)異常奄抽,這是一個安全隱患蔼两。
??泛型的好處:使用泛型,首先可以通過IDE進行代碼類型初步檢查逞度,然后在編譯階段進行編譯類型檢查额划,以保證類型轉(zhuǎn)換的安全性;并且所有的強制轉(zhuǎn)換都是自動和隱式的档泽,可以提高代碼的重用率俊戳。
3.泛型基礎
Java泛型有三種使用方式:泛型類、泛型方法馆匿、泛型接口抑胎。
泛型類
泛型類的語法如下:
class 類名稱 <泛型類型標識>{}
我們首先定義一個簡單的類:
public class Generic{
private String object;
public void set(String object) {
this.object = object;
}
public String get() {
return object;
}
}
??這是一個常見的Java bean,這樣做的一個壞處是Box里面現(xiàn)在只能裝入String類型的元素渐北,今后如果我們需要裝入Integer等其他類型的元素阿逃,還必須要另外重寫一個類型是Integer的Box類,代碼得不到復用腔稀。
然而通過泛型類可以很好的解決復用的問題:
public class Generic<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
}
這樣的Box類就可以裝入任何我們想要的類型:
Generic<Integer> integerGeneric = new Generic<Integer>();
Generic<Double> doubleGeneric = new Generic<Double>();
Generic<String> stringGeneric = new Generic<String>();
泛型方法
泛型方法的語法如下:
[作用域修飾符] <泛型類型標識> [返回類型] 方法名稱(參數(shù)列表){}
按照這個語法聲明一個泛型方法很簡單盆昙,只要在返回類型前面加上一個類似<K, V,...>的形式就行了:
public class Util {
public static <K, V> boolean compare(Generic<K, V> g1, Generic<K, V> g2) {
return g1.getKey().equals(g2.getKey()) &&
g1.getValue().equals(g2.getValue());
}
}
public class Generic<K, V> {
private K key;
private V value;
public Generic(K key, V value) {
this.key = key;
this.value = value;
}
public void setKey(K key) { this.key = key; }
public void setValue(V value) { this.value = value; }
public K getKey() { return key; }
public V getValue() { return value; }
}
Util.compare()就是一個泛型方法羽历,于是我們可以像下面這樣調(diào)用泛型:
Generic<Integer, String> g1 = new Generic<>(1, "apple");
Generic<Integer, String> g2 = new Generic<>(2, "pear");
boolean same = Util.<Integer, String>compare(g1, g2);
??在Java1.7之后焊虏,編譯器可以通過type inference(類型推導),根據(jù)實參的類型自動推導出相應參數(shù)的類型秕磷,可以縮寫成這樣:
Generic<Integer, String> p1 = new Generic<>(1, "apple");
Generic<Integer, String> p2 = new Generic<>(2, "pear");
boolean same = Util.compare(p1, p2);
泛型接口
??泛型接口的定義與泛型類的定義很相似诵闭。
public interface Generator<T> {
public T next();
}
??當實現(xiàn)泛型接口的類,未給泛型傳入實參時:
class FruitGenerator<T> implements Generator<T>{
public T next() {
return null;
}
}
??未傳入泛型實參時,與泛型類的定義相同疏尿,在聲明類的時候瘟芝,需將泛型的聲明也一起加到類中。即:class FruitGenerator<T> implements Generator<T>褥琐。我們可以為T傳入任意一種實參锌俱,形成無數(shù)種類型的Generator接口。
Generator<T>如果不聲明泛型敌呈,如:class FruitGenerator implements Generator<T>贸宏,編譯器會報錯:"Unknown class"。
當實現(xiàn)泛型接口的類磕洪,給泛型傳入了實參時:
public class FruitGenerator implements Generator<String> {
private String[] fruits = new String[]{"Apple", "Banana", "Pear"};
@Override
public String next() {
Random rand = new Random();
return fruits[rand.nextInt(3)];
}
}
??如果類已經(jīng)將泛型類型傳入實參類型吭练,則所有使用泛型的地方都要替換成傳入的實參類型。即:Generator<T>析显,public T next();中的的T都要替換成傳入的String類型鲫咽。
4.通配符
??介紹通配符之前,我們先思考一個場景谷异。
??我們知道Ingeter是Number的一個子類分尸,由于泛型擦除的存在,對于編譯器來說Generic<Ingeter>與Generic<Number>實際上是同一種基本類型歹嘹。那么問題來了寓落,在使用Generic<Number>作為形參的方法中,能否使用Generic<Ingeter>的實例傳入呢荞下?在邏輯上類似于Generic<Number>和Generic<Ingeter>是否可以看成是父子關(guān)系呢伶选?
為了弄清楚這個問題,我們定義一個方法:
public void showKeyValue(Generic<Number> obj){
System.out.println("泛型測試,value is " + obj.get());
}
如果我們像下面這樣使用該方法:
Generic<Number> gNumber = new Generic<Number>(456);
Generic<Integer> gInteger = new Generic<Integer>(123);
gNumber.showKeyValue(gInteger);
當我們調(diào)用該方法時尖昏,編譯器會提示我們:
Generic<java.lang.Integer> cannot be applied to Generic<java.lang.Number>
showKeyValue(gInteger);
??通過提示信息我們可以看到Generic<Integer>不能被看作為Generic<Number>的子類仰税。由此可以看出:同一種泛型可以對應多個版本(因為參數(shù)類型是不確定的),而不同版本的泛型類實例之間是不兼容的抽诉。
??由此會產(chǎn)生一個問題陨簇,如果我們想對Generic<Integer>類型使用showKeyValue方法,我們就必須重新定義一個新的方法迹淌,這顯然與Java的多態(tài)理念相違背河绽。因此我們需要一個在邏輯上可以表示同時是Generic<Integer>和Generic<Number>父類的引用類型,通配符應運而生唉窃。
我們可以將上面的方法改一下:
public void showKeyValue(Generic<?> obj){
System.out.println("泛型測試,value is " + obj.get());
}
此時耙饰,showKeyValue方法可以傳入任意類型的Generic參數(shù),這是一個無界的通配符纹份。
泛型上下邊界
在Java泛型定義時:
??用<T>等大寫字母標識泛型類型苟跪,用于表示未知類型廷痘。
??用<T extends ClassA & InterfaceB …>等標識有界泛型,用于表示有邊界的類型件已。
??在Java泛型實例化時:
??用<?>標識通配符笋额,用于表示實例化時的類型。
??用<? extends 父類型>標識上邊界通配符篷扩,用于表示實例化時可以確定父類型的類型兄猩。
??用<? super 子類型>標識下邊界通配符,用于表示實例化時可以確定子類型的類型鉴未。
對上面的Generic類增加一個新方法:
public void showKeyValue1(Generic<? extends Number> obj){
System.out.println("泛型測試,value is " + obj.get());
}
Generic<String> generic1 = new Generic<String>("11111");
Generic<Integer> generic2 = new Generic<Integer>(2222);
Generic<Float> generic3 = new Generic<Float>(2.4f);
Generic<Double> generic4 = new Generic<Double>(2.56);
//這一行代碼編譯器會提示錯誤厦滤,因為String類型并不是Number類型的子類
showKeyValue1(generic1);
showKeyValue1(generic2);
showKeyValue1(generic3);
showKeyValue1(generic4);
如果把泛型類的定義也改一下:
public class Generic<T extends Number>{
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey(){
return key;
}
}
//這一行代碼也會報錯,因為String不是Number的子類
Generic<String> generic1 = new Generic<String>("11111");
泛型的上下邊界添加歼狼,必須與泛型的聲明在一起 掏导。
PECS原則
首先我們先定義幾個簡單的類,下面我們將用到它:
class Fruit {}
class Apple extends Fruit {}
class Orange extends Fruit {}
然后定義一個主類:
public class Generics {
public static void main(String[] args) {
// 通過通配符申明一個List
List<? extends Fruit> flist = new ArrayList<Apple>();
// Compile Error: can't add any type of object:
// flist.add(new Apple())
// flist.add(new Orange())
// flist.add(new Fruit())
// flist.add(new Object())
flist.add(null); // Legal but uninteresting
// We Know that it returns at least Fruit:
Fruit f = flist.get(0);
}
}
對于上面的flist羽峰,Java編譯器不允許我們add任何對象趟咆,為什么呢?對于這個問題我們不妨從編譯器的角度去考慮梅屉。因為List<? extends Fruit> flist它自身可以有多種含義:
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>:
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元素了舱沧,從編譯器的角度考慮這個問題,對于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>();
當我們嘗試通過list來get一個Apple的時候,可能會get得到一個Fruit拐揭,這個Fruit可以是Orange等其他類型的Fruit撤蟆。
所以對于實現(xiàn)了<? super T>的集合類只能將它視為Consumer消費(add)元素,而不能作為Producer來對外獲取(get)元素堂污。
在Java的集合類中家肯,我們可以發(fā)現(xiàn)通常會將兩者結(jié)合起來一起用,比如像下面這樣:
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));
}
}
5.類型擦除
類型擦除就是說Java泛型只能用于在編譯期間的靜態(tài)類型檢查敷鸦,然后編譯器生成的代碼會擦除相應的類型信息息楔,這樣到了運行期間實際上JVM根本就知道泛型所代表的具體類型。這樣做的目的是因為Java泛型是1.5之后才被引入的扒披,為了保持向下的兼容性值依,所以只能做類型擦除來兼容以前的非泛型代碼。
泛型擦除到底是什么碟案,來看一個簡單的例子:
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; }
// ...
}
編譯器做完相應的類型檢查之后愿险,實際上到了運行期間上面這段代碼實際上將轉(zhuǎn)換成:
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; }
// ...
}
6.泛型使用的幾個限制
Java泛型由于類型擦除的存在扮叨,會存在一些使用限制:
1.Java泛型不能使用基本類型
使用基本類型的泛型會編譯報錯,代碼如下:
List<int> list = new List<int>();// 編譯前類型檢查報錯
2.Java泛型不允許進行直接實例化
錯誤代碼如下:
<T> void test(T t){
t = new T();//編譯前類型檢查報錯
}
通過類型擦除领迈,上面的泛型方法會轉(zhuǎn)換為如下的原始方法:
void test(Object t){
t = new Object();
}
實例化的兩種方法:
- 通過集合來保存泛型對應的實例
public class DbHelper {
private static final DbHelper instance;
static {
instance = new DbHelper();
}
private DbHelper() {
}
private Map<Class<?>, ChangedListener> changedListeners = new HashMap<>();
public <Model extends BaseModel> ChangedListener getListener(Class<Model> modelClass) {
if (changedListeners.containsKey(modelClass)) {
return changedListeners.get(modelClass);
}
return null;
}
public <Model extends BaseModel> void addChangedListener(final Class<Model> tClass,
ChangedListener<Model> listener) {
ChangedListener changedListener = getListener(tClass);
// 添加到中的Map
changedListeners.put(tClass, changedListener);
}
public interface ChangedListener<Data extends BaseModel> {
void onDataSave(Data... list);
void onDataDelete(Data... list);
}
}
- 通過反射來實例化泛型類型
public class GenericInstance {
public static <T> T createModelInstance(Class<T> tClass) {
try {
// 獲取直接父類的類型Type
Type superClass = tClass.getGenericSuperclass();
// 調(diào)用getActualTypeArguments()方法獲得實際綁定的類型
Type type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
Class<?> clazz = getRawType(type);
return (T) clazz.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
// type不能直接實例化對象彻磁,通過type獲取class的類型,然后實例化對象
private static Class<?> getRawType(Type type) {
if (type instanceof Class) {
return (Class) type;
} else if (type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) type;
Type rawType = parameterizedType.getRawType();
return (Class) rawType;
} else if (type instanceof GenericArrayType) {
Type componentType = ((GenericArrayType) type).getGenericComponentType();
return Array.newInstance(getRawType(componentType), 0).getClass();
} else if (type instanceof TypeVariable) {
return Object.class;
} else if (type instanceof WildcardType) {
return getRawType(((WildcardType) type).getUpperBounds()[0]);
} else {
String className = type == null ? "null" : type.getClass().getName();
throw new IllegalArgumentException("Expected a Class, ParameterizedType, or GenericArrayType, but <"
+ type + "> is of type " + className);
}
}
}
為了防止此類類型轉(zhuǎn)換錯誤的發(fā)生狸捅,Java禁止進行泛型實例化衷蜓。
3.Java泛型不允許進行靜態(tài)化
參考下面的代碼:
class StaticGeneric<T>{
private static T t;// 編譯前類型檢查報錯
public static T getT() {// 編譯前類型檢查報錯
return t;
}
}
靜態(tài)變量在類中共享,而泛型類型是不確定的尘喝,因此編譯器無法確定要使用的類型磁浇,所以不允許進行靜態(tài)化。
4.Java泛型不允許直接進行類型轉(zhuǎn)換(通配符可以)
List<Integer> integerList = new ArrayList<Integer>();
List<Double> doubleList = new ArrayList<Double>();
//不能直接進行類型轉(zhuǎn)換朽褪,類型檢查報錯
integerList = doubleList;
雖然在編譯期間integerList與doubleList都會經(jīng)過類型擦除置吓,但是這種類型轉(zhuǎn)換違反了Java泛型降低類型轉(zhuǎn)換安全隱患的設計初衷。當integerList中存儲Interge元素缔赠,而doubleList中存儲Double元素時交洗,如果不限制類型轉(zhuǎn)換,很容易產(chǎn)生ClassCastException異常橡淑。
但是通配符有可以實現(xiàn):
<!--List<Integer> integerList = new ArrayList<Integer>();-->
<!--List<Double> doubleList = new ArrayList<Double>();-->
<!--//通過通配符進行類型轉(zhuǎn)換-->
<!--doubleList = integerList;-->
static void cast(List<?> orgin, List<?> dest){
dest = orgin;
}
5.Java泛型不允許直接使用instanceof運算符進行運行時類型檢查(通配符可以)
直接使用instanceof運算符進行運行時類型檢查:
List<String> stringList = new ArrayList<String >();
//不能直接使用instanceof构拳,類型檢查報錯
System.out.println(stringList instanceof ArrayList<Double>);
因為Java編譯器在生成代碼的時候會擦除所有相關(guān)泛型的類型信息,正如我們上面驗證過的JVM在運行時期無法識別出ArrayList<Integer>和ArrayList<String>的之間的區(qū)別梁棠。
而我們可以通過通配符的方式進行instanceof運行期檢查:
// 這個時候的類型檢查沒有意義
System.out.println(stringList instanceof ArrayList<?>);
6.Java泛型不允許創(chuàng)建確切類型的泛型數(shù)組(通配符可以)
創(chuàng)建整型泛型數(shù)組如下:
//類型檢查錯誤
List<Integer>[] list = new ArrayList<Integer>[2];
可以通過通配符創(chuàng)建:
Generic<?>[] generics = new Generic<?>[2];
generics[0] = new Generic<Integer>(123);
generics[1] = new Generic<String>("hello");
for (Generic<?> generic : generics) {
System.out.println(generic.get());
}
結(jié)果會正常打印出123和"hello"置森。
7.Java泛型不允許作為參數(shù)進行重載
例如:
public class GenericTest<T>{
void test(List<Integer> list){}
//不允許作為參數(shù)列表進行重載
void test(List<Double> list){}
}
原因是:類型擦除后兩個方法是一樣的參數(shù)列表,無法重載符糊。