1. 程式人生 > 遊戲攻略 >《艾爾登法環》預購獎勵內容及領取方法

《艾爾登法環》預購獎勵內容及領取方法

在前面我們學習了最重要的類和物件,瞭解了面向物件程式設計的思想,注意,非常重要,面向物件是必須要深入理解和掌握的內容,不能草草結束。在本章節,我們會繼續深入瞭解,從我們的泛型開始,再到我們的資料結構,最後再開始我們的集合類學習。

走進泛型

為了統計學生成績,要求設計一個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的那個方法
}