JavaRush /Java Blog /Random-TW /擦除類型

擦除類型

在 Random-TW 群組發布
你好!我們繼續我們的仿製藥系列講座。之前,我們大致了解了它是什麼以及為什麼需要它。今天我們將討論泛型的一些特性,並探討使用它們時的一些陷阱。去! 擦除類型 - 1一講,我們討論了泛型類型原始類型的區別。如果您忘記了,原始類型是一個泛型類,其類型已從中刪除。
List list = new ArrayList();
這是一個例子。這裡我們沒有指定什麼類型的物件將被放置在我們的List. 如果我們嘗試建立一個物件List並向其中添加一些對象,我們將在 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,之後它們將被添加到該物件中TestClassmain()在我們創造的方法上TestClass<Integer>,也就是在T我們將擁有的品質上Integer。但同時,createAndAdd2Values()我們將一個數字Double和一個物件傳遞給該方法String。您認為我們的計劃可行嗎?畢竟,我們指定為參數類型Integer,但String它肯定不能轉換為Integer!讓我們運行該方法main()並檢查一下。控制台輸出: 22.111 測試字串 意外結果!為什麼會發生這種情況? 正是因為類型擦除。Integer在程式碼編譯期間,有關物件的 參數類型的資訊TestClass<Integer> test被刪除。他變成了TestClass<Object> test。我們的參數毫無問題地轉換為Double(而不是像我們預期的那樣轉換為 !)並悄悄添加到。這是另一個簡單但非常說明性的類型擦除範例: StringObjectIntegerTestClass
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) {

       //ошибка компиляции! 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 行中,我們建立一個 Sheets 陣列List<String>[] stringLists。我們的數組包含一個List<String>. 在第 2 行,我們建立了一個數字清單List<Integer>。在第 3 行,我們將數組分配List<String>[]給一個變數Object[] objects。Java 語言可讓您這樣做:X您可以將物件X和所有子類別的物件放入物件陣列中Х。因此,Objects您可以將任何內容放入數組中。在第 4 行,我們將陣列的單一元素替換objects (List<String>)為 list List<Integer>。結果,我們放入List<Integer>了數組,該數組僅用於儲存List<String>!只有當程式碼到達第5行時我們才會遇到錯誤。程式執行過程中會拋出例外ClassCastException。因此,Java 語言中引入了創建此類數組的禁令——這使我們能夠避免此類情況。

如何繞過類型擦除?

好吧,我們已經了解了類型擦除。讓我們嘗試欺騙系統吧!:) 任務: 我們有一個通用類別TestClass<T>。我們需要在其中建立一個方法createNewT()來建立並傳回一個類型為 的新物件Т。但這是不可能做到的,對吧?所有關於類型的資訊Т都會在編譯過程中被刪除,而在程式執行時,我們將無法找出我們需要建立什麼類型的物件。事實上,有一種棘手的方法。你可能還記得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文件中說:“T 是這個 Class 物件建模的類別的類型。” 如果我們將其從文檔語言翻譯成人類語言,這意味著物件的類別Integer.class不僅僅是Class,而是Class<Integer>。物件類型string.class不僅僅是ClassClass<String>等。如果還不清楚,請嘗試在前面的範例中新增類型參數:
public class Main2 {

   public static void main(String[] args) {

       Class<Integer> classInt = Integer.class;
       //ошибка компиляции!
       Class<String> classInt2 = Integer.class;


       Class<String> classString = String.class;
       //ошибка компиляции!
       Class<Double> classString2 = String.class;
   }
}
現在,利用這些知識,我們可以繞過類型擦除並解決我們的問題!讓我們嘗試獲取有關參數類型的信息。它的角色將由類別扮演MySecretClass
public class MySecretClass {

   public MySecretClass() {

       System.out.println("Объект секретного класса успешно создан!");
   }
}
以下是我們在實踐中使用我們的解決方案的方式:
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();

   }
}
控制台輸出:

Объект секретного класса успешно создан!
我們只需將所需的類別參數傳遞給泛型類別的建構子:
TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
因此,我們保存了有關參數類型的信息並防止其被刪除。結果,我們能夠創建一個物件T!:) 今天的講座到此結束。使用泛型時始終要記住類型擦除。這看起來不太方便,但您需要了解,泛型在創建時並不是 Java 語言的一部分。這是後來添加的功能,可以幫助我們創建類型化集合並在編譯階段捕獲錯誤。從版本 1 開始就存在泛型的其他一些語言沒有類型擦除(例如 C#)。然而,我們對泛型的研究還沒結束!在下一講中,您將熟悉使用它們的更多功能。同時,如果能解決幾個問題就好了!:)
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION