面向對象的一個重要目標是對代碼重用的支持。支持這個目標的一個重要機制就是泛型機制。在1.5版本之前,java並沒有直接支持泛型實現,泛型編程的實現時通過使用繼承的一些基本概念來完成的。
這種方式的局限性有:
1. 使用此種方式會不可避免地用到強制類型轉換。
2. 不能使用基本類型,只有引用類型能和Object相容。(通過使用包裝器類)
例如使用Comparable接口來暫時代表所有實現了該接口的類。
什么是協變性?
簡而言之,如果A IS-A B,那么A[] IS-A B[]。
舉例:現在有類型Person、Employee和Student。Employee 是一個(IS-A) Person,Student是一個(IS-A)Person。那么下面的語句可以通過編譯:
但是上面的代碼在運行時卻會出錯。因為arr[0]實際上是引用一個Employee,可是Student IS-NOT-A Employee。這樣就產生了混亂。這種錯誤正是由於Java數組的協變性而產生的。那么Java為什么不禁止數組協變呢?
因為SE5之前還沒有泛型,但很多代碼迫切需要泛型來解決問題。 例如:
Arrays.equals()方法的底層實現調用的是Object.equals()方法,和數組中元素的具體類型無關,這充分利用了Java中任何類型都繼承自Object類的特性,避免了為每個類型都重新定義Arrays.equals()方法。而在沒有泛型的時代,要讓Object[]能接受所有數組類型,最簡單的辦法就是讓數組接受協變,把String[],Integer[]都定義成Object[]的派生類,然后多態就起作用了。
為什么數組設計成”協變“不會有大問題呢?
這是基於數組的一個獨有特性:
數組記得它內部元素的具體類型,並且會在運行時做類型檢查。
因為arr[0]記得它內部的元素類型是Employee,所以運行時給它插入一個Student類型會報錯。
這個特性使得Java數組協變帶來的影響不會釀成大錯——錯誤最終還是會被檢測出來,只不過是從編譯時推遲到了運行時。
正是有這個特性,Java當初才敢於把數組設計成協變的。雖然向上轉型以后,編譯期類型檢查放松了,但因為數組運行時對內部元素類型的嚴格檢查,不匹配的類型還是插不進去的。這也是為什么容器Collection不能設計成協變的原因——Collection不做運行時類型檢查。
簡單泛型類
簡單泛型接口
如果一個int型量被傳遞到需要一個Integer對象的地方,那么,編譯器將在幕后插入一個對Integer構造方法的調用以獲得Integer對象。這就叫做自動裝箱。
反過來,如果一個Integer對象被放到需要int型量的地方,則編譯器將在幕后插入一個對intValue方法的調用以獲得int值,這就叫做自動拆箱。
對於其他7對基本類型/包裝類型,同樣會發生類似的情形。
Java7增加了一種新特性,稱作菱形運算符。使得下面的代碼:
可以寫成:
菱形運算符在不增加開發者負擔的情況下簡化了代碼。
在Java中,數組是協變的,如果B IS-A C,那么B[] IS-A C[] 。但是集合Collection不是協變的,這就使得集合缺少靈活性。為了彌補這個不足,Java5引入了通配符。舉例如下
既然有那么也有與之對應的,他們分別為泛型參數的上界和下界。
1.泛型類
2.泛型方法
泛型方法分為兩種,區別在於是否帶特定參數列表。
- 一、 不帶特定參數列表的泛型方法
因為它能接受不同類型的參數,所以,它是泛型方法。
- 二、帶特定參數列表的泛型方法
通過在返回類型前聲明特定參數,能夠獲得以下好處:
- 可以將T用作返回類型
- 不止一個方法參數的聲明需要用到T
- 將T用於聲明局部變量
泛型方法與泛型類很相似,因為參數列表使用相同的語法。在泛型方法中,泛型參數的聲明在返回類型之前,而泛型類在類名之后。
一、什么是類型擦除
讓我們看一個有趣的例子:
和很明顯是不同的類型。但是上面的程序卻認為它們是相同的類型。因此,存在一個殘酷的現實:
在泛型代碼內部,無法獲得任何有關泛型參數類型的信息。
Java泛型是使用擦除來實現的,這意味着當你在使用泛型的時候,任何具體的類型信息都被擦除了,你唯一知道的就是你在使用一個對象。因此和在運行時事實上是相同的類型。這兩種類型都被擦除成了他們的“原生”類型,即List。
再看另外一個例子:
類似的代碼,在c++中能夠正常運行。但是由於類型擦除,Java編譯器無法將useF()能夠在obj上調用f()這一需求映射到A擁有f()這一事實上。在類B中,由於T沒有指定上下界,於是T只擁有默認的上界Object(等同於),因此在泛型方法中我們只能調用Object的方法。類似的,如果我們想要在泛型方法中使用泛型參數的方法,那么我們必須設定上下界。下面的代碼就可以編譯了:
邊界聲明T必須具有類型A或者從A導出的類型。
泛型參數將擦除到它的第一個邊界。編譯器實際上會把類型參數替換為它的擦除。就像上面的示例那樣,T擦除到了A。.
二、怎么看待類型擦除
類型擦除不是一個語言特性,它只是一種折中。因為Java1中沒有泛型,泛型是后來加入的。為了不影響現有的類庫和已經在使用的代碼,Java沒有采用C++等語言那樣徹底的泛型,而是使用類型擦除這種溫和但有缺陷的方式類讓Java擁有泛型。
擦除的代價是明顯的。泛型不能用於顯示地引用運行時類型的操作之中,例如轉型、instanceof操作和new表達式。因為所有關於參數的類型信息都丟失了,無論何時,當你在編寫泛型代碼時,必須時刻提醒自己,雅思考試報名官網你只是看起來擁有有關參數的類型信息而已,你實際操作的只是一個Object。
- 基本類型不能用作類型參數,類型參數必須是引用類型。必須使用包裝類。
- 不能用instanceof檢測泛型,由於類型擦除的原因,任何泛型類型都會別擦除為它的原生類型。
- 泛型類中,static方法和static域不能引用類的泛型變量。因為在類型擦除過后,類型參數就不存在了。同時,我們知道同一個類的所有實例共用類的static域和static方法,如果靜態域接受泛型參數,那么這個參數到底是類型參數的哪一種實際類型就無法確定了。
- 不能創建一個泛型類型的實例。是非法的。
- 不能創建一個泛型數組,因為數組有嚴格的類型檢查。通常用泛型容器,如來實現同樣的需求。