Java?SE之了解泛型
如何創(chuàng)建可以存放各種類型的數(shù)組?
根據(jù)JavaSE的語法知識儲備,如果現(xiàn)在讓你們創(chuàng)建如標(biāo)題一樣的數(shù)組,你會怎么創(chuàng)建呢?
答案是:使用 Object 類來定義數(shù)組,因為 Object 是所有類的父類, 可以接收任意子類對象,也即實現(xiàn)了向上轉(zhuǎn)型,于是我們就寫出了這樣的代碼:
private Object[] array = new Object[3];
那么這種方法可取嗎?
顯然是可取的,但只是使用起來會很不方便,具體不方便在哪,我們接著往后看,在這里我們要寫一個類,里面提供了獲取array指定下標(biāo)的數(shù)據(jù),和設(shè)置array指定下標(biāo)的數(shù)據(jù),于是寫出了這樣的代碼:
public class DrawForth { private Object[] array = new Object[3]; public void setPosArray(int pos, Object o) { this.array[pos] = o; } public Object getPosValue(int pos) { return this.array[pos]; } }
代碼到這里仍然是正確的,那我們就要去使用這個類,也就是在main方法中用這個類實例對象,去操作里面的數(shù)組,所以main方法的代碼就是這個樣子:
public static void main(String[] args) { DrawForth draw = new DrawForth(); draw.setPosArray(0, 123); draw.setPosArray(1, "hello"); draw.setPosArray(2, 12.5); int a = (int)draw.getPosValue(0); String str = (String)draw.getPosValue(1); double d = (double)draw.getPosValue(1); }
看到這里,你是不是就發(fā)現(xiàn)這樣做很不方便呢?
當(dāng)我們往數(shù)組里面設(shè)置數(shù)據(jù)的時候開心了,想設(shè)置成什么類型就是什么類型,但是!當(dāng)我們要獲取對應(yīng)位置的元素就麻煩了,我們必須知道他是什么類型,然后進(jìn)行強制類型轉(zhuǎn)換才能接收,(返回是Object類型所以需要強轉(zhuǎn)),難道往后每次取數(shù)據(jù)的時候我還得看一看是什么類型嗎?
泛型的概念
淺聊泛型
泛型是在JDK1.5引入的新的語法,通過上面的例子,由此我們就引出了泛型,泛型簡單來說就是把類型當(dāng)成參數(shù)傳遞,指定當(dāng)前容器,你想持有什么類型的對象,你就傳什么類型過去,讓編譯器去做類型檢查!
從而實現(xiàn)類型參數(shù)化(不能是基本數(shù)據(jù)類型)
泛型的簡單語法
class Test1<類型形參列表> { } class Test2<類型形參1, 類型形參2, ...> { }
類型形參列表的命名規(guī)范
類名后面的 <類型形參列表> 這是一個占位符,表示當(dāng)前類是一個泛型類,形參列表里面如何寫?
通常用一個大寫字母表示,當(dāng)然,你也可以怎么開心怎么來,但是小心辦公室談話警告哈(dog),這里有幾個常用的名稱:
- E:表示 Element
- K:表示 Key
- V:表示 Value
- N:表述 Number
- T:表示 Type
- S,U,V表示,第二,第三,第四個類型
使用泛型知識創(chuàng)建數(shù)組
這里就來修改一下剛開始的代碼,使用到泛型的知識,那么我們就可以這樣修改:
public class DrawForth<T> { //private T[] array = new T[3]; error private T[] array = (T[])new Object[3]; public void setPosArray(int pos, T o) { this.array[pos] = o; } public T getPosValue(int pos) { return this.array[pos]; } public static void main(String[] args) { DrawForth<Integer> draw = new DrawForth<>(); draw.setPosArray(0, 123); //draw.setPosArray(1, "hello"); error //draw.setPosArray(2, 12.5); error draw.setPosArray(1, 1234); draw.setPosArray(2, 12345); int a = draw.getPosValue(0); int b = draw.getPosValue(1); int c = draw.getPosValue(2); } }
如上修改之后的代碼,我們可以得到以下知識點:
- <T> 是一個占位符,僅表示這個類是泛型類
- 不能 new 泛型數(shù)組,此代碼的寫法也不是最好的方法!
- 實例化泛型類的語法是:類名<類型實參>變量名 = new 泛型類<類型實參>(構(gòu)造方法實參);
- 注意:new 泛型類<>尖括號中可以省略類型實參,編譯器可以根據(jù)上下文推導(dǎo)!
- 編譯時自動進(jìn)行類型檢查和轉(zhuǎn)換。
什么是裸類型
裸類型就是指在實例化泛型類對象的時候,沒有傳類型實參,比如下面的代碼就是一個裸類型:
DrawForth draw = new DrawForth();
我現(xiàn)在可以告訴你,這樣做編譯完全正常,但我們不要去使用裸類型,因為這是為了兼容老版本的 API 保留的機制,畢竟泛型是 Java1.5 新增的語法。
泛型是如何編譯的?
泛型的擦除機制
如果我們要看泛型是如何編譯的,可以通過命令 javap -c 字節(jié)碼文件 來進(jìn)行查看:
如上代碼是 2.4 段落中的代碼,奇怪,明明傳的實參是 Integer 類型,最后所有的 T 卻變成了 Object 類型,這就是擦除機制
所以在Java中,泛型機制是在編譯級別實現(xiàn)的,運行期間不會包含任何泛型信息。
提示:類型擦除,不一定是把 T 變成 Object(泛型的上界會提到)
再談為什么不能實例化泛型數(shù)組?
知道了擦除機制后,那么 T[] array = new T[3]; 是不對的,編譯的時候,替換為Object,不是相當(dāng)于:Object[] array = new Object[3]嗎?
在Java中,數(shù)組是一個很特殊的類型,數(shù)組是在運行時存儲和檢查類型信息, 泛型則是在編譯時檢查類型錯誤。
而且Java設(shè)定擦除機制就只針對變量的類型和返回值的類型,所以在編譯時候壓根不會擦除 new T[3]; 這個 T ,所以自然編譯就會報錯!
我們前面通過強制類型轉(zhuǎn)換的方式創(chuàng)建了泛型數(shù)組,說過那樣寫并不好,正確的方式是通過反射創(chuàng)建指定類型的數(shù)組,由于現(xiàn)在沒學(xué)習(xí)到反射,這里先放著就行。
什么是泛型的上界?
有了擦除機制的學(xué)習(xí),泛型在運行時都會被擦除成 Object 但是并不是所有的都是這樣,泛型的上界就是對泛型類傳入的類型變量做一定的約束,可以通過類型邊界來進(jìn)行約束。
語法:
class 泛型類名稱<類型形參 extends 類型邊界> {
//...code
}
這里我們來舉兩個例子:
例1:
這里簡單分析一下,Student 繼承了 Person 類,而 Teacher 沒有繼承 Person 類,接著 Test 類給定了泛型的上界, 那么 Test 類中 <> 里面是什么意思呢?
表示只接收 Person 或 Person 的子類作為 T 的類型實參。
通過 main 方法中的例子也可也看出,類型傳參只能傳 Person 或 Person 的子類。
例2:
還是簡單分析一下,Student 類實現(xiàn)了 Comparable 接口,而 Teacher 類并沒有實現(xiàn), 接著 Test 類給定了泛型的上界, 那么 Test 類中 <> 里面是什么意思呢?
表示 T 接收的類型必須是實現(xiàn) Comparable 這個接口的!
通過 main 方法中的例子也可也看出,類型傳參只能傳實現(xiàn)了 Comparable 接口的類 。
注意:如果泛型類沒有指定邊界,則可以默認(rèn)視為 T extends Object。
再談擦除機制
如果給泛型設(shè)置了上界,則會擦除到邊界處,也就不會擦除成 Object!
class Person {} class Student extends Person {} public class Main<T extends Person> { T array[] = (T[])new Object[10]; public static void main(String[] args) { Main<Student> main = new Main<>(); } }
這里 Main 方法中設(shè)定了泛型的上界,傳的類型實參必須是Person的子類,所以編譯時會不會被擦除成 Person呢?下面我們查看一下對應(yīng)的字節(jié)碼文件:
顯而易見,確實被擦除成了泛型的上界!
包裝類的知識
基本數(shù)據(jù)類型和包裝類
在Java中,由于基本類型不是繼承自O(shè)bject,為了在泛型代碼中可以支持基本類型,Java給每個基本類型都對應(yīng)了 一個包裝類型。
裝箱和拆箱
裝箱和拆箱也可也被稱為裝包和拆包。
裝箱:將一個基本數(shù)據(jù)類型值放入對象的某個屬性中。
拆箱:將一個包裝類型中的值取出放到一個基本數(shù)據(jù)類型中。
這里我們舉例來更清楚的認(rèn)識裝箱和拆箱:
public class Test { public static void main(String[] args) { int a = 10; Integer integer1 = new Integer(a); //手動裝箱 Integer integer2 = Integer.valueOf(100); //手動裝箱 int b = integer1.intValue(); //手動拆箱 } }
自動裝箱和拆箱
由上面的例子我們可以看出,手動裝箱和拆箱會帶來不少的代碼量,為了減少開發(fā)者的負(fù)擔(dān),Java中提供了自動轉(zhuǎn)換機制,比如:
public class Test { public static void main(String[] args) { Integer integer = 100; //自動裝箱 int a = integer; //自動拆箱 } }
一道面試題
以下代碼輸出什么?
public class Test { public static void main(String[] args) { Integer a1 = 100; Integer a2 = 100; System.out.println(a1 == a2); Integer a3 = 200; Integer a4 = 200; System.out.println(a3 == a4); } }
結(jié)果是:true false
為什么是這樣的答案?這里我們?nèi)タ匆幌聦?yīng)的字節(jié)碼文件再分析:
通過觀察字節(jié)碼文件,我們可以看到,在自動裝箱的過程中,調(diào)用了 Integer.valueOf 方法,那么我們就去看一看 valueOf 方法中做了一件什么事:
通過查看源碼,我們也能看出此方法將始終緩存 -128到127范圍內(nèi)的值, 通過查看對應(yīng)的 low 和 high 值也可也發(fā)現(xiàn) low為 -128,high為127,cache 是一個緩存數(shù)組。
接著我們來閱讀下這段代碼的操作,如果傳入的值是介于 -128和127 之間,則直接返回緩存數(shù)組對應(yīng)下標(biāo)的值,比如傳入的值是 -127 也就返回 chache[-127+(-(-128))],也即1下標(biāo)位置的值!
如果超出了 -128到127 的范圍則是新 new 一個對象返回,只要是 new 就一定是一個新對象,地址也是唯一的。
而且引用類型用 == 比較,比較的是引用的對象的地址,看完上面的介紹,你能弄明白為什么輸出 true 和 false 嗎?
泛型方法
定義泛型方法的語法:
方法限定符 <類型形參列表> 返回值類型 方法名稱(形參列表) {
//...code
}
普通泛型方法
這里我們就舉一個很簡單的例子:
public class Test { public <T> T getValue(T value) { return value; } public static void main(String[] args) { Test test = new Test(); int ret = test.<Integer>getValue(150); //不使用類型推導(dǎo) System.out.println(ret); double d = test.getValue(12.5); //使用類型推導(dǎo) System.out.println(d); } }
這就是泛型方法,這里面有個關(guān)鍵詞,類型推導(dǎo),什么是類型推導(dǎo)呢?
類型推導(dǎo)就是編譯器會根據(jù)你傳參的數(shù)據(jù),自動推斷出你要傳遞的類型實參,你也可以不使用類型推導(dǎo),他們的效果都是一樣的。
靜態(tài)泛型方法
既然有普通泛型方法,同理,也有靜態(tài)的泛型方法,也就是在修飾符后面加上 static,靜態(tài)泛型方法跟普通靜態(tài)方法一樣,都是通過類名訪問,不依賴于對象:
public class Test { public static<T> T getValue(T value) { return value; } public static void main(String[] args) { int ret = Test.<Integer>getValue(150); //不使用類型推導(dǎo) System.out.println(ret); double d = getValue(12.5); //使用類型推導(dǎo)(靜態(tài)方法可以直接訪問同類中靜態(tài)方法,可以不借助類名) System.out.println(d); } }
通配符
引出通配符
我們先來看這樣的一段代碼:
class Message<T> { private T message ; public T getMessage() { return message; } public void setMessage(T message) { this.message = message; } } public class TestDemo { public static void fun(Message<String> temp){ System.out.println(temp.getMessage()); } public static void main(String[] args) { Message<String> message = new Message<>(); message.setMessage("歡迎來到籃球哥的博客!"); fun(message); } }
如果你仔細(xì)觀察,TestDemo 類中的 fun 方法是有局限性的,他的形參就限制了傳過來的 Missage類的類型必須是String,也就是說,形參能接收的對象的類型參數(shù)必須是String類型。
所以如果我們 new Missage對象時,類型實參傳的是 Integer 呢?fun方法就會報錯:
所以為了解決以上的問題,就有了通配符的概念!
認(rèn)識通配符
泛型T是確定的類型,一旦傳類型了,就定下來了,而通配符的出現(xiàn),就會使得更靈活,或者說更不確定,就好像他是一個垃圾箱,可以接收所有的泛型類型,但又不能讓用戶隨意更改!
通配符:?
現(xiàn)在我們就把上面的代碼更改一下,運用上通配符:
public class TestDemo { public static void fun(Message<?> temp){ System.out.println(temp.getMessage()); } public static void main(String[] args) { Message<Integer> message1 = new Message<>(); message1.setMessage(123); fun(message1); Message<String> message2 = new Message<>(); message2.setMessage("歡迎來到籃球哥的博客!"); fun(message2); } }
這樣我們的代碼就不會出錯,但是,你不能通過 fun 方法去修改你傳遞對象的內(nèi)容,為什么呢?
站在 fun 的角度,他使用了 ? 接收可以任意泛型類,所以他不能確定自己接收了什么對象的!也就無法對對象的值進(jìn)行更改!
這樣代碼還是不夠好,如果真的什么泛型類都能接收,那不是亂套了,所以在此基礎(chǔ)上,又增加了通配符的上界和下界!
通配符的上界
語法:<? extends 上界> 例如:<? extends Person>
表示只能接收的實參類型是 Person 或者 Person的子類!
圖例:
這里我們寫一段偽代碼,更改上面用例的方法:
public static void fun(Message<? extends Person> temp){ //temp.setMessage(new Student()); //仍然無法修改! //temp.setMessage(new Person()); //仍然無法修改! Person person = temp.getMessage(); System.out.println(person); }
為什么還是不能修改對象的屬性呢?
因為 temp 接收的是 Person 或 Person的子類,此時接收的是哪個子類無法確定,也就無法設(shè)置對象的屬性。
因為我們知道只能接收 Person以及他的子類,所以我們就可以拿 Person 類型來接收 getMessage 的對象,因為 Person是他們的父類,獲取的是子類對象就可以實現(xiàn)向上轉(zhuǎn)型,是安全的。
總結(jié): 通配符的上界,不能進(jìn)行寫入數(shù)據(jù),只能進(jìn)行讀取數(shù)據(jù)。
通配符的下界
語法:<? extends 下界> 例如:<? super Person>
表示只能接收的實參類型是 Person 或者 Person的父類!
圖例:
這里我們寫一段偽代碼,更改上面用例的方法:
public static void fun(Message<? super Person> temp){ temp.setMessage(new Student()); //可以修改,因為添加的是他的子類 temp.setMessage(new Person()); //可以修改,因為添加的是他本身 //Person person = temp.getMessage(); // 不能接收,不知道獲取的是哪個父類 System.out.println(temp.getMessage()); //只能輸出 }
為啥下界就可以設(shè)置對象的屬性呢?
因為只能接收本身以及父類的類型,所以我們可以setMessage 傳子類對象,但是不能傳遞父類,因為修改成子類對象是向上轉(zhuǎn)型是安全的,如果 setMessaget 傳父類對象的話就是向下轉(zhuǎn)型則不安全!
為啥不能 getMessage呢?因為你不知道形參接收的類型是哪個父類,只能去輸出內(nèi)容!
總結(jié):通配符的下界,不能進(jìn)行讀取數(shù)據(jù),只能寫入數(shù)據(jù)。
以上就是Java SE之了解泛型的詳細(xì)內(nèi)容,更多關(guān)于Java SE泛型的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Mybatis實現(xiàn)數(shù)據(jù)的增刪改查實例(CRUD)
本篇文章主要介紹了Mybatis實現(xiàn)數(shù)據(jù)的增刪改查實例(CRUD),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-05-05Java項目開發(fā)中實現(xiàn)分頁的三種方式總結(jié)
這篇文章主要給大家介紹了關(guān)于Java項目開發(fā)中實現(xiàn)分頁的三種方式,通過這一篇文章可以很快的學(xué)會java分頁功能,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-02-02