Java編程思想 類型信息總結

運行時類型信息使得你可以在程序運行時發現和使用類型信息

爲什麼需要RTTI
在這裏插入圖片描述
在這裏插入圖片描述
在這個例子中 當把Shape對象放入List的數組時會向上轉型 但在向上轉型爲Shape的時候也丟失了Shape對象的具體類型 對於數組而言 它們只是Shape類的對象
當從數組中取出元素時 這種容器——實際上它將所有的事物都當作Object持有——會自動將結果轉型回Shape 這是RTTI最基本的使用形式 因爲在Java中 所有的類型轉換都是在運行時進行正確性檢查的 這也是RTTI最基本的使用形式 因爲在Java中 所有的類型轉換都是在運行時進行正確性檢查的 這也是RTTI名字的含義 在運行時 識別一個對象的類型
在這個例子中 RTTI類型轉換並不徹底 Object被轉型爲Shape 而不是轉型爲Circle Square或者Triangle 這是因爲目前我們只知道這個List保存的都是Shape 在編譯時 將由容器和Java的泛型系統來強制確保這一點 而在運行時 由類型轉換操作來確保這一點
接下來就是多態機制的事情了 Shape對象實際執行什麼樣的代碼 是由引用所指向的具體對象Circle Square或者Triangle而決定的 通常 也正是這樣要求的 你希望大部分代碼儘可能少地瞭解對象的具體類型 而是隻與對象家族中的一個通用表示打交道(在這個例子中是Shape) 這樣代碼會更容易寫 更容易讀 且更便於維護 設計也更容易實現 理解和改變 所以 多態 是面向對象編程的基本目標

Class對象
要理解RTTI在Java中的工作原理 首先必須知道類型信息在運行時是如何表示的 這項工作是由稱爲Class對象的特殊對象完成的 它包含了與類有關的信息 事實上 Class對象就是用來創建類的所有的 常規 對象的 Java使用Class對象來執行其RTTI 即使你正在執行的是類似轉型這樣的操作 Class類還擁有大量的使用RTTI的其他方式
類是程序的一部分 每個類都有一個Class對象 換言之 每當編寫並且編譯了一個新類 就會產生一個Class對象(更恰當地說 是被保存在一個同名的.class文件中) 爲了生成這個類的對象 運行這個程序的Java虛擬機(JVM)將使用被稱爲 類加載器 的子系統

一旦某個類的Class對象被載入內存 它就被用來創建這個類的所有對象 下面的示範程序可以證明這一點
在這裏插入圖片描述
在這裏插入圖片描述

Class.forName(“Gum”);
這個方法是Class類(所有Class對象都屬於這個類)的一個static成員 Class對象就和其他對象一樣 我們可以獲取並操作它的引用(這也就是類加載器的工作) forName()是取得Class對象的引用的一種方法 它是用一個包含目標類的文本名(注意拼寫和大小寫)的String作輸入參數 返回的是一個Class對象的引用 上面的代碼忽略了返回值 對forName()的調用是爲了它產生的 副作用 如果類Gum還沒有被加載就加載它 在加載的過程中 Gum的static子句被執行

無論何時 只要你想在運行時使用類型信息 就必須首先獲得對恰當的Class對象的引用 Class.forName()就是實現此功能的便捷途徑 因爲你不需要爲了獲得Class引用而持有該類型的對象 但是 如果你已經擁有了一個感興趣的類型的對象 那就可以通過調用getClass()方法來獲取Class引用了 這個方法屬於根類Object的一部分 它將返回表示該對象的實際類型的Class引用 Class包含很多有用的方法 下面是其中的一部分
在這裏插入圖片描述
在這裏插入圖片描述

類字面常量
Java還提供了另一種方法來生成對Class對象的引用 即使用類字面常量 對上述程序來說 就像下面這樣
FancyToy.class;
這樣做不僅更簡單 而且更安全 因爲它在編譯時就會受到檢查(因此不需要置於try語句塊中) 並且它根除了對forName()方法的調用 所以也更高效
類字面常量不僅可以應用於普通的類 也可以應用於接口 數組以及基本數據類型 另外 對於基本數據類型的包裝器類 還有一個標準字段TYPE TYPE字段是一個引用 指向對應的基本數據類型的Class對象 如下所示
在這裏插入圖片描述
建議使用 .class 的形式 以保持與普通類的一致性
注意 有一點很有趣 當使用 .class 來創建對Class對象的引用時 不會自動地初始化該Class對象 爲了使用類而做的準備工作實際包含三個步驟

  1. 加載 這是由類記載器執行的 該步驟將查找字節碼(通常在classpath所指定的路徑中查找 但這並非是必需的) 並從這些字節碼中創建一個Class對象
  2. 鏈接 在鏈接階段將驗證類中的字節碼 爲靜態域分配存儲空間 並且如果必需的話 將解析這個類創建的對其他類的所有引用
  3. 初始化 如果該類具有超類 則對其初始化 執行靜態初始化器和靜態初始化塊
    初始化被延遲到了對靜態方法(構造器隱式地是靜態的)或者非常數靜態域進行首次引用時才執行
    在這裏插入圖片描述
    在這裏插入圖片描述
    在這裏插入圖片描述

泛化的Class引用
Class引用總是指向某個Class對象 它可以製造類的實例 幷包含可作用於這些實例的所有方法代碼 它還包含該類的靜態成員 因此 Class引用表示的就是它所指向的對象的確切類型 而該對象便是Class類的一個對象
但是 Java SE5的設計者們看準機會 將它的類型變得更具體了一些 而這是通過允許你對Class引用所指向的Class對象的類型進行限定而實現的 這裏用到了泛型語法 在下面的實例中 兩種語法都是正確的
在這裏插入圖片描述

爲了在使用泛化的Class引用時放鬆限制 我使用了通配符 它是Java泛型的一部分 通配符就是 ? 表示 任何事物 因此 我們可以在上例的普通Class引用中添加通配符 併產生相同的結果
在這裏插入圖片描述
在這裏插入圖片描述
爲了創建一個Class引用 它被限定爲某種類型 或該類型的任何子類型 你需要將通配符與extends關鍵字相結合 創建一個範圍 因此 與僅僅聲明Class不同 現在做如下聲明
在這裏插入圖片描述

下面的示例使用了泛型類語法 它存儲了一個類引用 稍後又產生了一個List 填充這個List的對象是使用newInstance()方法 通過該引用生成的
在這裏插入圖片描述

當你將泛型語法用於Class對象時 會發生一件很有趣的事情 newInstance()將返回該對象的確切類型 而不僅僅只是在ToyTest.java中看到的基本的Object 這在某種程度上有些受限
在這裏插入圖片描述

新的轉型語法
Java SE5還添加了用於Class引用的轉型語法 即cast()方法
在這裏插入圖片描述

類型轉換前先做檢查
迄今爲止 我們已知的RTTI形式包括

  1. 傳統的類型轉換 如 (Shape) 由RTTI確保類型轉換的正確性 如果執行了一個錯誤的類型轉換 就會拋出一個ClassCastException異常
  2. 代表對象的類型的Class對象 通過查詢Class對象可以獲取運行時所需的信息
    RTTI在Java中還有第三種形式 就是關鍵字instanceof 它返回一個布爾值 告訴我們對象是不是某個特定類型的實例 可以用提問的方式使用它 就像這樣
    if(x instanceof Dog)
    ((Dog)x).bark();

一般 可能想要查找某種類型(比如要找三角形 並填充成紫色) 這時可以輕鬆地使用instanceof來計數所有對象 例如 假設你有一個類的繼承體系 描述了Pet(以及它們的主人 這是在後面的示例中出現的一個非常方便的特性) 在這個繼承體系中的每個Individual都有一個id和一個可選的名字 正如你可以看到的 此處並不需要去了解Individual的代碼——你只需瞭解可以創建其具名或不具名的對象 並且每個Individual都有一個id()方法 可以返回其唯一的標識符(通過對每個對象計數而創建的) 還有一個toString()方法 如果你沒有爲Individual提供名字 toString()方法只產生類型名
下面是繼承自Individual的類繼承體系
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
接下來 我們需要一種方法 通過它可以隨機地創建不同類型的寵物 並且爲方便起見 還可以創建寵物數組和List 爲了使該工具能夠適應多種不同的實現 我們將其定義爲抽象類
在這裏插入圖片描述

當你導出PetCreator的子類時 唯一所需提供的就是你希望使用randomPet()和其他方法來創建的寵物類型的List getTypes()方法通常只返回對一個靜態List的引用 下面是使用forName()的一個具體實現
在這裏插入圖片描述

爲了對Pet進行計數 我們需要一個能夠跟蹤各種不同類型的Pet的數量的工具 Map是此需求的首選 其中鍵是Pet類型名 而值是保存Pet數量的Integer 通過這種方式 你可以詢問 有多少個Hamster對象 我們可以使用instanceof來對Pet進行計數
在這裏插入圖片描述
在這裏插入圖片描述

對instanceof有比較嚴格的限制:只可將其與命名類型進行比較 而不能與Class對象作比較 在前面的例子中 可能覺得寫出那麼一大堆instanceof表達式是很乏味的 的確如此 但是也沒有辦法讓instanceof聰明起來 讓它能夠自動地創建一個Class對象的數組 然後將目標對象與這個數組中的對象進行逐一的比較 其實這並非是一種如你想象中那般好的限制 如果程序中編寫了許多的instanceof表達式 就說明你的設計可能存在瑕疵

使用類字面常量
如果我們用類字面常量重新實現PetCount 那麼改寫後的結果在許多方面都會顯得更加清晰
在這裏插入圖片描述
在這裏插入圖片描述

我們現在在typeinfo.pets類庫中有了兩種PetCreator的實現 爲了將第二種實現作爲默認實現 我們可以創建一個使用了LiteralPetCreator的外觀
在這裏插入圖片描述

因爲PetCount.countPets()接受的是一個PetCreator參數 我們可以很容易地測試LiteralPetCreator(通過上面的外觀)
在這裏插入圖片描述

動態的instanceof
Class.isInstance方法提供了一種動態地測試對象的途徑 於是所有那些單調的instanceof語句都可以從PetCount.java的例子中移除了 如下所示
在這裏插入圖片描述

遞歸計數
在PetCount3.PetCount中的Map預加載了所有不同的Pet類 與預加載映射表不同的是 我們可以使用Class.isAssignableFrom() 並創建一個不侷限於對Pet計數的通用工具
在這裏插入圖片描述
count()方法獲取其參數的Class 然後使用isAssignableFrom()來執行運行時的檢查 以效驗你傳遞的對象確實屬於我們感興趣的繼承結構 countClass()首先對該類的確切類型計數 然後 如果其超類可以賦值給baseType countClass()將其超類上遞歸計數
在這裏插入圖片描述
在這裏插入圖片描述

註冊工廠
生成Pet繼承結構中的對象存在着一個問題 即每次向該繼承結構添加新的Pet類型時 必須將其添加爲LiteralPetCreator.java中的項 如果在系統中已經存在了繼承結構的常規的基礎 然後在其上要添加更多的類 那麼就有可能會出現問題

這裏我們需要做的其他修改就是使用工廠方法設計模式 將對象的創建工作交給類自己去完成 工廠方法可以被多態地調用 從而爲你創建恰當類型的對象 在下面這個非常簡單的版本中 工廠方法就是Factory接口中的create()方法
在這裏插入圖片描述
泛型參數T使得create()可以在每種Factory實現中返回不同的類型 這也充分利用了協變返回類型
在下面的示例中 基類Part包含一個工廠對象的列表 對應於這個由createRandom()方法產生的類型 它們的工廠都被添加到了partFactories List中 從而被註冊到了基類中
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

instanceof與Class的等價性
在查詢類型信息時 以instanceof的形式(即以instanceof的形式或isInstance()的形式 它們產生相同的結果) 與直接比較Class對象有一個很重要的差別 下面的例子展示了這種差別
在這裏插入圖片描述
在這裏插入圖片描述

反射:運行時的類信息
如果不知道某個對象的確切類型 RTTI可以告訴你 但是有一個限制 這個類型在編譯時必須已知 這樣才能使用RTTI識別它 並利用這些信息做一些有用的事 換句話說 在編譯時 編譯器必須知道所有要通過RTTI來處理的類

Class類與java.lang.reflect類庫一起對反射的概念進行了支持 該類庫包含了Field Method以及Constructor類(每個類都實現了Member接口) 這些類型的對象是由JVM在運行時創建的 用以表示未知類裏對應的成員 這樣你就可以使用Constructor創建新的對象 用get()和set()方法讀取和修改與Field對象關聯的字段 用invoke()方法調用與Method對象關聯的方法 另外 還可以調用getFields() getMethods()和getConstructor()等很便利的方法 以返回表示字段 方法以及構造器的對象的數組(在JDK文檔中 通過查找Class類可瞭解更多相關資料) 這樣 匿名對象的類信息就能在運行時被完全確定下來 而在編譯時不需要知道任何事情
重要的是 要認識到反射機制並沒有什麼神奇之處 當通過反射與一個未知類型的對象打交道時 JVM只是簡單地檢查這個對象 看它屬於哪個特定的類(就像RTTI那樣) 在用它做其他事情之前必須先加載那個類的Class對象 因此 那個類的.class文件對於JVM來說必須是可獲取的 要麼在本地機器上 要麼可以通過網絡取得 所以RTTI和反射之間真正的區別只在於 對RTTI來說 編譯器在編譯時打開和檢查.class文件 (換句話說 我們可以用 普通 方式調用對象的所有方法) 而對於反射機制來說 .class文件在編譯時是不可獲取的 所以是在運行時打開和檢查.class文件

類方法提取器
通常你不需要直接使用反射工具 但是它們在你需要創建更加動態的代碼時會很有用 反射在Java中是用來支持其他特性的 例如對象序列化和JavaBean 但是 如果能動態地提取某個類的信息有的時候還是很有用的 請考慮類方法提取器 瀏覽實現了類定義的源代碼或是其JDK文檔 只能找到在這個類定義中被定義或被覆蓋的方法 但對你來說 可能有數十個更有用的方法都是繼承自基類的 要找出這些方法可能會很乏味且費時 幸運的是 反射機制提供了一種方法 使我們能夠編寫可以自動展示完整接口的簡單工具 下面就是其工作方式
在這裏插入圖片描述
在這裏插入圖片描述

上面的輸出是從下面的命令行產生的
java ShowMethods ShowMethods
你可以看到 輸出中包含一個public的默認構造器 即便能在代碼中看到根本沒有定義任何構造器 所看到的這個包含在列表中的構造器是編譯器自動合成的 如果將ShowMethods作爲一個非public的類(也就是擁有包訪問權限) 輸出中就不會再顯示出這個自動合成的默認構造器了 該自動合成的默認構造器會自動被賦予與類一樣的訪問權限

動態代理
代理是基本的設計模式之一 它是你爲了提供額外的或不同的操作 而插入的用來代替 實際 對象的對象 這些操作通常涉及與 實際 對象的通信 因此代理通常充當着中間人的角色 下面是一個用來展示代理結構的簡單示例
在這裏插入圖片描述
在這裏插入圖片描述

Java的動態代理比代理的思想更向前邁進了一步 因爲它可以動態地創建代理並動態地處理對所代理方法的調用 在動態代理上所做的所有調用都會被重定向到單一的調用處理器上 它的工作是揭示調用的類型並確定相應的對策 下面是用動態代理重寫的SimpleProxyDemo.java
在這裏插入圖片描述
在這裏插入圖片描述

通常 你會執行被代理的操作 然後使用Method.invoke()將請求轉發給被代理對象 並傳入必須的參數 這初看起來可能有些受限 就像你只能執行泛化操作一樣 但是 你可以通過傳遞其他的參數 來過濾某些方法調用
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

空對象
當你使用內置的null表示缺少對象時 在每次使用引用時都必須測試其是否爲null 這顯得枯燥 而且勢必產生相當乏味的代碼 問題在於null除了在你試圖用它執行任何操作來產生NullPointerException之外 它自己沒有其他任何行爲 有時引入空對象 的思想將會很有用 它可以接受傳遞給它的所代表的對象的消息 但是將返回表示爲實際上並不存在任何 真實 對象的值 通過這種方式 你可以假設所有的對象都是有效的 而不必浪費編程精力去檢查null(並閱讀所產生的代碼)
儘管想象一種可以自動爲我們創建空對象的編程語言顯得很有趣 但是實際上 到處使用空對象並沒有任何意義——有時檢查null就可以了 有時你可以合理地假設你根本不會遇到null 有時甚至通過NullPointerException來探測異常也可以接受的 空對象最有用之處在於它更靠近數據 因爲對象表示的是問題空間內的實體 有一個簡單的例子 許多系統都有一個Person類 而在代碼中 有很多情況是你沒有一個實際的人(或者你有 但是你還沒有這個人的全部信息) 因此 通常你會使用一個null引用並測試它 與此不同的是 我們可以使用空對象 但是即使空對象可以相應 實際 對象可以響應的所有消息 你仍需要某種方式去測試其是否爲空 要達到此目的 最簡單的方式是創建一個標記接口
在這裏插入圖片描述
這使得instanceof可以探測空對象 更重要的是 這並不要求你在所有的類中都添加isNull()方法(畢竟 這只是執行RTTI的一種不同方式——爲什麼不使用內置的工具呢)
在這裏插入圖片描述
通常 空對象都是單例 因此這裏將其作爲靜態final實例創建 這可以正常工作的 因爲Person是不可變的——你只能在構造器中設置它的值 然後讀取這些值 但是你不能修改它們(因爲String自身具備內在的不可變性) 如果你想要修改一個NullPerson 那隻能用一個新的Person對象來替換它 注意 你可以選擇使用instanceof來探測泛化的Null還是更具體的NullPerson 但是由於使用了單例方式 所以你還可以只使用equals()甚至==來與Person.Null比較
現在假設你回到了互聯網剛出現時的雄心萬丈的年代 並且你已經因你驚人的理念而獲得了一大筆的風險投資 你現在要招兵買馬了 但是在虛位以待時 你可以將Person空對象放在每個Position上
在這裏插入圖片描述
有了Position 你就不需要創建空對象了 因爲Person.Null的存在就表示這是一個空Position 稍後 你可能會發現需要增加一個顯式的用於Position的空對象 但是YAGNI(You Aren’t Going to Need It 你永不需要它)聲明 在你的設計草案的初稿中 應該努力使用最簡單且可以工作的事物 直至程序的某個方面要求你添加額外的特性 而不是一開始就假設它是必須的
Staff類現在可以在你填充職位時查詢空對象
在這裏插入圖片描述
在這裏插入圖片描述
注意 你在某些地方仍必須測試空對象 這與檢查是否爲null沒有差異 但是在其他地方(例如本例中的toSting()轉換) 你就不必執行額外的測試了 而可以直接假設所有對象都是有效的
如果你用接口取代具體類 那麼就可以使用DynamicProxy來自動地創建空對象 假設我們有一個Robot接口 它定義了一個名字 一個模型和一個描述Robot行爲能力的List Operation包含一個描述和一個命令(這是一種命令模式類型)
在這裏插入圖片描述
你可以通過調用operations()來訪問Robot的服務
在這裏插入圖片描述
在這裏插入圖片描述
這裏也使用了嵌套類來執行測試
我們現在可以創建一個掃雪Robot
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
假設存在許多不同類型的Robot 我們想對每一種Robot類型都創建一個空對象 去執行某些特殊操作——在本例中 即提供空對象所代表的Robot確切類型的信息 這些信息是通過動態代理捕獲的
在這裏插入圖片描述
在這裏插入圖片描述
無論何時 如果你需要一個空Robot對象 只需調用newNullRobot() 並傳遞需要代理的Robot的類型 代理會滿足Robot和Null接口的需求 並提供它所代理的類型的確切名字

模擬對象與樁
空對象的邏輯變體是模擬對象和樁 與空對象一樣 它們都表示在最終的程序中使用的 實際 對象 但是 模擬對象和樁都只是假扮可以傳遞實際信息的存貨對象 而不是像空對象那樣可以成爲null的一種更加智能化的替代物
模擬對象和樁之間的差異在於程度不同 模擬對象往往是輕量級和自測試的 通常很多模擬對象被創建出來是爲了處理各種不同的測試情況 樁只是返回樁數據 它通常是重量級的 並且經常在測試之間被複用 樁可以根據它們被調用的方式 通過配置進行修改 因此樁是一種複雜對象 它要做很多事 然而對於模擬對象 如果你需要做很多事情 通常會創建大量小而簡單的模擬對象

接口與類型信息
interface關鍵字的一種重要目標就是允許程序員隔離構件 進而降低耦合性 如果你編寫接口 那麼就可以實現這一目標 但是通過類型信息 這種耦合性還是會傳播出去——接口並非是對解耦的一種無懈可擊的保障 下面有一個示例 先是一個接口
在這裏插入圖片描述
然後實現這個接口 你可以看到其代碼是如何圍繞着實際的實現類型潛行的
在這裏插入圖片描述
通過使用RTTI 我們發現a是被當作B實現的 通過將其轉型爲B 我們可以調用不在A中的方法
這是完全和法和可接受的 但是你也許並不想讓客戶端程序員這麼做 因爲這給了他們一個機會 使得他們的代碼與你的代碼的耦合程度超過你的期望 也就是說 你可能認爲interface關鍵字正在保護着你 但是它並沒有 在本例中使用B來實現A這一事實是公開有案可查的
一種解決方案是直接聲明 如果程序員決定使用實際的類而不是接口 他們需要自己對自己負責 這在很多情況下可能都是合理的 但 可能 還不夠 你也許希望應用一些更嚴苛的控制
最簡單的方式是對實現使用包訪問權限 這樣在包外部的客戶端就不能看到它了
在這裏插入圖片描述
在這個包中唯一public的部分 即HiddenC 在被調用時將產生A接口類型的對象 這裏有趣之處在於 即使你從makeA()返回的是C類型 你在包的外部仍舊不能使用A之外的任何方法 因爲你不能在包的外部命名C
現在如果你試圖將其向下轉型爲C 則將被禁止 因爲在包的外部沒有任何C類型可用
在這裏插入圖片描述
在這裏插入圖片描述
正如你所看到的 通過使用反射 仍舊可以到達並調用所有方法 甚至是private方法 如果知道方法名 你就可以在其Method對象上調用setAccessible(true) 就像在callHiddenMethod()中看到的那樣
你可能會認爲 可以通過只發布編譯後的代碼來阻止這種情況 但是這並不解決問題 因爲只需運行javap 一個隨JDK發佈的反編譯器即可突破這一限制 下面是一個使用它的命令行
javap -private C
-private標誌表示所有的成員都應該表示 甚至包括私有成員 下面是輸出
在這裏插入圖片描述
因此任何人都可以獲取你最私有的方法的名字和簽名 然後調用它們
如果你將接口實現爲一個私有內部類 又會怎樣呢 下面展示了這種情況
在這裏插入圖片描述
在這裏插入圖片描述
這裏對反射仍舊沒有隱藏任何東西 那麼如果是匿名類呢
在這裏插入圖片描述
看起來沒有任何方式可以阻止反射到達並調用那些非公共訪問權限的方法 對於域來說 的確如此 即便是private域
在這裏插入圖片描述
在這裏插入圖片描述
但是 final域實際上在遭遇修改時是安全的 運行時系統會在不拋異常的情況下接受任何修改嘗試 但是實際上不會發生任何修改
通常 所有這些違反訪問權限的操作並非世上最遭之事 如果有人使用這樣的技術去調用標識爲private或包訪問權限的方法(很明顯這些訪問權限表示這些人不應該調用它們) 那麼對他們來說 如果你修改了這些方法的某些方面 他們不應該抱怨 另一方面 總是在類中留下後門的這一事實 也許可以使得你能夠解決某些特定類型的問題 但如果不這樣做 這些問題將難以或者不可能解決 通常反射帶來的好處是不可否認的

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章