Java泛型總結(jié)#
泛型是什么##
從本質(zhì)上講闸度,泛型就是參數(shù)化類型猾骡。泛型十分重要瑞躺,使用該特性可以創(chuàng)建類敷搪、接口以及方法,并且可以使用類型參數(shù)來指定類型幢哨。沒有泛型以前赡勘,對(duì)于不確定的類型常常使用Object類,因?yàn)镺bject類是其他任何類的超類捞镰,可以創(chuàng)建任何類型的對(duì)象狮含。但是缺點(diǎn)也十分明顯,不能以類型安全的形式工作曼振。泛型提供了以前沒有的類型安全性几迄,簡化處理過程,不需要像使用Object類時(shí)需要類型轉(zhuǎn)換冰评,泛型提供自動(dòng)的隱式類型轉(zhuǎn)換映胁。
一個(gè)簡單的泛型實(shí)例##
創(chuàng)建Gen泛型類,類型參數(shù)為T甲雅。T可以是任何引用類型解孙,使用時(shí),可以傳遞任何引用類型抛人。T在此處相當(dāng)于就是一個(gè)占位符弛姜,其可以是任意引用類型。
public class Gen <T> {
T ob;
Gen(T ob){
this.ob = ob;
}
T getOb(){
return ob;
}
//顯示類型參數(shù)的類型
void showType(){
System.out.println("T's Type is: " + ob.getClass().getName());
}
}
使用泛型妖枚,用具體的類型替換占位符T廷臼。
public class GenDemo {
public static void main(String[] args) {
Gen<String> strGen = new Gen<>("string type");
strGen.showType();
Gen<Integer> intGen = new Gen<>(100);
intGen.showType();
}
}
執(zhí)行結(jié)果:
T's Type is: java.lang.String
T's Type is: java.lang.Integer
Process finished with exit code 0
可以看出,T的類型就是在使用泛型時(shí)傳遞的類型參數(shù)的類型绝页。
Gen<String> strGen = new Gen<>("string type");
上面的語句將表示T的類型為String荠商,此語句也可以聲明為下面的形式,但是上面的形式更加簡潔续誉,從jdk 1.7開始可以去掉構(gòu)造器中的類型參數(shù)莱没,泛型提供了類型推斷功能,因此可以省略酷鸦,這也被稱為泛型的菱形語法饰躲。
Gen<String> strGen = new Gen<String>("string type");
泛型只適用引用類型###
當(dāng)聲明泛型時(shí),傳遞的參數(shù)必須是引用類型臼隔,使用簡單類型是非法的嘹裂。比如下面的聲明:
Gen<int> genInt = new Gen<int>(100);//非法的
基于不同類型參數(shù)的泛型類型是不同的###
Gen<String> strGen = new Gen<>("string type");
Gen<Integer> intGen = new Gen<>(100);
上面的兩行代碼聲明的泛型類型是不同的
strGen == intGen
上面的代碼會(huì)出現(xiàn)編譯錯(cuò)誤。
泛型類型提升類型安全性的原理
我們先來看一個(gè)不使用泛型的例子躬翁。
public class Nor {
Object obj;
Nor(Object obj){
this.obj=obj;
}
Object getObj(){
return obj;
}
}
首先編譯器不知道任何關(guān)于構(gòu)造器傳入的類型信息焦蘑,這是一件壞事盯拱。其二盒发,對(duì)于數(shù)據(jù)需要進(jìn)行強(qiáng)制類型轉(zhuǎn)換例嘱。
public class NorDemo {
public static void main(String[] args) {
Nor intNor = new Nor(11);
int a = (Integer) intNor.getObj();
String str = (String) intNor.getObj();
}
}
上面的代碼可以看出,inNor.getObj()返回類型是Integer類型宁舰,但是卻賦值給了String型拼卵,但是編譯時(shí)能通過,因?yàn)榫幾g器不知道Object的類型蛮艰,但是很遺憾腋腮,運(yùn)行時(shí)肯定會(huì)有類型轉(zhuǎn)換的問題。
運(yùn)行上面的代碼拋出ClassCastException異常
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at simpledemo.NorDemo.main(NorDemo.java:11)
但是使用泛型就不會(huì)發(fā)生以上情況壤蚜,泛型顯示的聲明了類型參數(shù)即寡,這讓開發(fā)人員和編譯器都知道參數(shù)的類型。
帶有兩個(gè)類型參數(shù)的泛型類
public class TwoGen <T,V>{
T obj1;
V obj2;
TwoGen(T obj1, V obj2){
this.obj1 = obj1;
this.obj2=obj2;
}
T getObj1(){
return obj1;
}
V getObj2(){
return obj2;
}
}
和一個(gè)類型參數(shù)的泛型類差不多袜刷,菱形框里的參數(shù)可以有多個(gè)聪富,如果類型參數(shù)總是一樣,那就沒有必要聲明多個(gè)著蟹。比如下面的代碼墩蔓,聲明一個(gè)類型參數(shù)就夠了。
TwoGen<String,String> t = new TwoGen<>("aaa", "bbb");
有界類型##
從前面的例子可以看出萧豆,類型參數(shù)可以是任意的奸披,但是往往一件事沒有約束并不是好的。如果現(xiàn)在我們想寫一個(gè)加法器涮雷,讓任意的兩個(gè)數(shù)字相加阵面,這些數(shù)字可以是整型、浮點(diǎn)型的洪鸭,如果使用泛型的方式膜钓,根據(jù)前面的經(jīng)驗(yàn),可以這樣編碼
public class Caculator <T> {
T a;
T b;
T getSum(T a, T b){
return a+b;//錯(cuò)誤
}
}
那么問題來了卿嘲,因?yàn)門可以任意類型的颂斜,而加運(yùn)算只針對(duì)與數(shù)字和字符串連接,編譯器并不知道T的類型拾枣,所以就更不知道這個(gè)+號(hào)是用來干嘛的了沃疮。
為了處理這種情況java提供了類型邊界,我們先來看使用類型邊界的實(shí)現(xiàn):
public class Caculator <T extends Number> {
double sum;
double getSum(T a, T b){
sum = a.doubleValue()+b.doubleValue();
return sum;
}
public static void main(String[] args) {
Caculator<Integer> integerCaculator = new Caculator<>();
double a = integerCaculator.getSum(3,4);
System.out.println(a);
Caculator<Float> floatCaculator = new Caculator<>();
double b = floatCaculator.getSum(9.0f,10.45f);
System.out.println(b);
Caculator<Double> doubleCaculator = new Caculator<>();
double c = doubleCaculator.getSum(1.0,2.4);
System.out.println(c);
}
}
結(jié)果:
7.0
19.449999809265137
3.4
Process finished with exit code 0
分析:
下面的代碼說明類型參數(shù)為Number的子類梅肤,也就是說將T的范圍限制在Number和Number的子類中
Caculator <T extends Number>
所以才可以使用a.doubleValue()方法司蔬,因?yàn)門繼承自Number。還有一個(gè)好處就是可以防止傳入不能處理的類型姨蝴,比如String俊啼,因?yàn)镾tring并不是Number子類,編譯會(huì)報(bào)錯(cuò)左医。
當(dāng)然也可以擴(kuò)展一個(gè)或多個(gè)接口授帕,使用&連接
Class SomeClass <T extends SuperClass & Interface1 &Interface2> { ...}
綜上同木,Number就是類型參數(shù)的上界,T類型只能是Number和Number的子類跛十。
通配符參數(shù)##
上例中彤路,我們使用了getSum()方法來求不同數(shù)字類型的兩個(gè)數(shù)之和,如果要擴(kuò)展Caculator類芥映,讓其比較兩個(gè)數(shù)之和是否相等洲尊。按照上面思路我們可以這樣做:
public class Caculator <T extends Number> {
double sum;
double getSum(T a, T b){
sum = a.doubleValue()+b.doubleValue();
return sum;
}
Boolean isEquals(Caculator<T> caculator){
return this.sum == caculator.sum;
}
public static void main(String[] args) {
Caculator<Integer> integerCaculator = new Caculator<>();
double a = integerCaculator.getSum(3,4);
System.out.println(a);
Caculator<Double> doubleCaculator1 = new Caculator<>();
double d = doubleCaculator.getSum(3.0,4.0);
System.out.println(d);
//非法,因?yàn)門的類型為Integer奈偏,isEquals()方法中傳入的類型為Caculator<String>.
integerCaculator.isEquals(doubleCaculator1);
Caculator<Integer> integerCaculator1 = new Caculator<>();
double e = integerCaculator.getSum(1,6);
System.out.println(e);
//合法坞嘀,因?yàn)閭魅氲念愋蛥?shù)都是Integer
integerCaculator.isEquals(integerCaculator1);
}
}
從上面的代碼可以看出,上面的類值能適用于參數(shù)類型相等的情況惊来,如果我們要比較3+4和2.5+4.5姆吭,此方法就不適用了。為了實(shí)現(xiàn)更好的一般性唁盏,我們可以使用通配符内狸。
先來看一下用通配符怎么實(shí)現(xiàn)
public class Caculator <T extends Number> {
double sum;
double getSum(T a, T b){
sum = a.doubleValue()+b.doubleValue();
return sum;
}
//注意,這里修改了類型參數(shù)厘擂,把T換成了?
Boolean isEquals(Caculator<?> caculator){
return this.sum == caculator.sum;
}
public static void main(String[] args) {
Caculator<Integer> integerCaculator = new Caculator<>();
double a = integerCaculator.getSum(11,3);
System.out.println(a);
Caculator<Double> doubleCaculator = new Caculator<>();
double b = doubleCaculator.getSum(11.0, 3.0);
System.out.println(b);
System.out.println(doubleCaculator.isEquals(integerCaculator));
Caculator<Integer> integerCaculator2=new Caculator<>();
double c = integerCaculator2.getSum(2,12);
System.out.println(c);
System.out.println(integerCaculator.isEquals(integerCaculator2));
}
}
通配符----昆淡?,顧名思義就是可以給泛型類傳遞任何類型的類型參數(shù)刽严,通配符對(duì)參數(shù)類型沒有限制昂灵,限制條件是<T extends SuperClass>決定的。
有界通配符###
下圖定義三個(gè)坐標(biāo)類舞萄,F(xiàn)ourD繼承自TreeD眨补,ThreeD繼承自TwoD。就是說倒脓,四維坐標(biāo)可以有三維坐標(biāo)和二維坐標(biāo)的行為撑螺,三維坐標(biāo)可以有二維坐標(biāo)的行為。
class TwoD {
int x;
int y;
public TwoD(int x, int y) {
this.x = x;
this.y = y;
}
}
class ThreeD extends TwoD {
int z;
public ThreeD(int x, int y, int z) {
super(x, y);
this.z = z;
}
}
class FourD extends ThreeD {
int t;
public FourD(int x, int y, int z, int t) {
super(x, y, z);
this.t = t;
}
}
Coords類把坐標(biāo)類型限制在TwoD以及TwoD的子類崎弃。
BoundeWidecard類定義了三個(gè)打印坐標(biāo)的方法:
- show2D(Coords<? extends TwoD> coords)方法可以打印TwoD甘晤、ThreeD和FourD中任意一個(gè)類的xy坐標(biāo)。
- show3D(Coords<? extends ThreeD> coords)方法可以打印ThreeD和FourD中任意一個(gè)類的xyz坐標(biāo)饲做。
- show4D(Coords<? extends FourD> coords)方法可以打印FourD中的xyzt坐標(biāo)线婚。
class Coords<T extends TwoD> {
T ob;
Coords(T ob) {
this.ob = ob;
}
}
public class BoundeWildcard {
static void show2D(Coords<? extends TwoD> coords) {
System.out.println("2d, x: " + coords.ob.x + " y: " + coords.ob.y);
}
static void show3D(Coords<? extends ThreeD> coords) {
System.out.println("3d, x: " + coords.ob.x + " y: " + coords.ob.y + "z: " + coords.ob.z);
}
static void show4D(Coords<? extends FourD> coords) {
System.out.println("4d, x: " + coords.ob.x + " y: " + coords.ob.y + "z: " + coords.ob.z + "z: " + coords.ob.t);
}
public static void main(String[] args) {
Coords twoD = new Coords(new TwoD(11,22));
Coords threeD = new Coords(new ThreeD(111,222,333));
Coords fourD = new Coords(new FourD(1111,2222,3333,4444));
show2D(twoD);
show3D(threeD);
show4D(fourD);
//show3D(twoD); //運(yùn)行時(shí)錯(cuò)誤
show3D(threeD);
show3D(fourD);
//show4D(twoD); //運(yùn)行時(shí)錯(cuò)誤
//show4D(threeD); //運(yùn)行時(shí)錯(cuò)誤
show4D(fourD);
}
}
結(jié)果
2d, x: 11 y: 22
3d, x: 111 y: 222z: 333
4d, x: 1111 y: 2222z: 3333z: 4444
3d, x: 111 y: 222z: 333
3d, x: 1111 y: 2222z: 3333
4d, x: 1111 y: 2222z: 3333z: 4444
Process finished with exit code 0
泛型方法
下面我們創(chuàng)建一個(gè)泛型方法來判斷一個(gè)對(duì)象是都存在于一個(gè)對(duì)象數(shù)組中。
public class GenMethDemo {
static <T extends Comparable<T>, V extends T> boolean isIn(T x, V[] y){
for (V v : y) {
if ((x.compareTo(v))==0)
return true;
}
return false;
}
public static void main(String[] args) {
int x =2;
Integer [] x_array = {1,2,3,4,5,6}; //不能用簡單類型創(chuàng)建
Integer [] y_array = {1,3,4,5,6};
boolean flag;
flag = isIn(x,x_array);
System.out.println("2 is in x_array: " + flag);
flag = isIn(x,y_array);
System.out.println("2 is in y_array: " +flag);
}
}
說明參數(shù)類型T都要實(shí)現(xiàn)Comparable<T>泛型接口盆均,這樣才能使用compareTo方法來比較兩個(gè)對(duì)象塞弊。V extends T表示V是T或者是T的子類,這樣聲明了兼容的數(shù)據(jù)類型,方便比較游沿。
泛型構(gòu)造函數(shù)##
可以將構(gòu)造函數(shù)泛型化饰抒,即使不是泛型類。
public class GenConstructor {
double val;
<T extends Number> GenConstructor(T val){
this.val = val.doubleValue();
}
void showVal(){
System.out.println("val: "+ val);
}
public static void main(String[] args) {
GenConstructor genConstructor = new GenConstructor(11);
GenConstructor genConstructor1 = new GenConstructor(11.3f);
genConstructor.showVal();
genConstructor1.showVal();
}
}
結(jié)果:
val: 11.0
val: 11.300000190734863
Process finished with exit code 0
泛型接口##
一般來說奏候,泛型接口的聲明和泛型類的聲明是一樣的。值得注意的是唇敞,因?yàn)閷?shí)現(xiàn)類需要實(shí)現(xiàn)泛型接口蔗草,所以實(shí)現(xiàn)類必須是泛型類。下面是一個(gè)泛型接口和實(shí)現(xiàn)的例子:
public interface GenInterface <T extends Comparable<T>> {
}
class Gen<T extends Comparable<T>> implements GenInterface<T>{
}
因?yàn)閷?shí)現(xiàn)類實(shí)現(xiàn)了接口疆柔,實(shí)現(xiàn)類就必須和接口的參數(shù)類型相同(包含接口的參數(shù)類型)咒精,所以在implements子句中就不用再將參數(shù)類型全部寫全,而且這樣是錯(cuò)誤的:
class Gen<T extends Comparable<T>> implements GenInterface<T extends Comparable<T>>{//非法
}
使用泛型接口有兩個(gè)優(yōu)勢:
- 針對(duì)不同類型實(shí)現(xiàn)
- 可以給類型設(shè)置邊界
使用泛型的一些限制
不能實(shí)例化類型參數(shù)###
class Gen<T>{
T ob;
Gen(){
this.ob = new T(); //非法
}
}
很明顯旷档,這里T只是一個(gè)占位符模叙,系統(tǒng)不知道具體的類型。
對(duì)靜態(tài)成員的一些限制###
class Wrong<T>{
static T ob; //非法鞋屈,不能聲明T類型的靜態(tài)變量
//非法范咨,靜態(tài)方法返回值不能是T
static T getOb(){
return ob;
}
}
盡管不能聲明某些帶有類型參數(shù)的靜態(tài)成員,但是可以聲明泛型方法厂庇。下面的泛型方法用來判斷一個(gè)對(duì)象是否存在于另外一個(gè)對(duì)象中渠啊,因?yàn)椴荒芘袛囝愋偷木唧w類型,所以使用泛型方法权旷,可以看出泛型方法的使用范圍更加廣泛替蛉,這就是泛型的“泛”的體現(xiàn)。
public class GenMethDemo {
static <T extends Comparable<T>, V extends T> boolean isIn(T x, V[] y){
for (V v : y) {
if ((x.compareTo(v))==0)
return true;
}
return false;
}
public static void main(String[] args) {
int x =2;
Integer [] x_array = {1,2,3,4,5,6}; //不能用簡單類型創(chuàng)建
Integer [] y_array = {1,3,4,5,6};
boolean flag;
flag = isIn(x,x_array);
System.out.println("2 is in x_array: " + flag);
flag = isIn(x,y_array);
System.out.println("2 is in y_array: " +flag);
}
}
泛型類層次##
和非泛型類一樣拄氯,泛型類可以繼承和被繼承躲查。但是需要注意,和非泛型類不一樣的是:泛型類需要向上傳遞超類需要類型參數(shù)译柏,就想非泛型類中構(gòu)造器向上傳遞一樣镣煮。
泛型超類##
泛型超類示例:
public class Gen <T> {
public static void main(String[] args) {
SubGen<String> subGen = new SubGen<>();
}
}
class SubGen<T> extends Gen<T>{
}
下面的代碼向超類傳遞了參數(shù)類型String,對(duì)于Gen來說鄙麦,他的參數(shù)類型為String怎静。
SubGen<String> subGen = new SubGen<>()
還要注意的是,泛型子類除了將類型參數(shù)傳遞給泛型超類黔衡,再也沒有使用類型參數(shù)T蚓聘。所以,即使泛型超類的子類不必泛型化盟劫,其也必須指定泛型超類需要的類型參數(shù)夜牡。
當(dāng)然,泛型子類也可以添加自身的類型參數(shù)
class SubGen<T,V> extends Gen<T>{
}
泛型子類###
非泛型類可以是泛型類的超類。
public class Gen {
}
class SubGen<T> extends Gen{
}
強(qiáng)制轉(zhuǎn)換
需要注意的是塘装,使用強(qiáng)制轉(zhuǎn)換時(shí)兩個(gè)泛型類的類型必須兼容急迂,并且類型參數(shù)也要相同。
類型擦除##
通常蹦肴,我們不需要知道Java源碼轉(zhuǎn)化為對(duì)象代碼的細(xì)節(jié)僚碎,但是對(duì)于泛型而言大致了解這個(gè)過程是很重要的,這有助于我們理解泛型的工作機(jī)制阴幌。
影響泛型如何添加到Java的一個(gè)最重要的約束:就是要和之前的Java版本兼容勺阐,簡單的說就是要和之前的非泛型代碼兼容,對(duì)Java語法和虛擬機(jī)做的修改不能破壞以前的代碼矛双。為了實(shí)現(xiàn)泛型渊抽,Java使用了類型擦除。
總的來說议忽,運(yùn)行時(shí)沒有參數(shù)類型懒闷,所有的類型參數(shù)都會(huì)轉(zhuǎn)化為具體的類型,如果沒有界定類型栈幸,則用Object表示愤估。
橋接方法###
有時(shí)候,編譯器需要添加一些橋接方法速址,比如泛型子類重寫方法的類型擦除灵疮,不能產(chǎn)生超類中方法的類型擦除。對(duì)于這種情況壳繁,會(huì)生成使用超類類型擦除的方法震捣,并且這個(gè)方法調(diào)用具有由子類指定的類型擦除的方法。當(dāng)然闹炉,橋接方法只會(huì)在字節(jié)碼級(jí)別發(fā)生蒿赢,你不會(huì)看到,也不能使用渣触。
比如在父類中有一個(gè)getOb()方法
public class Gen<T> {
T ob;
Gen(T ob){
this.ob = ob;
}
T getOb(){
return ob;
}
}
但是這個(gè)泛型類的子類重寫了該方法
class Gen2<String> extends Gen<String>{
Gen2(String ob) {
super(ob);
}
String getOb(){
System.out.println("aaaa");
return ob;
}
}
對(duì)于上面的兩段代碼羡棵,子類Gen2擴(kuò)展了Gen,但是使用特定于String的Gen版本嗅钻,同時(shí)Gen2還重寫了getOb()方法皂冰。所有這些都是可以接受的,但是對(duì)于類型擦除卻稍顯麻煩养篓。本來期待的下面的方法:
T getOb(){
return ob;
}
為了處理這個(gè)問題秃流,編譯器生成一個(gè)橋接方法,這個(gè)橋接方法調(diào)用String版本的那個(gè)簽名柳弄。因此如果檢查有javap生成的Gen2類文件舶胀,就會(huì)看到下面的方法:
java.lang.String getOb();
java.lang.Object getOb(); //橋接方法
對(duì)于上面兩個(gè)方法,唯一不同的就是返回類型,通常來講這是錯(cuò)誤的嚣伐,因?yàn)檫@不是源碼引起的糖赔,JVM會(huì)正確的處理它。
對(duì)泛型數(shù)組的一些限制
不能實(shí)例化參數(shù)類型的數(shù)組轩端,這和不能實(shí)例化參數(shù)類型的對(duì)象類似放典。
class Gen<T extends Number> {
T[] a;
// a = {1,2,3};//錯(cuò)誤,不能初始化
Gen(T[] a){
// this.a = new T[10];//錯(cuò)誤,不能實(shí)例化
this.a=a;//可以賦值
}
}
Java 8官方教程說指定類型參數(shù)的泛型數(shù)組不能實(shí)例化基茵。
public class Restriction {
public static void main(String[] args) {
Integer[] integers = {1,2,3,4,5,6};
Gen<Integer> gen[] = new Gen<Integer>[10]; //編譯錯(cuò)誤
Gen<?> gen1[] = new Gen<Integer>[10];//可以通過
}
}
由于泛型內(nèi)容比較多奋构,如有遺漏請(qǐng)?jiān)谟懻搮^(qū)補(bǔ)充,本文會(huì)持續(xù)更新修改耿导,歡迎關(guān)注声怔。