Java通俗易懂講解泛型
1.什么是泛型
泛型是在JDK1.5引入的新的語法,通俗講,泛型:就是適用于許多許多類型。從代碼上講,就是對類型實現(xiàn)了參數(shù)化。
看到標(biāo)紅的這句話,有些兄弟可能會有疑惑,我們以前傳參,傳得是一個整形數(shù)據(jù),傳得是一個引用,我從來沒傳過類型嘛,類型難道還能作為參數(shù)傳遞???對,沒錯,接下來,我們就來看一下類型作為參數(shù)傳遞的實現(xiàn)方式
2.引出泛型
我們以前學(xué)過繼承和多態(tài),我們知道所有類的父類默認(rèn)是Object類,我們現(xiàn)在目標(biāo)是:數(shù)組中可以存放任何類型的數(shù)據(jù),這時候我們就可以new一個Object類型的數(shù)組,,,
class MyArray {
public Object[] array = new Object[10];
public Object getPos(int pos) {
return array[pos];
}
public void setPos(int pos, Object value) {
array[pos] = value;
}
}
public class TestDemo {
public static void main(String[] args) {
MyArray myArray = new MyArray();
myArray.setPos(0,1);
myArray.setPos(1,"hello");
}
}當(dāng)我們寫出這樣一個代碼的時候,我們發(fā)現(xiàn)數(shù)組中什么元素都能放了,可以放整形數(shù)據(jù),可以放字符串,還可以放小數(shù),這里面能放的東西太多了,你能知道某個下標(biāo)的元素是什么樣的類型嗎??當(dāng)我們?nèi)≡氐臅r候,會發(fā)現(xiàn)你的代碼編譯都不能通過,,,

為什么會出現(xiàn)這樣的情況呢,我相信大部分兄弟們都知道,我們觀察getPos的返回值是Object,也就意味著我們拿出來的可能是任意元素,但編譯器知道嗎???它只知道返回的是一個Object:這就很明顯了,父類給子類,,這不純純的向下轉(zhuǎn)型嗎,向下轉(zhuǎn)型就必須要強(qiáng)轉(zhuǎn)類型,否則你的代碼就不可能通過。
那么問題又來了,,我這里只是放了兩個元素,肉眼還能看的過來,但是這里面的類型時刻在發(fā)生改變,就算你知道類型,你每次都需要強(qiáng)轉(zhuǎn)嗎,我是動態(tài)的啊,我代碼是寫死的啊,所以這個地方如果用Object去做的話,會顯得格外麻煩,而且局限性非常的大
我們現(xiàn)在的困境有兩個:
1.任何類型都可以放,不好控制,,,
2.每次取元素的時候,都得進(jìn)行強(qiáng)制類型轉(zhuǎn)換,,,
這時候我們得搞一手希望工程,我們的希望是:
1.我們能不能自己指定類型
2.我們能不能,不再進(jìn)行類型轉(zhuǎn)換呀,能不能把這一步省略呀,兄弟!??!
為了解決這一問題,我們java就引入了這個東西----》泛型
那到底怎么指定類型呢,來看代碼:
class MyArray <T>{
//public Object[] array = new Object[10];
public T[] array = (T[]) new Object[10];//這樣寫也不好,待會說為什么??
public T getPos(int pos) {
return array[pos];
}
public void setPos(int pos, T value) {
array[pos] = value;
}
}
public class TestDemo {
public static void main(String[] args) {
//我指定放整形
MyArray<Integer> myArray = new MyArray<>();
//后面的尖括號里的類型可以省略,在實例化對象的時候根據(jù)前面可以做出判斷
//MyArray<Integer> myArray = new MyArray<Integer>();
myArray.setPos(0,1);
myArray.setPos(1,2);
Integer ret = myArray.getPos(1);
System.out.println(ret);
//我指定放字符串類型
MyArray<String> myArray2 = new MyArray<>();
myArray2.setPos(0,"abc");
myArray2.setPos(1,"bit");
String ret2 = myArray2.getPos(0);
System.out.println(ret2);
}
}這個代碼有幾個注意事項:
1.尖括號<>里面只能放引用類型;
2.<T>是一個占位符,代表當(dāng)前類是一個泛型類;
3.MyArray<Integer> array = new MyArray<>();后面的尖括號可以省略Integer不寫,實例化對象的 時候編譯器根據(jù)前面能做出判斷。
看完上面的代碼,我相信兄弟們已經(jīng)明白是怎么一回事了吧
我們剛剛的希望工程也已經(jīng)實現(xiàn)了
1.<Integer>,指定當(dāng)前類中,使用的類型是Integer類型
2.泛型幫我在編譯期間做了2件事情(目前為止的優(yōu)點):
- 存放元素的時候,進(jìn)行類型的檢查
- 取元素的時候,幫我進(jìn)行類型的轉(zhuǎn)換
3.泛型類的語法
有了剛剛知識的鋪墊,這一塊咱就形式化一下啦,,當(dāng)然,語法還是很重要滴??!
語法:
泛型類<類型實參> 變量名; // 定義一個泛型類引用
new 泛型類<類型實參>(構(gòu)造方法實參); // 實例化一個泛型類對象
栗子:
MyArray<Integer> list = new MyArray<Integer>();
4.裸類型
說明:
兄弟們,咱學(xué)了新東西,這舊東西,咱也得了解一下是不咯,,萬一以后跟別人聊到了,自己好像從來沒聽過就尷尬了,,
裸類型是一個泛型類但沒有帶著類型實參,例如 MyArrayList 就是一個裸類型
MyArray list = new MyArray();
注意: 我們不要自己去使用裸類型,裸類型是為了兼容老版本的 API 保留的機(jī)制
5.泛型如何編譯的
5.1 擦除機(jī)制
我用的是Powershell窗口查看的字節(jié)碼文件,當(dāng)然用idea的兄弟們也可以去下載一個展示字節(jié)碼的插件,,接下來我們看看泛型到底是怎么編譯的,,javap -c查看字節(jié)碼

我們發(fā)現(xiàn)在編譯期間,所有的T都被擦除為了Object,那既然所有的T都變成了Object,那我們?yōu)槭惨付愋湍??注意這里不是編譯的時候替換,指定類型只是為了在編譯的時候幫我們進(jìn)行類型的檢查和轉(zhuǎn)換,并不是替換?。?!最終字節(jié)碼編譯完成的時候,所有的T變成了Object,而這就是我們Java當(dāng)中泛型所謂的擦除機(jī)制?。?!
提問:那既然T在編譯的時候被擦除為了Object,那為啥 T[] array = new T[] 會報錯呢?
5.2會給你答案??????
5.2.泛型數(shù)組為什么不能實例化
先看一段代碼和運(yùn)行結(jié)果:
class MyArray<T> {
public T[] array = (T[])new Object[10];
public T getPos(int pos) { return this.array[pos]; }
public void setVal(int pos,T val) { this.array[pos] = val; }
public T[] getArray() { return array; }
}
public class TestDemo {
public static void main(String[] args) {
MyArray<Integer> myArray1 = new MyArray<>();
Integer[] strings = myArray1.getArray();//這里運(yùn)行時會報錯
String[] strings1 = (String)myArray1.getArray();//這里編譯都不能通過
}
}
出現(xiàn)這種情況的原因何在呢?
這里最大的問題就是數(shù)組,java中的數(shù)組是非常特殊的,它是在堆上分配內(nèi)存的,它有自己的一套特殊的機(jī)制,而T[] array = new T[] 這個數(shù)組它沒有指定具體的類型,肯定是不讓這樣做的;再者,get方法的返回類型是Object類型,如果用整形數(shù)組來接受,父類給子類,編譯器是直接不讓你做的。
那問題來了???既然 T[] array = new T[] 這種寫法是錯誤的,T[] array = (T[])new Object[10] 這種寫法又不好,那正確的寫法應(yīng)該是怎么的呢???
正確寫法:(了解即可)
/**
* 通過反射創(chuàng)建指定類型的數(shù)組
*/
public MyArray(Class<T> clazz, int capacity) {
array = (T[]) Array.newInstance(clazz, capacity);
}看起來很復(fù)雜,對吧,但是沒關(guān)系,java官方都不這么用,所以我們也只需要了解即可。
我們可以看下ArrayList的源碼來確認(rèn)一下(以免存在忽悠的成份):

