1. 程式人生 > >扒一拔:Java 中的泛型(一)

扒一拔:Java 中的泛型(一)

目錄

@

1 泛型

泛型的本質是引數化型別,也就是說所操作的資料型別被指定為一個引數。這種引數型別可以用在類、介面和方法的建立中,分別稱為泛型類、泛型介面、泛型方法。

1.1 為什麼需要泛型

泛型是JDK1.5才出來的, 在泛型沒出來之前, 我們可以看看集合框架中的類都是怎麼樣的。

以下為JDK1.4.2的 HashMap

1.4 HashMap

可以看到, 在該版本中, 引數和返回值(引用型別)的都是 Object 物件。 而在 Java 中, 所有的類都是 Object 子類, 實用時, 可能需要進行強制型別轉換。 這種轉換在編譯階段並不會提示有什麼錯誤, 因此, 在使用時, 難免會出錯。

而有了泛型之後, HashMap

的中使用泛型來進行型別的檢查

Java 8 HashMap

通過泛型, 我們可以傳入相同的引數又能返回相同的引數, 由編譯器為我們來進行這些檢查。

這樣可以減少很多無關程式碼的書寫。

因此, 泛型可以使得型別引數化, 泛型有如下的好處

  1. 型別引數化, 實現程式碼的複用
  2. 強制型別檢查, 保證了型別安全,可以在編譯時就發現程式碼問題, 而不是到在執行時才發現錯誤
  3. 不需要進行強制轉換。

1.2 型別引數命名規約

按照慣例,型別引數名稱是單個大寫字母。 通過規約, 我們可以容易區分出型別變數和普通類、介面。

  • E - 元素
  • T - 型別
  • N - 數字
  • K - 鍵
  • V - 值
  • S,U,V - 第2種類型, 第3種類型, 第4種類型

2 泛型的簡單實用

2.1 最基本最常用

最早接觸的泛型, 應該就是集合框架中的泛型了。

List<Integer> list = new ArrayList<Integer>();
 
list.add(100086);     //OK
 
list.add("Number"); //編譯錯誤 

在以上的例子中, 將 String 加入時, 會提示錯誤。 編譯器不會編譯通過, 從而保證了型別安全。

2.2 簡單泛型類

2.2.1 非泛型類

先來定義一個簡單的類

public class SimpleClass {
    private Object obj;

    public Object getObj() {
        return obj;
    }

    public void setObj(Object obj) {
        this.obj = obj;
    }
}

這麼寫是沒問題的。 但是在使用上可能出現如下的錯誤:

    public static void main(String[] args) {
        SimpleClass simpleClass = new SimpleClass();
        simpleClass.setObj("ABC");// 傳入 String 型別
        Integer a = (Integer) simpleClass.getObj(); // Integer 型別接受
    }

以上寫是不會報錯的, 但是在執行時會出現報錯

java.lang.ClassCastException

如果是一個人使用, 那確實有可能會避免類似的情況。 但是, 如果是多人使用, 則你不能保證別人的用法是對的。 其存在著隱患。

2.2.2 泛型類的定義

我們可以使用泛型來強制型別限定

public class GenericClass<T> {
    private T obj;

    public T getObj() {
        return obj;
    }

    public void setObj(T obj) {
        this.obj = obj;
    }
}

2.2.3 泛型類的使用

在使用時, 在類的後面, 使用尖括號指明引數的型別就可以

    @Test
    public void testGenericClass(){
        GenericClass<String> genericClass = new GenericClass<>();
        genericClass.setObj("AACC");
    /*    Integer str = genericClass.getObj();//*/
    }

如果型別不符, 則編譯器會幫我們發現錯誤, 導致編譯不通過。

檢查

2.3 簡單泛型介面

2.3.1 定義

與類相似, 以 JDK 中的 Comparable 介面為例

package java.lang;
import java.util.*;

public interface Comparable<T> {
    public int compareTo(T o);
}

2.3.2 實現

在實現時, 指定具體的引數型別即可。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    ...
    public int compareTo(String anotherString) {
        byte v1[] = value;
        byte v2[] = anotherString.value;
        if (coder() == anotherString.coder()) {
            return isLatin1() ? StringLatin1.compareTo(v1, v2)
                              : StringUTF16.compareTo(v1, v2);
        }
        return isLatin1() ? StringLatin1.compareToUTF16(v1, v2)
                          : StringUTF16.compareToLatin1(v1, v2);
     }
     ...
    
}

2.4 簡單泛型方法

泛型方法可以引入自己的引數型別, 如同宣告泛型類一樣, 但是其型別引數我的範圍只是在宣告的方法本身。 靜態方法和非靜態方法, 以及建構函式都可以使用泛型。

2.4.1 泛型方法宣告

泛型方法的宣告, 型別變數放在修飾符之後, 在返回值之前

public class EqualMethodClass {
    public static <T> boolean equals(T t1, T t2){
        return t1.equals(t2);
    }
}

如上所示, 其中 是不能省略的。 而且可以是多種型別, 如 <K, V>

public class Util {
    public static <K, V> boolean sameType(K k, V v) {
        return k.getClass().equals(v.getClass());
    }
}

2.4.2 泛型方法的呼叫

呼叫時, 在方法之前指定引數的型別

    @Test
    public void equalsMethod(){
        boolean same = EqualMethodClass.<Integer>equals(1,1);
        System.out.println(same);
    }

3 型別變數邊界

3.1 定義

如果我們需要指定型別是某個類(介面)的子類(介面)

<T extends BundingType>

使用 extends , 表示 TBundingType 的子類, 兩者都可以是類或介面。

此處的 extends 和繼承中的是不一樣的。

如果有多個邊界限定:

 <T extends Number & Comparable>

使用的是 & 符號。

注意事項

如果邊界型別中有類, 則類必須是放在第一個

也就是說

 <T extends Comparable & Number> // 編譯錯誤

會報錯

3.2 示例

有時, 我們需要對型別進行一些限定, 比如說, 我們要獲取陣列的最小元素

public class ArrayUtils {
    public static <T> T min(T[] arr) {
        if (arr == null || arr.length == 0) {
            return null;
        }
        T smallest = arr[0];
        for (int i = 0; i < arr.length; i++) {
            if (smallest.compareTo(arr[i]) > 0) {
                smallest = arr[i];
            }
        }
        return smallest;
    }
}

上面的是報錯的。 因為, 在該函式中, 我們需要使用 compareTo 函式, 但是, 並不是所欲的類都有這個函式的。 因此, 我們可以這樣子限定

轉換成 <T extends Comparable 即可。

測試

    @Test
    public void testMin() {
        Integer a[] = {1, 4, 5, 6, 0, 2, -1};
        Assertions.assertEquals(ArrayUtils.<Integer>min(a), Integer.valueOf(-1));

    }

4 泛型, 繼承和子型別

4.1 泛型和繼承

在 Java 繼承中, 如果變數 A 是 變數 B 的子類, 則我們可以將 A 賦值給 B。 但是, 在泛型中則不能進行類似的賦值。

對繼承來說, 我們可以這樣做

public class Box<T> {
    List<T> boxs = new ArrayList<>();

    public void add(T element) {
        boxs.add(element);
    }

    public static void main(String[] args) {
        Box<Number> box = new Box<Number>();
        box.add(new Integer(10));   // OK
        box.add(new Double(10.1));  // OK
    }
}

但是, 在泛型中, Box 不能賦值給 Box(即兩個不是子類或父類的關係)。

泛型之間沒有繼承

可以使用下圖來進行闡釋
在這裡插入圖片描述

注意:

對於給定的具體型別 A 和 B(如 Number 和 Integer), MyClassMyClass 沒有任何的關係, 不管 A 和 B 之間是否有關係。

4.2 泛型和子型別

在 Java 中, 我們可以通過繼承或實現來獲得一個子型別。 以 Collection 為例

Collection

由於 ArrayList 實現了 List, 而 List 繼承了Collection。 因此, 只要型別引數沒有更改(如都是 String 或 都是 Integer), 則型別之間子父類關係會一直保留。

5 型別推斷

型別推斷並不是什麼高大上的東西, 我們日常中其實一直在用到。它是 Java 編譯器的能力, 其檢視每個方法呼叫和相應宣告來決定型別引數, 以便呼叫時相容。

值得注意的是, 型別推斷演算法僅僅是在呼叫引數, 目標型別和明顯的預期返回型別時使用

5.1 型別推斷和泛型方法

在下面的泛型方法中

public class Box<T> {
    private T t;

    public void set(T t) { this.t = t; }
    public T get() { return t; }

}

public class BoxDemo {

  public static <U> void addBox(U u, 
       List<Box<U>> boxes) {
    Box<U> box = new Box<>();
    box.set(u);
    boxes.add(box);
  }

  public static <U> void outputBoxes(List<Box<U>> boxes) {
    int counter = 0;
    for (Box<U> box: boxes) {
      U boxContents = box.get();
      System.out.println("Box #" + counter + " contains [" +
             boxContents.toString() + "]");
      counter++;
    }
  }

  public static void main(String[] args) {
    ArrayList<Box<Integer>> listOfIntegerBoxes =
      new ArrayList<>();
    BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);
    BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
    BoxDemo.addBox(Integer.valueOf(30), listOfIntegerBoxes);
    BoxDemo.outputBoxes(listOfIntegerBoxes);
  }
}

輸出

Box #0 contains [10]
Box #1 contains [20]
Box #2 contains [30]

我們可以看到, 泛型方法 addBox 中定義了一個型別引數 U, 在泛型方法的呼叫時, Java 編譯器可以推斷出該型別引數。 因此, 很多時候, 我們不需要指定他們。

如上面的例子, 我們可以顯示的指出

 BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);

也可以省略, 這樣, Java 編譯器可以從方法引數中推斷出

BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);

由於方法引數是 Integer, 因此, 可以推斷出型別引數就是 Integer。

5.2 泛型類的型別推斷和例項化

這是我們最常用到的型別推斷了: 將建構函式中的型別引數替換成<>(該符號被稱為“菱形(The diamond)”), 編譯器可以從上下文中推斷出該型別引數。

比如說, 正常情況先, 我們是這樣子宣告的

Map<String, List<String>> myMap = new HashMap<String, List<String>>();

但是, 實際上, 建構函式的型別引數是可以推斷出來的。 因此, 這樣子寫即可

Map<String, List<String>> myMap = new HashMap<>();

但是, 不能將 <> 去掉, 否則編譯器會報警告。

Map<String, List<String>> myMap = new HashMap(); // 警告

警告

5.3 類的型別推斷和建構函式

在泛型類和非泛型類中, 建構函式都是可以宣告自己的型別引數的。

class MyClass<X> {
  <T> MyClass(T t) {
    // ...
  }

  public static void main(String[] args) {
    MyClass<Integer> myObject = new MyClass<>("");
  }
}

在以上程式碼 main 函式中, X 對應的型別是 Integer, 而 T 對應的型別是 String

那麼, 菱形 <> 對應的是 X 還是 T 呢?

在 Java SE 7 之前, 其對應的是建構函式的型別引數。 而在 Java SE 7及以後, 其對應的是類的型別引數。

也就是說, 如果類不是泛型, 則程式碼是這樣子寫的

class MyClass{
  <T> MyClass(T t) {
    // ...
  }

  public static void main(String[] args) {
    MyClass myObject = new MyClass("");
  }
}

T 的實際型別, 編譯器根據方法的引數推斷出來。

5.4 型別推斷和目標型別

Java 編譯器利用目標型別來推斷泛型方法呼叫的型別引數。 表示式的目標型別就是 Java 編譯器所期望的資料型別, 根據該資料型別, 我們可以推斷出泛型方法的型別。

Collections 中的方法為例

static <T> List<T> emptyList();

我們在賦值時, 是這樣子

List<String> listOne = Collections.emptyList();

該表示式想要得到 List 的例項, 那麼, 該資料型別就是目標型別。 由於 emptyList 的返回值是 List, 因此, 編譯器就推斷, T對應的實際型別就是 String

當然, 我們也可以顯示的指定該型別引數

List<String> listOne = Collections.<String>emptyList();

6 萬用字元

在泛型中, 使用 ? 作為萬用字元, 其代表的是未知的型別。

6.1 設定萬用字元的下限

有時候, 我們想寫一個方法, 它可以傳遞 List, ListList。 此時, 可以使用萬用字元來幫助我們了。

設定萬用字元的上限

使用?, 其後跟隨著 extends, 再後面是 BundingType(即上邊界)

<? extends BundingType>

示例

class MyClass{
  public static void process(List<? extends Number> list) {
    for (Number elem : list) {
      System.out.println(elem.getClass().getName());
    }
  }
  public static void main(String[] args) {
    List<Integer> integers = new LinkedList<>(Arrays.asList(1));
    List<Double> doubles = new LinkedList<>(Arrays.asList(1.0));
    List<Number> numbers = new LinkedList<>(Arrays.asList(1));
    process(integers);
    process(doubles);
    process(numbers);
  }
}

輸出

java.lang.Integer
java.lang.Double
java.lang.Integer

也就是說, 我們通過萬用字元, 可以將List, ListList作為引數傳遞到同一個函式中。

6.2 設定萬用字元的下限

上限萬用字元是限定了引數的型別是指定的型別或者是其子類, 使用 extends 來進行。

而下限萬用字元, 使用的是 super 關鍵字, 限定了未知的型別是指定的型別或者其父類。

設定萬用字元的下限

<? super bundingType>

? 後跟著 super, 在跟上對應的邊界型別。

示例

  public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
      list.add(i);
    }
  }

對於該方法, 由於我們是要將整型新增到列表中, 因此, 需要傳入的列表必須是整型或者其父類。

6.3 未限定的萬用字元

當然, 我們也可以使用未限定的萬用字元。 如List<?>, 表示未知型別的列表。

使用萬用字元的情景

  1. 所寫的方法需要使用 Object 類所提供的功能
  2. 所寫的方法, 不依賴於具體的型別引數。 比較常見的是反射中, 用Class<?>而非Class, 因為絕大部分方法都不依賴於具體的型別。

那麼, 為什麼不使用 List