JavaRush /Java Blog /Random-TW /貓的仿製藥
Viacheslav
等級 3

貓的仿製藥

在 Random-TW 群組發布
貓的仿製藥 - 1

介紹

今天是記住我們對 Java 的了解的好日子。根據最重要的文件,即 Java 語言規格(JLS - Java Language Specifiaction),Java 是一種強類型語言,如「第 4 章。類型、值和變數」一章所述。這是什麼意思?假設我們有一個 main 方法:
public static void main(String[] args) {
String text = "Hello world!";
System.out.println(text);
}
強類型確保編譯此程式碼時,編譯器將檢查是否將文字變數的類型指定為 String,然後我們不會嘗試在任何地方將其用作其他類型的變數(例如,作為 Integer) 。例如,如果我們嘗試保存一個值而不是文字2L(即 long 而不是 String),我們將在編譯時收到錯誤:

Main.java:3: error: incompatible types: long cannot be converted to String
String text = 2L;
那些。強類型可讓您確保僅當這些操作對於這些物件合法時才執行對物件的操作。這也稱為類型安全。正如 JLS 中所述,Java 中有兩類類型:原始型別和參考型別。您可以記得評論文章中有關原始類型的內容:“ Java 中的原始類型:它們並不那麼原始。” 引用類型可以由類別、介面或陣列表示。今天我們將對引用類型感興趣。讓我們從數組開始:
class Main {
  public static void main(String[] args) {
    String[] text = new String[5];
    text[0] = "Hello";
  }
}
這段程式碼運行沒有錯誤。我們知道(例如,來自“ Oracle Java 教程:數組”),數組是僅存儲一種類型資料的容器。在這種情況下 - 只有線條。讓我們嘗試在陣列中加入 long 而不是 String:
text[1] = 4L;
讓我們運行這段程式碼(例如,在Repl.it Online Java Compiler中)並得到一個錯誤:
error: incompatible types: long cannot be converted to String
該語言的陣列和類型安全性不允許我們將不適合該類型的內容儲存到陣列中。這是類型安全的體現。我們被告知:“修復錯誤,但在那之前我不會編譯程式碼。” 最重要的是,這發生在編譯時,而不是程式啟動時。也就是說,我們會立即看到錯誤,而不是「有一天」。既然我們記住了數組,那麼讓我們也記住Java 集合框架。我們在那裡有不同的結構。例如,列表。讓我們重寫這個例子:
import java.util.*;
class Main {
  public static void main(String[] args) {
    List text = new ArrayList(5);
    text.add("Hello");
    text.add(4L);
    String test = text.get(0);
  }
}
編譯時,我們會test在變數初始化行收到錯誤:
incompatible types: Object cannot be converted to String
在我們的例子中,List 可以儲存任何物件(即 Object 類型的物件)。因此,編譯器表示不能承擔這樣的責任。因此,我們需要明確指定從清單中取得的類型:
String test = (String) text.get(0);
這種指示稱為類型轉換或型別轉換。現在一切都會正常運作,直到我們嘗試取得索引 1 處的元素,因為 它是 Long 類型。我們會得到一個公平的錯誤,但已經在程式運行時(在運行時):

type conversion, typecasting
Exception in thread "main" java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.String
正如我們所看到的,這裡有幾個重要的缺點。首先,我們被迫將從列表中獲得的值「轉換」到 String 類別。同意,這很醜。其次,如果出現錯誤,我們只有在程式執行時才會看到它。如果我們的程式碼更複雜,我們可能不會立即檢測到此類錯誤。而開發人員開始思考如何讓這種情況下的工作變得更輕鬆,讓程式碼更清晰。他們就這樣誕生了──泛型。
貓的仿製藥 - 2

泛型

所以,泛型。它是什麼?泛型是描述所使用類型的特殊方式,程式碼編譯器可以在其工作中使用它來確保類型安全。它看起來像這樣:
貓的仿製藥 - 3
這是一個簡短的範例和解釋:
import java.util.*;
class Main {
  public static void main(String[] args) {
    List<String> text = new ArrayList<String>(5);
    text.add("Hello");
    text.add(4L);
    String test = text.get(1);
  }
}
在這個例子中,我們說我們不僅有List,還有List,它只適用於 String 類型的物件。沒有其他人。括號裡的內容就是我們可以儲存的。這樣的“括號”稱為“尖括號”,即 尖括號。編譯器會友好地檢查我們在處理字串列表(該列表名為文字)時是否犯了任何錯誤。編譯器會發現我們正在厚顏無恥地嘗試將 Long 放入 String 列表中。並且在編譯的時候會報錯:
error: no suitable method found for add(long)
您可能還記得 String 是 CharSequence 的後代。並決定做類似的事情:
public static void main(String[] args) {
	ArrayList<CharSequence> text = new ArrayList<String>(5);
	text.add("Hello");
	String test = text.get(0);
}
但這是不可能的,我們會得到錯誤: error: incompatible types: ArrayList<String> cannot be converted to ArrayList<CharSequence> 這看起來很奇怪,因為。該行CharSequence sec = "test";不包含錯誤。讓我們弄清楚一下。他們談到這種行為時說:“泛型是不變的。” 什麼是「不變量」?我喜歡維基百科上「協變與逆變」一文中對此的說法:
貓的仿製藥 - 4
因此,不變性是指派生類型之間不存在繼承。如果 Cat 是 Animals 的子類型,則 Set<Cats> 不是 Set<Animals> 的子類型,且 Set<Animals> 也不是 Set<Cats> 的子類型。順便說一下,值得一提的是,從 Java SE 7 開始,出現了所謂的「鑽石操作符」。因為兩個尖括號<>就像一個菱形。這允許我們使用像這樣的泛型:
public static void main(String[] args) {
  List<String> lines = new ArrayList<>();
  lines.add("Hello world!");
  System.out.println(lines);
}
根據這段程式碼,編譯器可以理解,如果我們在左側表示它將List包含 String 類型的對象,那麼在右側我們的意思是我們要將lines一個新的ArrayList 保存到一個變數中,該變數也將存儲一個物件左側指定的類型。因此,左側的編譯器可以理解或推斷右側的類型。這就是為什麼這種行為在英語中被稱為類型推斷或“類型推斷”。另一個值得注意的有趣的事情是 RAW 類型或「原始類型」。因為 泛型並不總是存在,Java 會盡可能保持向後相容性,然後泛型被迫以某種方式處理未指定泛型的程式碼。讓我們來看一個例子:
List<CharSequence> lines = new ArrayList<String>();
我們記得,由於泛型的不變性,這樣的行將無法編譯。
List<Object> lines = new ArrayList<String>();
同樣的原因,這個也不會編譯。
List lines = new ArrayList<String>();
List<String> lines2 = new ArrayList();
這些行將編譯並運行。其中使用了原始類型,即 未指定類型。再次值得指出的是,原始類型不應該在現代程式碼中使用。
貓的仿製藥 - 5

類型化類

所以,輸入類別。讓我們看看如何編寫我們自己的類型化類別。例如,我們有一個類別層次結構:
public static abstract class Animal {
  public abstract void voice();
}

public static class Cat extends Animal {
  public void voice(){
    System.out.println("Meow meow");
  }
}

public static class Dog extends Animal {
  public void voice(){
    System.out.println("Woof woof");
  }
}
我們想要創建一個實作動物容器的類別。可以編寫一個包含任何Animal. 這很簡單,可以理解,但是……狗和貓混在一起是不好的,它們不是彼此的朋友。此外,如果有人收到這樣的容器,他可能會錯誤地將貓從容器中扔進一群狗中..​​....這不會帶來任何好處。在這裡,泛型將幫助我們。例如,讓我們這樣寫實作:
public static class Box<T> {
  List<T> slots = new ArrayList<>();
  public List<T> getSlots() {
    return slots;
  }
}
我們的類別將使用由名為 T 的泛型指定的類型的物件。這是一種別名。因為 泛型是在類別名稱中指定的,那麼我們在聲明類別時就會收到它:
public static void main(String[] args) {
  Box<Cat> catBox = new Box<>();
  Cat murzik = new Cat();
  catBox.getSlots().add(murzik);
}
正如我們所看到的,我們表明我們有Box,它僅適用於Cat。編譯器意識到,您需要在指定泛型名稱的catBox地方T替換類型,而不是泛型: CatT
貓的學名藥 - 6
那些。多虧了Box<Cat>編譯器,它才明白slots它實際上應該是什麼List<Cat>。因為Box<Dog>裡面會有slots,包含List<Dog>。類型聲明中可以有多個泛型,例如:
public static class Box<T, V> {
泛型的名稱可以是任何東西,儘管建議遵守一些潛規則 - “類型參數命名約定”:元素類型 - E,鍵類型 - K,數字類型 - N,T - 類型,V - for值類型。順便說一下,請記住我們說過泛型是不變的,即 不保留繼承層次結構。事實上,我們可以影響這一點。也就是說,我們有機會使泛型變得協變,即 以相同的順序保持繼承。這種行為稱為“有界類型”,即 種類有限。例如,我們的類別Box可以包含所有動物,那麼我們將聲明一個像這樣的泛型:
public static class Box<T extends Animal> {
也就是我們幫class設定了上限Animal。我們也可以在關鍵字後面指定幾種類型extends。這意味著我們將使用的類型必須是某個類別的後代,並且同時實作某個介面。例如:
public static class Box<T extends Animal & Comparable> {
在這種情況下,如果我們嘗試將Box一些內容放入非繼承者Animal且未實現的內容中Comparable,那麼在編譯過程中我們將收到錯誤:
error: type argument Cat is not within bounds of type-variable T
貓的仿製藥 - 7

方法輸入

泛型不僅用在型別中,也用在單一方法中。方法的應用可以參考官方教學:《泛型方法》。

背景:

貓的仿製藥 - 8
讓我們看看這張照片。正如您所看到的,編譯器查看方法簽名並發現我們正在將一些未定義的類別作為輸入。它並不能透過簽名來確定我們正在傳回某種對象,即 目的。因此,如果我們想建立一個 ArrayList,那麼我們需要這樣做:
ArrayList<String> object = (ArrayList<String>) createObject(ArrayList.class);
你必須明確地寫出輸出將是一個 ArrayList,這很醜陋並且增加了出錯的機會。例如,我們可以寫這樣的廢話,它會編譯:
ArrayList object = (ArrayList) createObject(LinkedList.class);
我們可以幫助編譯器嗎?是的,泛型允許我們做到這一點。讓我們來看看同一個例子:
貓的仿製藥 - 9
然後,我們可以像這樣簡單地創建一個物件:
ArrayList<String> object = createObject(ArrayList.class);
貓的仿製藥 - 10

通配符

根據Oracle的泛型教程,特別是「通配符」部分,我們可以用問號來描述「未知類型」。通配符是一個方便的工具,可以減輕泛型的一些限制。例如,正如我們之前討論的,泛型是不變的。這意味著雖然所有類別都是 Object 類型的後代(子類型),但它List<любой тип>不是子類型List<Object>。但是,List<любой тип>它是一個子類型List<?>。所以我們可以寫如下程式碼:
public static void printList(List<?> list) {
  for (Object elem: list) {
    System.out.print(elem + " ");
  }
  System.out.println();
}
與常規泛型(即不使用通配符)一樣,帶有通配符的泛型也可能受到限制。上界通配符看起來很熟悉:
public static void printCatList(List<? extends Cat> list) {
  for (Cat cat: list) {
    System.out.print(cat + " ");
  }
  System.out.println();
}
但您也可以透過下界通配符來限制它:
public static void printCatList(List<? super Cat> list) {
因此,該方法將開始接受所有貓,以及層次結構中較高的貓(直到對象)。
貓的仿製藥 - 11

類型擦除

說到泛型,值得了解一下「類型擦除」。事實上,類型擦除是關於泛型是編譯器的資訊這一事實。在程式執行過程中,不再有關於泛型的信息,稱為「擦除」。此擦除的效果是泛型類型被特定類型取代。如果泛型沒有邊界,則將取代物件類型。如果指定了邊框(例如<T extends Comparable>),則它將被替換。以下是 Oracle 教學中的範例:「通用類型的擦除」:
貓的仿製藥 - 12
如上所述,在這個例子中,泛型T被擦除到其邊界,即 前Comparable
貓的仿製藥 - 13

結論

泛型是一個非常有趣的話題。我希望您對這個主題感興趣。總而言之,我們可以說泛型是開發人員收到的一個優秀工具,它可以向編譯器提示附加信息,一方面確保類型安全,另一方面確保靈活性。如果您有興趣,那麼我建議您查看我喜歡的資源: #維亞切斯拉夫
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION