JavaRush /Blog Java /Random-PL /Symbole wieloznaczne w języku Java Generics

Symbole wieloznaczne w języku Java Generics

Opublikowano w grupie Random-PL
Cześć! Kontynuujemy badanie tematu leków generycznych. Masz już na ich temat sporą wiedzę z poprzednich wykładów (o używaniu zmiennych varargs podczas pracy z rodzajami generycznymi i o usuwaniu typów ), ale nie poruszyliśmy jeszcze jednego ważnego tematu: symboli wieloznacznych . Jest to bardzo ważna cecha leków generycznych. Do tego stopnia, że ​​poświęciliśmy mu osobny wykład! Jednak w symbolach wieloznacznych nie ma nic skomplikowanego, przekonasz się teraz :) Symbole wieloznaczne w rodzajach - 1Spójrzmy na przykład:
public class Main {

   public static void main(String[] args) {

       String str = new String("Test!");
       // ниJakих проблем
       Object obj = str;

       List<String> strings = new ArrayList<String>();
       // ошибка компиляции!
       List<Object> objects = strings;
   }
}
Co tu się dzieje? Widzimy dwie bardzo podobne sytuacje. W pierwszym z nich próbujemy rzucić obiekt Stringna typ Object. Nie ma z tym żadnych problemów, wszystko działa jak należy. Ale w drugiej sytuacji kompilator zgłasza błąd. Chociaż mogłoby się wydawać, że robimy to samo. Tyle, że teraz używamy kolekcji kilku obiektów. Ale dlaczego pojawia się błąd? Jaka jest w zasadzie różnica, czy rzutujemy jeden obiekt Stringna typ Object, czy na 20 obiektów? Istnieje istotna różnica pomiędzy obiektem a zbiorem obiektów . Jeśli klasa jest następcą klasy , to nie jest następcą . Z tego powodu nie mogliśmy sprowadzić naszego do . jest spadkobiercą , ale nie jest spadkobiercą . Intuicyjnie nie wygląda to zbyt logicznie. Dlaczego twórcy języka kierowali się tą zasadą? Wyobraźmy sobie, że kompilator nie dałby nam tutaj błędu: BАCollection<B>Collection<A>List<String>List<Object>StringObjectList<String>List<Object>
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 błędów i pozwolił nam utworzyć referencję List<Object> objectdo kolekcji ciągów znaków strings, możemy dodać stringsnie ciąg znaków, ale po prostu dowolny obiekt Object! W ten sposób straciliśmy gwarancję, że nasza kolekcja zawiera wyłącznie obiekty określone w pliku rodzajowymString . Oznacza to, że straciliśmy główną zaletę leków generycznych – bezpieczeństwo typu. A skoro kompilator pozwolił nam to wszystko zrobić, oznacza to, że podczas wykonywania programu otrzymamy tylko błąd, który zawsze jest znacznie gorszy niż błąd kompilacji. Aby zapobiec takim sytuacjom, kompilator wyrzuca nam błąd:
// ошибка компиляции
List<Object> objects = strings;
...i przypomina mu, że List<String>nie jest spadkobiercą List<Object>. Jest to niezawodna zasada działania leków generycznych i należy o niej pamiętać podczas ich stosowania. Przejdźmy dalej. Powiedzmy, ż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 czele hierarchii znajdują się po prostu Zwierzęta: Zwierzęta są po nich dziedziczone. Zwierzęta dzielą się na 2 typy – psy i koty. Teraz wyobraźmy sobie, że musimy stworzyć prostą metodę iterateAnimals(). Metoda powinna akceptować kolekcję dowolnych zwierząt ( Animal, Pet, Cat, Dog), iterować po wszystkich elementach i za każdym razem wysyłać coś do konsoli. Spróbujmy napisać tę metodę:
public static void iterateAnimals(Collection<Animal> animals) {

   for(Animal animal: animals) {

       System.out.println("Еще один шаг в цикле пройден!");
   }
}
Wydawać by się mogło, że problem został rozwiązany! Jednakże, jak niedawno się dowiedzieliśmy, List<Cat>albo nie List<Dog>jesteśmy List<Pet>spadkobiercami List<Animal>! Dlatego przy próbie wywołania metody iterateAnimals()z listą kotów otrzymamy błąd kompilatora:
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);
   }
}
Sytuacja nie wygląda dla nas dobrze! Okazuje się, że będziemy musieli napisać osobne metody wyliczania wszystkich typów zwierząt? Właściwie nie, nie musisz :) A symbole wieloznaczne nam w tym pomogą ! Problem rozwiążemy w ramach jednej prostej metody, stosując następującą konstrukcję:
public static void iterateAnimals(Collection<? extends Animal> animals) {

   for(Animal animal: animals) {

       System.out.println("Еще один шаг в цикле пройден!");
   }
}
To jest symbol wieloznaczny. Dokładniej, jest to pierwszy z kilku typów symboli wieloznacznych - „ rozszerza ” (inna nazwa to symbole wieloznaczne z górnym ograniczeniem ). Co nam mówi ten projekt? Oznacza to, że metoda przyjmuje jako dane wejściowe kolekcję obiektów klas Animallub obiektów dowolnej klasy potomnej Animal (? extends Animal). AnimalInnymi słowy, metoda może przyjąć kolekcję Petlub jako dane wejściowe — Dognie Catma to znaczenia. Upewnijmy 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:

Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Stworzyliśmy w sumie 4 kolekcje i 8 obiektów, a w konsoli jest dokładnie 8 wpisów. Wszystko działa świetnie! :) Wildcard pozwolił nam łatwo zmieścić w jednej metodzie niezbędną logikę z powiązaniem z konkretnymi typami. Pozbyliśmy się konieczności pisania osobnej metody dla każdego rodzaju zwierzęcia. Wyobraź sobie, ile metod mielibyśmy, gdyby nasza aplikacja została wykorzystana w zoo lub klinice weterynaryjnej :) Teraz spójrzmy na inną sytuację. Nasza hierarchia dziedziczenia pozostanie taka sama: klasa najwyższego poziomu to Animal, tuż poniżej znajduje się klasa Pets Pet, a na kolejnym poziomie znajdują się Cati Dog. Teraz musisz przepisać tę metodę iretateAnimals(), aby mogła działać na każdym typie zwierzęcia z wyjątkiem psów . Oznacza to, że powinien akceptować lub Collection<Animal>jako dane wejściowe , ale nie powinien działać z . Jak możemy to osiągnąć? Wygląda na to, że ponownie rysuje się przed nami perspektywa napisania osobnej metody dla każdego typu :/ Jak inaczej wytłumaczyć kompilatorowi naszą logikę? A można to zrobić w bardzo prosty sposób! Tutaj znów z pomocą przyjdą nam dzikie karty. Ale tym razem użyjemy innego typu - „ super ” (inna nazwa to Lower Bounded Wildcards ). Collection<Pet>Collection<Cat>Collection<Dog>
public static void iterateAnimals(Collection<? super Cat> animals) {

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

       System.out.println("Еще один шаг в цикле пройден!");
   }
}
Tutaj zasada jest podobna. Konstrukcja <? super Cat>mówi kompilatorowi, że metoda iterateAnimals()może przyjąć jako dane wejściowe kolekcję obiektów tej klasy Catlub dowolnej innej klasy-przodka Cat. CatW naszym przypadku sama klasa , jej przodek Petsi przodek przodka pasują do tego opisu Animal. Klasa Dognie pasuje do tego ograniczenia, dlatego próba użycia metody z listą 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);

   //ошибка компиляции!
   iterateAnimals(dogs);
}
Nasz problem został rozwiązany i znów symbole wieloznaczne okazały się niezwykle przydatne :) Na tym kończymy wykład. Teraz widzisz, jak ważny jest temat generyków podczas nauki języka Java - spędziliśmy na nim 4 wykłady! Ale teraz już dobrze ogarnąłeś temat i będziesz mógł wykazać się na rozmowie kwalifikacyjnej :) A teraz czas wracać do zadań! Powodzenia na studiach! :)
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION