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