扒一拔: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);
    }
}

如上所示, 其中 <T> 是不能省略的。 而且可以是多種類型, 如 <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> 轉換成 <T extends Comparable<T>> 即可。

測試

    @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<Intager> 不能賦值給 Box<Number>(即兩個不是子類或父類的關係)。

泛型之間沒有繼承

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

注意:

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

4.2 泛型和子類型

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

Collection

由於 ArrayList<E></code> 實現了List, 而 List<E> 繼承了Collection<E>。 因此, 只要類型參數沒有更改(如都是 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<String> 的實例, 那麼, 該數據類型就是目標類型。 由於 emptyList 的返回值是 List<T>, 因此, 編譯器就推斷, T對應的實際類型就是 String

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

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

6 通配符

在泛型中, 使用 ? 作爲通配符, 其代表的是未知的類型。

6.1 設定通配符的下限

有時候, 我們想寫一個方法, 它可以傳遞 List<Integer>, List<Double>List<Number>。 此時, 可以使用通配符來幫助我們了。

設定通配符的上限

使用?, 其後跟隨着 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<Integer>, List<Double>List<Number>作爲參數傳遞到同一個函數中。

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<T>, 因爲絕大部分方法都不依賴於具體的類型。

那麼, 爲什麼不使用 List<Object> 進行替代呢?

public static void printList(List<Object> list) {
    for (Object elem : list)
        System.out.println(elem + " ");
    System.out.println();
}

在以上的方法中, 我們想帶引出列表的各項。 但是以上的函數只能輸出的是 Object 的實例(我們只能傳入List<Object>, 而不是 List<Interger>等, 因爲不是子類和父類的關係)。

而更改爲通配符之後

public static void printList(List<?> list) {
    for (Object elem: list)
        System.out.print(elem + " ");
    System.out.println();
}

我們可以傳入任意的 List.

  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));
    printList(integers);
    printList(doubles);
    printList(numbers);
  }

以上的代碼運行正常。

6.4 通配符和子類型

在泛型和子類型中, 我們論證了

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

但是, 通配符可以在類或接口之間創建關係。 實現了子類和父類的關係。 因爲 IntegerNumber的子類, 因此, 可以有如下的關係。

繼承

正因爲如此, 我們在前面進行參數傳遞時, 纔可以進行多種類型參數的傳遞。

6.5 通配符捕獲

我們想編寫一個方法, 該方法

public class WildcardError {

    void foo(List<?> i) {
        ? t = i.get(0); // 錯誤
        i.set(0, t);
    }
}

我們需要取得傳入的類型, 但是, 在編寫時, 不能使用 “?” 來作爲一種類型。 此時, 我們可以使用類型捕獲來解決幹問題。

public class WildcardError {

    void foo(List<?> i) {
        fooHelper(i);
    }
    private <T> void fooHelper(List<T> l) {
         T t = l.get(0); // 錯誤
        l.set(0, t);
    }

}

在此過程中, fooHelper 是泛型方法, 而 foo 方法不是, 它具有固定類型的參數。 在此情況下, T 捕獲通配符。 它不知道具體的類型是哪一個, 但是, 這是一個明確的類型。

慣例上, helper 方法, 被命名爲 xxxHelper。

7 類型擦除

爲了實現泛型, 編譯器使用類型擦除:

  1. 替換所有的類型爲其邊界類或 沒有邊界則爲 Object。 因此, 其所產生的字節碼, 僅僅 包含的是原始的類,接口, 方法。
  2. 在必要的地方插入類型轉換以保證類型安全
  3. 生成橋接方法以保留擴展泛型類型的多態。

也就是說, 經過編譯之後, 任何的類型都會被擦除。 因此, List<Integer>List<String>在運行時是一樣的類型, 進行類型擦除之後, 都是 List

7.1 類型擦除

定義一個泛型類

public class Node<T> {

    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    public T getData() { return data; }

    public static void main(String[] args) {
        Node<String> node = new Node<>("11", null);
        System.out.println(node.getData());
    }
}

對其進行反編譯, 可以獲得:

public class Node
{

	private Object data;
	private Node next;

	public Node(Object data, Node next)
	{
		this.data = data;
		this.next = next;
	}

	public Object getData()
	{
		return data;
	}

	public static void main(String args[])
	{
		Node node = new Node("11", null);
		System.out.println((String)node.getData());
	}
}

可以看到, 類型已經被替換成 Object, 然後在 main 方法中, 將 Object 轉換爲 String, 因爲我們傳入的是 String 類型。

同理, 將

public class Node<T> {

替換爲

public class Node<T extends Serializable> {

則, 反編譯後, 替換 T 爲邊界類型

public class Node
{

	private Serializable data;
	private Node next;

	public Node(Serializable data, Node next)
	{
		this.data = data;
		this.next = next;
	}

	public Serializable getData()
	{
		return data;
	}

	public static void main(String args[])
	{
		Node node = new Node("11", null);
		System.out.println((String)node.getData());
	}
}

方法的類型擦除也是一樣的。

7.2 類型擦除和橋接方法

正因爲有類型擦除的存在, 因此, 任何在運行時需要知道確切類型信息的操作都無法工作。

有時候也會導致一些我們無法預料到的情況。

在方法的重寫時, 我們會遇到這樣的情況

聲明一個泛型類

public class Node<T> {

    public T data;

    public Node(T data) { this.data = data; }

    public T getData() {
        return data;
    }
    public void setData(T data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

繼承泛型類, 並指明瞭它的類型爲 Integer

public class MyNode extends Node<Integer> {

    public MyNode(Integer data) {
        super(data);
    }

    @Override
    public Integer getData() {
        return super.getData();
    }

    @Override
    public void setData(Integer data) {
        super.setData(data);
    }

    public static void main(String[] args) {
        Class<?> clazz = MyNode.class;
        for (Method m:
             clazz.getDeclaredMethods()) {
            System.out.println(m + ":" + m.isBridge());
        }
    }
}

那麼, 這個時候, 由於類型擦除,Node 類變成了這樣子

public class Node
{

	public Object data;

	public Node(Object data)
	{
		this.data = data;
	}

	public Object getData()
	{
		return data;
	}

	public void setData(Object data)
	{
		System.out.println("Node.setData");
		this.data = data;
	}
}

那麼問題就出現了。 如果沒有任何的情況, 對於 setData 方法來說, 在父類中

	public void setData(Object data)
	{
		System.out.println("Node.setData");
		this.data = data;
	}

在子類中

    public void setData(Integer data) {
        super.setData(data);
    }

顯然, 這兩個方法並不是重寫的關係。

爲了解決這個問題, 以便在泛型擦除之後保持多態性, 編譯器會產生橋接方法, 以保證子類運行時正確的。

生成的橋接方法:

public volatile void setData(Object obj){
		setData((Integer)obj);
}

先寫到這吧, 後面在繼續深入。已經太長了!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章