Java編程思想 泛型總結

一般的類和方法 只能使用具體的類型 要麼是基本類型 要麼是自定義的類 如果要編寫可以應用於多種類型的代碼 這種刻板的限制對代碼的束縛就會很大

與C++的比較
Java中的泛型就需要與C++進行一番比較 理由有二 首先 瞭解C++模板的某些方面 有助於你理解泛型的基礎 同時 非常重要的一點是 你可以瞭解Java泛型的侷限是什麼 以及爲什麼會有這些限制 最終的目的是幫助你理解 Java泛型的邊界在哪裏 根據我的經驗 理解了邊界所在 你才能成爲程序高手 因爲只有知道了某個技術不能做到什麼 你才能更好地做到所能做的(部分原因是 不必浪費時間在死衚衕裏亂轉)
第二個原因是 在Java社區中 人們普遍對C++模板有一種誤解 而這種誤解可能會誤導你 令你在理解泛型的意圖時產生偏差

簡單泛型
有許多原因促成了泛型的出現 而最引人注目的一個原因 就是爲了創造容器類 容器 就是存放要使用的對象的地方 數組也是如此 不過與簡單的數組相比 容器類更加靈活 具備更多不同的功能 事實上 所有的程序 在運行時都要求你持有一大堆對象 所以 容器類算得上最具重用性的類庫之一
我們先來看看一個只能持有單個對象的類 當然了 這個類可以明確指定其持有的對象的類型
在這裏插入圖片描述
不過 這個類的可重用性就不怎麼樣了 它無法持有其他類型的任何對象 我們可不希望爲碰到的每個類型都編寫一個新的類
在Java SE5之前 我們可以讓這個類直接持有Object類型的對象
在這裏插入圖片描述
現在 Holder2可以存儲任何類型的對象 在這個例子中 只用了一個Holder2對象 卻先後三次存儲了 三種不同類型的對象
有些情況下 我們確實希望容器能夠同時持有多種類型的對象 但是 通常而言 我們只會使用容器來存儲一種類型的對象 泛型的主要目的之一就是用來指定容器要持有什麼類型的對象 而且由編譯器來保證類型的正確性
因此 與其使用Object 我們更喜歡暫時不指定類型 而是稍後再決定具體使用什麼類型 要達到這個目的 需要使用類型參數 用尖括號括住 放在類名後面 然後在使用這個類的時候 再用實際的類型替換此類型參數 在下面的例子中 T就是類型參數
在這裏插入圖片描述
現在 當你創建Holder3對象時 必須指明想持有什麼類型的對象 將其置於尖括號內 就像main()中那樣 然後 你就只能在Holder3中存入該類型(或其子類 因爲多態與泛型不衝突)的對象了 並且 在你從Holder3中取出它持有的對象時 自動地就是正確的類型
這就是Java泛型的核心概念 告訴編譯器想使用什麼類型 然後編譯器幫你處理一切細節
一般而言 你可以認爲泛型與其他的類型差不多 只不過它們碰巧有類型參數罷了

一個元組類庫
僅一次方法調用就能返回多個對象 你應該經常需要這樣的功能吧 可是return語句只允許返回單個對象 因此 解決辦法就是創建一個對象 用它來持有想要返回的多個對象 當然 可以在每次需要的時候 專門創建一個類來完成這樣的工作 可是有了泛型 我們就能夠一次性地解決該問題 以後再也不用再這個問題上浪費時間了 同時 我們在編譯期就能確保類型安全
這個概念稱爲元祖(tuple) 它是將一組對象直接打包存儲於其中的一個單一對象 這個容器對象允許讀取其中元素 但是不允許向其中存放新的對象(這個概念也稱爲數據傳送對象 或信使)
通常 元組可以具有任意長度 同時 元組中的對象可以是任意不同的類型 不過 我們希望能夠爲每一個對象指明其類型 並且從容器中讀取出來時 能夠得到正確的類型 要處理不同長度的問題 我們需要創建多個不同的元組 下面的程序是一個2維元組 它能夠持有兩個對象
在這裏插入圖片描述
在這裏插入圖片描述

我們可以利用繼承機制實現長度更長的元組 從下面的例子中可以看到 增加類型參數是件很簡單的事情
在這裏插入圖片描述
在這裏插入圖片描述
爲了使用元組 你只需定義一個長度適合的元組 將其作爲方法的返回值 然後在return語句中創建該元組 並返回即可
在這裏插入圖片描述

一個堆棧類
接下來我們看一個稍微複雜一點的例子 傳統的下推堆棧
現在我們不用LinkedList 來實現自己的內部鏈式存儲機制
在這裏插入圖片描述
內部類Node也是一個泛型 它擁有自己的類型參數
這個例子使用了一個末端哨兵(end sentinel)來判斷堆棧何時爲空 這個末端哨兵是在構造LinkedStack時創建的 然後 每調用一次push()方法 就會創建一個Node對象 並將其鏈接到前一個Node對象 當你調用pop()方法時 總是返回top.item 然後丟棄當前top所指的Node 並將top轉移到下一個Node 除非你已經碰到了末端哨兵 這時候就不再移動top了 如果已經到了末端 客戶端程序還繼續調用pop()方法 它只能得到null 說明堆棧已經空了

RandomList
作爲容器的另一個例子 假設我們需要一個持有特定類型對象的列表 每次調用其上的select()方法時 它可以隨機地選取一個元素 如果我們希望以此構建一個可以應用於各種類型的對象的工具 就需要使用泛型
在這裏插入圖片描述
在這裏插入圖片描述

泛型接口
泛型也可以應用於接口 例如生成器(generator) 這是一種專門負責創建對象的類 實際上 這是工廠方法設計模式的一種應用 不過 當使用生成器創建新的對象時 它不需要任何參數 而工廠方法一般需要參數 也就是說 生成器無需額外的信息就知道如何創建新對象
一般而言 一個生成器只定義一個方法 該方法用以產生新的對象 在這裏 就是next()方法
在這裏插入圖片描述
方法next()的返回類型是參數化的T 正如你所見到的 接口使用泛型與類使用泛型沒什麼區別
爲了演示如何實現Generator接口 我們還需要一些別的類 例如 Coffee類層次結構如下
在這裏插入圖片描述
在這裏插入圖片描述
現在 我們可以編寫一個類 實現Generator接口 它能夠隨機生成不同類型的Coffee對象
在這裏插入圖片描述
在這裏插入圖片描述
參數化的Generator接口確保next()的返回值是參數的類型 CoffeeGenerator同時還實現了Iterable接口 所以它可以在循環語句中使用 不過 它還需要一個 末端哨兵 來判斷何時停止 這正是第二個構造器的功能
下面的類是Generator接口的另一個實現 它負責生成Fibonacci數列
在這裏插入圖片描述

如果還想更進一步 編寫一個實現了Iterable的Fibonacci生成器 我們的一個選擇是重寫這個類 令其實現Iterable接口 不過 你並不是總能擁有源代碼的控制權 並且 除非必須這麼做 否則 我們也不願意重寫一個類 而且我們還有另一種選擇 就是創建一個適配器(adapter)來實現所需的接口
有多種方法可以實現適配器 例如 可以通過繼承來創建適配器類
在這裏插入圖片描述
在這裏插入圖片描述
如果要在循環語句中使用IterableFibonacci 必須向IterableFibonacci的構造器提供一個邊界值 然後hasNext()方法才能知道何時應該返回false

泛型方法
泛型方法使得該方法能夠獨立於類而產生變化 以下是一個基本的指導原則 無論何時 只要你能做到 你就應該儘量使用泛型方法 也就是說 如果使用泛型方法可以取代將整個類泛型化 那麼就應該只使用泛型方法 因爲它可以使事情更清楚明白 另外 對於一個static的方法而言 無法訪問泛型類的類型參數 所以 如果static方法需要使用泛型能力 就必須使其成爲泛型方法
要定義泛型方法 只需將泛型參數列表置於返回值之前 就像下面這樣
在這裏插入圖片描述

