CodeGym /Java Blog /Toto sisi /泛型中的通配符
John Squirrels
等級 41
San Francisco

泛型中的通配符

在 Toto sisi 群組發布
你好!讓我們繼續研究泛型。您已經從之前的課程中獲得了大量關於它們的知識(關於在處理泛型時使用可變參數類型擦除),但是我們還沒有考慮一個重要的主題——通配符。這是泛型非常重要的特性。如此之多,以至於我們專門為此開設了一堂課!也就是說,通配符並沒有什麼特別複雜的地方。你馬上就會看到 :)泛型中的通配符 - 1讓我們看一個例子:

public class Main {

   public static void main(String[] args) {
      
       String str = new String("Test!");
       // No problem
       Object obj = str;
      
       List<String> strings = new ArrayList<String>();
       // Compilation error!
       List<Object> objects = strings;
   }
}
這裡發生了什麼?我們看到兩種非常相似的情況。在這種情況下,我們將一個String對象轉換為一個Object對象。這裡沒有問題——一切都按預期進行。但在第二種情況下,編譯器會產生錯誤。但我們正在做同樣的事情,不是嗎?這次我們只是使用幾個對象的集合。但是為什麼會出現這個錯誤呢?有什麼不同?我們是將一個String對象投射到一個Object還是 20 個對象?對象對象集合之間有一個重要的區別。 如果B班級是班級的孩子A,則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>指向 的引用strings,所以我們可以將任何舊Object對象添加到strings集合中!因此,我們失去了我們的集合僅包含泛型類型調用中類型參數指定的對象的保證String。換句話說,我們失去了泛型的主要優勢——類型安全。而且因為編譯器沒有阻止我們這樣做,我們只會在運行時得到一個錯誤,這總是比編譯錯誤糟糕得多。為了防止出現這種情況,編譯器會報錯:

// Compilation error
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()");
   }
}
該層次結構頂部是一個簡單的 Animal 類,該類由 Pet 繼承。Pet 有 2 個子類:Dog 和 Cat。現在假設我們需要創建一個簡單的iterateAnimals()方法。該方法應該採用任何動物的集合 ( Animal, Pet, Cat, Dog),迭代所有元素,並在每次迭代期間在控制台上顯示一條消息。讓我們嘗試編寫這樣一個方法:

public static void iterateAnimals(Collection<Animal> animals) {

   for(Animal animal: animals) {

       System.out.println("Another iteration in the loop!");
   }
}
看來問題解決了!然而,正如我們最近了解到的,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("Another iteration in the loop!");
       }
   }

   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());

       // Compilation error!
       iterateAnimals(cats);
   }
}
情況對我們來說不太好!我們是否必須編寫單獨的方法來枚舉每種動物?實際上,不,我們沒有 :) 碰巧的是,通配符可以幫助我們解決這個問題!我們可以使用以下構造通過一種簡單的方法解決問題:

public static void iterateAnimals(Collection<? extends Animal> animals) {

   for(Animal animal: animals) {

       System.out.println("Another iteration in the loop!");
   }
}
這是一個通配符。更準確地說,這是幾種類型的通配符中的第一種。它被稱為上限通配符,用? 延伸。這個結構告訴我們什麼?這意味著該方法接受對象集合或從(? extends Animal)Animal派生的任何類的對象集合。Animal換句話說,該方法可以接受AnimalPetDogCat對象的集合——這沒有區別。讓我們說服自己它有效:

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);
}
控制台輸出:

Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
我們一共創建了4個集合,8個對象,控制台上正好有8個入口。一切都很好!:) 通配符使我們能夠輕鬆地將綁定到特定類型的必要邏輯放入一個方法中。我們消除了為每種動物編寫單獨方法的需要。想像一下,如果我們的應用程序被動物園或獸醫辦公室使用,我們需要多少方法 :) 但是現在讓我們看看不同的情況。我們的繼承層次結構保持不變:頂級類是AnimalPet下面是類,下一層是CatDog類。現在您需要重寫該iterateAnimals()方法,以便可以處理任何類型的動物,狗除外。也就是說,它應該接受Collection<Animal>Collection<Pet>Collection<Car>,但它不應該與 一起使用Collection<Dog>。我們怎樣才能做到這一點?似乎我們再次面臨為每種類型編寫單獨方法的前景:/否則我們如何向編譯器解釋我們想要發生的事情?其實很簡單!在這裡,通配符再次為我們提供幫助。但這次我們將使用另一種類型的通配符——下界通配符,它​​使用super表示。

public static void iterateAnimals(Collection<? super Cat> animals) {

   for(int i = 0; i < animals.size(); i++) {

       System.out.println("Another iteration in the loop!");
   }
}
這裡原理類似。該<? super Cat>構造告訴編譯器該iterateAnimals()方法可以接受Cat對象集合或類的任何祖先Cat作為輸入作為輸入。在這種情況下,該類Cat、它的父級Pet及其父級的父級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);
  
   // Compilation error!
   iterateAnimals(dogs);
}
我們已經解決了我們的問題,通配符再次證明非常有用 :) 至此,本課結束。現在您知道泛型在您的 Java 學習中有多麼重要了——我們已經學習了 4 節關於它們的完整課程!但是現在您已經精通該主題並且可以在求職面試中證明您的技能 :) 現在,是時候回到任務了!祝你學業成功!:)
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION