一文通关苦涩难懂的Java泛型
前言
相信大家對Java泛型并不陌生,無論是開源框架還是JDK源碼都能看到它,毫不夸張的說,泛型是通用設(shè)計(jì)上必不可少的元素,所以真正理解與正確使用泛型,是一門必修課,本文將解開大家對泛型的疑惑,并通過大量實(shí)踐,讓你get到泛型正確的使用姿勢,下面開始進(jìn)入正題吧!
大綱
基礎(chǔ)
因?yàn)楸疚闹貙?shí)踐,而且面對的是Java開發(fā)人員群體,大家對泛型都有基礎(chǔ),所以泛型基礎(chǔ)這塊會(huì)快速過,幫助大家回憶下即可,后面主要的則重點(diǎn)是通配符
編譯期與運(yùn)行期
編譯期是指把源碼交給編譯器編譯成計(jì)算機(jī)可執(zhí)行文件的過程,運(yùn)行期是指把編譯后的文件交給計(jì)算機(jī)執(zhí)行,直到程序結(jié)束。
在Java中就是把.java文件編譯成.class文件,再把編譯后的文件交給J V M加載執(zhí)行,如下圖
泛型
泛型又叫“參數(shù)化類型”,這么抽象的專業(yè)詞匯不好理解,阿星就用大白話的形式來解釋。
人是鐵,飯是剛,吃飯是剛需,要吃飯自然就少不了碗筷,但是沒有規(guī)定碗只能盛飯,除了盛飯它還能盛湯、盛菜,制造者只造這個(gè)碗,不關(guān)心碗盛什么,具體要盛什么由使用者來決定,這就是泛型的概念。
泛型就是在定義類、接口、方法的時(shí)候指定某一種特定類型(碗),讓類、接口、方法的使用者來決定具體用哪一種類型的參數(shù)(盛的東西)。
Java的泛型是在1.5引入的,只在編譯期做泛型檢查,運(yùn)行期泛型就會(huì)消失,我們把這稱為“泛型擦除”,最終類型都會(huì)變成 Object。
在沒有泛型之前,從集合中讀取到的每一個(gè)對象都必須進(jìn)行類型轉(zhuǎn)換,如果不小心插入了錯(cuò)誤的類型對象,在運(yùn)行時(shí)的轉(zhuǎn)換處理就會(huì)出錯(cuò),有了泛型后,你可以告訴編譯器每個(gè)集合接收的對象類型是什么,編譯器在編譯期就會(huì)做類型檢查,告知是否插入了錯(cuò)誤類型的對象,使得程序更加安全,也更加清楚。
最后插一句,泛型擦除與原生態(tài)類型(List就是原生態(tài),List非原生態(tài))是為了照顧1.5以前設(shè)計(jì)上的缺陷,為兼容非泛型代碼,所作出的折中策略,所以不推薦使用原生態(tài)類型,如果使用了原生態(tài)類型,就失去了泛型在安全性與描述性方面的優(yōu)勢。
泛型類
類上定義泛型,作用于類的成員變量與函數(shù),代碼實(shí)例如下
public class GenericClass{ //成員變量 private T t; public void function(T t){ } public T functionTwo(T t){ //注意,這個(gè)不是泛型方法!?。? return t; }}
泛型接口
接口上定義泛型,作用于函數(shù),代碼實(shí)例如下
public interface GenericInterface { public T get(); public void set(T t); public T delete(T t); default T defaultFunction(T t){ return t; }}
泛型函數(shù)
函數(shù)返回類型旁加上泛型,作用于函數(shù),代碼實(shí)例如下
public class GenericFunction { public void function(T t) { } public T functionTwo(T t) { return t; } public String functionThree(T t) { return ""; }}
通配符
通配符是為了讓Java泛型支持范圍限定,這樣使得泛型的靈活性提升,同時(shí)也讓通用性設(shè)計(jì)有了更多的空間。
- :無界通配符,即類型不確定,任意類型
- :上邊界通配符,即?是繼承自T的任意子類型,遵守只讀不寫
- :下邊界通配符,即?是T的任意父類型,遵守只寫不讀
相信大部分人,都是倒在通配符這塊,這里多嘮叨點(diǎn),「通配符限定的范圍是體現(xiàn)在確認(rèn)“參數(shù)化類型”的時(shí)候,而不是“參數(shù)化類型”填充后」,可能這句話不太好理解,來看看下面的代碼
/** * 1.創(chuàng)建泛型為Number的List類,Integer、Double、Long等都是Number的子類 * new ArrayList<>() 等價(jià)于 new ArrayList() */List numberList = new ArrayList();/** * 2.添加不同子類 */numberList.add(1);//添加Integer類型numberList.add(0.5);//添加Double類型numberList.add(10000L);//添加Long類型/** * 3.創(chuàng)建泛型為Number的List類,Integer、Double、Long等都是Number的子類 * 引用是泛型類別是Number,但具體實(shí)現(xiàn)指定的泛型是Integer */List numberListTwo = new ArrayList();//err 異常編譯不通過/** * 4.創(chuàng)建泛型為Integer的List類,把該對象的引用地址指向泛型為Number的List */List integerList = new ArrayList();List numberListThree = integerList;//err 異常編譯不通過
- 第一步:我們創(chuàng)建一個(gè)泛型為Number的List,編譯器檢查泛型類別是否一致,一致編譯通過(確認(rèn)參數(shù)化類型)
- 第二步:泛型Number已經(jīng)填充完畢,調(diào)用add函數(shù),此時(shí)add入?yún)⒎盒蚑已經(jīng)填充為Number,add可入?yún)umber或其子類
- 第三步:我們又創(chuàng)建一個(gè)泛型為Number的List,編譯器檢查泛型類別是否一致,不一致編譯失敗,提示錯(cuò)誤(確認(rèn)參數(shù)化類型)
- 第四步:其實(shí)與第三步一樣,只是做了一個(gè)間接的引用(確認(rèn)參數(shù)化類型)
如果要解決上面的編譯不通過問題,就需要使用通配符,代碼如下
/** * 1.上邊界通配符,Number與Number子類 */List numberListFour = new ArrayList();numberListFour = new ArrayList();numberListFour = new ArrayList();numberListFour = new ArrayList();/** * 2.下邊界通配符,Integer與Integer父類 */List integerList = new ArrayList();integerList = new ArrayList();integerList = new ArrayList
最后再來說上邊界通配符只讀不寫,下邊界通配符只寫不讀到底是什么意思,用最簡單的話來說
- 上邊界通配符不作為函數(shù)入?yún)?,只作為函?shù)返回類型,比如List的使用add函數(shù)會(huì)編譯不通過,get函數(shù)則沒問題
- 下邊界通配符不作為函數(shù)返回類型,只作為函數(shù)入?yún)ⅲ热鏛ist的add函數(shù)正常調(diào)用,get函數(shù)也沒問題,但只會(huì)返回Object,所以意義不大
大家只需要記住上面的規(guī)則即可,如果想知道為什么這樣設(shè)計(jì),可以去了解下P E C S (producer-extends,consumer-super)原則
最佳實(shí)踐
相信過完基礎(chǔ)理論大家很多東西都回憶起來了,不要著急,現(xiàn)在開始進(jìn)入正題,后面內(nèi)容會(huì)有大量的代碼實(shí)踐,所以大家要坐穩(wěn)了,別暈車了,暈車的話多看幾遍,或者評論區(qū)提出你的疑問~
無限通配符場景
使用泛型,類型參數(shù)不確定并且不關(guān)心實(shí)際的類型參數(shù),就可以使用,像下面的代碼
/** * 獲取集合長度 */public static int size(Collection list){ return list.size();}/** * 獲取集合長度-2 */public static int sizeTwo(Collection list){ return list.size();}/** * 獲取任意Set兩個(gè)集合交集數(shù)量 */public static int beMixedSum(Set s1,Set s2){ int i = 0; for (T t : s1) { if (s2.contains(t)) { i++; } } return i;}/** * 獲取任意兩個(gè)Set集合交集數(shù)量-2 */public static int beMixedSumTwo(Set s1,Set s2){ int i = 0; for (Object o : s1) { if (s2.contains(o)) { i++; } } return i;}
size與sizeTwo這兩個(gè)函數(shù)都可以正常使用,但是站在設(shè)計(jì)的角度,sizeTwo會(huì)更合適,函數(shù)的目標(biāo)是返回任意集合的長度,入?yún)⒉捎没蚨伎梢越邮?,但是函?shù)本身并不關(guān)心你是什么類型參數(shù),僅僅只要返回長度即可,所以采用。
beMixedSum與beMixedSumTwo這兩個(gè)函數(shù)比較,道理同上面一樣,beMixedSumTwo會(huì)更合適,函數(shù)的目標(biāo)是返回兩個(gè)任意Set集合的交集數(shù)量,beMixedSum函數(shù)雖然內(nèi)部有使用到,但是意義不大,因?yàn)閏ontains入?yún)⑹荗bject,函數(shù)本身并不關(guān)心你是什么類型參數(shù),所以采用。
忘了補(bǔ)充另一個(gè)場景,就是原生態(tài)類型,上述代碼使用原生態(tài)類型函數(shù)使用也沒問題,但是強(qiáng)烈不推薦,因?yàn)槭褂迷鷳B(tài)就丟失了泛型帶來的安全性與描述性!??!
上下邊界通配符場景
首先泛型是不變的,換句話說List
/** * 集合工具類 */public class CollectionUtils{ /** * 復(fù)制集合-泛型 */ public List listCopy(Collection collection){ List newCollection = new ArrayList<>(); for (T t : collection) { newCollection.add(t); } return newCollection; } }
上面聲明了一個(gè)CollectionUtils類,擁有l(wèi)istCopy方法,傳入任意一個(gè)集合返回新的集合,看似沒有什么問題,也很靈活,那再看看下面這段代碼。
public static void main(String[] agrs){ CollectionUtils collectionUtils = new CollectionUtils<>(); List list = new ArrayList<>(); //list.add.... List listTwo = new ArrayList<>(); //listTwo.add.... List listThree = new ArrayList<>(); //listThree.add.... List list1 = collectionUtils.listCopy(list); list1 = collectionUtils.listCopy(listTwo);//err 編譯異常 list1 = collectionUtils.listCopy(listThree);//err 編譯異常}
創(chuàng)建CollectionUtils類,泛型的類型參數(shù)為Number,listCopy函數(shù)入?yún)⒌姆盒吞畛錇镹umber,此時(shí)listCopy只支持泛型為Number的List,如果要讓它同時(shí)支持泛型為Number子類的List,就需要使用上邊界通配符,我們再追加一個(gè)方法
/** * 集合工具 */public class CollectionUtils{ /** * 復(fù)制集合-泛型 */ public List listCopy(Collection collection){ List newCollection = new ArrayList<>(); for (T t : collection) { newCollection.add(t); } return newCollection; } /** * 復(fù)制集合-上邊界通配符 */ public List listCopyTwo(Collection collection){ List newCollection = new ArrayList<>(); for (T t : collection) { newCollection.add(t); } return newCollection; }}public static void main(String[] agrs){ CollectionUtils collectionUtils = new CollectionUtils<>(); List list = new ArrayList<>(); //list.add.... List listTwo = new ArrayList<>(); //listTwo.add.... List listThree = new ArrayList<>(); //listThree.add.... List list1 = collectionUtils.listCopyTwo(list); list1 = collectionUtils.listCopyTwo(listTwo); list1 = collectionUtils.listCopyTwo(listThree);}
現(xiàn)在使用listCopyTwo就沒有問題,listCopyTwo對比listCopy它的適用范圍更廣泛也更靈活,listCopy能做的listCopyTwo能做,listCopyTwo能做的listCopy就不一定能做了,除此之外,細(xì)心的小伙伴肯定發(fā)現(xiàn)了,使用上邊界通配符的collection在函數(shù)內(nèi)只使用到了讀操作,遵循了只讀不寫原則。
看完了上邊界通配符,再來看看下邊界通配符,依然是復(fù)制方法
/** * 兒子 */public class Son extends Father{}/** * 父親 */public class Father extends Grandpa{}/** * 爺爺 */public class Grandpa {}/** * 集合工具 */public class CollectionUtils{ /** * 復(fù)制集合-泛型 * target目標(biāo) src來源 */ public void copy(List target,List src){ if (src.size() > target.size()){ for (int i = 0; i < src.size(); i++) { target.set(i,src.get(i)); } } } }
定義了3個(gè)類,分別是Son兒子、Father父親、Grandpa爺爺,它們是繼承關(guān)系,作為集合元素,還聲明了一個(gè)CollectionUtils類,擁有copy方法,傳入兩個(gè)集合,目標(biāo)集合與來源集合,把來源集合元素復(fù)制到目標(biāo)集合中,再看看下面這段代碼
public static void main(String[] agrs){ CollectionUtils collectionUtils = new CollectionUtils<>(); List fatherTargets = new ArrayList<>(); List fatherSources = new ArrayList<>(); //fatherSources.add... collectionUtils.copy(fatherTargets,fatherSources); //子類復(fù)制到父類 List sonSources = new ArrayList<>(); //sonSources.add... collectionUtils.copy(fatherTargets,sonSources);//err 編譯異常 }
創(chuàng)建CollectionUtils類,泛型的類型參數(shù)為Father父親,copy函數(shù)入?yún)⒌姆盒吞畛錇镕ather,此時(shí)copy只支持泛型為Father的List,也就說,只支持泛型的類型參數(shù)為Father之間的復(fù)制,如果想支持把子類復(fù)制到父類要怎么做,先分析下copy函數(shù),copy函數(shù)的入?yún)rc在函數(shù)內(nèi)部只涉及到了get函數(shù),即讀操作(泛型只作為get函數(shù)返回類型),符合只讀不寫原則,可以采用上邊界通配符,調(diào)整代碼如下
/** * 集合工具 */public class CollectionUtils{ /** * 復(fù)制集合-泛型 * target目標(biāo) src來源 */ public void copy(List target,List src){ if (src.size() > target.size()){ for (int i = 0; i < src.size(); i++) { target.set(i,src.get(i)); } } }}public static void main(String[] agrs){ CollectionUtils collectionUtils = new CollectionUtils<>(); List fatherTargets = new ArrayList<>(); List fatherSources = new ArrayList<>(); //fatherSources.add... collectionUtils.copy(fatherTargets,fatherSources); //子類復(fù)制到父類 List sonSources = new ArrayList<>(); //sonSources.add... collectionUtils.copy(fatherTargets,sonSources); //把子類復(fù)制到父類的父類 List grandpaTargets = new ArrayList<>(); collectionUtils.copy(grandpaTargets,sonSources);//err 編譯異常}
src入?yún)⒄{(diào)整為上邊界通配符后,copy函數(shù)傳入List sonSources就沒問題了,此時(shí)的copy函數(shù)比相較之前的更加靈活了,支持同類與父子類復(fù)制,接著又發(fā)現(xiàn)了一個(gè)問題,目前能復(fù)制到上一級父類,如果是多級父類,還無法支持,繼續(xù)分析copy函數(shù),copy函數(shù)的入?yún)arget在函數(shù)內(nèi)部只涉及到了add函數(shù),即寫操作(只作為add函數(shù)入?yún)ⅲ现粚懖蛔x原則,可以采用下邊界通配符,調(diào)整代碼如下
/** * 集合工具 */public class CollectionUtils{ /** * 復(fù)制集合-泛型 * target目標(biāo) src來源 */ public void copy(List target,List src){ if (src.size() > target.size()){ for (int i = 0; i < src.size(); i++) { target.set(i,src.get(i)); } } }}public static void main(String[] agrs){ CollectionUtils collectionUtils = new CollectionUtils<>(); List fatherTargets = new ArrayList<>(); List fatherSources = new ArrayList<>(); //fatherSources.add... collectionUtils.copy(fatherTargets,fatherSources); //子類復(fù)制到父類 List sonSources = new ArrayList<>(); //sonSources.add... collectionUtils.copy(fatherTargets,sonSources); //把子類復(fù)制到父類的父類 List grandpaTargets = new ArrayList<>(); collectionUtils.copy(grandpaTargets,sonSources);}
copy函數(shù)終于是完善了,可以說現(xiàn)在是真正支持父子類復(fù)制,不難發(fā)現(xiàn)copy函數(shù)的設(shè)計(jì)還是遵循通配符原則的,target作為目標(biāo)集合,只做寫入,符合只寫不讀原則,采用了下邊界通配符,src作為來源集合寫入到target目標(biāo)集合,只做讀取,符合只讀不寫原則,采用了上邊界通配符,最后設(shè)計(jì)出來的copy函數(shù),它的靈活性與適用范圍是遠(yuǎn)超方式設(shè)計(jì)的。
最后總結(jié)一下,什么時(shí)候用通配符,如果參數(shù)泛型類即要讀也要寫,那么就不推薦使用,使用正常的泛型即可,如果參數(shù)泛型類只讀或?qū)?,就可以根?jù)原則采用對應(yīng)的上下邊界,是不是十分簡單,最后再說一次讀寫的含義,這塊確實(shí)很容易暈
- 讀:所謂讀是指參數(shù)泛型類,泛型只作為該參數(shù)類的函數(shù)返回類型,那這個(gè)函數(shù)就是讀,List作為參數(shù)泛型類,它的get函數(shù)就是讀
- 寫:所謂寫是指參數(shù)泛型類,泛型只作為該參數(shù)類的函數(shù)入?yún)?,那這個(gè)函數(shù)就是寫,List作為參數(shù)泛型類,它的add函數(shù)就是讀
最后可以推薦下大家可以思考下Stream的forEach函數(shù)與map函數(shù)的設(shè)計(jì),在Java1.8 Stream中是大量用到了通配符設(shè)計(jì)
-----------------------------------------------------------------/** * 下邊界通配符 */void forEach(Consumer action);public interface Consumer { //寫方法 void accept(T t);}-----------------------------------------------------------------/** * 上下邊界通配符 */ Stream map(Function mapper)public interface Function { //讀寫方法,T只作為入?yún)⒎蠈懀琑只作為返回值,符合讀 R apply(T t);}-----------------------------------------------------------------//代碼案例public static void main(String[] agrs) { List fatherList = new ArrayList<>(); Consumer action = new Consumer() { @Override public void accept(Father father) { //執(zhí)行father邏輯 } }; //下邊界通配符向上轉(zhuǎn)型 Consumer actionTwo = new Consumer() { @Override public void accept(Grandpa grandpa) { //執(zhí)行g(shù)randpa邏輯 } }; Function mapper = new Function() { @Override public Grandpa apply(Father father) { //執(zhí)行father邏輯后返回Grandpa return new Grandpa(); } }; //下邊界通配符向上轉(zhuǎn)型,下邊界通配符向下轉(zhuǎn)型 Function mapperTwo = new Function() { @Override public Son apply(Grandpa grandpa) { //執(zhí)行g(shù)randpa邏輯后,返回Son return new Son(); } }; fatherList.stream().forEach(action); fatherList.stream().forEach(actionTwo); fatherList.stream().map(mapper); fatherList.stream().map(mapperTwo); }-----------------------------------------------------------------
有限制泛型場景
有限制泛型很簡單了,應(yīng)用場景就是你需要對泛型的參數(shù)類型做限制,就可以使用它,比如下面這段代碼
public class GenericClass { public void test(T t){ //.... }}public static void main(String[] agrs){ GenericClass grandpaGeneric = new GenericClass<>(); grandpaGeneric.test(new Grandpa()); grandpaGeneric.test(new Father()); grandpaGeneric.test(new Son()); GenericClass fatherGeneric = new GenericClass<>(); fatherGeneric.test(new Father()); fatherGeneric.test(new Son()); GenericClass sonGeneric = new GenericClass<>(); sonGeneric.test(new Son()); GenericClass
GenericClass泛型參數(shù)化類型被限制為Grandpa或其子類,就這么簡單,千萬不要把有限制泛型與上邊界通配符搞混了,這兩個(gè)不是同一個(gè)東西( != ),不需要遵循上邊界通配符的原則,它就是簡單的泛型參數(shù)化類型限制,而且沒有super的寫法。
遞歸泛型場景
在有限制泛型的基礎(chǔ)上,又可以衍生出遞歸泛型,就是自身需要使用到自身,比如集合進(jìn)行自定義元素大小比較的時(shí)候,通常會(huì)配合Comparable接口來完成,看看下面這段代碼
public class Person implements Comparable
{ private int age; public Person(int age) { this.age = age; } public int getAge() { return age; } @Override public int compareTo(Person o) { // 0代表相等 1代表大于 <0代表小于 return this.age - o.age; }}/** * 集合工具 */public class CollectionUtils{ /** * 獲取集合最大值 */ public static <e extends="" comparable> E max(List list){ E result = null; for (E e : list) { if (result == null || e.compareTo(result) > 0){ result = e; } } return result; }}public static void main(String[] agrs){ List
personList = new ArrayList<>(); personList.add(new Person(12)); personList.add(new Person(19)); personList.add(new Person(20)); personList.add(new Person(5)); personList.add(new Person(18)); //返回年齡最大的Person元素 Person max = CollectionUtils.max(personList);}
重點(diǎn)關(guān)注max泛型函數(shù),max泛型函數(shù)的目標(biāo)是返回集合最大的元素,內(nèi)部比較元素大小,取最大值返回,也就說需要和同類型元素做比較,<e extends="" comparable>含義是,泛型E必須是Comparable或其子類/實(shí)現(xiàn)類,因?yàn)楸容^元素是同類型,所以Comparable泛型也是E,最終接收的List泛型參數(shù)化類型必須實(shí)現(xiàn)了Comparable接口,并且Comparable接口填充的泛型也是該參數(shù)化類型,就像上述代碼一樣。
關(guān)于我
這里是阿星,一個(gè)熱愛技術(shù)的Java程序猿,在公眾號 「程序猿阿星」 里將會(huì)定期分享操作系統(tǒng)、計(jì)算機(jī)網(wǎng)絡(luò)、Java、分布式、數(shù)據(jù)庫等精品原創(chuàng)文章,2021,與您在 Be Better 的路上共同成長!。
非常感謝各位人才能 看到這里,原創(chuàng)不易,文章有幫助可以「點(diǎn)個(gè)贊」或「分享與評論」,都是支持(莫要白嫖)!
愿你我都能奔赴在各自想去的路上,我們下篇文章見!
總結(jié)
以上是生活随笔為你收集整理的一文通关苦涩难懂的Java泛型的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: centos普通用户修改文件权限_用户管
- 下一篇: 如何登录公司邮箱如何电脑登录邮箱