CodeGym /Blog Java /Random-PL /Symbole wieloznaczne w rodzajach
Autor
Artem Divertitto
Senior Android Developer at United Tech

Symbole wieloznaczne w rodzajach

Opublikowano w grupie Random-PL
Cześć! Kontynuujmy nasze badanie leków generycznych. Masz już znaczną wiedzę na ich temat z poprzednich lekcji (o używaniu vararg podczas pracy z rodzajami generycznymi oraz o wymazywaniu typów ), ale jest jeszcze ważny temat, którego jeszcze nie rozważyliśmy — symbole wieloznaczne . Jest to bardzo ważna cecha leków generycznych. Do tego stopnia, że ​​poświęciliśmy mu osobną lekcję! To powiedziawszy, nie ma nic szczególnie skomplikowanego w symbolach wieloznacznych. Zobaczysz to od razu :) Symbole wieloznaczne w rodzajach - 1Spójrzmy na przykład:

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;
   }
}
Co tu się dzieje? Widzimy dwie bardzo podobne sytuacje. W takim przypadku rzutujemy Stringobiekt na Objectobiekt. Tutaj nie ma żadnych problemów — wszystko działa zgodnie z oczekiwaniami. Ale w drugiej sytuacji kompilator generuje błąd. Ale robimy to samo, prawda? Tym razem po prostu używamy kolekcji kilku obiektów. Ale dlaczego występuje błąd? Co za różnica? Czy rzucamy jeden Stringobiekt na jeden, Objectczy na 20 obiektów? Istnieje istotna różnica między obiektem a zbiorem obiektów . Jeśli Bklasa jest dzieckiem klasy A, to Collection<B>nie jest dzieckiem klasy Collection<A>. Dlatego nie byliśmy w stanie rzucić naszego List<String>na aList<Object>. Stringjest dzieckiem Object, ale List<String>nie jest dzieckiem List<Object>. To może nie wydawać się super intuicyjne. Dlaczego twórcy języka zrobili to w ten sposób? Wyobraźmy sobie, że kompilator nie zgłasza nam błędu:

List<String> strings = new ArrayList<String>();
List<Object> objects = strings;
W takim przypadku moglibyśmy na przykład wykonać następujące czynności:

objects.add(new Object());
String s = strings.get(0);
Ponieważ kompilator nie dał nam żadnego błędu i pozwolił nam utworzyć referencję List<Object>wskazującą na strings, możemy dodać dowolny stary Objectobiekt do stringskolekcji! W ten sposób straciliśmy gwarancję, że nasza kolekcja zawiera tylko Stringobiekty określone przez argument typu w wywołaniu typu ogólnego . Innymi słowy, straciliśmy główną zaletę leków generycznych — bezpieczeństwo typów. A ponieważ kompilator nie powstrzymał nas przed zrobieniem tego, otrzymamy błąd tylko w czasie wykonywania, co jest zawsze znacznie gorsze niż błąd kompilacji. Aby zapobiec takim sytuacjom, kompilator zwraca nam błąd:

// Compilation error
List<Object> objects = strings;
... i przypomina nam, że List<String>nie jest potomkiem List<Object>. Jest to żelazna zasada dotycząca leków generycznych i należy o niej pamiętać podczas pracy z nimi. Przejdźmy dalej. Załóżmy, że mamy małą hierarchię klas:

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()");
   }
}
Na szczycie hierarchii znajduje się prosta klasa Animal, którą dziedziczy Pet. Zwierzak ma 2 podklasy: Pies i Kot. Załóżmy teraz, że musimy stworzyć prostą iterateAnimals()metodę. Metoda powinna pobierać kolekcję dowolnych zwierząt ( Animal, Pet, Cat, Dog), iterować po wszystkich elementach i wyświetlać komunikat na konsoli podczas każdej iteracji. Spróbujmy napisać taką metodę:

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

   for(Animal animal: animals) {

       System.out.println("Another iteration in the loop!");
   }
}
Wygląda na to, że problem został rozwiązany! Jednak, jak się niedawno dowiedzieliśmy, List<Cat>, List<Dog>i List<Pet>nie są potomkami List<Animal>! Oznacza to, że przy próbie wywołania iterateAnimals()metody z listą kotów dostajemy błąd kompilacji:

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);
   }
}
Sytuacja nie wygląda dla nas najlepiej! Czy musimy pisać osobne metody liczenia każdego rodzaju zwierząt? Właściwie nie, nie :) I tak się składa, że ​​dzikie karty nam w tym pomagają! Możemy rozwiązać problem za pomocą jednej prostej metody, używając następującej konstrukcji:

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

   for(Animal animal: animals) {

       System.out.println("Another iteration in the loop!");
   }
}
To jest symbol wieloznaczny. Mówiąc dokładniej, jest to pierwszy z kilku rodzajów symboli wieloznacznych. Jest znany jako symbole wieloznaczne górnej granicy i jest wyrażony przez ? rozciąga się . Co mówi nam ta konstrukcja? Oznacza to, że metoda akceptuje kolekcję Animalobiektów lub kolekcję obiektów dowolnej klasy, która pochodzi od Animal(? rozszerza Animal). Innymi słowy, metoda może akceptować zbiór obiektów Animal, Pet, Dog, lub Cat— nie ma to znaczenia. Przekonajmy się, że to działa:

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);
}
Wyjście konsoli:

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!
Stworzyliśmy w sumie 4 kolekcje i 8 obiektów, a na konsoli jest dokładnie 8 wpisów. Wszystko działa świetnie! :) Symbol wieloznaczny pozwolił nam łatwo dopasować niezbędną logikę powiązaną z określonymi typami w jednej metodzie. Wyeliminowaliśmy potrzebę pisania osobnej metody dla każdego rodzaju zwierzęcia. Wyobraź sobie, ile metod byśmy potrzebowali, gdyby z naszej aplikacji korzystało zoo lub gabinet weterynaryjny :) Ale teraz spójrzmy na inną sytuację. Nasza hierarchia dziedziczenia pozostaje niezmieniona: klasa najwyższego poziomu to Animal, z Petklasą tuż poniżej, a klasy Cati Dogna następnym poziomie. Teraz trzeba przepisać iterateAnimals()metodę tak, aby działała z każdym typem zwierzęcia, z wyjątkiem psów . Czyli powinien Collection<Animal>przyjąćCollection<Pet>lub Collection<Car>, ale nie powinno działać z Collection<Dog>. Jak możemy to osiągnąć? Wygląda na to, że znów staniemy przed perspektywą napisania osobnej metody dla każdego typu :/ Jak inaczej wytłumaczyć kompilatorowi co chcemy aby się stało? To całkiem proste! Po raz kolejny z pomocą przychodzą nam dzikie karty. Ale tym razem użyjemy innego rodzaju symbolu wieloznacznego — symbolu wieloznacznego z dolną granicą , który jest wyrażany za pomocą 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!");
   }
}
Tutaj zasada jest podobna. Konstrukcja <? super Cat>mówi kompilatorowi, że iterateAnimals()metoda może przyjąć jako dane wejściowe kolekcję Catobiektów lub dowolnego przodka klasy Catjako dane wejściowe. W tym przypadku Catklasa, jej rodzic Peti rodzic swojego rodzica, Animalwszystkie pasują do tego opisu. Klasa Dognie pasuje do naszego ograniczenia, więc próba użycia metody z argumentem List<Dog>zakończy się błędem kompilacji:

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);
}
Rozwiązaliśmy nasz problem i po raz kolejny wieloznaczniki okazały się niezwykle przydatne :) Na tym lekcja dobiegła końca. Teraz widzisz, jak ważne są typy generyczne w Twojej nauce o Javie — mieliśmy na ich temat całe 4 lekcje! Ale teraz jesteś już dobrze zorientowany w temacie i możesz udowodnić swoje umiejętności na rozmowach kwalifikacyjnych :) A teraz czas wrócić do zadań! Życzymy sukcesów w nauce! :)
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION