Java高階特性入門——泛型、反射和註解

NO IMAGE
1 Star2 Stars3 Stars4 Stars5 Stars 給文章打分!
Loading...

摘要: 只有掌握了Java的高階特性,這門語言才算真正地登堂入室。本文將帶領大家一同瞭解Java語言的三個常用的高階特性——泛型、反射和註解。

本次直播視訊精彩回顧,戳這裡

專家簡介:
澳明,阿里巴巴高階開發工程師,來自於阿里巴巴研發效能事業部-研發平臺-程式碼智慧化團隊。

以下內容根據演講嘉賓視訊分享以及PPT整理而成。

本次的分享主要圍繞以下三個方面:

一、泛型介紹
二、反射機制
三、註解的使用

一、泛型介紹

在日常程式設計的過程中,泛型在這三個特性之中使用頻率是最高的。”泛型”一詞中的泛字可以理解為泛化的意思,即由具體的、個別的擴大為一般的。Oracle對泛型的官方定義是:泛型型別是通過型別引數化的泛型類或介面。一言以蔽之,泛型就是通過型別引數化,來解決程式的通用性設計和實現的若干問題。
Java泛型是1.5版本後引入的特性,它主要被用於解決三類問題:
1、編譯器型別檢查

例如上圖中的例項1設計了一個簡單的Box類,在其中定義了一個private的object的屬性,同時定義了get()和set()兩個行為,其中set()用於儲存object到Box內,set()用於獲取Box中的object物件。從抽象的角度看,Box類抽象了一個用於在盒子中存放物品物件和存取的行為,存取的方法接受或者返回Object型別的物件。在這個抽象的基礎上,可以存放除原始型別外任意型別的物件,Object型別的宣告體現了物件導向中繼承的理念。
在例項2中,實現了不同業務場景下對Box的使用方式。其中列舉了兩種不同的業務場景,場景一需要在Box中存放String型別的物件,場景二需要在Box中存放Integer型別的物件,這種情況下,在實際開發時,場景二中很有可能會錯誤地傳入一個String物件,導致執行時錯誤的發生,而這正是因為Box可以被只有傳入任意型別的物件導致的,這種情況在集合類操作時尤為突出。例如例項3中的情況:

首先宣告瞭一個List型別的boxes物件,其中存放了兩個物件,一個是String型別的“aaaaa”,另一個是Integer型別的11111。在業務場景一下,使用者認為boxes中存放的所有物件都是String型別的,因此在取出第二個物件並進行型別轉換的時候就發生了錯誤。這種情況往往讓使用者十分迷惑,明明編譯時沒有問題,但是在執行時卻產生了異常。也就是說,在這種物件導向的抽象過程中,無法通過編譯來驗證型別該如何進行使用。

那麼泛型是如何解決這類問題的呢?

Oracle意識到了上述的問題,在引入泛型之後,通過將程式碼中的“public class Box”更改為“public class Box<T>”來建立泛型型別的宣告,而這個宣告的背後實質上是引入了可以在類中任何地方使用的型別變數T。如例項4中所示:可以看到,除了新增的泛型型別宣告<T>外,所有在原來程式碼中出現的Object都被型別變數T所替換。
乍一看型別變數這個詞,感覺有點晦澀難懂,但其實如果仔細思量一番會發現它其實並不難理解,上面的例項4可以理解為“在使用泛型時,可以將型別引數T傳遞給Box型別本身”,結合Oracle給出的官方定義“泛型的本質是型別引數化”會有更深的理解。
在例項5中,在物件宣告和初始化的時候,都指定了型別引數T,在場景一種,T為String;在場景二中,T為Integer。這樣,在場景二中向IntegerBox中傳入String型別的資料“aaaaa”時,程式會報錯。例項6中的泛型集合物件的操作也與之類似,在宣告瞭一個List<String>的boxes物件之後,如果向boxes中傳入Integer物件11111,程式會報錯。
可以看到,通過對於泛型的使用,之前的多業務場景中的問題都得到了解決,因為現在在編譯階段就可以解決之前型別不匹配的問題,而不用等到執行時才暴露問題,只要合理使用泛型,就能在很大程度上規避此類風險。對於泛型的使用,這種引數化型別的作用表面上看是宣告,背後其實是約定。

2、強制型別轉換

再回顧一下例項3,在List型別的boxes物件中存放了兩個物件,分別是String型別的“aaaaa”和Integer型別的11111。其中存在一個問題,在對於boxes的宣告中,使用者不知道boxes的list中到底應該存放什麼型別的物件,而編譯器也不知道集合存放的資料型別,只能通過實際的業務場景來決定這個box是什麼型別,採用將Object強制轉換成String的方式,來達到業務要求的效果。

在使用泛型之後,解決了這種場景下必須進行強制型別轉換的問題。如例項7中,通過泛型宣告,指定集合內元素的型別引數為String型別,這樣編譯器就直接知曉了元素的型別,而無需依靠實際的業務邏輯進行轉換,從而解決了這類型別強制轉換的問題。

3、可讀性和靈活性

泛型除了能進行編譯器型別檢查和規避型別強制轉換外,還能有效地提高程式碼的可讀性。對於例項3,如果不使用泛型,當一個不清楚業務場景的人在對集合進行操作時,無法知道list中儲存的是什麼型別的物件,如果使用了泛型,就能夠通過其型別引數判斷出當前的業務場景,也增加了程式碼的可讀性,同時也可以大膽地在抽象繼承的基礎上進行開發了。
泛型使用上的靈活性體現在很多方面,因為它本身實質上就是對於繼承在使用上的一種增強。因為泛型在具體工作時,當編譯器在編譯原始碼的時候,首先要進行泛型型別引數的檢查,檢查出型別不匹配等問題,然後進行型別擦除並同時在型別引數出現的位置插入強制轉換指令,從而實現泛型。
除了上述的基礎用法之外,泛型還有幾種特殊的高階用法:

萬用字元的設計存在一定的場景,例如在使用泛型後,首先宣告瞭一個Animal的類,而後宣告瞭一個繼承自Animal類的Cat類,顯然Cat類是Animal類的子類,但是List<Cat>卻不是List<Animal>的子型別,而在程式中往往需要表達這樣的邏輯關係。為了解決這種類似的場景,在泛型的引數型別的基礎上新增了萬用字元的用法,具體來說有三種用法:<? extends T>、<? super T>、<?>。其中前兩者被稱為限定萬用字元,<?>被稱為非限定萬用字元。
1、<? extends T> 上界萬用字元
上界萬用字元顧名思義,<? extends T>表示的是型別的上界(包含自身),因此通配的引數化型別可能是T或T的子類。正因為無法確定具體的型別是什麼,add方法受限(可以新增null,因為null表示任何型別),但可以從列表中獲取元素後賦值給父型別。如上圖中的第一個例子,第三個add()操作會受限,原因在於List<Animal>和List<Cat>是List<? extends Animal>的子型別。

2、<? super T> 下界萬用字元
下界萬用字元<? super T>表示的是引數化型別是T的超型別(包含自身),層層至上,直至Object,編譯器無從判斷get()返回的物件的型別是什麼,因此get()方法受限。但是可以進行add()方法,add()方法可以新增T型別和T型別的子型別,如第二個例子中首先新增了一個Cat型別物件,然後新增了兩個Cat子類型別的物件,這種方法是可行的,但是如果新增一個Animal型別的物件,顯然將繼承的關係弄反了,是不可行的。

3、<?> 無界萬用字元
在理解了上界萬用字元和下界萬用字元之後,其實也自然而然的理解了無界萬用字元。無界萬用字元用<?>表示,?代表了任何的一種型別,能代表任何一種型別的只有null(Object本身也算是一種型別,但卻不能代表任何一種型別,所以List<Object>和List<null>的含義是不同的,前者型別是Object,也就是繼承樹的最上層,而後者的型別完全是未知的)。

二、反射機制

反射是Java語言本身具備的一個重要的動態機制。用一句話來解釋反射的定義:自控制,自描述。即通過反射可以動態的獲取類、屬性、方法的資訊,也能構造物件並控制物件的屬性和行為。

上圖中有一個Apple類,它有兩個構造器、一個屬性和get()、set()兩個行為。在左側的“自描述”中主要是嘗試在動態的過程中藉助反射獲取Apple類的構造器資訊和對應的引數個數、類的屬性資訊和類的方法資訊。其中有一個Class型別,它可以產生Class物件被ClassLoader載入,從而在jvm中實現對它的呼叫。在這段程式中,列印了一些類的資訊、類的屬性資訊和類的方法資訊。在右側的“自控制”的程式碼中,實現了在執行的過程中建立了一些物件並觸發這個物件的一些行為,最後還嘗試對物件的屬性進行賦值。反射的基本使用方法較為簡單,但是這種機制卻增強了Java語言的靈活性。

如上圖所示,非反射的Java類的大致執行流程是:編寫原始檔Apple.java,然後編譯器將其編譯成位元組碼檔案Apple.class,最後載入到jvm中並執行。而採用反射的方式時,編譯器一開始對其型別(編譯型別和動態型別)是一無所知的,只有在執行過後,編譯器才知道其真正的型別。
反射的優勢主要在於兩點:1、在一些場景中,這種“未知型別”實際上大大增強了程式執行時的靈活性,但是其效能會有一些損耗;2、對於物件的型別可以在執行時判斷,這樣的特性實質上是對多型極大地增強,進一步地將上層的抽象與下層的具體實現進行解耦。
這兩點在JDBC Driver中體現的非常明顯,例如上圖中的例項中,JDBC的驅動載入方式是通過反射機制實現的,從而保證執行時可以動態選擇要載入的驅動程式,程式靈活性大大增強。另外,JDBC只是設計了驅動需要實現的介面,並不關心驅動廠商的個數和實現方式,只要安裝統一的規範即可,至於型別的判斷和具體方法的觸發,交給執行期動態判斷即可,這種反射機制的使用淋漓盡致的體現了多型,並且降低了類與類之間的耦合度。

三、註解的使用

註解是在1.5版本引入的,現在已經成為日常程式開發中非常重要的一部分。註解是一種後設資料,本身沒有任何作用,如果要有,必須依附在具體的物件上,在日常使用中最常見的兩個註解是@Override和@Deprecated。
先不考慮註解具體的概念、用法和如何工作等問題,註解與“標籤”的概念十分相似,@Override可以理解為在方法上新增了一個標籤,其代表的就是“這是一個繼承關係中,子類已經重寫的方法。”更進一步理解,這個標籤在某個方法上加上之後,如果父類中沒有該方法,那麼在編譯的時候就會報錯,而且可以解決在繼承場景下一些不留心將方法名拼錯的情況,同時增強了一些程式的可讀性。

如上圖所示,同樣以@Override為例,對註解進行進一步的提取和抽象。具體抽象出了四個方面:首先在作用域方面,它只能作用於子類重寫的方法上;其次在生命週期方面,註解只是在編譯時進行檢查,在編譯結束後便沒有了任何作用;除此之外,在文件支援方面,為例解決可讀性的問題,設計了@Documented的註解,用來表示註解的說明註釋是否包含在JavaDoc中;在層級結構設計方面,設計了@inherited用來表示註解是否可以被子類繼承。

在上圖中定義了一個蘋果描述註解,包含了@Target、@Retention、@Inherited和@Documented四個註解,表示它生命週期是程式執行的宣告週期、可以被子類繼承、文件可以被包含。在設計出這個註解之後,可以將其用在前文中的Apple例項上,如圖中在類和方法上各新增了一個註解,在新增完後,便可以配合反射看到註解的效果,這樣可以更好的加強其自描述的能力和配置的靈活性。

本文作者:汪星人1997

閱讀原文

本文為雲棲社群原創內容,未經允許不得轉載。

相關文章

開發語言 最新文章