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 :) Spó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 String
na 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 String
na 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>
String
Object
List<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> object
do kolekcji ciągów znaków strings
, możemy dodać strings
nie 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 Animal
lub obiektów dowolnej klasy potomnej Animal (? extends Animal)
. Animal
Innymi słowy, metoda może przyjąć kolekcję Pet
lub jako dane wejściowe — Dog
nie Cat
ma 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ę Cat
i 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 Cat
lub dowolnej innej klasy-przodka Cat
. Cat
W naszym przypadku sama klasa , jej przodek Pets
i przodek przodka pasują do tego opisu Animal
. Klasa Dog
nie 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! :)
GO TO FULL VERSION