介紹
從 JSE 5.0 開始,泛型被加入到 Java 語言庫中。Java 中的泛型是什麼?
泛型(generals)是 Java 語言用於實現通用程式設計的特殊方法:一種描述資料和演算法的特殊方法,可讓您在不更改其描述的情況下處理不同類型的資料。在 Oracle 網站上,有一個專門針對泛型的單獨教學:「課程:泛型」。
import java.util.*;
public class HelloWorld{
public static void main(String []args){
List list = new ArrayList();
list.add("Hello");
String text = list.get(0) + ", world!";
System.out.print(text);
}
}
這段程式碼將運作良好。但如果他們來找我們並說「你好,世界!」怎麼辦?被打了只能回你囉?讓我們從程式碼中刪除與字串的連接", world!"
。看來還有什麼比這更無害的呢?但事實上,我們會在編譯期間收到一個錯誤: error: incompatible types: Object cannot be converted to String
在我們的例子中,List 儲存了一個 Object 類型的物件清單。由於 String 是 Object 的後代(因為 Java 中所有類別都是從 Object 隱式繼承的),因此它需要明確強制轉換,但我們沒有這樣做。並且在連接時,將在該物件上呼叫靜態方法 String.valueOf(obj) ,該方法最終將呼叫該物件上的 toString 方法。也就是說,我們的List包含Object。事實證明,當我們需要特定類型而不是 Object 時,我們必須自己進行類型轉換:
import java.util.*;
public class HelloWorld{
public static void main(String []args){
List list = new ArrayList();
list.add("Hello!");
list.add(123);
for (Object str : list) {
System.out.println((String)str);
}
}
}
然而,在這種情況下,因為 List接受物件列表,它不僅儲存String,還儲存Integer。但最糟糕的是,在這種情況下編譯器不會發現任何錯誤。在這裡,我們將在執行期間收到一個錯誤(他們還說錯誤是「在運行時」收到的)。錯誤將是:java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
同意,但不是最令人愉快的。而這一切都是因為編譯器不是人工智慧,它無法猜測程式設計師的所有意思。為了告訴編譯器更多關於我們將使用什麼類型的信息,Java SE 5 引入了泛型。讓我們透過告訴編譯器我們想要什麼來修正我們的版本:
import java.util.*;
public class HelloWorld {
public static void main(String []args){
List<String> list = new ArrayList<>();
list.add("Hello!");
list.add(123);
for (Object str : list) {
System.out.println(str);
}
}
}
正如我們所看到的,我們不再需要轉換為 String。此外,我們現在有了用來框定泛型的尖括號。現在,編譯器將不允許編譯該類,直到我們刪除清單中新增的 123,因為 這是整數。他會這樣告訴我們。許多人將泛型稱為「語法糖」。他們是對的,因為泛型在編譯時確實會變成相同的種姓。讓我們看看編譯後的類別的字節碼:使用手動轉換和使用泛型:
原始類型或原始類型
當談到泛型時,我們總是有兩類:類型化類型(泛型類型)和「原始」類型(原始類型)。 原始類型是沒有在尖括號中指定「限定」的類型:<>
的概念相關聯。畢竟,編譯器在看到右側的 <> 時,會查看左側,即賦值的變數類型的宣告所在的位置。從這部分他就明白右邊的數值是什麼類型了。事實上,如果在左側指定了泛型而不在右側指定,編譯器將能夠推斷出類型:
import java.util.*;
public class HelloWorld{
public static void main(String []args) {
List<String> list = new ArrayList();
list.add("Hello World");
String data = list.get(0);
System.out.println(data);
}
}
然而,這將是帶有泛型的新樣式和不帶泛型的舊樣式的混合。這是極為不可取的。編譯上面的程式碼時,我們將收到訊息:Note: HelloWorld.java uses unchecked or unsafe operations
。事實上,似乎不清楚為什麼我們需要在這裡添加鑽石。但這裡有一個例子:
import java.util.*;
public class HelloWorld{
public static void main(String []args) {
List<String> list = Arrays.asList("Hello", "World");
List<Integer> data = new ArrayList(list);
Integer intNumber = data.get(0);
System.out.println(data);
}
}
我們記得,ArrayList 還有第二個建構函數,它將集合作為輸入。而這正是欺騙之所在。如果沒有鑽石語法,編譯者不會明白它被欺騙了,但有了鑽石語法,編譯器就會明白。因此,規則#1:如果我們使用類型化類型,則始終使用菱形語法。否則,我們可能會在使用原始類型的地方遺失資料。為了避免日誌中出現「使用未經檢查或不安全的操作」的警告,可以在所使用的方法或類別上指定特殊的註釋:@SuppressWarnings("unchecked")
Suppress 翻譯為抑制,即字面意思是抑制警告。但想想你為什麼決定要表明這一點?記住第一條規則,也許您需要添加打字。
通用方法
泛型允許您鍵入方法。Oracle 教學中有一個單獨的部分專門介紹此功能:「通用方法」。在本教程中,記住語法很重要:- 包括尖括號內的類型參數清單;
- 類型化參數清單位於傳回的方法之前。
import java.util.*;
public class HelloWorld{
public static class Util {
public static <T> T getValue(Object obj, Class<T> clazz) {
return (T) obj;
}
public static <T> T getValue(Object obj) {
return (T) obj;
}
}
public static void main(String []args) {
List list = Arrays.asList("Author", "Book");
for (Object element : list) {
String data = Util.getValue(element, String.class);
System.out.println(data);
System.out.println(Util.<String>getValue(element));
}
}
}
如果您查看 Util 類,我們會在其中看到兩個類型化方法。透過類型推斷,我們可以直接向編譯器提供類型定義,也可以自己指定。範例中提供了這兩個選項。順便說一句,如果你仔細想想,文法是非常合乎邏輯的。當鍵入一個方法時,我們在方法之前指定泛型,因為如果我們在方法之後使用泛型,Java 將無法確定要使用哪種類型。因此,我們首先宣布我們將使用泛型 T,然後我們說我們將返回這個泛型。當然,Util.<Integer>getValue(element, String.class)
它會因錯誤而失敗incompatible types: Class<String> cannot be converted to Class<Integer>
。使用類型化方法時,您應該始終記住類型擦除。讓我們來看一個例子:
import java.util.*;
public class HelloWorld {
public static class Util {
public static <T> T getValue(Object obj) {
return (T) obj;
}
}
public static void main(String []args) {
List list = Arrays.asList(2, 3);
for (Object element : list) {
System.out.println(Util.<Integer>getValue(element) + 1);
}
}
}
效果會很好。但前提是編譯器知道被呼叫的方法具有 Integer 類型。讓我們用以下行取代控制台輸出: System.out.println(Util.getValue(element) + 1);
我們得到錯誤:二元運算子 '+' 的運算元型別錯誤,第一個型別: Object ,第二個型別: int 也就是說,型別已被刪除。編譯器發現沒有人指定類型,類型被指定為 Object,並且程式碼執行失敗並出現錯誤。
通用類型
您不僅可以鍵入方法,還可以鍵入類別本身。Oracle 在其指南中專門有一個「通用類型」部分。讓我們來看一個例子:public static class SomeType<T> {
public <E> void test(Collection<E> collection) {
for (E element : collection) {
System.out.println(element);
}
}
public void test(List<Integer> collection) {
for (Integer element : collection) {
System.out.println(element);
}
}
}
這裡一切都很簡單。如果我們使用類,則泛型會列在類別名稱後面。現在讓我們在 main 方法中建立此類的實例:
public static void main(String []args) {
SomeType<String> st = new SomeType<>();
List<String> list = Arrays.asList("test");
st.test(list);
}
效果會很好。編譯器看到有一個數字列表和字串類型的集合。但是如果我們刪除泛型並執行以下操作會怎麼樣:
SomeType st = new SomeType();
List<String> list = Arrays.asList("test");
st.test(list);
我們會得到錯誤:java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
再次鍵入擦除。由於該類別不再具有泛型,因此編譯器認為我們傳遞了一個 List,因此使用 List<Integer> 的方法更合適。我們犯了一個錯誤。因此,規則#2:如果類別是類型化的,則始終在 generic 中指定類型。
限制
我們可以對泛型中指定的類型套用限制。例如,我們希望容器僅接受 Number 作為輸入。Oracle 教學的「有界類型參數」部分描述了此功能。讓我們來看一個例子:import java.util.*;
public class HelloWorld{
public static class NumberContainer<T extends Number> {
private T number;
public NumberContainer(T number) { this.number = number; }
public void print() {
System.out.println(number);
}
}
public static void main(String []args) {
NumberContainer number1 = new NumberContainer(2L);
NumberContainer number2 = new NumberContainer(1);
NumberContainer number3 = new NumberContainer("f");
}
}
如您所看到的,我們將泛型類型限制為 Number 類別/介面及其後代。有趣的是,您不僅可以指定類別,還可以指定介面。例如: public static class NumberContainer<T extends Number & Comparable> {
泛型還有通配符的概念 https://docs.oracle.com/javase/tutorial/java/generics/wildcards.html 它們又分為三種:
所謂的Get Put原則適用於通配符。它們可以用以下形式表示:
public static class TestClass {
public static void print(List<? extends String> list) {
list.add("Hello World!");
System.out.println(list.get(0));
}
}
public static void main(String []args) {
List<String> list = new ArrayList<>();
TestClass.print(list);
}
但如果你把extends換成super,一切都會好起來的。由於我們在輸出之前先給列表填充了一個值,所以它對我們來說就是一個消費者,也就是一個消費者。因此,我們使用super。
遺產
泛型還有另一個不尋常的特性──它們的繼承性。Oracle 教程的「泛型、繼承和子類型」部分描述了泛型的繼承。最主要的是記住並認識到以下幾點。我們不能這樣做:List<CharSequence> list1 = new ArrayList<String>();
因為繼承與泛型的工作方式不同:
List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
這裡的一切也很簡單。儘管 String 是 Object 的後代,但 List<String> 不是 List<Object> 的後代。
GO TO FULL VERSION