Wildcards
Sissejuhatus
Geneeriliste tüüpidega tegeledes on sageli vaja kasutada mitte üht konkreetset parameetriseeritud tüüpi, vaid tervet hulka võimalikke variante.
Näiteks meetod, mis prindib välja kõik listi elemendid, peaks töötama nii List<String>, List<Integer> kui ka List<Person> korral - sisuliselt iga listiga.
Tüübiparameetrid (näiteks <T>) aitavad seda lahendada meetodite defineerimisel.
Mõnikord on aga vaja paindlikkust just seal, kus viidatakse konkreetsele parameetriseeritud tüübile, näiteks meetodi parameetri tüübis.
Wildcard mehhanism (ehk ? parameetri tüüp) on Javas selliste olukordade jaoks.
Sisuliselt need tähistavad „mingit tundmatut tüüpi“.
Geneerilised tüübid on invariantsed
Enne wildcard’ide juurde liikumist on oluline mõista, miks neid üldse vaja on.
Integer on klassi Number alamklass.
Sellest võiks järeldada, et ka List<Integer> on List<Number> alamtüüp, kuid see ei ole nii.
Java geneerilised tüübid on invariantsed: List<Integer> ja List<Number> on teineteisest täiesti sõltumatud tüübid, hoolimata sellest, et Integer on Number-i alamklass.
Seetõttu ei kompileeru järgmine kood:
void printAll(List<Number> list) { ... }
List<Integer> integers = List.of(1, 2, 3);
printAll(integers); // compile error — List<Integer> is not a List<Number>
See ei ole piirang, vaid teadlik ja tähtis disainiotsus.
Kui List<Integer> oleks omistatav tüübile List<Number>, saaks kirjutada järgmist koodi:
List<Integer> integers = new ArrayList<>();
List<Number> numbers = integers; // if this were allowed...
numbers.add(3.14); // ...you could add a Double into a List<Integer>
Integer first = integers.get(0); // ClassCastException at runtime
Kompilaator väldib selliseid vigu täielikult, käsitledes neid tüüpe omavahel seostamatuna.
Massivid käituvad teisiti.
Integer[] on Number[] alamtüüp, massiivid on kovariantsed.
See võib tunduda mugav, kuid viga nihkub nüüd kompileerimisajalt käitusajale:
Number[] numbers = new Integer[3]; // compiles
numbers[0] = 3.14; // ArrayStoreException at runtime
Geneeriliste tüüpide puhul valiti turvalisem reegel: viga tuvastatakse kompileerimisel, mitte alles programmi töötamise ajal.
Wildcard’id pakuvad paindlikkust mitme parameetriseeritud tüübi käsitlemisel, ilma kompileerimisaegset kontrolli rikkumata.
Piiramata wildcard <?>
Vahest ei ole oluline, mis tüüpi elemente mingi geneeriline struktuur sisaldab. Oluline on vaid see, et tegemist on näiteks kollektsiooni, konteineri või muu parameetriseeritud tüübiga. Kõige sagedamini kohtab seda olukorda kollektsioonidega. Näiteks kui soovime lihtsalt kõik elemendid välja printida või teada saada järjendi suurust, ei sõltu see elementide konkreetsest tüübist.
Sellistes olukordades ei sobi kasutada konkreetset tüüpi (List<String>) ega ka tüübiparameetrit (<T>), sest me ei defineeri uut geneerilist meetodit.
Vajame viisi, kuidas kompilaatorile märku anda, et “see on mingit tüüpi järjend, aga täpselt ei hooli, millist tüüpi”.
Selleks kasutatakse piiramata wildcard’i <?> (unbound wildcard).
Antud juhul List<?> tähendaks „mingit tundmatut tüüpi järjend".
Sellisest järjendist saab elemente lugeda (neid käsitletakse Object tüübina), kuid sinna ei saa midagi lisada, kuna kompilaator ei tea, milline tüüp oleks lisamiseks ohutu.
void printAll(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
Nüüd töötab meetod ükskõik millise järjendiga:
printAll(List.of(1, 2, 3)); // List<Integer>
printAll(List.of("a", "b", "c")); // List<String>
printAll(List.of(1.0, 2.5)); // List<Double>
Piiramata wildcard väljendab teadlikku otsust, et meetod ei sõltu elementide konkreetsest tüübist. Oluline on vaid see, et tegemist on mingit tüüpi kollektsiooniga. Kuna tüüpi pole täpsustatud, ei saa elemente turvaliselt lisada, kuid lugemine on alati lubatud. Just see piirang hoiab tüübisüsteemi turvalisena. Sama põhimõte kehtib ka teiste geneeriliste tüüpide puhul, mitte ainult järjendite korral.
Kui aga on vaja elemente mitte ainult lugeda, vaid ka täpsemalt piirata, milliseid tüüpe lubatakse, tulevad mängu piiratud wildcard’id — ? extends ja ? super.
Ülemise piiranguga wildcard <? extends T>
List<? extends Number> tähendab järjend mingist tüübist, mis laiendab Number-it".
See võib näiteks olla List<Integer>, List<Double>, List<Long> jne.
Oluline on, et täpset tüüpi me ei tea, teame vaid seda, et iga element on vähemalt Number tüüpi.
Seetõttu on lugemine turvaline, kuna kõiki elemente saab käsitleda Number-ina.
Lisamine siiski ei ole lubatud.
Kui antud järjend on tegelikult näiteks List<Integer>, ei tohiks sinna lisada Double tüüpi arve.
Kuna kompilaator ei tea täpset tüüpi, keelab ta lisamise täielikult.
Näiteks:
double sum(List<? extends Number> list) {
double total = 0;
for (Number n : list) {
total += n.doubleValue(); // safe — every element is at least a Number
}
return total;
}
System.out.println(sum(List.of(1, 2, 3))); // 6.0 (List<Integer>)
System.out.println(sum(List.of(1.5, 2.5, 3.0))); // 7.0 (List<Double>)
<? extends T> on sobilik olukorras, kus on vaja struktuurist lugeda väärtusi ja käsitleda neid tüübina T, lubades samal ajal ka T alamklasse.
Alumise piiranguga wildcard <? super T>
List<? super Integer> tähendab „järjendit mingist tüübist, mis on Integer või selle ülemklass“.
See võib olla näiteks List<Integer>, List<Number> või List<Object>.
Erinevalt <? extends T>-ist on siin rõhk kirjutamisel.
Kuna me teame, et järjendi elementide tüüp on vähemalt Integer või sellest midagi üldisemat, on Integer-ide lisamine alati turvaline.
Lugemine on seevastu piiratud, kuna kompilaator ei tea, kas list on List<Integer>, List<Number> või List<Object> tüüpi.
Sellest tulenevalt saab elemente käsitleda vaid kui Object-ina.
Näiteks:
void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 5; i++) {
list.add(i); // safe — the list accepts at least Integer
}
}
List<Number> numbers = new ArrayList<>();
addNumbers(numbers); // works — List<Number> accepts integers
List<Object> objects = new ArrayList<>();
addNumbers(objects); // also works — List<Object> accepts anything
<? super T> on sobilik olukorras, kus on vaja struktuuri kirjutada T tüüpi väärtusi, lubades samal ajal ka T-st üldisemaid tüüpe.
Millist wildcard'i kasutada?
Kui wildcard’id on selged, tekib järgmine küsimus: millal kasutada extends ja millal super?
Erinevus ei ole süntaksis, vaid selles, mida sa parameetriga teed.
Kas sa loed sellest väärtusi?
Või kirjutad sinna midagi juurde?
Hea meelespea wildcard’ide kasutamisel on PECS ehk Producer Extends, Consumer Super.
Sisuliselt see tähendab, et:
- Kui parameeter toodab väärtusi, mida sa loed, kasuta
<? extends T>. - Kui parameeter tarbib väärtusi, mida sa sinna kirjutad, kasuta
<? super T>. - Kui sul on vaja nii lugeda kui ka kirjutada, kasuta täpset tüüpi (
<T>), mitte wildcard’i.
Näide, kus üks list toodab ja teine tarbib:
// Reading from source — source produces values
void copy(List<? extends Number> source, List<? super Number> destination) {
for (Number n : source) {
destination.add(n);
}
}
Antud näites:
sourceon producer - kasutameextendsdestinationon consumer - kasutamesuper
Kokkuvõtteks:
| Tüüp | Saab lugeda | Saab kirjutada |
|---|---|---|
<?> | Jah (ainult Object-ina) | Ei |
<? extends T> | Jah (kui T) | Ei |
<? super T> | Ei (ainult Object-ina) | Jah (T tüüpi väärtusi) |
PECS-mnemoonika on pärit Joshua Bloch'i raamatust Effective Java, mis on üks mõjukamaid Java-teemalisi teoseid. Antud raamat on laialdaselt tunnustatud ning pakub põhjalikku ülevaadet Java headest tavadest. Sellest tulenevalt on tungivalt soovituslik see läbi lugeda.
Wildcard’id vs tüübiparameetrid
Wildcard’id ja tüübiparameetrid mõlemad lisavad paindlikkust, kuid nende roll on erinev. Oluline erinevus on, kas sul on vaja tüüpi nimetada ja seostada, või lihtsalt öelda, et tegemist on mingi sobiva tüübiga?
Tüübiparameeter (<T>) deklareeritakse definitsiooni juures ja annab tüübile nime, mida saab meetodi või klassi sees korduvalt kasutada.
Wildcard (?) seevastu esineb kasutuskohas ning tähendab: „mingi tüüp, mida ma ei pea nimepidi tundma“.
Millal kasutada tüübiparameetrit?
Kasuta tüübiparameetrit siis, kui sama tüüp peab esinema mitmes kohas ja olema omavahel seotud. Näiteks nii parameetris kui ka tagastusväärtuses:
// Type parameter — the return type is the same T as the input
<T> T firstOrDefault(List<T> list, T defaultValue) {
return list.isEmpty() ? defaultValue : list.get(0);
}
Siin on oluline, et defaultValue ja tagastusväärtus oleksid täpselt sama tüüpi T.
Millal kasutada wildcard'i?
Kasuta wildcard’i siis, kui sul ei ole vaja tüüpi nimepidi kasutada. Oluline on vaid piirang või üldine sobivus.
// Wildcard — you only read elements, you do not need to name the type
void printAll(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
Siin ei ole vaja teada, mis tüüpi elemendid täpselt on.
Sarnane eesmärk, erinev tähendus
Mõnikord saab sama eesmärki väljendada nii tüübiparameetri kui ka piiratud wildcard’iga:
// Type parameter version — T is fixed for the call; all elements are of that same type T
public <T extends Number> double sum(List<T> list) {
double total = 0;
for (T each : list) {
total += each.doubleValue();
}
return total;
}
// Wildcard version — simpler when you do not need to name the element type
public double sum(List<? extends Number> list) {
double total = 0;
for (Number each : list) {
total += each.doubleValue();
}
return total;
}
Mõlemad variandid aktsepteerivad List<Integer>, List<Double> jne.
Wildcard’i versioon on lihtsam siis, kui elementide täpset tüüpi ei ole vaja mujal kasutada.
Tüübiparameeter on vajalik siis, kui sama tüüp peab esinema mitmes kohas (näiteks tagastusväärtuses või mitmes parameetris).
List<?> ja List<Object> ei ole võrdväärsed.
List<Object>võib sisaldada suvalisi objekte ja sinna võib vabalt väärtusi lisada.List<?>on tundmatut tüüpi list. Sinna ei saa midagi lisada (välja arvatudnull-väärtust), sest kompilaator ei saa kontrollida, kas lisatav tüüp on sobiv.