一、泛型的由來
ArrayList collection = new ArrayList();
collection.add(1);
collection.add(1L);
collection.add("abc");
int i = (Integer) collection.get(1);//編譯要強制類型轉換且運行時出錯!
- Jdk 1.5的集合類希望你在定義集合時,明確表示你要向集合中裝哪種類型的資料,無法加入指定類型以外的資料
ArrayList<Integer> collection2 = new ArrayList<Integer>();
collection2.add(1);
/*collection2.add(1L);
collection2.add(“abc”);*///這兩行代碼編譯時間就報告了語法錯誤
int i2 = collection2.get(0);//不需要再進行類型轉換
- 泛型是提供給javac編譯器使用的,可以限定集合中的輸入類型,讓編譯器擋住來源程式中的非法輸入,編譯器編譯帶類型說明的集合時會去除掉“類型”資訊,使程式運行效率不受影響,對於參數化的泛型型別,getClass()方法的傳回值和原始類型完全一樣。由於編譯產生的位元組碼會去掉泛型的類型資訊,只要能跳過編譯器,就可以往某個泛型集合中加入其它類型的資料,例如,用反射得到集合,再調用其add方法即可。
二、關於泛型
- ArrayList<E>類定義和ArrayList<Integer>類引用中涉及如下術語:
整個稱為ArrayList<E>泛型型別
ArrayList<E>中的E稱為類型變數或型別參數
整個ArrayList<Integer>稱為參數化類別型
ArrayList<Integer>中的Integer稱為型別參數的執行個體或實際型別參數
ArrayList<Integer>中的<>念著typeof
ArrayList稱為原始類型
參數化型別可以引用一個原始類型的對象,編譯報警示告,例如, Collection<String> c = new Vector();//OK
原始類型可以引用一個參數化型別的對象,編譯報警示告,例如, Collection c = new Vector<String>();//原來的方法接受一個集合參數,新的類型也要能傳進去 泛型中的型別參數嚴格說明集合中裝載的資料類型是什麼和可以加入什麼類型的資料,記住:Collection<String>和Collection<Object>是兩個沒有轉換關係的參數化類別型。假設Vector<String> v = new Vector<Object>();可以的話,那麼以後從v中取出的對象當作String用,而v實際指向的對象中可以加入任意的類型對象;假設Vector<Object> v = new Vector<String>();可以的話,那麼以後可以向v中加入任意的類型對象,而v實際指向的集合中只能裝String類型的對象。
Vector<String> v = new Vector<Object>(); //錯誤!///不寫<Object>沒錯,寫了就是明知故犯
Vector<Object> v = new Vector<String>(); //也錯誤!
編譯器不允許建立泛型變數的數組。即在建立數組執行個體時,數組的元素不能使用參數化類別型,例如,下面語句有錯誤:
Vector<Integer> vectorList[] = new Vector<Integer>[10];
Vector v1 = new Vector<String>();
Vector<Object> v = v1;//編譯是OK的,運行就發生錯誤!三 、泛型萬用字元問題:
定義一個方法,該方法用於列印出任意參數化型別的集合中的所有資料,該方法如何定義呢?
錯誤方式:
public static void printCollection(Collection<Object> cols) {
for(Object obj:cols) {
System.out.println(obj);
}
/* cols.add("string");//沒錯
cols = new HashSet<Date>();//會報告錯誤!*/
}
正確方式:
public static void printCollection(Collection<?> cols) {
for(Object obj:cols) {
System.out.println(obj);
}
//cols.add("string");//錯誤,因為它不知自己未來匹配就一定是String
cols.size();//沒錯,此方法與型別參數沒有關係
cols = new HashSet<Date>();
}
總結:
使用?萬用字元可以引用其他各種參數化類別型,?萬用字元定義的變數主要用作引用,可以調用與參數化無關的方法,不能調用與參數化有關的方法。
public static void printCollection(Collection<?> cols) { //col.add("123");錯誤,與類型有關的操作不行 System.out.println("size:"+cols.size()); for(Object obj:cols) { System.out.println(obj); } cols=new HashSet<Date>();//it is ok }
四、泛型中的?萬用字元的擴充
正確:Vector<? extends Number> x = new Vector<Integer>();
錯誤:Vector<? extends Number> x = new Vector<String>();
正確:Vector<? super Integer> x = new Vector<Number>();
錯誤:Vector<? super Integer> x = new Vector<Byte>();
提示:
- 限定萬用字元總是包括自己。?只能用作引用,不能用它去給其他變數賦值
Vector<? extends Number> y = new Vector<Integer>();
Vector<Number> x = y;
上面的代碼錯誤,原理與Vector<Object > x11 = new Vector<String>();相似,只能通過強制類型轉換方式來賦值。五 、泛型方法Java的泛型方法沒有C++模板函數功能強大,java中的如下代碼無法通過編譯:
<T> T add(T x,T y) {
return (T) (x+y);
//return null;
}
用於放置泛型的型別參數的角括弧應出現在方法的其他所有修飾符之後和在方法的傳回型別之前,也就是緊鄰傳回值之前。按照慣例,型別參數通常用單個大寫字母表示。
交換數組中的兩個元素的位置的泛型方法文法定義如下:
static <E> void swap(E[] a, int i, int j) {
E t = a[i];
a[i] = a[j];
a[j] = t;
}//或用一個面試題講:把一個數組中的元素的順序顛倒一下
只有參考型別才能作為泛型方法的實際參數,swap(new int[3],3,5);語句會報告編譯錯誤。
除了在應用泛型時可以使用extends限定符,在定義泛型時也可以使用extends限定符,例如,Class.getAnnotation()方法的定義。並且可以用&來指定多個邊界,如<V extends Serializable & cloneable> void method(){}
普通方法、構造方法和靜態方法中都可以使用泛型。
也可以用類型變數表示異常,稱為參數化的異常,可以用於方法的throws列表中,但是不能用於catch子句中。
在泛型中可以同時有多個型別參數,在定義它們的角括弧中用逗號分,例如:
public static <K,V> V getValue(K key) { return map.get(key);}
/*1.Java中的泛型型別(或者泛型)類似於 C++ 中的模板。但是這種相似性僅限於表面,Java 語言中的泛型基本上完全是在編譯器中實現,用於編譯器執行類型檢查和類型推斷,然後產生普通的非泛型的位元組碼,這種實現技術稱為擦除(erasure)(編譯器使用泛型型別資訊保證型別安全,然後在產生位元組碼之前將其清除)。這是因為擴充虛擬機器指令集來支援泛型被認為是無法接受的,這會為 Java 廠商升級其 JVM 造成難以逾越的障礙。所以,java的泛型採用了可以完全在編譯器中實現的擦除方法。 例如,下面這兩個方法,編譯器會報告錯誤,它不認為是兩個不同的參數類型,而認為是同一種參數類型。private static void applyGeneric(Vector<String> v){}private static void applyGeneric(Vector<Date> v){}*/private static <T>void applyGeneric(Vector<T> v){ }
//用下面的代碼說明對異常如何採用泛型:private static <T extends Exception> sayHello() throws T{try{ }catch(Exception e){ throw (T)e;}}
public static <T> T autoConvertType(Object obj)//編寫一個泛型方法,自動將Object類型的對象轉換成其他類型。{return (T)obj;}//測試代碼://Object xxx = "abc";//String yyy = autoConvertType(xxx); //定義一個方法,可以將任意類型的數組中的所有元素填充為相應類型的某個對象。 public static <T> T[] fill(T[] a, T v) { for(int i = 0; i < a.length; i++) a[i] = v; return a; } public static <E> void printCollection(Collection<E> cols) {//採用自定泛型方法的方式列印出任意參數化型別的集合中的所有內容。for(E obj:cols) {System.out.println(obj);}}/*定義一個方法,把任意參數類型的集合中的資料安全地複製到相應類型的數組中?這個需要使用泛型方法進行定義,如果使用如下形式:static void copy(Collection a, Object[] b);否則有可能出現A類型的資料複製進B類型的數組中的情況。使用泛型方法的定義形式為:static <T> void copy(Collection<T> a,T[] b); */六、類型推斷
- 譯器判斷範型方法的實際型別參數的過程稱為類型推斷,類型推斷是相對於知覺推斷的,其實現方法是一種非常複雜的過程。
- 根據調用泛型方法時實際傳遞的參數類型或傳回值的類型來推斷,具體規則如下:
當某個類型變數只在整個參數列表中的所有參數和傳回值中的一處被應用了,那麼根據調用方法時該處的實際應用類型來確定,這很容易憑著感覺推斷出來,即直接根據調用方法時傳遞的參數類型或傳回值來決定泛型參數的類型,例如:
swap(new String[3],3,4) static <E> void swap(E[] a, int i, int j)
當某個類型變數在整個參數列表中的所有參數和傳回值中的多處被應用了,如果調用方法時這多處的實際應用類型都對應同一種類型來確定,這很容易憑著感覺推斷出來,例如:
add(3,5) static <T> T add(T a, T b)
當某個類型變數在整個參數列表中的所有參數和傳回值中的多處被應用了,如果調用方法時這多處的實際應用類型對應到了不同的類型,且沒有使用傳回值,這時候取多個參數中的最大交集類型,例如,下面語句實際對應的類型就是Number了,編譯沒問題,只是運行時出問題:
fill(new Integer[3],3.5f) static <T> void fill(T[] a, T v)
當某個類型變數在整個參數列表中的所有參數和傳回值中的多處被應用了,如果調用方法時這多處的實際應用類型對應到了不同的類型, 並且使用傳回值,這時候優先考慮傳回值的類型,例如,下面語句實際對應的類型就是Integer了,編譯將報告錯誤,將變數x的類型改為float,對比eclipse報告的錯誤提示,接著再將變數x類型改為Number,則沒有了錯誤:
int x =(3,3.5f) static <T> T add(T a, T b)
- 參數類型的類型推斷具有傳遞性,下面第一種情況推斷實際參數類型為Object,編譯沒有問題,而第二種情況則根據參數化的Vector類執行個體將類型變數直接確定為String類型,編譯將出現問題:
copy(new Integer[5],new String[5]) static <T> void copy(T[] a,T[] b);
copy(new Vector<String>(), new Integer[5]) static <T> void copy(Collection<T> a , T[] b);七、泛型類如果類的執行個體對象中的多處都要用到同一個泛型參數,即這些地方引用的泛型型別要保持同一個實際類型時,這時候就要採用泛型型別的方式進行定義,也就是類層級的泛型,文法格式如下:
public class GenericDao<T> {
private T field1;
public void save(T obj){}
public T getById(int id){}
}
類層級的泛型是根據引用該類名時指定的類型資訊來參數化型別變數的,例如,如下兩種方式都可以:
GenericDao<String> dao = null;
new genericDao<String>();
注意:
在對泛型型別進行參數化時,型別參數的執行個體必須是參考型別,不能是基本類型。
當一個變數被聲明為泛型時,只能被執行個體變數、方法和內部類調用,而不能被靜態變數和靜態方法調用。因為靜態成員是被所有參數化類別所共用的,所以靜態成員不應該有類層級的型別參數。
問題:類中只有一個方法需要使用泛型,是使用類層級的泛型,還是使用方法層級的泛型?(方法層級的)
//先不用泛型的方式定義一個PersonDao和一個Person實體物件,寫完後,話鋒一轉,告訴大家一個項目中有很多實體類型和要編寫很多Dao,有什麼好辦法嗎?//泛型類:class GenericDAO<T> {public void save(T t) {}public void delete(int id) {}public void update(T t) {}public T findById(int id){return null;}public Set<T> findByConditions(String conditions) {return null;}}八、通過反射獲得泛型的參數化型別
//範例程式碼:Class GenericalReflection { private Vector<Date> dates = new Vector<Date>(); public void setDates(Vector<Date> dates) { this.dates = dates; } public static void main(String[] args) { Method methodApply = GenericalReflection.class.getDeclaredMethod("applyGeneric", Vector.class); ParameterizedType pType = (ParameterizedType) (methodApply .getGenericParameterTypes())[0]; System.out.println("setDates(" + ((Class) pType.getRawType()).getName() + "<"//獲得原始參數類型 + ((Class) (pType.getActualTypeArguments()[0])).getName()//獲得實際參數類型 Date + ">)" ); }}//泛型DAO的應用:public abstract class DaoBaseImpl<T> implements DaoBase<T> {protected Class<T> clazz;public DaoBaseImpl() {Type type = this.getClass().getGenericSuperclass();ParameterizedType pt = (ParameterizedType) type;this.clazz = (Class) pt.getActualTypeArguments()[0];System.out.println("clazz = " + this.clazz);}}public class ArticleDaoImpl extends DaoBaseImpl<Article> implements ArticleDao {}
其他代碼:
import java.util.*;public class GenericTest{public static void main(String[] args)throws Exception{ //泛型參數類型的檢查是給編譯器用的 ArrayList list1=new ArrayList(); list1.add(1); ArrayList<Integer> list2=new ArrayList<Integer>(); list2.add(3); //System.out.println(list2.get(0).getClass());//class java.lang.Integer System.out.println(list1.getClass()==list2.getClass());//輸出true,list1和list2用的是同一份位元組碼 //利用反射,可以繞過編譯器向集合中添加非Integer類型資料 list2.getClass().getMethod("add", Object.class).invoke(list2, "java"); System.out.println(list2.get(1)); System.out.println("----------------------------------"); printCollection(list2); //只需要記住:左邊參數類型接收的範圍要大於等於右邊的 Vector<? extends Number> y = new Vector<Integer>(); //Vector<Number> x = y; error Vector<Number> x=new Vector<Number>(); y=x; System.out.println(new ArrayList<Integer>().getClass().getName()); System.out.println(new ArrayList<String>().getClass().getName()); //遍曆Map集合 Map<String, Integer> map=new HashMap<String, Integer>(); map.put("張三", 23); map.put("李四", 24); map.put("王五", 33); Set<Map.Entry<String, Integer>> entrySet=map.entrySet(); //用foreach遍曆 for(Map.Entry<String, Integer> entry:entrySet) { System.out.println(entry.getKey()+"-->"+entry.getValue()); } //另一個方式遍曆Iterator Iterator<Map.Entry<String , Integer>> it=entrySet.iterator(); while(it.hasNext()) { Map.Entry<String, Integer> entry=it.next(); System.out.println(entry.getKey()+""+entry.getValue()); } Number x1=add(4, 6.3);//4 會自動裝箱 Object x2=add(3,"aa"); swap(new String[]{"a","b","c"}, 1, 2); //swap(new int[]{1,3,5,9}, 1, 3); error! int是基本類型不能用在泛型的實際參數中 System.out.println(convert("fjdks")); System.out.println(convert(123)); }//定義一個方法,可以將任意類型的數組中的所有元素填充為相應類型的某個對象。public static <T> T[] fill(T[] a,T obj){for (int i = 0; i < a.length; i++){a[i]=obj;}return a;}//編寫一個泛型方法,自動將Object類型的對象轉換成其他類型。 public static <T> T convert(Object obj) { return (T)obj; }//對任意類型的數字位置交換public static <T> void swap(T[] a,int x,int y){T temp=a[x];a[x]=a[y];a[y]=temp;} public static <T>T add(T x,T y){return null;} public static void printCollection(Collection<?> cols) { //col.add("123");錯誤,與類型有關的操作不行 System.out.println("size:"+cols.size()); for(Object obj:cols) { System.out.println(obj); } cols=new HashSet<Date>();//it is ok } private static <T>void applyGeneric(Vector<T> v){ } }
我是怎麼知道什麼情況下可以用泛型的啊?這看類的定義,只有類被定義成了泛型,才可以對其進行參數化應用。
用下面的代碼查看getClass()方法返回的結果已經去掉了“類型”資訊。
System.out.println(new ArrayList<Integer>().getClass().getName());
System.out.println(new ArrayList<String>().getClass().getName());
下面程式的main方法中的第二行代碼和注釋中的兩行代碼錶達的意思完全相同,注釋中的兩行代碼不能通過編譯,而第二行(採用方法調用鏈)卻可以順利通過編譯。
public class Test
{
public void func()
{
System.out.println("func");
}
public static void main(String args[]) throws Exception
{
Object obj = new Test();
//下面這行可以成功編譯
((Test)obj).getClass().newInstance().func();
//下面這兩行無法通過編譯
/*Class c = ((Test)obj).getClass();
c.newInstance().func(); */
}
}
因為Generic, 編譯器可以在編譯期獲得類型資訊所以可以編譯這類代碼。你將下面那兩行改成
Class<? extends Test> c = ((Test)obj).getClass();
c.newInstance().func();
應該就能通過編譯了。
在JDK 1.5中引入範型後,Object.getClass()方法的定義如下:
public final Class<? extends Object> getClass()
這說明((Test)obj).getClass()語句返回的物件類型為Class<? extends Test>,而Class<T>的newInstance()方法的定義如下:
public T newInstance() throws InstantiationException,IllegalAccessException
即對於編譯器看來,Class<Test>的newInstance()方法的物件類型為Test,而((Test)obj).getClass()返回的為物件類型為Class<? extends Test>,所以,編譯器認為((Test)obj).getClass().newInstance()返回的物件類型為Test。
下面這兩行代碼之所以無法通過編譯
Class c = ((Test)obj).getClass();
c.newInstance().func();
是因為((Test)obj).getClass()返回的為物件類型為Class<? extends Test>,但是我們在第一行將結果強制轉換成了Class,然後再去調用Class的newInstance方法,而不是去調用Class<Test>的newInstance方法,編譯器當然不再認為Class的newInstance方法返回的對象為Test了。