6.泛型的上界
什么叫泛型的上界呢??先給你們看一下語法:
class 泛型類名稱<類型形參 extends 類型邊界> {
...
}
這一模塊我將舉兩個栗子來和大家一起認(rèn)識或溫習(xí)一下這個知識點,,,
簡單示例:
class Test<T extends Number> {
}
public class TestDemo {
public static void main(String[] args) {
//Number的子類
Test<Integer> test = new Test<>();
Test<Double> test1 = new Test<>();
Test<Float> test2 = new Test<>();
//Number本身
Test<Number> test3 = new Test<>();
}
}<T extends Number>中的 extends可不敢理解為繼承喔,可以理解為T擦除為了Number的子類或自己本身,也就是說在主函數(shù)中用這個類實例化的時候,可以指定的類型一定要 <= Number ,,差不多就這意思,,,簡單的你們都能理解,我就不多廢話了,給你們來點稍微有難度的(我覺得難)
進(jìn)階示例:
class Alg<T extends Comparable<T>> {
//如果不設(shè)置上界為Comparable,就用不了compareTo這個方法,因為T如果不指定的話,
// 在編譯的時候被擦除成了Object,而Object并沒有實現(xiàn)這個接口
public T findMax(T[] array) {
T max = array[0];
for (int i = 1; i < array.length; i++) {
if(max.compareTo(array[i]) < 0) {
max = array[i];
}
}
return max;
}
}
class Student implements Comparable<Student> {
public String name;
public int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age='" + age + '\'' +
'}';
}
@Override
public int compareTo(Student o) {
return 0;
}
}
public class TestDemo {
public static void main(String[] args) {
//指定類型為整形
Alg<Integer> alg = new Alg<>();
Integer[] array = {1,2,3,4,5};
Integer max = alg.findMax(array);
System.out.println(max);
//指定類型為Student
Alg<Student> alg2 = new Alg<>();
Student[] students = new Student[3];
students[0] = new Student("毛毛",15);
students[1] = new Student("吉吉",13);
students[2] = new Student("毛二",16);
Student student = alg2.findMax(students);
System.out.println(student);
}
}<T extends Comparable<T>>意為擦除到實現(xiàn)了這個接口,簡而言之就是這里面接收的類型一定是實現(xiàn)了Comparable接口的,給你們證明一下,我那里面Student類型毋庸置疑,自己實現(xiàn)的Comparable接口,我們來看看Integer的源碼:

再者,我們觀察一下 Alg 這個類中的 findMax 方法,我們都知道引用類型比較大小要用比較器或者compareTo方法,如果 Student 這個類沒有實現(xiàn) Comparable 接口,我們在寫代碼的時候,會發(fā)現(xiàn)點都點不出來,比較一下,就通透了,,
沒有實現(xiàn)Comparable接口:

實現(xiàn)了Comparable接口:

當(dāng)然,又有人會問,我能不能不實例化這個方法,直接調(diào)用這個方法,這時候我們把這個方法搞成靜態(tài)的來看一下:
發(fā)現(xiàn)它報錯了,,,別急哈,咱們一步一步是爪牙,,哦,不對,是一步一步來揭開面紗,,

那如何將這個代碼改成正確的呢??細(xì)心的兄弟會發(fā)現(xiàn),既然這個方法是靜態(tài)的, 那我們只需要通過類名來方法,如果還按照之前的寫法,類后面跟一個 <T>,那我們啥時候給它指定類型??不知道吧,這時候我們要將代碼改成這樣,在 static 的后面跟一個 <T>,這樣你的代碼就不會報錯了,,,

當(dāng)然,我們也可以指定邊界,這里跟類有著異曲同工之妙,就不細(xì)說了,花兩秒鐘看一下代碼,你就能發(fā)出: 哦~~~~~的聲音,中國人不騙中國人
class Alg2 <T>{
public static<T extends Comparable> T findMax(T[] array) {
T max = array[0];
for (int i = 1; i < array.length; i++) {
if(max.compareTo(array[i]) < 0) {
max = array[i];
}
}
return max;
}
}最后,你們可能會問,那它既然有上界,那它有下界嗎??
那肯定是沒有的啊,咱學(xué)習(xí)編程多辛苦啊 ??!是不是兄弟們
7.通配符
兄弟們,最后兩個知識點,咱泛型就告一段落了,,
什么是通配符??我還是換一行吧
?這個就是通配符,對,沒錯,就是這個問號 "?"
7.1.通配符能用來干嘛
JAVA中引入通配符這一玩意是用來解決泛型無法協(xié)變這一問題的,那什么是協(xié)變呢?
協(xié)變這東西怎么說呢,類似于,繼承當(dāng)中的父類引用子類的對象,但是泛型中不支持協(xié)變類型,而JAVA的數(shù)組是支持的,數(shù)組中明確了它所允許存儲的對象類型,并且會在運(yùn)行時做類型檢查(這也是為什么不能創(chuàng)建泛型數(shù)組的原因,數(shù)組創(chuàng)建時必須知道確切類型)。
下面看具體代碼,看看是如何解決協(xié)變問題的,,,
class Message<T> {
private T message;
public T getMessage() {
return message;
}
public void setMessage(T message) {
this.message = message;
}
}
public class TestDemo2 {
public static void main(String[] args) {
Message<Integer> message = new Message<>();
message.setMessage(1);
fun(message);//編譯不通過
}
public static void fun(Message<String> temp) {
System.out.println(temp.getMessage());
}
}首先,上面這一段代碼編譯是不能通過的,很明顯我 fun 方法中只能接收 String 類型的數(shù)據(jù),而你給我一個整形數(shù)據(jù),肯定會報錯嗎,當(dāng)我把它改成這樣的時候,,你會發(fā)現(xiàn)代碼不報錯了:
class Message<T> {
private T message;
public T getMessage() {
return message;
}
public void setMessage(T message) {
this.message = message;
}
}
public class TestDemo2 {
public static void main(String[] args) {
Message<Integer> message = new Message<>();
message.setMessage(1);
fun(message);
}
public static void fun(Message<?> temp) {
//temp.setMessage(1);//是不能往里面加元素的,所以源碼當(dāng)中通配符用的比較多
System.out.println(temp.getMessage());
}
}這里尖括號里面放?意味著可以接收任何類型,人家用戶你可不知道你這是什么類型,所以通配符在這一塊就起了非常大的作用;再者,既然可以接收所有類型,就不能讓用戶隨便修改。具體是怎么做到的呢?也就是說當(dāng)你用通配符在函數(shù)中接收任意類型之后,你不能調(diào)用 set 方法往里面設(shè)值,你根本不知道你接收到的是什么類型,,,所以,我們也看到源碼當(dāng)中通配符用的比較多就是這么回事
7.2.通配符的上界(讀數(shù)據(jù))
通配符的上界,這東西跟剛剛的泛型上界很相似,有了剛剛的只是鋪墊,應(yīng)該不難理解了,,語法這一塊咱就不演示了
class Food {}
class Fruit extends Food {}
class Apple extends Fruit {}
class Banana extends Fruit {}
class Plate<T> {
private T pear;
public T getPlate() {
return pear;
}
public void setPlate(T a) {
this.pear = a;
}
}
public class TestDemo3 {
public static void main(String[] args) {
Plate<Apple> plate = new Plate<>();
Plate<Banana> plate1 = new Plate<> ();
fun1(plate);
fun1(plate1);
}
public static void fun1(Plate<? extends Fruit> temp) {
//temp.setPlate(new Apple());這里不知道temp引用的子類是誰,所以不能往里邊放元素
//可以接收元素,父類接收子類,頂多就是向上轉(zhuǎn)型
System.out.println(temp.getPlate());
//Fruit fruit = temp.getPlate();
}
}我們這里給了它香蕉/蘋果等一些水果類和 Fruit 這個父類,<? extends Fruit>這里可以接收任何Fruit 的子類類型,同理,既然可以接收任何類型,就不適合寫數(shù)據(jù),只適合讀數(shù)據(jù),,
7.3.通配符的下界(寫數(shù)據(jù))
通配符不同于泛型,它是有下界的,來看看它的下界,,,
語法大概就這樣子,簡單了解一下:<? super 下界>---重點在代碼
public class TestDemo3 {
public static void main(String[] args) {
Plate<Fruit> plate = new Plate<>();
fun(plate);
Plate<Food> plate2 = new Plate<>();
fun(plate2);
Plate<Apple> plate1 = new Plate<>();
//fun(plate1);
}
public static void fun(Plate<? super Fruit> temp) {
//寫數(shù)據(jù),但是不能寫父類
temp.setPlate(new Apple());
temp.setPlate(new Banana());
//編譯不通過:Fruit的父類有很多,不一定就是某一個
//temp.setPlate(new Food());
//不安全--它的父類有很多,不知道是哪一個父類發(fā)生了向下轉(zhuǎn)型
//Fruit fruit = (Fruit)temp.getPlate();
}
}通配符的下界呢,,我們可以看到它是用了一個關(guān)鍵字super,字面意思,大家都懂,就是說能接收的類型至少是 Fruit ,以及 Fruit 的父類,所以,在讀寫數(shù)據(jù)這一塊,它就不同于別人,它只喜歡寫數(shù)據(jù),就像咱人一樣,總得有些偏執(zhí)的愛好,我就喜歡敲代碼,是吧,,,為啥不能讀數(shù)據(jù),這里還是得解釋一下,因為 Fruit 的父類太多了,你根本不知道讀誰的數(shù)據(jù),再者就算你強(qiáng)轉(zhuǎn)類型,編譯器也會認(rèn)為這是不安全的,所以我們在讀數(shù)據(jù)和寫數(shù)據(jù)這一塊,?的上界和?的下界要搞清楚什么時候用哪一個,,,
下圖能幫助你理解讀寫數(shù)據(jù),到底適合哪一個

本來還有一個裝箱和拆箱的類容要給兄弟們分享的,但是現(xiàn)在已經(jīng)凌晨兩點了(小命要緊,建議大家不要像我這樣,身體是內(nèi)卷的本錢,我只是一時興起,不敢瞞著兄弟們偷偷卷),小編我要為那些目前還在渴望這一塊知識的年輕小伙子著想,就把那些內(nèi)容單獨抽到下一期去了,,,下期見,兄弟們?。。?/p>
到此這篇關(guān)于Java通俗易懂講解泛型的文章就介紹到這了,更多相關(guān)Java泛型內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
借助Maven搭建Hadoop開發(fā)環(huán)境的最詳細(xì)教程分享
在Maven插件的幫助下,VSCode寫Java其實非常方便,所以本文就來和大家詳細(xì)講講如何借助maven用VScode搭建Hadoop開發(fā)環(huán)境,需要的可以參考下2023-05-05
使用SpringBoot整合Activiti6工作流的操作方法
這篇文章主要介紹了使用SpringBoot整合Activiti6工作流,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-07-07
使用spring框架ResponseEntity實現(xiàn)文件下載
這篇文章主要介紹了使用spring框架ResponseEntity實現(xiàn)文件下載,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-02-02
Springboot實現(xiàn)給前端返回一個tree結(jié)構(gòu)方法
這篇文章主要介紹了SpringBoot返回給前端一個tree結(jié)構(gòu)的實現(xiàn)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07
Java中為什么要實現(xiàn)Serializable序列化
在Java編程中,Serializable序列化是一個常見的概念,它允許對象在網(wǎng)絡(luò)上傳輸或持久化到磁盤上,本文將深入探討為什么在Java中要實現(xiàn)Serializable序列化,并通過示例代碼來解釋其重要性2023-10-10
Java面向?qū)ο髮崿F(xiàn)汽車租賃系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了Java面向?qū)ο髮崿F(xiàn)汽車租賃系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-02-02
Spring Boot2.0整合ES5實現(xiàn)文章內(nèi)容搜索實戰(zhàn)
這篇文章主要介紹了Spring Boot2.0整合ES5實現(xiàn)文章內(nèi)容搜索實戰(zhàn),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-01-01
詳解SpringBoot中JdbcTemplate的事務(wù)控制
JdbcTemplate是spring-jdbc提供的數(shù)據(jù)庫核心操作類,那對JdbcTemplate進(jìn)行事務(wù)控制呢,本文就詳細(xì)的介紹一下2021-09-09

