Java核心之細說泛型

一锤子技术员發表於2024-03-08

泛型是什麼?

等你使用java逐漸深入以後會了解或逐步使用到Java泛型。Java 中的泛型是 JDK 5 中引入的功能之一。"Java 泛型 "是一個技術術語,表示一組與定義和使用泛型型別和方法有關的語言特性。在 Java 中,泛型型別或方法與普通型別和方法的區別在於它們具有型別引數。

入門

如果仔細觀察集合框架類,就會發現大多數類都使用物件型別的引數,並以物件形式從方法中返回值。現在,在這種形式下,它們可以將任何 Java 型別作為引數並返回相同的值。它們本質上是異構的,即不屬於特定的相似型別。

像我們這樣的程式設計師經常希望指定一個集合只包含某種型別的元素,例如Integer or String 或 Employee。在最初的集合框架中,如果不在程式碼中新增額外的檢查,就不可能實現同質集合。引入泛型就是為了消除這一限制。它們會在編譯時自動在程式碼中新增這種型別的引數檢查。這樣,我們就不必編寫大量不必要的程式碼,如果編寫得當,這些程式碼在執行時實際上不會增加任何價值。

泛型透過提供實際的型別引數來替代形式化的型別引數,從而例項化形成引數化的型別。例如下面這樣:

public class LinkedList<E> ...
LinkedList<String> list = new LinkedList();
  • 解釋
  1. 像 LinkedList 這樣的類是一種具有型別引數 E 的泛型。
  2. 像 LinkedList 或 LinkedList 這樣的例項被稱為引數化型別。
  3. 字串和整數是各自的實際型別引數。

通俗地說,泛型強制保證了 Java 語言的型別安全。

現在,我們已經對 Java 中為什麼會出現泛型有了一定的瞭解。下一步是瞭解 Java 中的泛型是如何工作的。在原始碼中使用泛型時究竟會發生什麼?

泛型是如何工作的?

泛型的核心是 "型別安全"。究竟什麼是型別安全?它只是編譯器的一種保證,即如果在正確的地方使用了正確的型別,那麼在執行時就不會出現任何 ClassCastException

一個用例可以是一個整數列表,即 List。如果您宣告瞭 List 這樣的列表,那麼 Java 保證會檢測並報告在上述列表中插入任何非整數型別的嘗試。

List<Integer> list = new ArrayList<>();
list.add(1);
list.add("one");  //compiler error

型別安全

泛型的核心是 "型別安全"。究竟什麼是型別安全?它只是編譯器的一種保證,即如果在正確的地方使用了正確的型別,那麼在執行時就不會出現任何 ClassCastException。

一個用例可以是一個整數列表,即 List。如果您宣告瞭 List 這樣的列表,那麼 Java 保證會檢測並報告在上述列表中插入任何非整數型別的嘗試。

List<Integer> list = new ArrayList<>();
list.add(1);
list.add("one");  //compiler error

型別擦除

泛型的另一個重要術語是 "型別擦除"。它的基本意思是,使用泛型新增到原始碼中的所有額外資訊都將從生成的位元組碼中刪除。在位元組碼中,如果完全不使用泛型,得到的將是舊的 Java 語法。這必然有助於生成和執行 Java 5 之前編寫的程式碼,因為 Java 5 尚未在語言中新增泛型。
來看一個例子:

List<Integer> list = new ArrayList<>();
list.add(1000);

如果將上述示例的位元組碼與帶/不帶泛型的位元組碼進行比較,那麼兩者將沒有任何區別。顯然,編譯器刪除了所有泛型資訊。因此,上面的程式碼與下面沒有使用泛型的程式碼非常相似。

List list = new ArrayList();
list.add(1000);

準確地說,Java 中的 "泛型 "只不過是為了型別安全而給程式碼新增的語法糖,所有這些型別資訊都會被編譯器的 "型別清除 "功能抹去。

泛型的分類

現在,我們對通用語有了一些瞭解。現在開始探索圍繞泛型的其他重要概念。首先,我將介紹將屬類應用於原始碼的各種方法。

類或介面

如果一個類宣告瞭一個或多個型別變數,那麼這個類就是泛型。這些型別變數被稱為類的型別引數。讓我們透過一個例子來理解。
DemoClass 是一個簡單的類,它有一個屬性 t(也可以多個),屬性型別是物件。

class DemoClass {
   private Object t;
   public void set(Object t) { this.t = t; }
   public Object get() { return t; }
}

例如,如果我們希望類的一個例項持有 "字串 "型別的值 t,那麼程式設計師就應該設定和獲取唯一的字串型別。

由於我們已將屬性型別宣告為物件,因此無法強制執行這一限制。程式設計師可以設定任何物件,也可以期望從 get() 方法中得到任何返回值型別,因為所有 Java 型別都是物件類的子型別。

為了實現這種限制,我們可以使用下面的泛型:

class DemoClass<T> {
   //T stands for "Type"
   private T t;
   public void set(T t) { this.t = t; }
   public T get() { return t; }
}

現在我們可以放心,類不會被錯誤地使用。DemoClass 的使用示例如下:

DemoClass<String> instance = new DemoClass<>();
instance.set("lokesh");   //Correct usage
instance.set(1);        //This will raise compile time error

上述類比同樣適用於介面。讓我們快速看一個例子,瞭解介面中如何使用泛型型別資訊。

//Generic interface definition
interface DemoInterface<T1, T2>
{
   T2 doSomeOperation(T1 t);
   T1 doReverseOperation(T2 t);
}
//A class implementing generic interface
class DemoClass implements DemoInterface<String, Integer>
{
   public Integer doSomeOperation(String t)
   {
      //some code
   }
   public String doReverseOperation(Integer t)
   {
      //some code
   }
}

我希望我已經說得足夠清楚,讓大家對泛型類和介面有了一些瞭解。現在我們來看看泛型方法和建構函式。

泛型方法和建構函式

泛型方法與泛型類非常相似。它們只有一點不同,即型別資訊的範圍只在方法(或建構函式)內部。泛型方法是引入自己的型別引數的方法。

讓我們透過一個例子來理解這一點。下面是一個泛型方法的程式碼示例,該方法可用於查詢型別引數在該型別變數列表中的所有出現次數

public static <T> int countAllOccurrences(T[] list, T item) {
   int count = 0;
   if (item == null) {
      for ( T listItem : list )
         if (listItem == null)
            count++;
   }
   else {
      for ( T listItem : list )
         if (item.equals(listItem))
            count++;
   }
   return count;
}

如果在此方法中傳遞一個字串列表和另一個要搜尋的字串,它將正常工作。但如果試圖在字串列表中查詢一個 Number,則會在編譯時出錯。

讓我們再舉一個泛型建構函式的例子。

public class MyClass<T> {
    private T value;

    // 泛型建構函式
    public MyClass(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}
MyClass<String> myString = new MyClass<>("Hello");
MyClass<Integer> myInt = new MyClass<>(42);

泛型陣列

任何語言中的陣列都有相同的含義,即陣列是相似型別元素的集合。在 Java 中,執行時在陣列中推送任何不相容的型別都會引發 ArrayStoreException。這意味著陣列會在執行時保留其型別資訊,而泛型會在執行時使用型別擦除或刪除任何型別資訊。由於上述衝突,不允許例項化泛型陣列。

public class GenericArray<T> {
    // this one is fine
    public T[] notYetInstantiatedArray;
    // causes compiler error; Cannot create a generic array of T
    public T[] array = new T[5];
}

與上述通用型別類和方法相同,我們也可以使用通用陣列。我們知道,陣列是相似型別元素的集合,推送任何不相容的型別都會在執行時丟擲 ArrayStoreException;而集合類則不會出現這種情況。

Object[] array = new String[10];
array[0] = "lokesh";
array[1] = 10;      //This will throw ArrayStoreException

上述錯誤並不難犯。它隨時都可能發生。因此,最好也為陣列提供型別資訊,以便在編譯時就能發現錯誤。

陣列不支援泛型的另一個原因是陣列是共變的,這意味著超型別引用陣列是子型別引用陣列的超型別。也就是說,Object[] 是 String[] 的超型別,可以透過 Object[] 型別的引用變數訪問字串陣列。

Object[] objArr = new String[10];  // fine
objArr[0] = new String();

泛型萬用字元

在泛型程式碼中,問號(?)被稱為萬用字元,代表未知型別。萬用字元引數化型別是泛型型別的例項化,其中至少有一個型別引數是萬用字元。萬用字元引數化型別的例子有 Collection 和 Pair<tring,?>。萬用字元可以在多種情況下使用:作為引數、欄位或區域性變數的型別;有時也可以作為返回型別(儘管程式設計實踐中最好更具體一些)。萬用字元絕對不能用作泛型方法呼叫、泛型類例項建立或超型別的型別引數。<ring,?>

在不同位置放置萬用字元也有不同的含義,例如:

Collection 表示 Collection 介面的所有例項,與型別引數無關。
List 表示元素型別為 Number 子型別的所有列表型別。
Comparator<? super String< 表示型別引數型別為 String 的超型別的比較器介面的所有例項。
萬用字元引數化型別並不是可以出現在新表示式中的具體型別。它只是暗示了泛型執行的規則,即在使用了萬用字元的任何特定場景中,哪些型別是有效的。

例如,下面是涉及萬用字元的有效宣告:

Collection<?> coll = new ArrayList<String>();
//OR
List<? extends Number> list = new ArrayList<Long>();
//OR
Pair<String,?> pair = new Pair<String,Integer>();

以下是萬用字元的無效使用,編譯時會出錯。

List<? extends Number> list = new ArrayList<String>();  //String is not subclass of Number; so error
//OR
Comparator<? super String> cmp = new RuleBasedCollator(new Integer(100)); //Integer is not superclass of String

泛型中的萬用字元可以是無界的,也可以是有界的。讓我們從不同的術語中找出區別。

無界萬用字元引數化型別

通用型別,所有型別引數都是無限制萬用字元"?",對型別變數沒有任何限制,例如:

ArrayList<?>  list = new ArrayList<Long>();
//or
ArrayList<?>  list = new ArrayList<String>();
//or
ArrayList<?>  list = new ArrayList<Employee>();

有界萬用字元引數化型別

有界萬用字元對我們可以用來例項化引數化型別的可能型別施加了一些限制。這種限制透過關鍵字 "super "和 "extends "來實現。為了更清楚地區分,我們把它們分為上界萬用字元和下界萬用字元。

上界萬用字元

例如,如果您想編寫一個適用於 List、List 和 List 的方法,您可以透過使用有上界的萬用字元來實現,例如,您可以指定 List<? extends Number>。這裡,Integer 和 Double 是 Number 類的子型別。通俗地說,如果您想讓泛型表示式接受某一特定型別的所有子類,您可以使用關鍵字 "extends "來使用上界萬用字元:

public class GenericsExample<T>
{
   public static void main(String[] args)
   {
      //List of Integers
      List<Integer> ints = Arrays.asList(1,2,3,4,5);
      System.out.println(sum(ints));
      //List of Doubles
      List<Double> doubles = Arrays.asList(1.5d,2d,3d);
      System.out.println(sum(doubles));
      List<String> strings = Arrays.asList("1","2");
      //This will give compilation error as :: The method sum(List<? extends Number>) in the 
      //type GenericsExample<T> is not applicable for the arguments (List<String>)
      System.out.println(sum(strings));
   }
   //Method will accept 
   private static Number sum (List<? extends Number> numbers){
      double s = 0.0;
      for (Number n : numbers)
         s += n.doubleValue();
      return s;
   }
}

下界萬用字元

如果想讓泛型表示式接受所有型別,這些型別都是某個特定型別的 "超級 "型別或某個特定類的父類,那麼就可以使用 "super "關鍵字的下界萬用字元來實現這一目的。

在下面的示例中,我建立了三個類,即 SuperClassChildClassGrandChildClass。它們之間的關係如下程式碼所示。現在,我們必須建立一個方法,以某種方式獲取 GrandChildClass 的資訊(例如,從資料庫中獲取)並建立一個例項。我們希望將這個新的 GrandChildClass 儲存在已經存在的 GrandChildClasses 列表中。

這裡的問題是,GrandChildClass 是 ChildClass 和 SuperClass 的子型別。因此,任何 SuperClasses 和 ChildClasses 的通用列表都可以容納 GrandChildClasses。在這裡,我們必須使用 "super "關鍵字,藉助下界萬用字元。

public class GenericsExample<T>
{
   public static void main(String[] args)
   {
      //List of grand children
      List<GrandChildClass> grandChildren = new ArrayList<GrandChildClass>();
      grandChildren.add(new GrandChildClass());
      addGrandChildren(grandChildren);
      //List of grand childs
      List<ChildClass> childs = new ArrayList<ChildClass>();
      childs.add(new GrandChildClass());
      addGrandChildren(childs);
      //List of grand supers
      List<SuperClass> supers = new ArrayList<SuperClass>();
      supers.add(new GrandChildClass());
      addGrandChildren(supers);
   }
   public static void addGrandChildren(List<? super GrandChildClass> grandChildren)
   {
      grandChildren.add(new GrandChildClass());
      System.out.println(grandChildren);
   }
}
class SuperClass{
}
class ChildClass extends SuperClass{
}
class GrandChildClass extends ChildClass{
}

哪些行為是不允許的?

到目前為止,我們已經瞭解了一些使用泛型可以避免在應用程式中出現大量 ClassCastException 例項的方法。我們還了解了萬用字元的用法。現在,我們要確定一些不允許使用泛型的行為。

靜態泛型成員

我們不能在類中定義靜態泛型引數化成員。任何這樣的嘗試都會在編譯時產生錯誤:無法對非靜態型別 T 進行靜態引用。

public class GenericsExample<T>
{
   private static T member; //This is not allowed
}

不能例項化泛型

任何建立 T 例項的嘗試都會失敗,並顯示錯誤:無法例項化 T 型別。

public class GenericsExample<T>
{
   public GenericsExample(){
      new T();
   }
}

泛型與宣告中的原始型別不相容

是的,沒錯。您不能宣告 List 或 Map<String, double> 這樣的泛型表示式。當然,您可以使用包裝類代替基本型別,然後在傳遞實際值時使用基本型別。這些基本型別值可以透過使用自動裝箱將基本型別轉換為相應的包裝類來接受。

final List<int> ids = new ArrayList<>();    //Not allowed
final List<Integer> ids = new ArrayList<>(); //Allowed

我們無法建立泛型異常類

有時,程式設計師可能需要在丟擲異常的同時傳遞一個泛型型別的例項。這在 Java 中是做不到的。

// causes compiler error
public class GenericException<T> extends Exception {}

當您嘗試建立這樣一個異常時,您將看到這樣一條資訊:通用類 GenericException 可能無法子類化 java.lang.Throwable。

關於 Java 泛型的先寫到這裡,凡事還是需要多實踐!

相關文章