你好!我們繼續我們關於泛型的系列課程。我們之前對它們是什麼以及為什麼需要它們有一個大概的了解。今天我們將更多地了解泛型的一些特性以及如何使用它們。我們走吧! 類型擦除 - 1上一課中,我們討論了泛型類型原始類型之間的區別。原始類型是其類型已被刪除的泛型類。

List list = new ArrayList();
這是一個例子。這裡我們不指明將放置什麼類型的對象List。如果我們嘗試創建這樣的 aList並向其中添加一些對象,我們將在 IDEA 中看到警告:

"Unchecked call to add(E) as a member of raw type of java.util.List".
但我們也談到了泛型只出現在 Java 5 中的事實。到這個版本發佈時,程序員已經使用原始類型編寫了一堆代碼,所以語言的這個特性不能停止工作,並且能夠在 Java 中創建原始類型被保留。然而,事實證明這個問題更為普遍。如您所知,Java 代碼被轉換為一種稱為字節碼的特殊編譯格式,然後由 Java 虛擬機執行。但是如果我們在轉換的過程中把類型參數的信息放到字節碼裡面,就會把之前寫的代碼全部打斷,因為Java 5之前是沒有類型參數的!使用泛型時,您需要記住一個非常重要的概念。它被稱為類型擦除. 這意味著類不包含有關類型參數的信息。此信息僅在編譯期間可用,並在運行前被擦除(變得不可訪問)。如果您嘗試將錯誤類型的對象放入您的 中List<String>,編譯器將生成錯誤。這正是該語言的創建者在創建泛型時想要實現的目標:編譯時檢查。但是當你所有的 Java 代碼都變成字節碼時,它就不再包含類型參數的信息了。在字節碼中,您的List<Cat>貓列表與字符串沒有什麼不同List<String>。在字節碼中,沒有什麼說這cats是一個對象列表Cat。此類信息在編譯期間會被刪除——只有您擁有一個列表這一事實List<Object> cats才會最終出現在程序的字節碼中。讓我們看看這是如何工作的:

public class TestClass<T> {

   private T value1;
   private T value2;

   public void printValues() {
       System.out.println(value1);
       System.out.println(value2);
   }

   public static <T> TestClass<T> createAndAdd2Values(Object o1, Object o2) {
       TestClass<T> result = new TestClass<>();
       result.value1 = (T) o1;
       result.value2 = (T) o2;
       return result;
   }

   public static void main(String[] args) {
       Double d = 22.111;
       String s = "Test String";
       TestClass<Integer> test = createAndAdd2Values(d, s);
       test.printValues();
   }
}
我們創建了自己的通用TestClass類。它非常簡單:它實際上是 2 個對象的一個小“集合”,在創建對象時立即存儲這些對象。它有 2 個T字段。方法執行的時候createAndAdd2Values(),傳入的兩個對象 (Object aObject b必須強制轉換為T類型,然後添加到對TestClass像中。在main()方法中,我們創建了一個TestClass<Integer>,即Integer類型參數替換了Integer類型參數。我們也將 aDouble和 a傳遞String給方法createAndAdd2Values()。你認為我們的程序會運行嗎?畢竟,我們指定Integer為類型參數,但String絕對不能將 a 轉換為 an Integer!讓我們運行main()方法和檢查。控制台輸出:

22.111 
Test String
這是出乎意料的!為什麼會這樣? 這是類型擦除的結果。編譯代碼時,有關Integer用於實例化對象的類型參數的 信息已被刪除。TestClass<Integer> test該領域成為TestClass<Object> test。我們的DoubleString參數很容易轉換為Object對象(它們沒有Integer像我們預期的那樣轉換為對象!)並悄悄地添加到TestClass. 這是類型擦除的另一個簡單但非常有啟發性的示例:

import java.util.ArrayList;
import java.util.List;

public class Main {

   private class Cat {

   }

   public static void main(String[] args) {

       List<String> strings = new ArrayList<>();
       List<Integer> numbers = new ArrayList<>();
       List<Cat> cats = new ArrayList<>();

       System.out.println(strings.getClass() == numbers.getClass());
       System.out.println(numbers.getClass() == cats.getClass());

   }
}
控制台輸出:

true 
true
看起來我們創建了具有三種不同類型參數的集合—— StringInteger和我們自己的Cat類。但是在轉換為字節碼的過程中,所有三個列表都變成了List<Object>,所以當程序運行時它告訴我們在所有三種情況下我們都使用了同一個類。

使用數組和泛型時類型擦除

在使用數組和泛型類(例如 )時,必須清楚地理解一個非常重要的點List。在為您的程序選擇數據結構時,您還應該考慮到這一點。泛型受類型擦除的影響。有關類型參數的信息在運行時不可用。相比之下,數組在程序運行時知道並可以使用有關其數據類型的信息。嘗試將無效類型放入數組將導致拋出異常:

public class Main2 {

   public static void main(String[] args) {

       Object x[] = new String[3];
       x[0] = new Integer(222);
   }
}
控制台輸出:

Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer
因為數組和泛型之間存在很大差異,所以它們可能存在兼容性問題。最重要的是,您不能創建通用對像數組,甚至不能創建參數化數組。這聽起來有點混亂嗎?讓我們來看看。例如,您不能在 Java 中執行以下任何操作:

new List<T>[]
new List<String>[]
new T[]
如果我們嘗試創建一個對像數組List<String>,我們會得到一個編譯錯誤,抱怨通用數組創建:

import java.util.List;

public class Main2 {

   public static void main(String[] args) {

       // Compilation error! Generic array creation
       List<String>[] stringLists = new List<String>[1];
   }
}
但是為什麼這樣做呢?為什麼不允許創建這樣的數組?這一切都是為了提供類型安全。如果編譯器讓我們創建這樣的通用對像數組,我們可能會給自己帶來很多問題。這是 Joshua Bloch 的書“Effective Java”中的一個簡單示例:

public static void main(String[] args) {

   List<String>[] stringLists = new List<String>[1];  //  (1)
   List<Integer> intList = Arrays.asList(42, 65, 44);  //  (2)
   Object[] objects = stringLists;  //  (3)
   objects[0] = intList;  //  (4)
   String s = stringLists[0].get(0);  //  (5)
}
讓我們想像一下,創建一個數組List<String>[] stringLists是允許的,並且不會產生編譯錯誤。如果這是真的,那麼我們可以做一些事情: 在第 1 行中,我們創建了一個列表數組:List<String>[] stringLists。我們的數組包含一個List<String>. 在第 2 行中,我們創建了一個數字列表:List<Integer>。在第 3 行中,我們將 our 分配List<String>[]給一個Object[] objects變量。Java語言允許這樣:一個對像數組X可以存儲X所有子類的對象和對象X。因此,您可以將任何內容放入Object數組中。objects()在第 4 行中,我們將數組 (a )的唯一元素替換List<String>為 a List<Integer>。因此,我們將 a 放入一個List<Integer>僅用於存儲的數組中List<String>對象!我們只有在執行第 5 行時才會遇到錯誤。ClassCastException運行時會拋出A。因此,在 Java 中添加了禁止創建此類數組的規定。這讓我們避免了這種情況。

我怎樣才能繞過類型擦除?

好吧,我們了解了類型擦除。讓我們嘗試欺騙系統!:) 任務: 我們有一個通用TestClass<T>類。我們想createNewT()為這個類編寫一個方法來創建並返回一個新T對象。但這是不可能的,對吧?所有關於T類型的信息在編譯期間都會被擦除,在運行時我們無法確定我們需要創建什麼類型的對象。實際上有一種棘手的方法可以做到這一點。您可能還記得 Java 有一個Class類。我們可以使用它來確定任何對象的類:

public class Main2 {

   public static void main(String[] args) {

       Class classInt = Integer.class;
       Class classString = String.class;

       System.out.println(classInt);
       System.out.println(classString);
   }
}
控制台輸出:

class java.lang.Integer 
class java.lang.String
但這是我們還沒有談到的一個方面。在 Oracle 文檔中,您會看到 Class 類是通用的! 類型擦除 - 3

https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html

文檔說,“T - 此 Class 對象建模的類的類型。” 將其從文檔語言翻譯成普通話,我們了解到對象的類別Integer.class不僅僅是Class,而是Class<Integer>。對象的類型String.class不只是Class,而是 等Class<String>。如果還是不清楚,可以嘗試在前面的示例中添加一個類型參數:

public class Main2 {

   public static void main(String[] args) {

       Class<Integer> classInt = Integer.class;
       // Compilation error!
       Class<String> classInt2 = Integer.class;
      
      
       Class<String> classString = String.class;
       // Compilation error!
       Class<Double> classString2 = String.class;
   }
}
現在,利用這些知識,我們可以繞過類型擦除並完成我們的任務! 讓我們嘗試獲取有關類型參數的信息。我們的類型參數將是MySecretClass

public class MySecretClass {

   public MySecretClass() {

       System.out.println("A MySecretClass object was created successfully!");
   }
}
以下是我們如何在實踐中使用我們的解決方案:

public class TestClass<T> {

   Class<T> typeParameterClass;

   public TestClass(Class<T> typeParameterClass) {
       this.typeParameterClass = typeParameterClass;
   }

   public T createNewT() throws IllegalAccessException, InstantiationException {
       T t = typeParameterClass.newInstance();
       return t;
   }

   public static void main(String[] args) throws InstantiationException, IllegalAccessException {

       TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
       MySecretClass secret = testString.createNewT();

   }
}
控制台輸出:

A MySecretClass object was created successfully!
我們剛剛將所需的類參數傳遞給了泛型類的構造函數:

TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
這允許我們保存有關類型參數的信息,防止它被完全刪除。結果,我們能夠創建一個T目的!:) 至此,今天的課程就結束了。使用泛型時,您必須始終記住類型擦除。這種變通方法看起來不太方便,但您應該明白泛型在創建時並不是 Java 語言的一部分。此功能可幫助我們創建參數化集合併在編譯期間捕獲錯誤,此功能是後來添加的。在包含第一個版本泛型的其他一些語言中,沒有類型擦除(例如,在 C# 中)。順便說一句,我們還沒有完成對泛型的研究!在下一課中,您將熟悉泛型的更多特性。現在,最好解決幾個任務!:)