你好!我們繼續研究泛型這個主題。從先前的講座中,您已經對它們有了足夠的了解(關於在使用泛型時使用可變參數以及關於類型擦除),但我們還沒有討論一個重要的主題:通配符。這是泛型的一個非常重要的特性。以至於我們特別為此舉辦了一場講座!然而,通配符並不複雜,你現在就會明白了:)讓我們來看一個例子:
public class Main {
public static void main(String[] args) {
String str = new String("Test!");
// ниHowих проблем
Object obj = str;
List<String> strings = new ArrayList<String>();
// ошибка компиляции!
List<Object> objects = strings;
}
}
這裡發生了什麼事?我們看到兩種非常相似的情況。在第一個中,我們嘗試將物件強制轉換String
為類型Object
。這沒有任何問題,一切都按其應有的方式進行。但在第二種情況下,編譯器會拋出錯誤。儘管看起來我們在做同樣的事情。只是現在我們使用的是幾個物件的集合。但為什麼會出現錯誤呢?String
我們將一個對象轉換為一種類型還是 20 個對象,本質上有什麼區別Object
?物件和物件集合之間存在重要差異。 如果一個類別B
是某個類別的後繼者А
,Collection<B>
那麼它就不是後繼者Collection<A>
。 正是由於這個原因,我們無法將我們的List<String>
帶到List<Object>
。String
是繼承人Object
,但List<String>
不是繼承人List<Object>
。 直觀上,這看起來不太符合邏輯。為什麼語言的創造者要遵循這個原則?讓我們想像一下編譯器不會在這裡給我們一個錯誤:
List<String> strings = new ArrayList<String>();
List<Object> objects = strings;
在這種情況下,我們可以執行以下操作:
objects.add(new Object());
String s = strings.get(0);
由於編譯器沒有給我們錯誤並允許我們創建List<Object> object
對字串集合的引用strings
,因此我們可以添加的strings
不是字串,而是任何物件Object
!因此,我們失去了只有泛型中指定的物件才在我們的集合中的保證String
。也就是說,我們失去了泛型的主要優點——型別安全。而且由於編譯器允許我們執行所有這些操作,這意味著我們只會在程式執行期間遇到錯誤,這總是比編譯錯誤糟糕得多。為了防止這種情況,編譯器給我們一個錯誤:
// ошибка компиляции
List<Object> objects = strings;
……並提醒他List<String>
他不是繼承人List<Object>
。這是泛型操作的鐵定規則,使用時必須牢記。讓我們繼續。假設我們有一個小的類別層次結構:
public class Animal {
public void feed() {
System.out.println("Animal.feed()");
}
}
public class Pet extends Animal {
public void call() {
System.out.println("Pet.call()");
}
}
public class Cat extends Pet {
public void meow() {
System.out.println("Cat.meow()");
}
}
處於等級制度頂端的只是動物:寵物是從它們繼承的。寵物分為兩種 - 狗和貓。現在想像我們需要創建一個簡單的方法iterateAnimals()
。此方法應該接受任何動物的集合(Animal
、Pet
、Cat
、Dog
),迭代所有元素,並每次向控制台輸出一些內容。讓我們來嘗試寫一下這個方法:
public static void iterateAnimals(Collection<Animal> animals) {
for(Animal animal: animals) {
System.out.println("Еще один шаг в цикле пройден!");
}
}
看來問題已經解決了!然而,正如我們最近發現的那樣,List<Cat>
或者List<Dog>
不是List<Pet>
繼承人List<Animal>
!因此,當我們嘗試呼叫iterateAnimals()
帶有貓列表的方法時,我們將收到編譯器錯誤:
import java.util.*;
public class Main3 {
public static void iterateAnimals(Collection<Animal> animals) {
for(Animal animal: animals) {
System.out.println("Еще один шаг в цикле пройден!");
}
}
public static void main(String[] args) {
List<Cat> cats = new ArrayList<>();
cats.add(new Cat());
cats.add(new Cat());
cats.add(new Cat());
cats.add(new Cat());
//ошибка компилятора!
iterateAnimals(cats);
}
}
看來情勢對我們來說不太好!事實證明,我們必須編寫單獨的方法來列舉所有類型的動物?事實上,不,你不必:)通配符會幫助我們解決這個問題!我們將使用以下結構透過簡單的方法解決該問題:
public static void iterateAnimals(Collection<? extends Animal> animals) {
for(Animal animal: animals) {
System.out.println("Еще один шаг в цикле пройден!");
}
}
這是通配符。更準確地說,這是幾種通配符中的第一種 - “ extends ”(另一個名稱是Upper Bounded Wildcards)。這個設計告訴我們什麼?Animal
這意味著該方法將類別物件或任何後代類別的物件的集合作為輸入Animal (? extends Animal)
。Animal
換句話說,該方法可以接受集合、Pet
、Dog
或作為輸入Cat
- 這沒有什麼區別。讓我們確保這有效:
public static void main(String[] args) {
List<Animal> animals = new ArrayList<>();
animals.add(new Animal());
animals.add(new Animal());
List<Pet> pets = new ArrayList<>();
pets.add(new Pet());
pets.add(new Pet());
List<Cat> cats = new ArrayList<>();
cats.add(new Cat());
cats.add(new Cat());
List<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
dogs.add(new Dog());
iterateAnimals(animals);
iterateAnimals(pets);
iterateAnimals(cats);
iterateAnimals(dogs);
}
控制台輸出:
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
我們總共創建了4個集合和8個對象,控制台中剛好有8個條目。一切都很好!:) 通配符使我們能夠輕鬆地將必要的邏輯與特定類型的綁定整合到一個方法中。我們不再需要為每種類型的動物編寫單獨的方法。想像一下,如果我們的應用程式用於動物園或獸醫診所,我們會有多少種方法:) 現在讓我們看看不同的情況。我們的繼承層次結構將保持不變:頂級類別是Animal
,下面是類別 Pets Pet
,下一級是Cat
和Dog
。現在您需要重寫該方法,iretateAnimals()
以便它可以適用於狗以外的任何類型的動物。Collection<Animal>
也就是說,它應該接受, Collection<Pet>
or作為輸入Collection<Cat>
,但不應該與 一起使用Collection<Dog>
。我們怎樣才能做到這一點?似乎為每種類型編寫單獨的方法的前景再次浮現在我們面前:/我們還能如何向編譯器解釋我們的邏輯?這可以非常簡單地完成!在這裡,通配符將再次為我們提供幫助。但這次我們將使用不同的類型 - “ super ”(另一個名稱是Lower Bounded Wildcards)。
public static void iterateAnimals(Collection<? super Cat> animals) {
for(int i = 0; i < animals.size(); i++) {
System.out.println("Еще один шаг в цикле пройден!");
}
}
這裡的原理是類似的。該構造<? super Cat>
告訴編譯器該方法可以將該類別或任何其他祖先類別iterateAnimals()
的物件集合作為輸入。在我們的例子中,類別本身、它的祖先以及祖先的祖先都符合這個描述。該類別不符合此約束,因此嘗試使用帶有列表的方法將導致編譯錯誤: Cat
Cat
Cat
Pets
Animal
Dog
List<Dog>
public static void main(String[] args) {
List<Animal> animals = new ArrayList<>();
animals.add(new Animal());
animals.add(new Animal());
List<Pet> pets = new ArrayList<>();
pets.add(new Pet());
pets.add(new Pet());
List<Cat> cats = new ArrayList<>();
cats.add(new Cat());
cats.add(new Cat());
List<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
dogs.add(new Dog());
iterateAnimals(animals);
iterateAnimals(pets);
iterateAnimals(cats);
//ошибка компиляции!
iterateAnimals(dogs);
}
我們的問題得到了解決,通配符再次變得非常有用:) 講座到此結束。現在您知道了,學習 Java 時泛型主題是多麼重要 - 我們花了 4 個講座來討論它!但現在你已經很好地掌握了這個主題,並且能夠在面試中證明自己:) 現在是時候回到任務上了!祝你學習順利!:)
GO TO FULL VERSION