最近項目組在進行泛型代碼編寫時遇到很多困難,討論下來發(fā)現(xiàn)大家對這個概念都是一知片解酣胀,然而在我們的項目開發(fā)過程中刁赦,又會有大量需要用到泛型來簡化代碼、增加復(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的方式使用,因此程序崩潰了烙如。為了解決類似這樣的問題(在編譯階段就可以解決)么抗,泛型應(yīng)運而生。
將之前第一行聲明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為什么要設(shè)計泛型
在Java SE 1.5之前曾雕,沒有泛型的情況的下奴烙,通過對類型Object的引用來實現(xiàn)參數(shù)的“任意化”,“任意化”帶來的缺點是要做顯式的強制類型轉(zhuǎn)換剖张,而這種轉(zhuǎn)換是要求開發(fā)者對實際參數(shù)類型可以預(yù)知的情況下進行的切诀。對于強制類型轉(zhuǎn)換錯誤的情況 ,編譯器可能不提示錯誤修械,在運行的時候才出現(xiàn)異常趾牧,這是一個安全隱患。
泛型的好處:使用泛型肯污,首先可以通過IDE進行代碼類型初步檢查翘单,然后在編譯階段進行編譯類型檢查,以保證類型轉(zhuǎn)換的安全性蹦渣;并且所有的強制轉(zhuǎn)換都是自動和隱式的哄芜,可以提高代碼的重用率。
3.泛型基礎(chǔ)
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類拘央,代碼得不到復(fù)用涂屁。
然而通過泛型類可以很好的解決復(fù)用的問題:
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(類型推導(dǎo)),根據(jù)實參的類型自動推導(dǎo)出相應(yīng)參數(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>{
@Override
public T next() {
return null;
}
}
未傳入泛型實參時,與泛型類的定義相同竖般,在聲明類的時候甚垦,需將泛型的聲明也一起加到類中。即:class FruitGenerator<T> implements捻激。我們可以為T傳入任意一種實參制轰,形成無數(shù)種類型的Generator接口前计。
如果不聲明泛型胞谭,如: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);
showKeyValue(gInteger);
當我們調(diào)用該方法時寇蚊,編譯器會提示我們:
Generic<java.lang.Integer> cannot be applied to Generic<java.lang.Number>
showKeyValue(gInteger);
通過提示信息我們可以看到Generic<Integer>不能被看作為Generic<Number>的子類笔时。由此可以看出:同一種泛型可以對應(yīng)多個版本(因為參數(shù)類型是不確定的),而不同版本的泛型類實例之間是不兼容的仗岸。
由此會產(chǎn)生一個問題允耿,如果我們想對Generic<Integer>類型使用showKeyValue方法,我們就必須重新定義一個新的方法扒怖,這顯然與Java的多態(tài)理念相違背较锡。因此我們需要一個在邏輯上可以表示同時是Generic<Integer>和Generic<Number>父類的引用類型,通配符應(yīng)運而生姚垃。
我們可以將上面的方法改一下:
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元素應(yīng)該怎么做呢讶踪?可以使用<? 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)類型檢查,然后編譯器生成的代碼會擦除相應(yīng)的類型信息谣膳,這樣到了運行期間實際上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; }
// ...
}
編譯器做完相應(yīng)的類型檢查之后花履,實際上到了運行期間上面這段代碼實際上將轉(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>诡壁。有沒有什么辦法可以解決這個問題呢济瓢?這就需要我們自己重新設(shè)置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();
}
實例化的兩種方法:
1. 通過集合來保存泛型對應(yīng)的實例
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);
}
}
2. 通過反射來實例化泛型類型
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)換安全隱患的設(shè)計初衷。當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ù)列表,無法重載缘揪。