注意 當使用泛型類時 必須在創建對象的時候指定類型參數的值 而使用泛型方法的時候 通常不必指明參數類型 因爲編譯器會爲我們找出具體的類型 這稱爲類型參數推斷(typeargument inference) 因此 我們可以像調用普通方法一樣調用f() 而且就好像是f()被無限次地重載過 它甚至可以接受GenericMethods作爲其類型參數

槓桿利用類型參數推斷
人們對泛型有一個抱怨 使用泛型有時候需要向程序中加入更多的代碼 如果要創建一個持有List的Map 就要像下面這樣
在這裏插入圖片描述
看到了吧 你在重複自己做過的事情 編譯器本來應該能夠從泛型參數列表中的一個參數推斷出另一個參數 唉 可惜的是 編譯器暫時還做不到 然而 在泛型方法中 類型參數推斷可以爲我們簡化一部分工作 例如 我們可以編寫一個工具類 它包含各種各樣的static方法 專門用來創建各種常用的容器對象
在這裏插入圖片描述
main()方法演示瞭如何使用這個工具類 類型參數推斷避免了重複的泛型參數列表
在這裏插入圖片描述

類型推斷只對賦值操作有效 其他時候並不起作用 如果你將一個泛型方法調用的結果(例如New.map()) 作爲參數 傳遞給另一個方法 這時編譯器並不會執行類型推斷 在這種情況下 編譯器認爲 調用泛型方法後 其返回值被賦給一個Object類型的變量 下面的例子證明了這一點
在這裏插入圖片描述

顯式的類型說明
在泛型方法中 可以顯式地指明類型 不過這種語法很少使用 要顯式地指明類型 必須在點操作符與方法名之間插入尖括號 然後把類型置於尖括號內 如果是在定義該方法的類的內部 必須在點操作符之前使用this關鍵字 如果是使用static的方法 必須在點操作符之前加上類名
在這裏插入圖片描述

可變參數與泛型方法
泛型方法與可變參數列表能夠很好地共存
在這裏插入圖片描述
在這裏插入圖片描述

用於Generator的泛型方法
利用生成器 我們可以很方便地填充一個Collection 而泛型化這種操作是具有實際意義的
在這裏插入圖片描述

一個通用的Generator
下面的程序可以爲任何類構造一個Generator 只要該類具有默認的構造器 爲了減少類型聲明 它提供了一個泛型方法 用以生成BasicGenerator
在這裏插入圖片描述
在這裏插入圖片描述
這個類提供了一個基本實現 用以生成某個類的對象 這個類必須具備兩個特點

  1. 它必須聲明爲public(因爲BasicGenerator與要處理的類在不同的包中 所以該類必須聲明爲public 並且不只具有包內訪問權限)
  2. 它必須具備默認的構造器(無參數的構造器) 要創建這樣的BasicGenerator對象 只需調用create()方法 並傳入想要生成的類型 泛型化的create()方法允許執行BasicGenerator.create(MyType.class) 而不必執行麻煩的new BasicGenerator(MyType.class)
    例如 下面是一個具有默認構造器的簡單的類
    在這裏插入圖片描述
    CountedObject類能夠記錄下它創建了多少個CountedObject實例 並通過toString()方法告訴我們其編號
    使用BasicGenerator 你可以很容易地爲CountedObject創建一個Generator
    在這裏插入圖片描述
    可以看到 使用泛型方法創建Generator對象 大大減少了我們要編寫的代碼 Java泛型要求傳入Class對象 以便也可以在create()方法中用它進行類型推斷

簡化元組的使用
有了類型參數推斷 再加上static方法 我們可以重新編寫之前看到的元組工具 使其成爲更通用的工具類庫 在這個類中 我們通過重載static方法創建元組
在這裏插入圖片描述
下面是修改後的TupleTest.java 用來測試Tuple.java
在這裏插入圖片描述
注意 方法f()返回一個參數化的TwoTuple對象 而f2()返回的是非參數化的TwoTuple對象 在這個例子中 編譯器並沒有關於f2()的警告信息 因爲我們並沒有將其返回值作爲參數化對象使用 在某種意義上 它被 向上轉型 爲一個非參數化的TwoTuple 然而 如果試圖將f2()的返回值轉型爲參數化的TwoTuple 編譯器就會發出警告

一個Set實用工具
作爲泛型方法的另一個示例 我們看看如何用Set來表達數學中的關係式 通過使用泛型方法 可以很方便地做到這一點 而且可以應用於多種類型
在這裏插入圖片描述
在前三個方法中 都將第一個參數Set複製了一份 將Set中的所有引用都存入一個新的HashSet對象中 因此 我們並未直接修改參數中的Set 返回的值是一個全新的Set對象
這四個方法表達瞭如下的數學集合操作 union()返回一個Set 它將兩個參數合併在一起 intersection()返回的Set只包含兩個參數共有的部分 difference()方法從superset中移除subset包含的元素 complement()返回的Set包含除了交集之外的所有元素 下面提供了一個enum 它包含各種水彩畫的顏色 我們將用它來演示以上這些方法的功能和效果
在這裏插入圖片描述
爲了方便起見(可以直接使用enum中的元素名) 下面的示例以static的方式引入Watercolors 這個示例使用了EnumSet 這是Java SE5中的新工具 用來從enum直接創建Set 在這裏 我們向static方法EnumSet.range()傳入某個範圍的第一個元素與最後一個元素 然後它將返回一個Set 其中包含該範圍內的所有元素
在這裏插入圖片描述
我們可以從輸出中看到各種關係運算的結果
下面的示例使用Sets.difference()打印出java.util包中各種Collection類與Map類之間的方法差異
在這裏插入圖片描述
在這裏插入圖片描述

匿名內部類
泛型還可以應用於內部類以及匿名內部類 下面的示例使用匿名內部類實現了Generator接口
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

構建複雜模型
泛型的一個重要好處是能夠簡單而安全地創建複雜的模型 例如 我們可以很容易地創建List元組
在這裏插入圖片描述

下面是另一個示例 它展示了使用泛型類型來構建複雜模型是多麼的簡單 即使每個類都是作爲一個構建塊創建的 但是其整個還是包含許多部分 在本例中 構建的模型是一個零售店 它包含走廊 貨架和商品
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

擦除的神祕之處
當你開始更深入地鑽研泛型時 會發現有大量的東西初看起來是沒有意義的 例如 儘管可以聲明ArrayList.class 但是不能聲明ArrayList.class 請考慮下面的情況
在這裏插入圖片描述
在這裏插入圖片描述

下面的示例是對這個謎題的一個補充
在這裏插入圖片描述
根據JDK文檔的描述 Class.getTypeParameters()將 返回一個TypeVariable對象數組 表示有泛型聲明所聲明的類型參數 這好像是在暗示你可能發現參數類型的信息 但是 正如你從輸出中所看到的 你能夠發現的只是用作參數佔位符的標識符 這並非有用的信息
因此 殘酷的現實是
在泛型代碼內部 無法獲得任何有關泛型參數類型的信息

Java泛型是使用擦除來實現的 這意味着當你在使用泛型時 任何具體的類型信息都被擦除了 你唯一知道的就是你在使用一個對象 因此List和List在運行時事實上是相同的類型 這兩種形式都被擦除成它們的 原生 類型 即List

C++的方式
下面是使用模板的C++示例 你將注意到用於參數化類型的語法十分相似 因爲Java是受C++的啓發
在這裏插入圖片描述
Manipulator類存儲了一個類型T的對象 有意思的地方是manipulate()方法 它在obj上調用方法f() 它怎麼能知道f()方法是爲類型參數T而存在的呢 當你實例化這個模板時 C++編譯器將進行檢查 因此在Manipulator被實例化的這一刻 它看到HasF擁有一個方法f() 如果情況並非如此 就會得到一個編譯期錯誤 這樣類型安全就得到了保障
用C++編寫這種代碼很簡單 因爲當模板被實例化時 模板代碼知道其模板參數的類型 Java泛型就不同了 下面是HasF的Java版本
在這裏插入圖片描述
如果我們將這個示例的其餘代碼都翻譯成Java 那麼這些代碼將不能編譯
在這裏插入圖片描述
由於有了擦除 Java編譯器無法將manipulate()必須能夠在obj上調用f()這一需求映射到HasF擁有f()這一事實上 爲了調用f() 我們必須協助泛型類 給定泛型類的邊界 以此告知編譯器只能接受遵循這個邊界的類型 這裏重用了extends關鍵字 由於有了邊界 下面的代碼就可以編譯了
在這裏插入圖片描述
邊界聲明T必須具有類型HasF或者從HasF導出的類型 如果情況確實如此 那麼就可以安全地在obj上調用f()了
我們說泛型類型參數將擦除到它的第一個邊界(它可能會有多個邊界) 我們還提到了類型參數的擦除 編譯器實際上會把類型參數替換爲它的擦除 就像上面的示例一樣 T擦除到了HasF 就好像在類的聲明中用HasF替換了T一樣
在Manipulation2.java中 泛型沒有貢獻任何好處 只需很容易地自己去執行擦除 就可以創建出沒有泛型的類
在這裏插入圖片描述
這提出了很重要的一點 只有當你希望使用的類型參數比某個具體類型(以及它的所有子類型)更加 泛化 時——也就是說 當你希望代碼能夠跨多個類工作時 使用泛型纔有所幫助 因此 類型參數和它們在有用的泛型代碼中的應用 通常比簡單的類替換要更復雜 但是 不能因此而認爲形式的任何東西都是有缺陷的 例如 如果某個類有一個返回T的方法 那麼泛型就有所幫助 因爲它們之後將返回確切的類型
在這裏插入圖片描述
必須查看所有的代碼 並確定它是否 足夠複雜 到必須使用泛型的程度

遷移兼容性
在基於擦除的實現中 泛型類型被當作第二類類型處理 即不能在某些重要的上下文環境中使用的類型 泛型類型只有在靜態類型檢查期間纔出現 在此之後 程序中的所有泛型類型都將被擦除 替換爲它們的非泛型上界 例如 諸如List這樣的類型註解將被擦除爲List 而普通的類型變量在未指定邊界的情況下將被擦除爲Object
擦除的核心動機是它使得泛化的客戶端可以用非泛化的類庫來使用 反之亦然 這經常被稱爲 遷移兼容性 在理想情況下 當所有事物都可以同時被泛化時 我們就可以專注於此 在現實中 即使程序員只編寫泛型代碼 他們也必須處理在Java SE5之前編寫的非泛型類庫 那些類庫的作者可能從沒有想過要泛化它們的代碼 或者可能剛剛開始接觸泛型
因此Java泛型不僅必須支持向後兼容性 即現有的代碼和類文件仍舊合法 並且繼續保持其之前的含義 而且還要支持遷移兼容性 使得類庫按照它們自己的步調變爲泛型的 並且當某個類庫變爲泛型時 不會破壞依賴於它的代碼和應用程序 在決定這就是目標之後 Java設計者們和從事此問題相關工作的各個團隊決策認爲擦除是唯一可行的解決方案 通過允許非泛型代碼與泛型代碼共存 擦除使得這種向着泛型的遷移成爲可能

擦除的問題
擦除的代價是顯著的 泛型不能用於顯式地引用運行時類型的操作之中 例如轉型 instanceof操作和new表達式 因爲所有關於參數的類型信息都丟失了 無論何時 當你在編寫泛型代碼時 必須時刻提醒自己 你只是看起來好像擁有有關參數的類型信息而已 因此 如果你編寫了下面這樣的代碼段
在這裏插入圖片描述
那麼 看起來當你在創建Foo的實例時
在這裏插入圖片描述
class Foo中的代碼應該知道現在工作於Cat之上 而泛型語法也在強烈暗示 在整個類中的各個地方 類型T都在被替換 但是事實並非如此 無論何時 當你在編寫這個類的代碼時 必須提醒自己 不 它只是一個Object
另外 擦除和遷移兼容性意味着 使用泛型並不是強制的 儘管你可能希望這樣
在這裏插入圖片描述
在這裏插入圖片描述
Derived2繼承自GenericBase 但是沒有任何泛型參數 而編譯器不會發出任何警告 警告在set()被調用時纔會出現
爲了關閉警告 Java提供了一個註解 我們可以在列表中看到它(這個註解在Java SE5之前的版本中不支持)
在這裏插入圖片描述
注意 這個註解被放置在可以產生這類警告的方法之上 而不是整個類上 當你要關閉警告時 最好是儘量地 聚焦 這樣就不會因爲過於寬泛地關閉警告 而導致意外地遮蔽掉真正的問題
可以推斷 Derived3產生的錯誤意味着編譯器期望得到一個原生基類

邊界處的動作
正是因爲有了擦除 泛型最令人困惑的方面源自這樣一個事實 即可以表示沒有任何意義的事物 例如
在這裏插入圖片描述
在這裏插入圖片描述
即使kind被存儲爲Class 擦除也意味着它實際將被存儲爲Class 沒有任何參數 因此 當你在使用它時 例如在創建數組時 Array.newInstance()實際上並未擁有kind所蘊含的類型信息 因此這不會產生具體的結果 所以必須轉型 這將產生一條令你無法滿意的警告
注意 對於在泛型中創建數組 使用Array.newInstance()是推薦的方式
如果我們要創建一個容器而不是數組 情況就有些不同了
在這裏插入圖片描述
編譯器不會給出任何警告 儘管我們(從擦除中)知道在create()內部的new ArrayList中的被移除了——在運行時 這個類的內部沒有任何 因此這看起來毫無意義 但是如果你遵從這種思路 並將這個表達式改爲new ArrayList() 編譯器就會給出警告
在本例中 這是否真的毫無意義呢 如果返回list之前 將某些對象放入其中 就像下面這樣 情況又會如何呢
在這裏插入圖片描述
即使編譯器無法知道有關create()中的T的任何信息 但是它仍舊可以在編譯期確保你放置到result中的對象具有T類型 使其適合ArrayList 因此 即使擦除在方法或類內部移除了有關實際類型的信息 編譯器仍舊可以確保在方法或類中使用的類型的內部一致性
因爲擦除在方法體中移除了類型信息 所以在運行時的問題就是邊界 即對象進入和離開方法的地點 這些正是編譯器在編譯期執行類型檢查並插入轉型代碼的地點 請考慮下面的非泛型示例
在這裏插入圖片描述
在這裏插入圖片描述
如果用javap -c SimpleHolder反編譯這個類 就可以得到下面的(經過編輯的)內容
在這裏插入圖片描述
set()和get()方法將直接存儲和產生值 而轉型是在調用get()的時候接受檢查的
現在將泛型合併到上面的代碼中
在這裏插入圖片描述
從get()返回之後的轉型消失了 但是我們還知道傳遞給set()的值在編譯期會接受檢查 下面是相關的字節碼
在這裏插入圖片描述
在這裏插入圖片描述
所產生的字節碼是相同的 對進入set()的類型進行檢查是不需要的 因爲這將由編譯器執行 而對從get()返回的值進行轉型仍舊是需要的 但這與你自己必須執行的操作是一樣的——此處它將由編譯器自動插入 因此你寫入(和讀取)的代碼的噪聲將更小
由於所產生的get()和set()的字節碼相同 所以在泛型中的所有動作都發生在邊界處——對傳遞進來的值進行額外的編譯期檢查 並插入對傳遞出去的值的轉型 這有助於澄清對擦除的混淆 記住 邊界就是發生動作的地方

擦除的補償
正如我們看到的 擦除丟失了在泛型代碼中執行某些操作的能力 任何在運行時需要知道確切類型信息的操作都將無法工作
在這裏插入圖片描述
偶爾可以繞過這些問題來編程 但是有時必須通過引入類型標籤來對擦除進行補償 這意味着你需要顯式地傳遞你的類型的Class對象 以便你可以在類型表達式中使用它
例如 在前面示例中對使用instanceof的嘗試最終失敗了 因爲其類型信息已經被擦除了 如果引入類型標籤 就可以轉而使用動態的isInstance()
在這裏插入圖片描述
在這裏插入圖片描述
編譯器將確保類型標籤可以匹配泛型參數

創建類型實例
在Erased.java中對創建一個new T()的嘗試將無法實現 部分原因是因爲擦除 而另一部分原因是因爲編譯器不能驗證T具有默認(無參)構造器 但是在C++中 這種操作很自然 很直觀 並且很安全(它是在編譯期受到檢查的)
在這裏插入圖片描述
Java中的解決方案是傳遞一個工廠對象 並使用它來創建新的實例 最便利的工廠對象就是Class對象 因此如果使用類型標籤 那麼你就可以使用newInstance()來創建這個類型的新對象
在這裏插入圖片描述
在這裏插入圖片描述
這可以編譯 但是會因ClassAsFactory而失敗 因爲Integer沒有任何默認的構造器 因爲這個錯誤不是在編譯期捕獲的 所以Sun的夥計們對這種方式並不贊成 他們建議使用顯式的工廠 並將限制其類型 使得只能接受實現了這個工廠的類
在這裏插入圖片描述
注意 這確實只是傳遞Class的一種變體 兩種方式都傳遞了工廠對象 Class碰巧是內建的工廠對象 而上面的方式創建了一個顯式的工廠對象 但是你卻獲得了編譯期檢查
另一種方式是模板方法設計模式 在下面的示例中 get()是模板方法 而create()是在子類中定義的 用來產生子類類型的對象
在這裏插入圖片描述
在這裏插入圖片描述

泛型數組
正如你在Erased.java中所見 不能創建泛型數組 一般的解決方案是在任何想要創建泛型數組的地方都使用ArrayList
在這裏插入圖片描述
這裏你將獲得數組的行爲 以及由泛型提供的編譯期的類型安全
有時 你仍舊希望創建泛型類型的數組(例如 ArrayList內部使用的是數組) 有趣的是 可以按照編譯器喜歡的方式來定義一個引用 例如
在這裏插入圖片描述
編譯器將接受這個程序 而不會產生任何警告 但是 永遠都不能創建這個確切類型的數組(包括類型參數) 因此這有一點令人困惑 既然所有數組無論它們持有的類型如何 都具有相同的結構(每個數組槽位的尺寸和數組的佈局) 那麼看起來你應該能夠創建一個Object數組 並將其轉型爲所希望的數組類型 事實上這可以編譯 但是不能運行 它將產生ClassCaseException
在這裏插入圖片描述
在這裏插入圖片描述
問題在於數組將跟蹤它們的實際類型 而這個類型是在數組被創建時確定的 因此 即使gia已經被轉型爲Generic[] 但是這個信息只存在於編譯期(並且如果沒有@SuppressWarnings註解 你將得到有關這個轉型的警告) 在運行時 它仍舊是Object數組 而這將引發問題 成功創建泛型數組的唯一方式就是創建一個被擦除類型的新數組 然後對其轉型
讓我們看一個更復雜的示例 考慮一個簡單的泛型數組包裝器
在這裏插入圖片描述
與前面相同 我們並不能聲明T[] array = new T[sz] 因此我們創建了一個對象數組 然後將其轉型
rep()方法將返回T[] 它在main()中將用於gai 因此應該是Integer[] 但是如果調用它 並嘗試着將結果作爲Integer[]引用來捕獲 就會得到ClassCaseException 這還是因爲實際的運行時類型時Object[]
如果在註釋掉@SuppressWarnings註解之後再編譯GenericArray.java 編譯器就會產生警告
在這裏插入圖片描述
在這種情況下 我們將只獲得單個的警告 並且相信這事關轉型 但是如果真的想要確定是否是這麼回事 就應該用-Xlint:unchecked來編譯
在這裏插入圖片描述
在這裏插入圖片描述
這確實是對轉型的抱怨 因爲警告會變得令人迷惑 所以一旦我們驗證某個特定警告是可預期的 那麼我們的上策就是用@SuppressWarnings關閉它 通過這種方式 當警告確實出現時 我們就可以真正地展開對它的調查了
因爲有了擦除 數組的運行時類型就只能是Object[] 如果我們立即將其轉型爲T[] 那麼在編譯期該數組的實際類型就將丟失 而編譯器可能會錯過某些潛在的錯誤檢查 正因爲這樣 最好是在集合內部使用Object[] 然後當你使用數組元素時 添加一個對T的轉型 讓我們看看這是如何作用於GenericArray.java示例的
在這裏插入圖片描述
初看起來 這好像沒多大變化 只是轉型挪了地方 如果沒有@SuppressWarnings註解 你仍舊會得到unchecked警告 但是 現在的內部表示是Object[]而不是T[] 當get()被調用時 它將對象轉型爲T 這實際上是正確的類型 因此這是安全的 然而 如果你調用rep() 它還是嘗試着將Object[]轉型爲T[] 這仍舊是不正確的 將在編譯期產生警告 在運行時產生異常 因此 沒有任何方式可以推翻底層的數組類型 它只能是Object[] 在內部將array當做Object[]而不是T[]處理的優勢是 我們不太可能忘記這個數組的運行時類型 從而意外地引入缺陷(儘管大多數也可能是所有這類缺陷都可以在運行時快速地探測到)
對於新代碼 應該傳遞一個類型標記 在這種情況下 GenericArray看起來會像下面這樣
在這裏插入圖片描述
在這裏插入圖片描述
類型標記Class被傳遞到構造器中 以便從擦除中恢復 使得我們可以創建需要的實際類型的數組 儘管從轉型中產生的警告必須用@SuppressWarnings壓制住 一旦我們獲得了實際類型 就可以返回它 並獲得想要的結果 就像在main()中看到的那樣 該數組的運行時類型是確切類型T[]
遺憾的是 如果查看Java SE5標準類庫中的源代碼 你就會看到從Object數組到參數化類型的轉型遍及各處 例如 下面是經過整理和簡化之後的從Collection中複製ArrayList的構造器
在這裏插入圖片描述
如果你通讀ArrayList.java 就會發現它充滿了這種類型 如果我們編譯它 又會發生什麼呢
在這裏插入圖片描述
可以十分肯定 標準類庫會產生大量的警告 如果你曾經用過C++ 特別是ANSI C之前的版本 你就會記得警告的特殊效果 當你發現可以忽略它們時 你就可以忽略 正是因爲這個原因 最好是從編譯器中不要發出任何消息 除非程序員必須對其進行響應

邊界
因爲擦除移除了類型信息 所以 可以用無界泛型參數調用的方法只是那些可以用Object調用的方法 但是 如果能夠將這個參數限制爲某個類型子集 那麼你就可以用這些類型子集來調用方法 爲了執行這種限制 Java泛型重用了extends關鍵字 對你來說有一點很重要 即要理解extends關鍵字在泛型邊界上下文環境中和在普通情況下所具有的意義是完全不同的 下面的示例展示了邊界的基本要素
在這裏插入圖片描述
在這裏插入圖片描述
BasicBounds.java看上去包含可以通過繼承消除的冗餘 下面 可以看到如何在繼承的每個層次上添加邊界限制
在這裏插入圖片描述
HoldItem直接持有一個對象 因此這種行爲被繼承到了Colored2中 它也要求其參數與HasColor一致 ColoredDimension2和Solid2進一步擴展了這個層次結構 並在每個層次上都添加了邊界 現在這些方法被繼承 因而不必在每個類中重複
在這裏插入圖片描述
在這裏插入圖片描述
注意 通配符被限制爲單一邊界

通配符
數組的一種特殊行爲 可以嚮導出類型的數組賦予基類型的數組引用
在這裏插入圖片描述
在這裏插入圖片描述

實際上 向上轉型不合適用在這裏 你真正做的是將一個數組賦值給另一個數組 數組的行爲應該是它可以持有其他對象 這裏只是因爲我們能夠向上轉型而已 所以很明顯 數組對象可以保留有關它們包含的對象類型的規則 就好像數組對它們持有的對象是有意識的 因此在編譯期檢查和運行時檢查之間 你不能濫用它們
對數組的這種賦值並不是那麼可怕 因爲在運行時可以發現你已經插入了不正確的類型 但是泛型的主要目標之一是將這種錯誤檢測移入到編譯期 因此當我們試圖使用泛型容器來代替數組時 會發生什麼呢
在這裏插入圖片描述

真正的問題是我們在談論容器的類型 而不是容器持有的類型 與數組不同 泛型沒有內建的協變類型 這是因爲數組在語言中是完全定義的 因此可以內建了編譯期和運行時的檢查 但是在使用泛型時 編譯器和運行時系統都不知道你想用類型做些什麼 以及應該採用什麼樣的規則
但是 有時你想要在兩個類型之間建立某種類型的向上轉型關係 這正是通配符所允許的
在這裏插入圖片描述

現在你甚至不能向剛剛聲明過將持有Apple對象的List中放置一個Apple對象了 是的 但是編譯器並不知道這一點 List<? extends Fruit>可以合法地指向一個List 一旦執行這種類型的向上轉型 你就將丟失掉向其中傳遞任何對象的能力 甚至是傳遞Object也不行
另一方面 如果你調用一個返回Fruit的方法 則是安全的 因爲你知道在這個List中的任何對象至少具有Fruit類型 因此編譯器將允許這麼做

編譯器有多聰明
現在 你可能會猜想自己被阻止去調用任何接受參數的方法 但是請考慮下面的程序
在這裏插入圖片描述
通過查看ArrayList的文檔 我們可以發現 編譯器並沒有這麼聰明 儘管add()將接受一個具有泛型參數類型的參數 但是contains()和indexOf()將接受Object類型的參數 因此當你指定一個ArrayList<? extends Fruit>時 add()的參數就變成了 ? Extends Fruit 從這個描述中 編譯器並不能瞭解這裏需要Fruit的哪個具體子類型 因此它不會接受任何類型的Fruit 如果先將Apple向上轉型爲Fruit 也無關緊要——編譯器將直接拒絕對參數列表中涉及通配符的方法(例如add())的調用
在使用contains()和indexOf()時 參數類型是Object 因此不涉及任何通配符 而編譯器也將允許這個調用 這意味着將由泛型類的設計者來決定哪些調用是 安全的 並使用Object類型作爲其參數類型 爲了在類型中使用了通配符的情況下禁止這類調用 我們需要在參數列表中使用類型參數
可以在一個非常簡單的Holder類中看到這一點
在這裏插入圖片描述

逆變
還可以走另外一條路 即使用超類型通配符 這裏 可以聲明通配符是由某個特定類的任何基類來界定的 方法是指定<? super MyClass> 甚至或者使用類型參數 <? super T>()(儘管你不能對泛型參數給出一個超類型邊界 即不能聲明) 這使得你可以安全地傳遞一個類型對象到泛型類型中 因此 有了超類型通配符 就可以向Collection寫入了
在這裏插入圖片描述
參數Apple是Apple的某種基類型的List 這樣你就知道向其中添加Apple或Apple的子類型是安全的 但是 既然Apple是下界 那麼你可以知道向這樣的List中添加Fruit是不安全的 因爲這將使這個List敞開口子從而可以向其中添加非Apple類型的對象 而這是違反靜態類型安全的
因此你可能會根據如果能夠向一個泛型類型 寫入 (傳遞給一個方法) 以及如何能夠從一個泛型類型中 讀取 (從一個方法中返回) 來着手思考子類型和超類型邊界
超類型邊界放鬆了在可以向方法傳遞的參數上所作的限制
在這裏插入圖片描述
writeExact()方法使用了一個確切參數類型(無通配符) 在f1()中 可以看到這工作良好——只要你只向List中放置Apple 但是 writeExact()不允許將Apple放置到List中 即使知道這應該是可以的
在writeWithWildcard()中 其參數現在是List<? super T> 因此這個List將持有從T導出的某種具體類型 這樣就可以安全地將一個T類型的對象或者從T導出的任何對象作爲參數傳遞給List的方法 在f2()中可以看到這一點 在這個方法中我們仍舊可以像前面那樣 將Apple放置到List中 但是現在我們還可以如你所期望的那樣 將Apple放置到List中
我們可以執行下面這個相同的類型分析 作爲對協變和通配符的一個複習
在這裏插入圖片描述

無界通配符
無界通配符<?>看起來意味着 任何事物 因此使用無界通配符好像等價於使用原生類型 事實上 編譯器初看起來是支持這種判斷的
在這裏插入圖片描述

第二個示例展示了無界通配符的一個重要應用 當你在處理多個泛型參數時 有時允許一個參數可以是任何類型 同時爲其他參數確定某種特定類型的這種能力會顯得很重要
在這裏插入圖片描述

令人困惑的是 編譯器並非總是關注像List和List<?>之間的這種差異 因此它們看起來就像是相同的事物 因爲 事實上 由於泛型參數將擦除到它的第一個邊界 因此List<?>看起來等價於List 而List實際上也是List 除非這些語句都不爲真 List實際上表示 持有任何Object類型的原生List 而List<?>表示 具有某種特定類型的非原生List 只是我們不知道那種類型是什麼
編譯器何時纔會關注原生類型和涉及無界通配符的類型之間的差異呢 下面的示例使用了前面定義的Holder類 它包含接受Holder作爲參數的各種方法 但是它們具有不同的形式 作爲原生類型 具有具體的類型參數以及具有無界通配符參數
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

因此 使用確切類型來替代通配符類型的好處是 可以用泛型參數來做更多的事 但是使用通配符使得你必須接受範圍更寬的參數化類型作爲參數 因此 必須逐個情況地權衡利弊 找到更適合你的需求的方法

捕獲轉換
有一種情況特別需要使用<?>而不是原生類型 如果向一個使用<?>的方法傳遞原生類型 那麼對編譯器來說 可能會推斷出實際的類型參數 使得這個方法可以迴轉並調用另一個使用這個確切類型的方法 下面的示例演示了這種技術 它被稱爲捕獲轉換 因爲未指定的通配符類型被捕獲 並被轉換爲確切類型 這裏 有關警告的註釋只有在@SuppressWarnings註解被移除之後才能起作用
在這裏插入圖片描述
f1()中的類型參數都是確切的 沒有通配符或邊界 在f2()中 Holder參數是一個無界通配符 因此它看起來是未知的 但是 在f2()中 f1()被調用 而f1()被調用 而f()需要一個已知參數 這裏所發生的是 參數類型在調用f2()的過程中被捕獲 因此它可以在對f1()的調用中被使用
你可能想知道 這項技術是否可以用於寫入 但是這要求要在傳遞Holder<?>時同時傳遞一個具體類型 捕獲轉換隻有在這樣的情況下可以工作 即在方法內部 你需要使用確切的類型 注意 不能從f2()中返回T 因爲T對於f2()來說是未知的 捕獲轉換十分有趣 但是非常受限

問題

任何基本類型都不能作爲類型參數
不能將基本類型用作類型參數 因此 不能創建ArrayList之類的東西
解決之道是使用基本類型的包裝器類以及Java SE5的自動包裝機制 如果創建一個ArrayList 並將基本類型int應用於這個容器 那麼你將發現自動包裝機制將自動地實現int到Integer的雙向轉換 因此 這幾乎就像是有一個ArrayList一樣
在這裏插入圖片描述
注意 自動包裝機制甚至允許用foreach語法來產生int
通常 這種解決方案工作得很好 能夠成功地存儲和讀取int 有一些轉換碰巧在發生的同時會對你屏蔽掉 但是 如果性能成爲了問題 就需要使用專門適配基本類型的容器版本 Org.apache.commons.collection.primitives就是一種開源的這類版本
下面是另外一種方式 它可以創建持有Byte的Set
在這裏插入圖片描述
注意 自動包裝機制解決了一些問題 但並不是解決了所有問題 下面的示例展示了一個泛型的Generator接口 它指定next()方法返回一個具有其參數類型的對象 FArray類包含一個泛型方法 它通過使用生成器在數組中填充對象(這使得類泛型在本例中無法工作 因爲這個方法是靜態的)
在這裏插入圖片描述
由於RandomGenerator.Integer實現了Generator 所以我的希望是自動包裝機制可以自動地將next()的值從Integer轉換爲int 但是 自動包裝機制不能應用於數組 因此這無法工作

實現參數化接口
一個類不能實現同一個泛型接口的兩種變體 由於擦除的原因 這兩個變體會成爲相同的接口 下面是產生這種衝突的情況
在這裏插入圖片描述
Hourly不能編譯 因爲擦除會將Payable和Payable簡化爲相同的類Payable 這樣 上面的代碼就意味着在重複兩次地實現相同的接口 十分有趣的是 如果從Payable的兩種用法中都移除掉泛型參數(就像編譯器在擦除階段所做的那樣) 這段代碼就可以編譯

轉型和警告
使用帶有泛型類型參數的轉型或instanceof不會有任何效果 下面的容器在內部將各個值存儲爲Object 並在獲取這些值時 再將它們轉型回T
在這裏插入圖片描述
如果沒有@SuppressWarnings註解 編譯器將對pop()產生 unchecked cast 警告 由於擦除的原因 編譯器無法知道這個轉型是否是安全的 並且pop()方法實際上並沒有執行任何轉型 這是因爲 T被擦除到它的第一個邊界 默認情況下是Object 因此pop()實際上只是將Object轉型爲Object
有時 泛型沒有消除對轉型的需要 這就會由編譯器產生警告 而這個警告是不恰當的 例如
在這裏插入圖片描述
readObject()無法知道它正在讀取的是什麼 因此它返回的是必須轉型的對象 但是當註釋掉@SuppressWarnings註解 並編譯這個程序時 就會得到下面的警告
在這裏插入圖片描述
如果遵循這條指示 使用-Xlint:unchecked來重新編譯
在這裏插入圖片描述
你會被強制要求轉型 但是又被告知不應該轉型 爲了解決這個問題 必須使用在Java SE5中引入的新的轉型形式 既通過泛型類來轉型
在這裏插入圖片描述
但是 不能轉型到實際類型(List) 也就是說 不能聲明
在這裏插入圖片描述
甚至當你添加一個像下面這樣的另一個轉型時
在這裏插入圖片描述
仍舊會得到一個警告

重載
下面的程序是不能編譯的 即使編譯它是一種合理的嘗試
在這裏插入圖片描述
由於擦除的原因 重載方法將產生相同的類型簽名
與此不同的是 當被擦除的參數不能產生唯一的參數列表時 必須提供明顯有區別的方法名
在這裏插入圖片描述
幸運的是 這類問題可以由編譯器探測到

基類劫持了接口
假設你有一個Pet類 它可以與其他的Pet對象進行比較(實現了Comparable接口)
在這裏插入圖片描述
對可以與ComparablePet的子類比較的類型進行窄化是有意義的 例如 一個Cat對象就只能與其他的Cat對象比較
在這裏插入圖片描述
遺憾的是 這不能工作 一旦爲Comparable確定了ComparablePet參數 那麼其他任何實現類都不能與ComparablePet之外的任何對象比較
在這裏插入圖片描述
Hamster說明再次實現ComparablePet中的相同接口是可能的 只要它們精確地相同 包括參數類型在內 但是 這只是與覆蓋基類中的方法相同 就像在Gecko中看到的那樣

自限定的類型
在Java泛型中 有一個好像是經常性出現的慣用法 它相當令人費解
在這裏插入圖片描述
這就像兩面鏡子彼此照向對方所引起的目眩效果一樣 是一種無限反射 SelfBounded類接受泛型參數T 而T由一個邊界類限定 這個邊界就是擁有T作爲其參數的SelfBounded
它強調的是當extends關鍵字用於邊界與用來創建子類明顯是不同的

古怪的循環泛型
不能直接繼承一個泛型參數 但是 可以繼承在其自己的定義中使用這個泛型參數的類 也就是說 可以聲明
在這裏插入圖片描述
古怪的循環 是指類相當古怪地出現在它自己的基類中這一事實
我在創建一個新類 它繼承自一個泛型參數 這個泛型類型接受我的類的名字作爲其參數 當給出導出類的名字時 這個泛型基類能夠實現什麼呢 好吧 Java中的泛型關乎參數和返回類型 因此它能夠產生使用導出類作爲其參數和返回類型的基類 它還能將導出類型用作其域類型 甚至那些將被擦除爲Object的類型 下面是表示了這種情況的一個泛型類
在這裏插入圖片描述
這是一個普通的泛型類型 它的一些方法將接受和產生具有其參數類型的對象 還有一個方法將在其存儲的域上執行操作(儘管只是在這個域上執行Object操作)
我們可以在一個古怪的循環泛型中使用BasicHolder
在這裏插入圖片描述
注意 這裏有些東西很重要 新類Subtype接受的參數和返回的值具有Subtype類型而不僅僅是基類BasicHolder類型 這就是CRG的本質 基類用導出類替代其參數 這意味着泛型基類變成了一種其所有導出類的公共功能的模板 但是這些功能對於其所有參數和返回值 將使用導出類型 也就是說 在所產生的類中將使用確切類型而不是基類型 因此 在Subtype中 傳遞給set()的參數和從get()返回的類型都是確切的Subtype

自限定
BasicHolder可以使用任何類型作爲其泛型參數 就像下面看到的那樣
在這裏插入圖片描述
在這裏插入圖片描述
自限定將採取額外的步驟 強制泛型當作其自己的邊界參數來使用 觀察所產生的類可以如何使用以及不可以如何使用
在這裏插入圖片描述
自限定所做的 就是要求在繼承關係中 像下面這樣使用這個類
在這裏插入圖片描述
這會強制要求將正在定義的類當作參數傳遞給基類
自限定的參數有何意義呢 它可以保證類型參數必須與正在被定義的類相同 正如你在B類的定義中所看到的 還可以從使用了另一個SelfBounded參數的SelfBounded中導出 儘管在A類看到的用法看起來是主要的用法 對定義E的嘗試說明不能使用不是SelfBounded的類型參數
遺憾的是 F可以編譯 不會有任何警告 因此自限定慣用法不是可強制執行的 如果它確實很重要 可以要求一個外部工具來確保不會使用原生類型來替代參數化類型
注意 可以移除自限定這個限制 這樣所有的類仍舊是可以編譯的 但是E也會因此而變得可編譯
在這裏插入圖片描述
因此很明顯 自限定限制只能強制作用於繼承關係 如果使用自限定 就應該瞭解這個類所用的類型參數將與使用這個參數的類具有相同的基類型 這會強制要求使用這個類的每個人都要遵循這種形式
還可以將自限定用於泛型方法
在這裏插入圖片描述
這可以防止這個方法被應用於除上述形式的自限定參數之外的任何事物上

參數協變
自限定類型的價值在於它們可以產生協變參數類型 方法參數類型會隨子類而變化 儘管自限定類型還可以產生於子類型相同的返回類型 但是這並不十分重要 因爲協變返回類型是在Java SE5中引入的
在這裏插入圖片描述
DerivedGetter中的get()方法覆蓋了OrdinaryGetter中的get() 並返回了一個從OrdinaryGetter.get()的返回類型中導出的類型 儘管這是完全合乎邏輯的事情(導出類方法應該能夠返回比它覆蓋的基類方法更具體的類型) 但是這在早先的Java版本中是不合法的
自限定泛型事實上將產生確切的導出類型作爲其返回值 就像在get()中所看到的一樣
在這裏插入圖片描述
注意 這段代碼不能編譯 除非是使用囊括了協變返回類型的Java SE5 然而 在非泛型代碼中 參數類型不能隨子類型發生變化
在這裏插入圖片描述
set(derived)和set(base)都是合法的 因此DerivedSetter.set()沒有覆蓋OrdinarySetter.set() 而是重載了這個方法 從輸出中可以看到 在DerivedSetter中有兩個方法 因此基類版本仍舊是可用的 因此可以證明它被重載過
但是 在使用自限定類型時 在導出類中只有一個方法 並且這個方法接受導出類型而不是基類型爲參數
在這裏插入圖片描述
在這裏插入圖片描述
編譯器不能識別將基類型當作參數傳遞給set()的嘗試 因爲沒有任何方法具有這樣的簽名 實際上 這個參數已經被覆蓋
如果不使用自限定類型 普通的繼承機制就會介入 而你將能夠重載 就像在非泛型的情況下一樣
在這裏插入圖片描述
這段代碼在模仿OrdinaryArgument.java 在那個示例中 DerivedSetter繼承自包含一個set(Base)的OrdinarySetter 而這裏 DerivedGS繼承自泛型創建的也包含有一個set(Base)的GenericSetter 就像OrdinaryArgument.java一樣 你可以從輸出中看到 DerivedGS包含兩個set()的重載版本 如果不使用自限定 將重載參數類型 如果使用了自限定 只能獲得某個方法的一個版本 它將接受確切的參數類型

動態類型安全
因爲可以向Java SE5之前的代碼傳遞泛型容器 所以舊式代碼仍有可能會破壞你的容器 Java SE5的java.util.Collection中有一組便利工具 可以解決在這種情況下的類型檢查問題 它們是 靜態方法checkedCollection() checkedList() checkedMap() checkedSet() checkedSortedMap()和checkedSortedSet() 這些方法每一個都會將你希望動態檢查的容器當作第一個參數接受 並將你希望強制要求的類型作爲第二個參數接受

讓我們用受檢查的容器來看看 將貓插入到狗列表中 這個問題 這裏 oldStyleMethod()表示遺留代碼 因爲它接受的是原生的List 而@SuppressWarnings(“unchecked”)註解對於壓制所產生的警告是必需的
在這裏插入圖片描述

異常
由於擦除的原因 將泛型應用於異常是非常受限的 catch語句不能捕獲泛型類型的異常 因爲在編譯期和運行時都必須知道異常的確切類型 泛型類也不能直接或間接繼承自Throwable(這將進一步阻止你去定義不能捕獲的泛型異常)
但是 類型參數可能會在一個方法的throws子句中用到 這使得你可以編寫隨檢查型異常的類型而發生變化的泛型代碼
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
Processor執行process() 並且可能會拋出具有類型E的異常 process()的結果存儲在List resultCollector中(這被稱爲收集參數) ProcessRunner有一個ProcessAll()方法 它將執行所持有的每個Process對象 並返回resultCollector
如果不能參數化所拋出的異常 那麼由於檢查型異常的緣故 將不能編寫出這種泛化的代碼

混型
術語混型隨時間的推移好像擁有了無數的含義 但是其最基本的概念是混合多個類的能力 以產生一個可以表示混型中所有類型的類 這往往是你最後的手段 它將使組裝多個類變得簡單易行
混型的價值之一是它們可以將特性和行爲一致地應用於多個類之上 如果想在混型類中修改某些東西 作爲一種意外的好處 這些修改將會應用於混型所應用的所有類型之上 正由於此 混型有一點面向方面編程(AOP)的味道 而方面經常被建議用來解決混型問題

C++中的混型
在C++中 使用多重繼承的最大理由 就是爲了使用混型 但是 對於混型來說 更有趣 更優雅的方式是使用參數化類型 因爲混型就是繼承自其類型參數的類 在C++中 可以很容易的創建混型 因爲C++能夠記住其模板參數的類型
下面是一個C++示例 它有兩個混型類型:一個使得你可以在每個對象中混入擁有一個時間戳這樣的屬性 而另一個可以混入一個序列號
在這裏插入圖片描述
在這裏插入圖片描述
在main()中 mixin1和mixin2所產生的類型擁有所混入類型的所有方法 可以將混型看作是一種功能 它可以將現有類映射到新的子類上 注意 使用這種技術來創建一個混型是多麼地輕而易舉 基本上 只需要聲明 這就是我想要的 緊跟着它就發生了
在這裏插入圖片描述
遺憾的是 Java泛型不允許這樣 擦除會忘記基類類型 因此泛型類不能直接繼承自一個泛型參數

與接口混合
一種更常見的推薦解決方案是使用接口來產生混型效果 就像下面這樣
在這裏插入圖片描述
在這裏插入圖片描述
Mixin類基本上是在使用代理 因此每個混入類型都要求在Mixin中有一個相應的域 而你必須在Mixin中編寫所有必須的方法 將方法調用轉發給恰當的對象 這個示例使用了非常簡單的類 但是當使用更復雜的混型時 代碼數量會急速增加

使用裝飾器模式
裝飾器是通過使用組合和形式化結構(可裝飾物/裝飾器層次結構)來實現的 而混型是基於繼承的 因此可以將基於參數化類型的混型當作是一種泛型裝飾器機制 這種機制不需要裝飾器設計模式的繼承結構
前面的示例可以被改寫爲使用裝飾器
在這裏插入圖片描述
在這裏插入圖片描述
產生自泛型的類包含所有感興趣的方法 但是由使用裝飾器所產生的對象類型是最後被裝飾的類型 也就是說 儘管可以添加多個層 但是最後一層纔是實際的類型 因此只有最後一層的方法是可視的 而混型的類型是所有被混合到一起的類型 因此對於裝飾器來說 其明顯的缺陷是它只能有效地工作於裝飾中的一層(最後一層) 而混型方法顯然會更自然一些 因此 裝飾器只是對由混型提出的問題的一種侷限的解決方案

與動態代理混合
可以使用動態代理來創建一種比裝飾器更貼近混型模型的機制 通過使用動態代理 所產生的類的動態類型將會是已經混入的組合類型
由於動態代理的限制 每個被混入的類都必須是某個接口的實現
在這裏插入圖片描述
在這裏插入圖片描述
因爲只有動態類型而不是非靜態類型才包含所有的混入類型 因此這仍舊不如C++的方式好 因爲可以在具有這些類型的對象上調用方法之前 你被強制要求必須先將這些對象向下轉型到恰當的類型 但是 它明顯地更接近於真正的混型

潛在類型機制
泛型代碼典型地將在泛型類型上調用少量方法 而具有潛在類型機制的語言只要求實現某個方法子集 而不是某個特定類或接口 從而放鬆了這種限制(並且可以產生更加泛化的代碼) 正由於此 潛在類型機制使得你可以橫跨類繼承結構 調用不屬於某個公共接口的方法 因此 實際上一段代碼可以聲明 我不關心你是什麼類型 只要你可以speak()和sit()即可 由於不要求具體類型 因此代碼就可以更加泛化
潛在類型機制是一種代碼組織和複用機制 有了它編寫出的代碼相對於沒有它編寫出的代碼能夠更容易地複用 代碼組織和複用是所有計算機編程的基本手段 編寫一次 多次使用 並在一個位置保存代碼 因爲我並未被要求去命名我的代碼要操作於其上的確切接口 所以 有了潛在類型機制 我就可以編寫更少的代碼 並更容易地將其應用於多個地方
兩種支持潛在類型機制的語言實例是Python和C++ Python是動態類型語言(事實上所有的類型檢查都發生在運行時) 而C++是靜態類型語言(類型檢查發生在編譯期) 因此潛在類型機制不要求靜態或動態類型檢查
如果我們將上面的描述用Python來表示 如下所示
在這裏插入圖片描述
Python使用縮進來確定作用域(因此不需要任何花括號) 而冒號將表示新的作用域的開始 # 表示註釋到行尾 就像Java中的 // 類的方法需要顯式地指定this引用的等價物作爲第一個參數 按慣例成爲self 構造器調用不要求任何類型的 new 關鍵字 並且Python允許正則(非成員)函數 就像perform()所表明的那樣
注意 在perform(anything)中 沒有任何針對anything的類型 anything只是一個標識符 它必須能夠執行perform()期望它執行的操作 因此這裏隱含着一個接口 但是你從來都不必顯式地寫出這個接口 它是潛在的 perform()不關心其參數的類型 因此我可以向它傳遞任何對象 只要該對象支持speak()和sit()方法 如果傳遞給perform()的對象不支持這些操作 那麼將會得到運行時異常
我們可以用C++產生相同的效果
在這裏插入圖片描述

如果我們試圖用Java實現上面的示例 那麼就會被強制要求使用一個類或接口 並在邊界表達式中指定它
在這裏插入圖片描述
但是要注意 perform()不需要使用泛型來工作 它可以被簡單地指定爲接受一個Performs對象
在這裏插入圖片描述
在這裏插入圖片描述
在本例中 泛型不是必需的 因爲這些類已經被強制要求實現Performs接口

對缺乏潛在類型機制的補償
儘管Java不支持潛在類型機制 但是這並不意味着有界泛型代碼不能在不同的類型層次結構之間應用 也就是說 我們仍舊可以創建真正的泛型代碼 但是這需要付出一些額外的努力

反射
可以使用的一種方式是反射 下面的perform()方法就是用了潛在類型機制
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
上例中 這些類完全是彼此分離的 沒有任何公共基類(除了Object)或接口 通過反射 CommunicateReflectively.perform()能夠動態地確定所需要的方法是否可用並調用它們 它甚至能夠處理Mime只具有一個必須的方法這一事實 並能夠部分實現其目標

將一個方法應用於序列
反射提供了一些有趣的可能性 但是它將所有的類型檢查都轉移到了運行時 因此在許多情況下並不是我們所希望的 如果能夠實現編譯期類型檢查 這通常會更符合要求 但是有可能實現編譯期類型檢查和潛在類型機制嗎
讓我們看一個說明這個問題的示例 假設想要創建一個apply()方法 它能夠將任何方法應用於某個序列中的所有對象 這是接口看起來並不適合的情況 因爲你想要將任何方法應用於一個對象集合 而接口對於描述 任何方法 存在過多的限制 如何用Java來實現這個需求呢
最初 我們可以用反射來解決這個問題 由於有了Java SE5的可變參數 這種方式被證明是相當優雅的
在這裏插入圖片描述
在這裏插入圖片描述
在Apple中 我們運氣很好 因爲碰巧在Java中內建了一個由Java容器類庫使用的Iterable接口 正由於此 apply()方法可以接受任何實現了Iterable接口的事物 包括諸如List這樣的所有Collection類 但是它還可以接受其他任何事物 只要能夠使這些事物是Iterable的 例如 在main()中使用的下面定義的SimpleQueue類
在這裏插入圖片描述

儘管Java解決方案被證明很優雅 但是我們必須知道使用反射(儘管反射在最近版本的Java中已經明顯地改善了) 可能比非反射的實現要慢一些 因爲有太多的動作都是在運行時發生的 這不應該阻止你使用這種解決方案的腳步 至少可以將其作爲一種馬上就能想得到的解決方案(以防止陷入不成熟的優化中) 但這毫無疑問是這兩種方法之間的一個差異

當你並未碰巧擁有正確的接口時
上面的示例是受益的 因爲Iterable接口已經是內建的 而它正是我們需要的 但是更一般的情況又會怎樣呢 如果不存在剛好適合你的需求的接口呢
例如 讓我們泛化FilledList中的思想 創建一個參數化的方法fill() 它接受一個序列 並使用Generator填充它 當我們嘗試着用Java來編寫時 就會陷入問題之中 因爲沒有任何像前面示例中的Iterable接口那樣的 Addable 便利接口 因此你不能說 可以在任何事物上調用add() 而必須說 可以在Collection的子類型上調用add() 這樣產生的代碼並不是特別泛化 因爲它必須限制爲只能工作於Collection實現 如果我試圖使用沒有實現Collection的類 那麼我的泛化代碼將不能工作 下面是這段代碼的樣子
在這裏插入圖片描述
在這裏插入圖片描述

用適配器仿真潛在類型機制
從我們擁有的接口中編寫代碼來產生我們需要的接口 這是適配器設計模式的一個典型示例 我們可以使用適配器來適配已有的接口 以產生想要的接口 下面這種使用前面定義的Coffee繼承結構的解決方案演示了編寫適配器的不同方式
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

將函數對象用作策略
如果只查看嘗試添加對象的過程 就會看到這是在多個類中的公共操作 但是這個操作沒有在任何我們可以指定的基類中表示 有時甚至可以使用 + 操作符 而其他時間可以使用某種add方法 這是在試圖編寫泛化代碼的時候通常會碰到的情況 因爲你想將這些代碼應用於多個類上 特別是 像本例一樣 作用於多個已經存在且我們不能 修正 的類上 即使你可以將這種情況窄化到Number的子類 這個超類也不包括任何有關 可添加性 的東西
解決方案是使用策略設計模式 這種設計模式可以產生更優雅的代碼 因爲它將 變化的事物 完全隔離到了一個函數對象中 函數對象就是在某種程度上行爲像函數的對象 一般地 會有一個相關的方法(在支持操作符重載的語言中 可以創建對這個方法的調用 而這個調用看起來就和普通的方法調用一樣) 函數對象的價值就在於 與普通方法不同 它們可以傳遞出去 並且還可以擁有多個調用之間持久化的狀態 當然 可以用類中的任何方法來實現與此相似的操作 但是(與使用任何設計模式一樣)函數對象主要是由其目的來區別的 這裏的目的就是要創建某種事物 使它的行爲就像是一個可以傳遞出去的單個方法一樣 這樣 它就和策略設計模式緊耦合了 有時甚至無法區分
儘管使用了大量的設計模式 但是在這裏它們之間的界限是模糊的 我們在創建執行適配操作的函數對象 而它們將被傳遞到用作策略的方法中
通過採用這種方式 添加了最初着手創建的各種類型的泛型方法 以及其他的泛型方法 下面是產生的結果
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

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