一文帶你看懂 Java 的反射機制

點擊上方「藍字」關注咱們java


在運行期識別對象和類的信息有兩種方式:
編程

  1. 「傳統」 RTTI數組

    編譯時就已知全部的類型安全

  2. 反射機制服務器

    運行時發現和使用類的信息微信


1
爲何須要 RTTI


面向對象編程的一個基本目的:代碼只操縱對基類(這裏即 Shape )的引用。所以一般建立一個具體的對象(CircleSquare 或者 Triangle),把它向上轉型成 Shape ,忽略對象的具體類型,而且在後面的程序中使用 Shape 引用來調用在具體對象中被重載的方法。網絡

在把 Shape 對象放入 Stream<Shape> 中時就會進行隱式向上轉型,但在向上轉型的時候也丟失了這些對象的具體類型。stream 而言,它們只是 Shape 對象。app

Stream<Shape> 其實是把放入其中的全部對象都當作 Object ,只是取元素自動將類型轉爲 Shape這也是 RTTI 最基本的使用形式ide

全部類型轉換的正確性檢查都是在運行時。這也正是 RTTI 的含義所在:在運行時,識別一個對象的類型。flex

該例中,類型轉換並不完全:Object 被轉型爲 Shape ,而不是 CircleSquare 或者 Triangle。這是由於目前咱們只能確保這個 Stream<Shape> 保存的都是 Shape

  • 編譯期,stream 和 Java 泛型系統確保放入 stream 的都是 Shape 對象(Shape 子類的對象也可視爲 Shape 的對象),不然編譯器會報錯

  • 運行時,自動類型轉換確保了從 stream 中取出的對象都是 Shape 類型

多態



Shape 對象實際執行的代碼,由引用的具體對象決定。

一般,咱們但願大部分代碼儘量少了解對象具體類型,而是隻與對象家族中的一個通用的高層抽象打交道。這樣,代碼更易讀和維護;設計也更容易實現,更易於理解和修改。

但有時但願知道 Stream<Shape> 裏邊的形狀具體類型。使用 RTTI,便可查詢某個 Shape 引用所指向對象的確切類型。


2
2 Class 對象


2.1 類型信息在運行時的表示


Class 特殊對象完成,它包含了與類有關的信息。實際上,Class 對象就是用來建立該類全部」常規」對象的。Java 使用 Class 對象來實現 RTTI,即使是類型轉換這樣的操做都是用 Class 對象實現的。跟其餘普通對象同樣,咱們也能夠控制它的引用。

類是程序的一部分,每一個類都有一個 Class 對象。每編譯一個新類,就產生一個 Class 對象,即保存在一個同名 .class 文件。
爲了生成這個類的對象,JVM先會調用」類加載器」把該類加載到內存。

類加載器子系統包含一條類加載器鏈,但有且只有一個原生類加載器
原生類加載器加載的是」可信類」(如Java API),一般從本地盤加載。在這條鏈中,一般不須要添加額外的類加載器,可是若是你有特殊需求(例如以某種特殊的方式加載類,以支持 Web 服務器應用,或者經過網絡下載類),也能夠掛載額外的類加載器。

類在第一次被使用時動態加載到JVM,即程序建立第一個對類的靜態成員的引用時。構造器(new 時調用)實質也是靜態方法因此Java 程序不少部分在須要時才加載。

類加載器首先檢查類的 Class 對象是否已加載

  • 還沒有加載,則默認類加載器根據類名查找 .class 文件

  • 類的字節碼被加載後,JVM 會對其進行驗證


一旦某個類的 Class 對象被載入內存,就可用來建立該類的全部對象。


2.2 驗證類加載器的工做方式



  • 結果



可見Class 對象僅在須要的時候纔會被加載,static 初始化是在類加載時進行的。

    
2.3 Class.forName(className)


  • Class 類的靜態方法,可使用其根據類名獲得Class 對象:

上面程序forName() 調用是爲了獲得它產生的「反作用」:若是 Gum 類未被加載,那麼就加載之。因而在加載的過程當中,Gum static 初始化塊也被執行了。


想在運行時使用類型信息,就必須獲得其 Class 對象的引用

使用該方法無需先持有這個類型的對象。

                    2.4 Object.getClass                       


若是已持有類對象,可調用 getClass() 方法來獲取 Class 引用:

這個方法來自根類 Object,它將返回表示該對象實際類型的 Class 對象的引用。產生完整類名。

2.5 Class 類的其餘API


getSimpleName() 產生不帶包名的類名

getCanonicalName() 產生完整類名,除內部類和數組,對類產生的結果與 getName() 相同


isInterface() 判斷某個 Class 對象表明的是否爲接口

Class.getInterfaces() 返回存放 Class 對象的數組,裏面的 Class 對象表示的是那個類實現的接口。

getSuperclass() 獲得父類的 Class 對象,父類的 Class 對象繼續調用可得完整的類繼承結構。

newInstance() 實現「虛擬構造器」:在不知類的確切類型時,建立該類的對象

使用 newInstance() 來建立的類,必須帶無參構造器


3
類字面常量

另外一種生成類對象的引用之法。

形如


優勢


簡單安全,由於編譯時就會檢查(所以沒必要放在 try 中)

相比 forName() 方法調用,效率也更高。



適用範圍



普通類、接口、數組及基本數據類型。


TYPE


對於基本數據類型的包裝類,還有一個字段 TYPE

TYPE 字段是一個引用,指向對應的基本數據類型的 Class 對象

…等價於…
boolean.class Boolean.TYPE
char.class Character.TYPE
byte.class Byte.TYPE
short.class Short.TYPE
int.class Integer.TYPE
long.class Long.TYPE
float.class Float.TYPE
double.class Double.TYPE
void.class Void.TYPE

建議使用 .class 形式,以保持統一。


初始化有效地實現了儘量的「惰性」


  • 使用 .class 語法建立對 Class 對象的引用時,不會自動初始化該 Class 對象

  •  Class.forName() 產生 Class 引用,會當即進行對象的初始化


若是一個 static final 值是「編譯期常量」,那麼不須要初始化所在類就可被讀取。以下:

但只將字段設爲 static  final,並不足以確保這種行爲。例如,對 Initable.staticFinal2 的訪問將強制進行類的初始化,由於它不是一個編譯期常量。以下:

若是一個 static 字段非 final,那麼在對它訪問時,總要求在它被讀取前,先進行連接(爲字段分配存儲空間)和初始化(初始化該存儲空間)








4 泛化的 Class 引用


Class 對象可產生類的實例,包含可做用於這些實例的方法,還包含類的 static 成員,所以咱們說 Class 引用代表了它所指向對象的確切類型:Class 類的一個對象。

Java 設計者將它的類型變得更具體。Java 引入泛型,限定 Class 引用所指向的 Class 對象的類型。

普通的類引用不會產生警告信息,能夠從新賦值任何其餘的 Class 對象

可是當使用泛型限定類引用後,只能指向其聲明的類型,讓編譯器強制執行額外的類型檢查。







放寬限制


這彷佛起做用,由於 Integer 繼承自 Number。如上所見其實不行,由於 IntegerClass 對象並非 NumberClass 對象的子類。

  • 爲了在使用 Class 引用時放鬆限制,使用通配符 ?,表示「任何事物」







使用 Class<?> 代替 Class 


雖然它們等價,且單純使用 Class 不會產生警告。

Class<?> 的好處:表示你並不是是碰巧或者疏忽才使用了一個非具體的類引用,而是故意的。






限定類型的 Class 引用


建立指向限定類型的 Class 引用,須要搭配通配符與 extends ,建立範圍限定。這與僅僅聲明 Class<Number> 是不一樣的!

 Class 引用添加泛型只是爲了提供編譯期類型檢查。而使用普通的 Class 引用,一旦你犯了錯誤,就要到運行時才能發現。


下面的示例使用了泛型語法,它保存了一個類引用,稍後又用 newInstance() 方法產生類的對象:

package typeinfo.toys;
import java.util.function.*;import java.util.stream.*;
class CountedInteger { private static long counter; private final long id = counter++;
@Override public String toString() { return Long.toString(id); }}
public class DynamicSupplier<T> implements Supplier<T> { private Class<T> type; public DynamicSupplier(Class<T> type) { this.type = type; } @Override public T get() { try { return type.newInstance(); } catch (InstantiationException | IllegalAccessException e) { throw new RuntimeException(e); } }
public static void main(String[] args) { Stream.generate( new DynamicSupplier<>(CountedInteger.class)) .skip(10) .limit(5) .forEach(System.out::println); }}

將泛型語法用於 Class 對象時,newInstance() 將返回該對象的確切類型,而不只是Object

然而,這有些受限:


若是你想獲得超類,那編譯器將只容許你聲明超類引用爲「某個類,它是 FancyToy 的超類」,即 Class<? super FancyToy>不會接受Class<Toy

看上去很怪,由於 getSuperClass() 返回的是基類,編譯器在編譯期就知道它的類型( Toy.class),而不只僅只是」某個類」。

正是這種含糊性,up.newInstance 的返回值不是精確類型,而是 Object


掃碼二維碼

獲取更多精彩

JavaEdge


好文!必須在看

本文分享自微信公衆號 - JavaEdge(Java-Edge)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索