JavaRush /Java Blog /Random-TW /Java 中的泛型理論或如何在實務上使用括號
Viacheslav
等級 3

Java 中的泛型理論或如何在實務上使用括號

在 Random-TW 群組發布

介紹

從 JSE 5.0 開始,泛型被加入到 Java 語言庫中。
Java 中的泛型理論或如何在實務中使用括號 - 1

Java 中的泛型是什麼?

泛型(generals)是 Java 語言用於實現通用程式設計的特殊方法:一種描述資料和演算法的特殊方法,可讓您在不更改其描述的情況下處理不同類型的資料。在 Oracle 網站上,有一個專門針對泛型的單獨教學:「課程:泛型」。

首先,要了解泛型,您需要了解為什麼需要它們以及它們提供什麼。在教程的“為什麼使用泛型?”部分中 據說目的之一是更強的編譯時類型檢查並消除顯式轉換的需要。
Java 中的泛型理論或如何在實務中使用括號 - 2
讓我們準備我們最喜歡的教程點在線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);
	}
}
這段程式碼將運作良好。但如果他們來找我們並說「你好,世界!」怎麼辦?被打了只能回你囉?讓我們從程式碼中刪除與字串的連接", 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,因為 這是整數。他會這樣告訴我們。許多人將泛型稱為「語法糖」。他們是對的,因為泛型在編譯時確實會變成相同的種姓。讓我們看看編譯後的類別的字節碼:使用手動轉換和使用泛型:
Java 中的泛型理論或如何在實務中使用括號 - 3
編譯後,有關泛型的任何資訊都將被刪除。這稱為“類型擦除”或“類型擦除”。類型擦除和泛型旨在提供與舊版本 JDK 的向後相容性,同時仍允許編譯器協助新版本 Java 中的類型推斷。
Java 中的泛型理論或如何在實務中使用括號 - 4

原始類型或原始類型

當談到泛型時,我們總是有兩類:類型化類型(泛型類型)和「原始」類型(原始類型)。 原始類型是沒有在尖括號中指定「限定」的類型:
Java 中的泛型理論或如何在實務中使用括號 - 5
類型化類型則相反,帶有「澄清」的指示:
Java 中的泛型理論或如何在實務中使用括號 - 6
正如我們所看到的,我們使用了一種不尋常的設計,在螢幕截圖中用箭頭標記。這是Java SE 7中添加的一種特殊語法,它被稱為“ the Diamond ”,意思是鑽石。為什麼?您可以將菱形的形狀與大括號的形狀進行類比:菱形語法也與「類型推斷」或類型推斷<> 的概念相關聯。畢竟,編譯器在看到右側的 <> 時,會查看左側,即賦值的變數類型的宣告所在的位置。從這部分他就明白右邊的數值是什麼類型了。事實上,如果在左側指定了泛型而不在右側指定,編譯器將能夠推斷出類型:
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 翻譯為抑制,即字面意思是抑制警告。但想想你為什麼決定要表明這一點?記住第一條規則,也許您需要添加打字。
Java 中的泛型理論或如何在實務中使用括號 - 7

通用方法

泛型允許您鍵入方法。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,並且程式碼執行失敗並出現錯誤。
Теория дженериков в Java or How на практике ставить скобки - 8

通用類型

您不僅可以鍵入方法,還可以鍵入類別本身。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原則適用於通配符。它們可以用以下形式表示:
Теория дженериков в Java or How на практике ставить скобки - 9
該原則也稱為PECS(生產者擴展消費者超級)原則。您可以在文章「使用泛型通配符提高 Java API 的可用性」以及 stackoverflow 上的精彩討論中閱讀有關 Habré 的更多資訊:「在泛型 Java 中使用通配符」。以下是來自 Java 原始碼的一個小範例 - Collections.copy 方法:
Теория дженериков в Java or How на практике ставить скобки - 10
好吧,舉一個小例子來說明它如何不起作用:
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>();
因為繼承與泛型的工作方式不同:
Теория дженериков в Java or How на практике ставить скобки - 11
這是另一個會因錯誤而失敗的好例子:
List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
這裡的一切也很簡單。儘管 String 是 Object 的後代,但 List<String> 不是 List<Object> 的後代。

最終的

所以我們刷新了對泛型的記憶。如果很少充分利用它們的全部力量,一些細節就會被遺忘。我希望這篇簡短的評論能幫助您刷新記憶。為了獲得更好的結果,我強烈建議您閱讀以下材料: #維亞切斯拉夫
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION