你好!我們繼續我們的仿製藥系列講座。之前,我們大致了解了它是什麼以及為什麼需要它。今天我們將討論泛型的一些特性,並探討使用它們時的一些陷阱。去! 上一講,我們討論了泛型類型和原始類型的區別。如果您忘記了,原始類型是一個泛型類,其類型已從中刪除。
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 a
為Object b
我們的類型T
,之後它們將被添加到該物件中TestClass
。main()
在我們創造的方法上TestClass<Integer>
,也就是在T
我們將擁有的品質上Integer
。但同時,createAndAdd2Values()
我們將一個數字Double
和一個物件傳遞給該方法String
。您認為我們的計劃可行嗎?畢竟,我們指定為參數類型Integer
,但String
它肯定不能轉換為Integer
!讓我們運行該方法main()
並檢查一下。控制台輸出: 22.111 測試字串 意外結果!為什麼會發生這種情況? 正是因為類型擦除。Integer
在程式碼編譯期間,有關物件的 參數類型的資訊TestClass<Integer> test
被刪除。他變成了TestClass<Object> test
。我們的參數毫無問題地轉換為Double
(而不是像我們預期的那樣轉換為 !)並悄悄添加到。這是另一個簡單但非常說明性的類型擦除範例: String
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 看起來我們已經創建了具有三種不同參數類型的集合 - String
、Integer
和我們創建的類別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是一個泛型類別! 文件中說:“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;
//ошибка компиляции!
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#)。然而,我們對泛型的研究還沒結束!在下一講中,您將熟悉使用它們的更多功能。同時,如果能解決幾個問題就好了!:)
GO TO FULL VERSION