CodeGym /Java Blog /Toto sisi /Java 泛型:如何在實踐中使用尖括號
John Squirrels
等級 41
San Francisco

Java 泛型:如何在實踐中使用尖括號

在 Toto sisi 群組發布

介紹

從 JSE 5.0 開始,泛型被添加到 Java 語言的武器庫中。

java中的泛型是什麼?

泛型是 Java 用於實現泛型編程的特殊機制 — 一種描述數據和算法的方法,使您可以在不更改算法描述的情況下使用不同的數據類型。Oracle 網站有專門針對泛型的單獨教程:“課程”。要了解泛型,您首先需要弄清楚為什麼需要它們以及它們提供什麼。本教程的“為什麼使用泛型? ”部分說,有兩個目的是在編譯時進行更強的類型檢查和消除對顯式強制轉換的需要。讓我們在我們鍾愛的TutorialspointJava 中的泛型:如何在實踐中使用尖括號 - 1在線 Java 編譯器中準備一些測試。假設您有以下代碼:

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);
	}
}
這段代碼將運行得很好。但是,如果老闆來找我們說“你好,世界!”怎麼辦?是一個過度使用的短語,您必須只返回“Hello”嗎?我們將刪除連接“, world!”的代碼。這似乎是無害的,對吧?但是我們實際上在編譯時得到了一個錯誤:

error: incompatible types: Object cannot be converted to String
問題是在我們的列表中存儲對象。String是Object的後代(因為所有 Java 類都隱式繼承Object),這意味著我們需要顯式強制轉換,但我們沒有添加。在串聯操作期間,將使用該對象調用靜態String.valueOf(obj)方法。最終,它將調用Object類的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 添加到列表中的行,因為這是一個Integer。它會告訴我們。許多人稱泛型為“語法糖”。他們是對的,因為在編譯泛型之後,它們確實變成了相同的類型轉換。讓我們看一下已編譯類的字節碼:一個使用顯式轉換的字節碼和一個使用泛型的字節碼: Java 中的泛型:如何在實踐中使用尖括號 - 2編譯後,所有泛型都被擦除。這稱為“類型擦除“。類型擦除和泛型旨在與舊版本的 JDK 向後兼容,同時允許編譯器幫助在新版本的 Java 中進行類型定義。

原始類型

說起泛型,我們總有兩類:參數化類型和原始類型。原始類型是在尖括號中省略“類型說明”的類型: Java 中的泛型:如何在實踐中使用尖括號 - 3另一方面,參數化類型包括“說明”: Java 中的泛型:如何在實踐中使用尖括號 - 4如您所見,我們使用了一個不尋常的構造,在屏幕截圖中用箭頭標記。這是添加到 Java SE 7 的特殊語法。它被稱為“菱形”。為什麼?尖括號形成一個菱形:<>您還應該知道菱形語法與“類型推斷”的概念相關聯。畢竟,編譯器,看到<>在右側,查看賦值運算符的左側,它在其中找到要分配其值的變量的類型。根據它在這部分中找到的內容,它了解右側值的類型。事實上,如果一個泛型類型在左邊給出,而在右邊沒有給出,編譯器可以推斷出類型:

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")註釋。但是想想你為什麼決定使用它。記住第一條規則。也許您需要添加一個類型參數。

Java 泛型方法

泛型允許您創建參數類型和返回類型被參數化的方法。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類,您會發現它有兩個通用方法。由於類型推斷的可能性,我們可以直接向編譯器指示類型,也可以自己指定。示例中提供了這兩個選項。順便說一句,如果你仔細想想,語法就很有意義。當聲明泛型方法時,我們在方法之前指定類型參數,因為如果我們在方法之後聲明類型參數,JVM 將無法確定要使用的類型。因此,我們首先聲明我們將使用T類型參數,然後我們說我們將返回此類型。自然地,Util.<Integer>getValue(element, String.class)將失敗並出現錯誤:不兼容的類型:Class<String> 無法轉換為 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);
我們得到一個錯誤:

bad operand types for binary operator '+', first type: Object, second type: 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:如果您有泛型類,請始終指定類型參數。

限制

我們可以限制泛型方法和類中指定的類型。例如,假設我們希望容器只接受數字作為類型參數。Oracle 教程的Bounded Type Parameters部分描述了此功能。讓我們看一個例子:

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> {
泛型也支持通配符 ,分為三種: 您對通配符的使用應遵守Get-Put 原則。可以表示如下:
  • 當您僅從結構中獲取值時,請使用擴展通配符。
  • 僅將值放入結構時使用超級通配符。
  • 當您都想從結構中獲取和放入結構時,不要使用通配符。
該原則也稱為 Producer Extends Consumer Super (PECS) 原則。這是 Java 的Collections.copy方法的源代碼中的一個小示例: Java 中的泛型:如何在實踐中使用尖括號 - 5下面是一個不起作用的小示例:

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,那麼一切都很好。因為我們在顯示其內容之前用一個值填充列表,所以它是一個消費者。因此,我們使用超級。

遺產

泛型還有一個有趣的特性:繼承。Oracle 教程中的“ Generics, Inheritance, and Subtypes ”描述了泛型的繼承方式。重要的是要記住並認識到以下幾點。我們不能這樣做:

List<CharSequence> list1 = new ArrayList<String>();
因為繼承與泛型的工作方式不同: Java 中的泛型:如何在實踐中使用尖括號 - 6這是另一個會因錯誤而失敗的好例子:

List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
同樣,這裡的一切都很簡單。List<String>不是List<Object>的後代,即使String是Object的後代。 為了鞏固您所學的知識,我們建議您觀看我們的 Java 課程中的視頻課程

結論

所以我們刷新了關於泛型的記憶。如果你很少充分利用他們的能力,一些細節就會變得模糊。我希望這篇簡短的評論有助於喚起您的記憶。為了獲得更好的結果,我強烈建議您熟悉以下材料:
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION