Spróbuję w pigułce zawrzeć najczęściej (moim zdaniem) spotykane zagadnienia dotyczące typów ogólnych oraz dylematów z nimi związanych.
Nie tracimy czasu. Brum brum.
Co myślisz, kiedy słyszysz o typach ogólnych. Jeśli nic, to się nie przejmuj. To nawet zdrowo.
Ja myślę, hmm, no np.: o typie ogólnym jakim jest interfejs List<E>. Gdybym chciał zastosować ten interfejs i ograniczyć tę listę do elementów typu String, to napisałbym List<String>.
List<String> jest tzw.: typem parametryzowanym (typ to klasa lub interfejs upraszczając nieco na nasze potrzeby). String natomiast jest rzeczywistym parametrem typu (ang. actual parameter type).
Co jeszcze widzę. Widzę też <T> lub <?>
class Warehouse<T> { T product; }
Warehouse<T> to typ ogólny, który możemy sparametryzować przy pomocy wspomnianego już rzeczywistego parametru typu. Umożliwiam tym samym mojemu magazynowi przetrzymywanie produktów dowolnego typu np.: new Warehouse<Food>()
Widzę też parametry, które nieco bardziej ograniczają możliwości naszych typów, tj.: klas lub interfejsów. Na przykład:
class Warehouse<T extends Food>
Nie stworzymy już magazynu, który przetrzymuje lekarstwa:
new Warehouse<Medicine>
Klasa Medicine bowiem nie rozszerza klasy Food, a deklaracja klasy Warehouse takie ograniczenie nam tworzy.
Zatem:
fajne to czy głupie to?
Kompilator nie wykrył niczego złego w poniższym kodzie (kod dostępny jest też na githubie, o tu. Kod może trochę kłuć wytrawnego playera IT, ale musisz to jakoś znieść. To są tylko przykłady, które mają zawierać jak najmniej zbędnego kodu.
W mainie dodajemy do listy basket trzy elementy i wyświetlamy je na ekranie. Nic więcej. Nic mniej.
Nasz koszyk (referencja basket) w ShoppingBasket jest typu surowego List (linia 17). Sama w sobie jest listą elementów dowolnego typu. Pytanie co autor miał na myśli projektując byt jakim jest produkt. Jakiego typu on jest? Założeniem programisty były produkty typu String. Tak można podejrzewać patrząc metodę showBasket (linia 19). Iterujemy po koszyku, pobieramy z niego obiekt (jakiś produkt) i wyświetlamy go. Pobierając go w linii 21 rzutujemy go na typ String. Wszystko jest OK, dopóki dodaliśmy tylko Pizzę oraz Coca-Colę (linia 9, 10).
Inny programista natomiast, może najlepszy kolega tego pierwszego chciał przyfisiować i zamiast typu String dodał do listy typ StringBuilder. Następnie wyświetlamy zawartość koszyka za pomocą metody showBasket(), gdzie próbujemy rzutować każdy element na typ String, zgodnie z założeniem i implementacją metody showBasket. I tak się kończą przyjaźnie.
Uruchamiamy nasz program. Dostajemy na twarz wyjątek:
Exception in thread “main” java.lang.ClassCastException: class java.lang.StringBuilder cannot be cast to class java.lang.String
Próbujemy rzutować StringBuilder na String. Czemu dostaliśmy błąd w run-time’ie? Ano kompilator zaufał nam przy rzutowaniu stosując jednocześnie zasadę kontrola najwyższą formą zaufania. W bytecode’dzie (javap -c ShoppingBasket.class albo wtyczka Show Bytecode w IntelliJ), czyli po udanej kompilacji programu, zobaczymy, że widoczna jest tam instrukcja checkcast:
25: checkcast #7 // class java/lang/String
która zweryfikuje nasze programistyczne marzenia i stąd wyjątek.
Faktem jest, że nasza lista przyjmie wszystko. Nasza lista nie jest bezpieczna. Jeden programista założył, że będzie to lista przetrzymująca Stringi, a potem przylazł drugi mądry i stwierdził, że doda do listy obiekt innego typu. :-< Niestety, gdy nie parametryzujemy typów surowych, to programista musi pamiętać jaki typ danych przechowuje struktura danych tu: lista. A i pamięć może się na niewiele zdać, w końcu do listy mogą trafić elementy zwrócone z np.: jakiegoś API.
A my chcielibyśmy unikać takich sytuacji i błędów. Tzn., takich, które są wykrywane dopiero w czasie uruchomienia programu. Wyobraź sobie, że przychodzi trzeci mądry i wrzuca aplikację na produkcję, bo przecież się kompiluje, bo przecież tylko jedną linijkę dodałem, no nie ma bata żebym coś popsuł. Lepiej więc, jakby to kompilator zgłosił nam taką pomyłkę dodania nie-stringa do listy. Brzmi lepiej. Wiemy o błędzie wcześniej, program nie kompiluje się, świeci się na czerwono, jesteśmy w stanie w porę zareagować.
Wniosek. Fajne to. Oprócz sytuacji, gdy nie jest fajne tak jakby być mogło, ale o tym innym razem.
Type erasure VS reification
W paru prostych słowach.
Jak wklikasz w swoje IDE w klasie App.java:
List list_1 = new ArrayList(); List<String> list_2 = new ArrayList();
i następnie skompilujesz i wykonasz javap -c App.class to zauważysz, że obie instrukcje new bytecode’u w ogóle się nie różnią, mimo że list_1 jest typu surowego List, a list_2 jest typu List<String>. Czyli po kompilacji nie jest już istotne to, czy napisaliśmy List czy List<String>. Informacja ta znika. Jest czyszczona. Jest to tzw.: type erasure, czyli czyszczenie typów.
Skoro po kompilacji nie są one widoczne to…. to umieszczenie poniższych dwóch metod w jednej klasie nie powinno się skompilować.
void abc(List input) { }
void abc(List<String> input) {}
W trakcie uruchomienia, maszyna wirtualna Javy widziałaby dwie identyczne metody o sygnaturze: abc(List input). I faktycznie, kompilator podkreśla na czerwono powyższy kod.
Termin reifikacja (reification) w kontekście typów, polega na zachowaniu informacji o typie w run-time’ie, czego Java nie robi. Jest to przeciwieństwo czyszczenia typów (type erasure).
Czy to fajnie czy średnio, że jest to czyszczenie typów? Będzie i o tym wkrótce.
Substitution principle
Żeby ogarnąć te wszystkie T, ?, E i inne super ważne konstrukty poznajmy najpierw metodę podstawienia (Substitution principle).
Klasa LinkedList oraz klasa Vector implementują interfejs List (dokładnie to implementują List<E>)
Co to oznacza. Oznacza to między innymi to, że obie te klasy są podtypem (subtype) interfejsu List. Natomiast List jest supertypem (supertype) dla LinkedList oraz dla Vector.
A to z kolei oznacza, że interfejs List jest na tyle super, że referencja typu List (we wspomnianym przypadku referencją u nas jest basket, a jej typem właśnie List) może wskazywać na dowolny obiekt, którego typ implementuje interfejs List. Jeszcze innymi słowy, do referencji typu List można przypisać każdy typ, który implementuje interfejs List, np.:
List basket = new LinkedList(); basket = new Vector();
Tworzymy referencję basket typu List, a zaraz potem przypisujemy do niej typ Vector. Dzięki temu, że zarówno Vector jak i LinkedList implementują (są subtypem) typu List takie podstawienia są możliwe.
Zasada podstawienia brzmi dokładnie tak (definicja z Java Generics and Collections):
Substitution Principle: a variable of a given type may be assigned a value of any subtype of that type, and a method with a parameter of a given type may be invoked with an argument of any subtype of that type.
Czyli coś takiego właśnie zrobiliśmy z referencją basket.
Deklaracja interfejsu List poniżej:
public interface List<E> extends Collection<E> {}
Co to to <E>. Wiemy już, że użycie zwykłego List może doprowadzić do pewnych kłopotów. Wiemy też, że lista to kolekcja Elementów. Stąd to E przy liście. Taka konwencja. Fachowo to E nazywa się formalnym parametrem typu. Jak często mówisz do kolegi z zespołu: Wiciu, gdy sobie przypomnę jakiż to przekomiczny formalny parametr typu nadałeś onegdaj naszemu interfejsowi, to brzuch mnie od chichotu kłuje, hu hu hu, nigdy się nie zmienisz mój drogi.
Deklaracja LinkedList wygląda, w nieco skróconej formie, tak:
public class LinkedList<E> implements List<E> {}
Czyli precyzując naszą definicję dotyczącą supertypów oraz podtypów powiemy, że:
LinkedList<E> jest subtypem List<E> Vector<E> jest subtypem List<E>
Całkowicie poprawnym kodem będą poniższe trzy linie:
// przypisujemy podtyp do supertypu, gdzie naszym E jest typ Number List<Number> linkedList = new LinkedList<Number>(); Vector<Number> vector = new Vector<Number>(); linkedList = vector; // tym razem przypisujemy inny podtyp do supertypu
Jesteśmy zgodni z definicją.
Inny przykład:
Number jest supertypem dla klasy BigInteger oaz BigDecimal (obie dziedziczą po Number).
Mały test:
List<Number> linkedList = new LinkedList<Number>(); linkedList.add(new BigInteger("2")); linkedList.add(new BigDecimal("2.0"));
Świetnie. To działa.
A czy skoro BigInteger jest subtypem Number to czy
List<BigInteger> jest subtypem List<Number>
Nope. Czemu? A temu:
List<Number> numbers = new ArrayList<Number>(); List<BigInteger> integers = new ArrayList<BigInteger>(); // błąd, poniżej próbujemy zrobić de facto List<Number> numbers = new ArrayList<BigInteger>(); numbers = integers;
Czyli nasza referencja numbers typu List<Number> zaczęłaby nagle wskazywać na obiekt typu ArrayList, który przechowuje elementy typu BigInteger.
Przypomnienie parafrazy smutnej definicji polimorfizmu: referencja mówi nam, jakie metody możemy wykonać, natomiast która (implementacja metody) się dokładnie wykona, o tym mówi już typ obiektu na który wskazuje owa referencja (wyjaśnienie przeczytasz tu). Inaczej mówiąc, gdybyś wpisał w swoim IDE: numbers. (numbers kropka) to IDE podpowie Ci metodę add(Number e). Poza definicją polimorfizmu, która wyjaśnia tę sytuację, można to ująć jeszcze tak:
w interfejsie List<E> mamy oto taką sygnaturę metody add:
add(E e);
Zatem deklarując referencję typu List z parametrem typu Number:
List<Number> ref;
możemy (zgodnie z powyższą definicją polimorfizmu) na niej wykonać operację:
add(Number e) {}
Zatem gdyby istniała możliwość, że kompilator dopuszcza zapis:
numbers = integers; // błąd
co byłoby tożsame z zapisem (również oczywiście błędnym)
List<Number> numbers = new ArrayList<BigInteger>(); // błąd
to wówczas moglibyśmy (ale jest to niemożliwe przypominam) zrobić:
numbers.add(new BigDecimal("2.23")); // BigDecimal jest podtypem Number
Skoro nasza referencja numbers wskazuje, w ramach naszego nierealnego do zrealizowania przykładu, na obiekt ArrayList<BigInteger> to tym samym udałoby nam się dodać liczbę niecałkowitą do listy z liczbami całkowitymi. Ups.
Parametry, parametry, wszędzie parametry
class Warehouse<T> {}
Nasze magazyny będą przetrzymywać różne Typy (T – formalny parametr typu, Wiciu). My magazynujemy lekarstwa (typ Medicine) oraz produkty spożywcze (typ Food). Nie chcemy takich produktów w końcu trzymać razem, w jednym magazynie. Dlatego może nasz magazyn dowolnie sparametryzować. To jest chyba w miarę jasne. Jedźmy dalej.
Coś ciekawszego.
Mamy klasę bazową Report i klasy pochodne CsvReport oraz PdfReport.
Rys. 1
Na poniższym kodziku chcemy dodać do listy kilka raportów Csv i wygenerować je. Zapomnieliśmy co czytaliśmy do tej pory, więc myk myk myk, popełniamy błąd, linia 12 podkreślona na czerwono:
Rys. 2
Hmm, no tak. Owszem, CsvReport jest podtypem Report lecz List<CsvReport> nie jest przecież podtypem List<Report>. To może tak:
Rys. 3
E, nie. Świeci się. Kompilator nie ma pojęcia co to jest to T. Nie parametryzowaliśmy żadnym typem przecież naszej klasy. I nie musimy. My chcemy sparametryzować tylko jedną metodę. Powiedzmy kompilatorowi zatem co to jest T. Chcemy móc przekazywać listę klas Report oraz listę klas będących subtypem klasy Report. Będzie to wyglądać tak:
Rys. 4
O. Nie świeci się nic. Możemy wówczas przekazać do metody generate zarówno List<Report>:
List<Report> reports = new ArrayList<Report>();
reports.add(new CsvReport());
reports.add(new PdfReport());
generate(reports);
a także listę raportów, które są podtypem klasy Report.
List<CsvReport> csvReports = new ArrayList<CsvReport>();'
csvReports.add(new CsvReport());
generate(csvReports);
A co by się stało, gdybyśmy dodali do naszej klasy wspomnianą już metodę o sygnaturze generate(List<Report> reports), jak poniżej:
Rys. 5
Kompilator wyrzucił nam błąd: ‘generate(List)’ clashes with ‘generate(List)’; both methods have same erasure. Chodzi tu o wspomniane czyszczenie typów. Po kompilacji wszystkie metody z poniższego obrazka będą miały postać: void generate(List reports).
Rys. 6
Czyli z jednej strony ten sam bytecode zostanie wygenerowany dla tych sparametryzowanych metod, a z drugiej strony kompilator nie pozwala w identyczny sposób ich stosować. Spójrz na rysunek nr 2, to przypomnisz sobie, jaka była różnica między tymi dwoma metodami. Dlaczego tak się dzieje? Wspominam o tym wyżej w Substitution principle. Kompilator chroni nas na różne sposoby. Wiemy zawczasu, gdy popełnimy błąd. Fajnie.
? + extends
Wiemy, że typy ogólne wprowadzone zostały głównie po to, aby kompilator pilnował tego, abyśmy nie pomieszali typów. Nie wspominaliśmy jeszcze o wildcard, czyli o znaku ?
void generate(List<? extends Report> reports) { for(Report report : reports) { report.generate(); } }
Powyższa metoda przyjmie listę raportów będących typu List<Report> lub listę raportów będących subtypem klasy Report. Dozwolone będą więc wywołania:
generate(new ArrayList<CsvReport>()); generate(new ArrayList<PdfReport>()); generate(new ArrayList<Report>());
Ok. A co gdybyśmy chcieli w metodzie generate() dodatkowo dodać do przekazanej listy raportów zbiorowy raport GeneralReport, który również dziedziczy po bazowej klasie Report:
void generate(List<? extends Report> reports) { for(Report report : reports) { report.generate(); } reports.add(new GeneralReport()); // błąd }
Zależy nam, aby kompilator pilnował typów. Przekazujemy do metody generate listę raportów. Jakichś raportów. Lecimy w pętli po liście _jakichś_ (csv, pdf, Bóg jeden wie jakich) raportów i wykonujemy na nich ich własną implementację metody generate(). Patrząc na kod powyższej metody, nie możemy stwierdzić jakiego dokładnie typu raporty do niej trafią.
Czy będziesz mógł zatem spokojnie spać próbując do przekazanej listy raportów dodać raport typu GeneralReport. Kompilator nie może na to pozwolić, bo istnieje możliwość, że do metody przekazano listę raportów konkretnego typu, np.: List<CsvReport>. Próba dodania do tej listy raportu dowolnie innego typu nie może się więc powieść.
Podobny problem będziemy mieć jeśli zrobimy coś takiego:
void generate(List<? extends Report> reports) { for(Report report : reports) { report.generate(); CsvReport a = (CsvReport)report; } }
Powyżej próbujemy przypisać raport do referencji typu CsvReport. Rzutujemy w ciemno, nie mając pojęcia jakiego typu raporty znajdują się na liście. Dlatego w run-time’ie dostaniemy prawdopodobnie ClassCastException (no chyba, że szczęśliwie do metody zostanie przekazana lista raportów CSV, to nam się poszczęści i kod metody nie rzuci wyjątku).
Inny przykład zaczerpnięty z książki Java Generics and Collections:
List<Integer> ints = new ArrayList<Integer>(); ints.add(1); ints.add(2); List<? extends Number> nums = ints;
Wszystkie powyższe linie są akceptowane przez kompilator. Nic się nie świeci na czerwono. Co dokładnie oznacza to <? extends Number>. Mówi nam na co może wskazywać referencja nums. Otóż może wskazywać na dowolną implementację listy zawierającą elementy będące dowolnym typem dziedziczącym po klasie Number. Inaczej mówiąc, możemy na przykład przypisać do referencji nums:
nums = new ArrayList<Integer>(); // bo ArrayList<E> implements List<E> nums = new Vector<Double>(); // bo Vector<E> implements List<E>
Poniżej jeszcze inny przykład obrazujący znaczenie <? extends Number>.
void myAdd(List<? extends Number> nums) { nums.add(2.3); //błąd }
W powyższym przykładzie kompilator nie pozwoli nam dodać do tej listy żadnych elementów. Próbujemy dodać liczbę 2.3. A co jeśli do metody przekazano ArrayList<Integer>? Do listy liczb całkowitych nie możemy dodać przecież 2.3, które typu Integer z pewnością nie jest. Dlatego wywołanie metody add na liście nums zostanie zgłoszone przez kompilator jako błędne.
A czy możemy czytać z takiej listy? Oszem. Co by nie było, to wiemy na 100%, że ta lista zawiera typy dziedziczące po Number. Więc:
List<Integer> ints = new ArrayList<Integer>(); ints.add(1); ints.add(2); List<? extends Number> nums = ints; List<Double> doubles = new ArrayList<Double>(); // zmieniamy wskazanie listy, tym razem wskazuje na listę z liczbami Double nums = doubles; Number i = nums.get(0);
Ostatnia linia zawsze będzie dozwolona. Zarówno Double jak i Integer są podtypami klasy Number, więc nie podejmujemy żadnego ryzyka przypisując do niej jakąkolwiek wartość z listy nums. Gwarancję daje nam właśnie <? extends Number> – w tej liście nie będzie nic “wyżej” niż element typu Number (nie znajdzie się tam typ Object).
Widzimy więc, że tam gdzie używamy znaku wildcard, czyli ?, możemy czytać dane. Kompilator zabroni nam natomiast zapisywać, abyśmy nie zrobili sobie krzywdy.
O parametrze ? parę słów jeszcze na końcu posta.
? + super
void attachGeneralReport(List<? super Report> reports) { GeneralReport gr = GeneralReport.generate(reports); reports.add(gr); }
Sygnatura powyższej metody zawierająca słówko kluczowe super pozwala przekazać do metody argument, który jest listą dowolnego typu będącego supertypem dla klasy Report lub jest listą elementów typu Report, czyli:
List<Report> oraz List<Object>
Możemy wykonać metodę add na liście reports ponieważ mamy pewność, że to, co przekażemy do add będzie typu Report lub typu Object. Z definicji polimorfizmu wiemy, że GeneralReport IS A (jest) raportem (Report) ponieważ dziedziczy po nim. Tak samo jest z relacją względem klasy Object.
Inny przykład:
void myAdd(List<? super Number> nums) { nums.add(1); nums.add(2.3); }
Powyżej metoda myAdd gdzie użyliśmy konstruktu super. Do listy nums możemy dodać zarówno liczbę 1 (całkowitą) jak i 2.3 (niecałkowitą). Dlaczego? Bo wiemy, że do metody myAdd zostanie przekazana lista elementów, które będą przynajmniej typu Number np.:
List<Number> nums = new ArrayList(); nums.add(10); nums.add(10.5); myAdd(nums); List<Object> nums2 = new ArrayList(); nums2.add(20); nums2.add(30.5); myAdd(nums2);
Patrząc na kod poniższej metody attachGeneralReport nie wiemy czy wpadnie do niej lista raportów czy lista elementów typu Object. Oba te warianty są możliwe. Załóżmy, że przekazaliśmy najpierw listę elementów typu Object:
attachGeneralReport(new ArrayList<Object>()); void attachGeneralReport(List<? super Report> reports) { GeneralReport gr = GeneralReport.generate(reports); reports.add(new Object()); // błąd }
Na chłopski rozum, skoro przekazaliśmy listę elementów typu Obiekt to powinniśmy móc zrobić:
reports.add(new Object());
Kompilator jednak nie dopuści do tego. No bo co jeśli przekazaliśmy jednak listę elementów typu Report, czyli:
attachGeneralReport(new ArrayList<Report>());
Do listy zawierającej elementy typu Report nie możemy dodać elementu będącego jego supertypem (czyli Object). Gdyby dodanie elementu typu Object do ArrayList<Report> byłoby możliwe, to nie zrobimy potem:
Report report = reports.get(0);
Przecież pod indeksem 0 może być właśnie ten element typu Object. Powyższa instrukcja byłaby równoważna z poniższym zapisem. A supertypu do podtypu przypisać nie możemy:
Report report = new Object(); // błąd
Za dużo tego, T, ?, extends, super, czym to się różni
[1] void test(List<Object> list) {} [2] void test(List list) {} [3] void test(List<?> list) {} [4] <T> void test(List<T> list) {}
[1] void test(List<Object> list) {}
void test(List<Object> list) {}
Co możemy przekazać do metody test. Pamiętamy definicję zasady podstawienia, więc możemy przekazać do tej metody każdą klasę, która implementuje interfejs List<E> i zawiera elementy typu E, czyli w tym przypadku Object.
List<Object> v1 = new Vector(); List<Object> v2 = new LinkedList<>(); test(v1); test(v2); // A czy możemy zmienić wskazanie powyższej referencji 'v' na dowolny obiekt? Spróbujmy: List<String> stringList = new ArrayList<String>(); v1 = stringList; // błąd, uff, kompilator znowu ratuje nas
[2] void test(List list) {}
Dlaczego metoda nie może po prostu wyglądać tak:
void test(List list) {}
Generalnie to może. Nikt niczego nie zabrania. 🙂 Jednak do powyższej metody możesz przekazać już np.:
List<String> stringList = new ArrayList<>(); test(stringList); // OK
Wyobraź sobie, że korzystasz z jakiejś biblioteki, która oddaje Ci do użytku metodę np.: execute(List list). Nie znamy jej implementacji. Co tam przekazać? List<Integer>? List<String>? Wolałbym jednak dostać do użytku metodę execute(List<Object> list) – znane są mi wówczas intencje autora metody. Jako użytkownik nie muszą się zastanawiać co przekazać do niej i czy przekazana lista nie spowoduje wykrzaczenia się implementacji metody execute. Skoro sygnatura narzuca mi List<Object> to wiem, że jest to po coś. Czuję się teraz lepiej.
A co by było gdyby ta metoda wyglądała tak:
List execute(List list)
Jak to ustrojstwo wywołać?
List result = execute(...);
Jakie elementy zawiera ta zwrócona lista? Stringi, Integery, Bóg wie co.
Nie ma to jednak jak:
List<String> execute(List<Object> list) {} List<String> result = execute(...);
Od razu lepiej!
A czy…. możemy zmienić wskazanie poniższej referencji vInts na dowolny obiekt? Spróbujmy:
List vInts = new Vector(); List<String> stringList = new ArrayList<String>(); v = stringList; // OK! Kompilator już nie jest w stanie nas poratować, mimo że referencja 'vInts' według naszych wyimaginowanych założeń nie może wskazywać na listę stringów!
[3] void test(List<?> list) {}
To może tak! No też fajnie. Nie jest to już void test(List list) gdzie możemy dodać do listy element dowolnego typu, tzn.:
list.add("jakiś string"); list.add(123); list.add(new Object());
Dodaliśmy masę elementów, a potem co… jak operować na takiej liście. Jaki jest cel w ogóle takiej listy? Każdy element innego typu. Próba pobrania elementu do konkretnego typu może znowu zakończyć się błędem w run-time’ie. Zero pożytku z kompilatora. Więc:
List<?> unknownElements = new ArrayList<>(); unknownElements.add(1); // błąd
Taka lista jest read-only. Kompilator pozwala nam zrzucić na niego odpowiedzialność pilnowania, żeby lista była tylko do odczytu.
Co więcej? A czemu miałbym na przykład użyć:
test(List<?> list)
zamiast
test(List<Object> list)
Załóżmy, że jakaś metoda np.: execute zwróciła List<String> i chciałbyś teraz wrzucić tę listę do metody test, która powiedzmy zapisuje obiekty do bazy danych, whatever. Czy możemy przekazać List<String> do metody, która przyjmuje jako parametr List<Object>? No niestety nie. A do metody, która przyjmuje jako parametr List<?> – owszem. Musimy pamiętać, że List<Object> nie jest supertypem dla wszystkich implementacji interfejsu List. Dodatkowo użycie List<?> w parametrze metody daje do zrozumienia użytkownikowi API, że autor udostępnia tę metodę dla list zawierających dowolny typ.
[4] <T> void test(List<T> list) {}
Podejrzewam, że nikt Ci nie powie, że gdzieś masz użyć ? zamiast T czy na odwrót. Wszystko zależy.
Czym się różni:
<T> void test(List<T> list) { for(Object o : list) { System.out.println(o); } }
od
void test(List<?> list) { for(Object o : list) { System.out.println(o); } }
Obie metody nie różnią się. Bytecode ten sam, kompilator też nie zwraca nam na nic uwagi.
A czy ze znakiem ? możesz zrobić coś takiego jak:
void ? test(List<?> list) { (...) }
No nie da rady. A z T?
<T> T test(List<T> list) { (…) }
Owszem, da radę.
Czy ze znakiem ? możesz zrobić coś takiego, a mianowicie upewnić się że elementy obu kolekcji są tego samego typu?
void test(List<?> list_1, List<?> list_2) {(…)}
Nie możesz. Powyższa sygnatura na pewno nam tego nie gwarantuje. Z parametrem T to będzie możliwe.
Jest jeszcze kilka różnic między możliwościami parametru T oraz znaku wildcard. Ciężko tu mówić o przewadze jednego nad drugim, ponieważ obu parametrów używa się w zależności od kontekstu i od potrzeb.
Czyli tak…
Wprowadzenie typów ogólnych pozwala nam się cieszyć z wykrywania błędów już w czasie kompilacji. Dzięki czemu, nie będziemy zaskoczeni, że w czasie działania programu dostajemy na twarz wyjątek. Co więcej, gdy używamy API jakiejś biblioteki, dzięki typom ogólnym znane będą nam intencje autora. Fajnie, gdy IDE nam podpowie (albo wyczytamy w javadocach API), że metoda metodaJakiejśBiblioteki przyjmuje List<?> params a nie List params. Wbrew pozorom jest to istotna i przydatna informacja.
dobre
Miło mi, dzięki. W razie pytań pisz śmiało, me@wprostychslowach.pl