天天看點

Java 泛型 四 基本用法與類型擦除

java 在 1.5 引入了泛型機制,泛型本質是參數化類型,也就是說變量的類型是一個參數,在使用時再指定為具體類型。泛型可以用于類、接口、方法,通過使用泛型可以使代碼更簡單、安全。然而 java 中的泛型使用了類型擦除,是以隻是僞泛型。這篇文章對泛型的使用以及存在的問題做個總結,主要參考自 《java 程式設計思想》。

這個系列的另外兩篇文章:

如果有一個類 <code>holder</code> 用于包裝一個變量,這個變量的類型可能是任意的,怎麼編寫 <code>holder</code> 呢?在沒有泛型之前可以這樣:

}

在 <code>holder1</code> 中,有一個用 <code>object</code> 引用的變量。因為任何類型都可以向上轉型為 <code>object</code>,是以這個 <code>holder</code> 可以接受任何類型。在取出的時候 <code>holder</code> 隻知道它儲存的是一個 <code>object</code> 對象,是以要強制轉換為對應的類型。在 <code>main</code> 方法中, <code>holder1</code> 先是儲存了一個字元串,也就是 <code>string</code> 對象,接着又變為儲存一個 <code>integer</code> 對象(參數 <code>1</code> 會自動裝箱)。從 <code>holder</code> 中取出變量時強制轉換已經比較麻煩,這裡還要記住不同的類型,要是轉錯了就會出現運作時異常。

下面看看 <code>holder</code> 的泛型版本:

在 <code>holder2</code> 中, 變量 <code>a</code> 是一個參數化類型 <code>t</code>,<code>t</code> 隻是一個辨別,用其它字母也是可以的。建立 <code>holder2</code> 對象的時候,在尖括号中傳入了參數 <code>t</code> 的類型,那麼在這個對象中,所有出現 <code>t</code> 的地方相當于都用 <code>string</code> 替換了。現在的 <code>get</code> 的取出來的不是 <code>object</code> ,而是 <code>string</code> 對象,是以不需要類型轉換。另外,當調用 <code>set</code> 時,隻能傳入 <code>string</code> 類型,否則編譯無法通過。這就保證了 <code>holder2</code> 中的類型安全,避免由于不小心傳入錯誤的類型。

通過上面的例子可以看出泛使得代碼更簡便、安全。引入泛型之後,java 庫的一些類,比如常用的容器類也被改寫為支援泛型,我們使用的時候都會傳入參數類型,如:<code>arraylist&lt;integer&gt; list = arraylist&lt;&gt;();</code>。

泛型不僅可以針對類,還可以單獨使某個方法是泛型的,舉個例子:

<code>genericmethod</code> 類本身不是泛型的,建立它的對象的時候不需要傳入泛型參數,但是它的方法 <code>f</code> 是泛型方法。在傳回類型之前是它的參數辨別 <code>&lt;k,v&gt;</code>,注意這裡有兩個泛型參數,是以泛型參數可以有多個。

調用泛型方法時可以不顯式傳入泛型參數,上面的調用就沒有。這是因為編譯器會使用參數類型推斷,根據傳入的實參的類型 (這裡是 <code>integer</code> 和 <code>string</code>) 推斷出 <code>k</code> 和 <code>v</code> 的類型。

java 的泛型使用了類型擦除機制,這個引來了很大的争議,以至于 java 的泛型功能受到限制,隻能說是”僞泛型“。什麼叫類型擦除呢?簡單的說就是,類型參數隻存在于編譯期,在運作時,java 的虛拟機 ( jvm ) 并不知道泛型的存在。先看個例子:

上面的代碼有兩個不同的 <code>arraylist</code>:<code>arraylist&lt;integer&gt;</code> 和 <code>arraylist&lt;string&gt;</code>。在我們看來它們的參數化類型不同,一個儲存整性,一個儲存字元串。但是通過比較它們的 <code>class</code> 對象,上面的代碼輸出是 <code>true</code>。這說明在 jvm 看來它們是同一個類。而在 c++、c# 這些支援真泛型的語言中,它們就是不同的類。

泛型參數會擦除到它的第一個邊界,比如說上面的 <code>holder2</code> 類,參數類型是一個單獨的 <code>t</code>,那麼就擦除到 <code>object</code>,相當于所有出現 <code>t</code> 的地方都用 <code>object</code> 替換。是以在 jvm 看來,儲存的變量 <code>a</code> 還是 <code>object</code> 類型。之是以取出來自動就是我們傳入的參數類型,這是因為編譯器在編譯生成的位元組碼檔案中插入了類型轉換的代碼,不需要我們手動轉型了。如果參數類型有邊界那麼就擦除到它的第一個邊界,這個下一節再說。

擦除會出現一些問題,下面是一個例子:

上面的 <code>manipulator</code> 是一個泛型類,内部用一個泛型化的變量 <code>obj</code>,在 <code>manipulate</code> 方法中,調用了 <code>obj</code> 的方法 <code>f()</code>,但是這行代碼無法編譯。因為類型擦除,編譯器不确定 <code>obj</code> 是否有 <code>f()</code> 方法。解決這個問題的方法是給 <code>t</code> 一個邊界:

現在 <code>t</code> 的類型是 <code>&lt;t extends hasf&gt;</code>,這表示 <code>t</code> 必須是 <code>hasf</code> 或者 <code>hasf</code> 的導出類型。這樣,調用 <code>f()</code> 方法才安全。<code>hasf</code> 就是 <code>t</code> 的邊界,是以通過類型擦除後,所有出現 <code>t</code> 的

地方都用 <code>hasf</code> 替換。這樣編譯器就知道 <code>obj</code> 是有方法 <code>f()</code> 的。

但是這樣就抵消了泛型帶來的好處,上面的類完全可以改成這樣:

是以泛型隻有在比較複雜的類中才展現出作用。但是像 <code>&lt;t extends hasf&gt;</code> 這種形式的東西不是完全沒有意義的。如果類中有一個傳回 <code>t</code> 類型的方法,泛型就有用了,因為這樣會傳回準确類型。比如下面的例子:

這裡的 <code>get()</code> 方法傳回的是泛型參數的準确類型,而不是 <code>hasf</code>。

類型擦除導緻泛型喪失了一些功能,任何在運作期需要知道确切類型的代碼都無法工作。比如下面的例子:

通過 <code>new t()</code> 建立對象是不行的,一是由于類型擦除,二是由于編譯器不知道 <code>t</code> 是否有預設的構造器。一種解決的辦法是傳遞一個工廠對象并且通過它建立新的執行個體。

另一種解決的方法是利用模闆設計模式:

具體類型的建立放到了子類繼承父類時,在 <code>create</code> 方法中建立實際的類型并傳回。

本文介紹了 java 泛型的使用,以及類型擦除相關的問題。一般情況下泛型的使用比較簡單,但是某些情況下,尤其是自己編寫使用泛型的類或者方法時要注意類型擦除的問題。接下來會介紹數組與泛型的關系以及通配符的使用。