《艾爾登法環》預購獎勵內容及領取方法
在前面我們學習了最重要的類和物件,瞭解了面向物件程式設計的思想,注意,非常重要,面向物件是必須要深入理解和掌握的內容,不能草草結束。在本章節,我們會繼續深入瞭解,從我們的泛型開始,再到我們的資料結構,最後再開始我們的集合類學習。
走進泛型
為了統計學生成績,要求設計一個Score物件,包括課程名稱、課程號、課程成績,但是成績分為兩種,一種是以優秀、良好、合格
來作為結果,還有一種就是 60.0、75.5、92.5
這樣的數字分數,那麼現在該如何去設計這樣的一個Score類呢?現在的問題就是,成績可能是String
型別,也可能是Integer
型別,如何才能很好的去存可能出現的兩種型別呢?
public class Score { String name; String id; Object score; //因為Object是所有型別的父類,因此既可以存放Integer也能存放String public Score(String name, String id, Object score) { this.name = name; this.id = id; this.score = score; } }
以上的方法雖然很好地解決了多種型別儲存問題,但是Object型別在編譯階段並不具有良好的型別判斷能力,很容易出現以下的情況:
public static void main(String[] args) { Score score = new Score("資料結構與演算法基礎", "EP074512", "優秀"); //是String型別的 //.... Integer number = (Integer) score.score; //獲取成績需要進行強制型別轉換,雖然並不是一開始的型別,但是編譯不會報錯 } //執行時出現異常! Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer at com.test.Main.main(Main.java:14)
使用Object型別作為引用,取值只能進行強制型別轉換,顯然無法在編譯期確定型別是否安全,專案中程式碼量非常之大,進行型別比較又會導致額外的開銷和增加程式碼量,如果不經比較就很容易出現型別轉換異常,程式碼的健壯性有所欠缺!(此方法雖然可行,但並不是最好的方法)
為了解決以上問題,JDK1.5新增了泛型,它能夠在編譯階段就檢查型別安全,大大提升開發效率。
public class Score<T> { //將Score轉變為泛型類<T> String name; String id; T score; //T為泛型,根據使用者提供的型別自動變成對應型別 public Score(String name, String id, T score) { //提供的score型別即為T代表的型別 this.name = name; this.id = id; this.score = score; } }
public static void main(String[] args) {
//直接確定Score的型別是字串型別的成績
Score<String> score = new Score<String>("資料結構與演算法基礎", "EP074512", "優秀");
Integer i = score.score; //編譯不通過,因為成員變數score型別被定為String!
}
泛型將資料型別的確定控制在了編譯階段,在編寫程式碼的時候就能明確泛型的型別!如果型別不符合,將無法通過編譯!
泛型本質上也是一個語法糖(並不是JVM所支援的語法,編譯後會轉成編譯器支援的語法,比如之前的foreach就是),在編譯後會被擦除,變回上面的Object型別呼叫,但是型別轉換由編譯器幫我們完成,而不是我們自己進行轉換(安全)
//反編譯後的程式碼
public static void main(String[] args) {
Score score = new Score("資料結構與演算法基礎", "EP074512", "優秀");
String i = (String)score.score; //其實依然會變為強制型別轉換,但是這是由編譯器幫我們完成的
}
像這樣在編譯後泛型的內容消失轉變為Object的情況稱為型別擦除
(重要,需要完全理解),所以泛型只是為了方便我們在編譯階段確定型別的一種語法而已,並不是JVM所支援的。
綜上,泛型其實就是一種型別引數,用於指定型別。
泛型的使用
泛型類
上一節我們已經提到泛型類的定義,實際上就是普通的類多了一個型別引數,也就是在使用時需要指定具體的泛型型別。泛型的名稱一般取單個大寫字母,比如T代表Type,也就是型別
的英文單詞首字母,當然也可以新增數字和其他的字元。
public class Score<T> { //將Score轉變為泛型類<T>
String name;
String id;
T score; //T為泛型,根據使用者提供的型別自動變成對應型別
public Score(String name, String id, T score) { //提供的score型別即為T代表的型別
this.name = name;
this.id = id;
this.score = score;
}
}
在一個普通型別中定義泛型,泛型T稱為引數化型別
,在定義泛型類的引用時,需要明確指出型別:
Score<String> score = new Score<String>("資料結構與演算法基礎", "EP074512", "優秀");
此時類中的泛型T已經被替換為String了,在我們獲取此物件的泛型屬性時,編譯器會直接告訴我們型別:
Integer i = score.score; //編譯不通過,因為成員變數score明確為String型別
注意,泛型只能用於物件屬性,也就是非靜態的成員變數才能使用:
static T score; //錯誤,不能在靜態成員上定義
由此可見,泛型是隻有在建立物件後編譯器才能明確泛型型別,而靜態型別是類所具有的屬性,不足以使得編譯器完成型別推斷。
泛型無法使用基本型別,如果需要基本型別,只能使用基本型別的包裝類進行替換!
Score<double> score = new Score<double>("資料結構與演算法基礎", "EP074512", 90.5); //編譯不通過
那麼為什麼泛型無法使用基本型別呢?回想上一節提到的型別擦除,其實就很好理解了。由於JVM沒有泛型概念,因此泛型最後還是會被編譯器編譯為Object,並採用強制型別轉換的形式進行型別匹配,而我們的基本資料型別和引用型別之間無法進行型別轉換,所以只能使用基本型別的包裝類來處理。
類的泛型方法
泛型方法的使用也很簡單,我們只需要把它當做一個未知的型別來使用即可:
public T getScore() { //若方法的返回值型別為泛型,那麼編譯器會自動進行推斷
return score;
}
public void setScore(T score) { //若方法的形式引數為泛型,那麼實參只能是定義時的型別
this.score = score;
}
Score<String> score = new Score<String>("資料結構與演算法基礎", "EP074512", "優秀");
score.setScore(10); //編譯不通過,因為只接受String型別
同樣地,靜態方法無法直接使用類定義的泛型(注意是無法直接使用,靜態方法可以使用泛型)
自定義泛型方法
那麼如果我想在靜態方法中使用泛型呢?首先我們要明確之前為什麼無法使用泛型,因為之前我們的泛型定義是在類上的,只有明確具體的型別才能開始使用,也就是建立物件時完成型別確定,但是靜態方法不需要依附於物件,那麼只能在使用時再來確定了,所以靜態方法可以使用泛型,但是需要單獨定義:
public static <E> void test(E e){ //在方法定義前宣告泛型
System.out.println(e);
}
同理,成員方法也能自行定義泛型,在實際使用時再進行型別確定:
public <E> void test(E e){
System.out.println(e);
}
其實,無論是泛型類還是泛型方法,再使用時一定要能夠進行型別推斷,明確型別才行。
注意一定要區分類定義的泛型和方法前定義的泛型!
泛型引用
可以看到我們在定義一個泛型類的引用時,需要在後面指出此型別:
Score<Integer> score; //宣告泛型為Integer型別
如果不希望指定型別,或是希望此引用型別可以引用任意泛型的Score
類物件,可以使用?
萬用字元,來表示自動匹配任意的可用型別:
Score<?> score; //score可以引用任意的Score型別物件了!
那麼使用萬用字元之後,得到的泛型成員變數會是什麼型別呢?
Object o = score.getScore(); //只能變為Object
因為使用了萬用字元,編譯器就無法進行型別推斷,所以只能使用原始型別。
在學習了泛型的界限後,我們還會繼續瞭解萬用字元的使用。
泛型的界限
現在有一個新的需求,現在沒有String型別的成績了,但是成績依然可能是整數,也可能是小數,這時我們不希望使用者將泛型指定為除數字型別外的其他型別,我們就需要使用到泛型的上界定義:
public class Score<T extends Number> { //設定泛型上界,必須是Number的子類
private final String name;
private final String id;
private T score;
public Score(String name, String id, T score) {
this.name = name;
this.id = id;
this.score = score;
}
public T getScore() {
return score;
}
}
通過extends
關鍵字進行上界限定,只有指定型別或指定型別的子類才能作為型別引數。
同樣的,泛型萬用字元也支援泛型的界限:
Score<? extends Number> score; //限定為匹配Number及其子類的型別
同理,既然泛型有上限,那麼也有下限:
Score<? super Integer> score; //限定為匹配Integer及其父類
通過super
關鍵字進行下界限定,只有指定型別或指定型別的父類才能作為型別引數。
圖解如下:
那麼限定了上界後,我們再來使用這個物件的泛型成員,會變成什麼型別呢?
Score<? extends Number> score = new Score<>("資料結構與演算法基礎", "EP074512", 10);
Number o = score.getScore(); //得到的結果為上界型別
也就是說,一旦我們指定了上界後,編譯器就將範圍從原始型別Object
提升到我們指定的上界Number
,但是依然無法明確具體型別。思考:那如果定義下限呢?
那麼既然我們可以給泛型類限定上界,現在我們來看編譯後結果呢:
//使用javap -l 進行反編譯
public class com.test.Score<T extends java.lang.Number> {
public com.test.Score(java.lang.String, java.lang.String, T);
LineNumberTable:
line 8: 0
line 9: 4
line 10: 9
line 11: 14
line 12: 19
LocalVariableTable:
Start Length Slot Name Signature
0 20 0 this Lcom/test/Score;
0 20 1 name Ljava/lang/String;
0 20 2 id Ljava/lang/String;
0 20 3 score Ljava/lang/Number; //可以看到score的型別直接被編譯為Number類
public T getScore();
LineNumberTable:
line 15: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/test/Score;
}
因此,一旦確立上限後,編譯器會自動將型別提升到上限型別。
鑽石運算子
我們發現,每次建立泛型物件都需要在前後都標明型別,但是實際上後面的型別宣告是可以去掉的,因為我們在傳入引數時或定義泛型類的引用時,就已經明確了型別,因此JDK1.7提供了鑽石運算子來簡化程式碼:
Score<Integer> score = new Score<Integer>("資料結構與演算法基礎", "EP074512", 10); //1.7之前
Score<Integer> score = new Score<>("資料結構與演算法基礎", "EP074512", 10); //1.7之後
泛型與多型
泛型不僅僅可以可以定義在類上,同時也能定義在介面上:
public interface ScoreInterface<T> {
T getScore();
void setScore(T t);
}
當實現此介面時,我們可以選擇在實現類明確泛型型別或是繼續使用此泛型,讓具體建立的物件來確定型別。
public class Score<T> implements ScoreInterface<T>{ //將Score轉變為泛型類<T>
private final String name;
private final String id;
private T score;
public Score(String name, String id, T score) {
this.name = name;
this.id = id;
this.score = score;
}
public T getScore() {
return score;
}
@Override
public void setScore(T score) {
this.score = score;
}
}
public class StringScore implements ScoreInterface<String>{ //在實現時明確型別
@Override
public String getScore() {
return null;
}
@Override
public void setScore(String s) {
}
}
抽象類同理,這裡就不多做演示了。
多型型別擦除
思考一個問題,既然繼承後明確了泛型型別,那麼為什麼@Override
不會出現錯誤呢,重寫的條件是需要和父類的返回值型別、形式引數一致,而泛型預設的原始型別是Object型別,子類明確後變為Number型別,這顯然不滿足重寫的條件,但是為什麼依然能編譯通過呢?
class A<T>{
private T t;
public T get(){
return t;
}
public void set(T t){
this.t=t;
}
}
class B extends A<Number>{
private Number n;
@Override
public Number get(){ //這並不滿足重寫的要求,因為只能重寫父類同樣返回值和引數的方法,但是這樣卻能夠通過編譯!
return t;
}
@Override
public void set(Number t){
this.t=t;
}
}
通過反編譯進行觀察,實際上是編譯器幫助我們生成了兩個橋接方法用於支援重寫:
@Override
public Object get(){
return this.get();//呼叫返回Number的那個方法
}
@Override
public void set(Object t ){
this.set((Number)t ); //呼叫引數是Number的那個方法
}