Lijn 3 OK: yourPlace heeft type Vehicle, en Bike erft over van Vehicle(de naam van de variabele is wel slecht gekozen)
Lijn 4 OK: ArrayList<Vehicle> (hier ArrayList<>) is een subtype van Collection<Vehicle>
Lijn 5 OK: vehicles kan om het even welk Vehicle bevatten, dus ook een Motorized (type van myCar)
Lijn 6 OK: ArrayList<Car> is een subtype van List<Car>
Lijn 7 niet OK: List<Car> is geen subtype van List<Vehicles>
Lijn 8 OK: ArrayList<Car> is een subtype van List<? extends Motorized>
Lijn 9 niet OK: motorized is een lijst van een onbekend type gemotoriseerde voertuigen; we kunnen daar geen auto aan toevoegen.
Covariantie en contravariantie 1
Maak een schema met de overervingsrelaties tussen
List<Animal>
List<? super Animal>
List<? extends Animal>
List<Cat>
List<? extends Cat>
List<? super Cat>
Antwoord
graph BT
LA["List#lt;Animal>"]
LSA["List#lt;? super Animal>"]
LEA["List#lt;? extends Animal>"]
LC["List#lt;Cat>"]
LSC["List#lt;? super Cat>"]
LEC["List#lt;? extends Cat>"]
LA --> LSA
LA --> LSC
LA --> LEA
LC --> LSC
LC --> LEC
LC --> LEA
LSA --> LSC
LEC --> LEA
In onderstaande figuur worden de verschillende types weergegeven door verzamelingen: een deelverzameling betekent dat het binnenste type overerft van het buitenste.
Covariantie en contravariantie 2
Maak een schema met de overervingsrelaties tussen
List<Cat>
ArrayList<Cat>
List<? super Cat>
ArrayList<? super Cat>
List<? extends Cat>
ArrayList<? extends Cat>
Antwoord
graph BT
LC["List#lt;Cat>"]
LSC["List#lt;? super Cat>"]
LEC["List#lt;? extends Cat>"]
AC["ArrayList#lt;Cat>"]
ASC["ArrayList#lt;? super Cat>"]
AEC["ArrayList#lt;? extends Cat>"]
AC --> LC
AC --> ASC
AC --> LSC
AC --> AEC
AC --> LEC
LC --> LSC
LC --> LEC
AEC --> LEC
ASC --> LSC
In onderstaande figuur worden de verschillende types weergegeven door verzamelingen: een deelverzameling betekent dat het binnenste type overerft van het buitenste.
Repository
Schrijf een generische klasse Repository die een repository van objecten voorstelt. De objecten hebben ook een ID. Zowel het type van objecten als het type van de ID moeten generische parameters zijn.
Definieer en implementeer volgende methodes (maak gebruik van een ArrayList):
add(id, obj): toevoegen van een object
findById(id): opvragen van een object aan de hand van de id
findAll(): opvragen van alle objecten in de repository
update(id, obj): vervangen van een object met gegeven id door het meegegeven object
remove(id): verwijderen van een object aan de hand van een id
Shop
Maak een klasse Shop die een winkel voorstelt die items (subklasse van StockItem) aankoopt.
Af en toe wordt er een inventaris opgemaakt van alle items die op stock zijn.
Een Shop-object wordt geparametriseerd met het type items dat aangekocht kan worden.
We beschouwen hier Fruit en Electronics; daarmee kunnen we dus een fruitwinkel (Shop<Fruit>) en elektronica-winkel (Shop<Electronics>) maken.
De Shop-klasse heeft drie methodes:
buy, die een lijst van items toevoegt aan de stock;
addStockToInventory, die de lijst van items uit de stock toevoegt aan de meegegeven inventaris-lijst (de items blijven bewaard in de stock);
stockSize() die het huidig aantal items in de stock toevoegt.
Daarnaast maak je een statische methode merge waarmee je twee Shop-objecten kan samenvoegen tot een nieuw Shop-object.
De twee Shops die samengevoegd worden hoeven niet hetzelfde type items te hebben. Je kan bijvoorbeeld een fruitwinkel en electronicawinkel samenvoegen tot een supermarkt waar elk soort StockItem verkocht kan worden.
Na een merge is de stock verdwenen uit de oorspronkelijke shops en terechtgekomen in de nieuwe (samengevoegde) winkel.
In de startcode vind je reeds een implementatie van een abstracte klasse StockItem, Fruit, en subklassen Apple en Orange.
Daarnaast is er ook een abstracte klasse Electronics, met als subklasse Smartphone.
Zorg dat onderstaande code (ongewijzigd) compileert en dat de test slaagt:
Vul de types en generische parameters aan op de 7 genummerde plaatsen zodat onderstaande code en main-methode compileert (behalve de laatste regel van de main-methode) en voldaan is aan volgende voorwaarden:
Elk actie-type kan enkel uitgevoerd worden door een bepaald karakter-type. Bijvoorbeeld: een FightAction kan enkel uitgevoerd worden door een karakter dat CanFight implementeert.
doAction mag enkel opgeroepen worden met een actie die uitgevoerd kan worden door alle karakters in de meegegeven lijst.
Als er op een bepaalde plaats geen type of generische parameter nodig is, vul je $\emptyset$ in.
Verklaar je keuze voor de combinatie van (5), (6), en (7).
interfaceCharacter{}interfaceCanFightextendsCharacter{}recordWarrior()implementsCanFight{}recordKnight()implementsCanFight{}recordWizard()implementsCharacter{}// kan niet vechteninterfaceAction<___/* 1 */___>{voidexecute(___/* 2 */____character);}classFightActionimplementsAction<___/* 3 */_____>{@Overridepublicvoidexecute(___/* 4 */______character){System.out.println(character+" fights!");}}classGameEngine{public<___/* 5 */______>voiddoAction(List<___/* 6 */____>characters,Action<___/* 7 */____>action){for(varcharacter:characters){action.execute(character);}}}publicstaticvoidmain(String[]args){varengine=newGameEngine();Action<CanFight>fight=newFightAction();List<Warrior>warriors=List.of(newWarrior(),newWarrior());engine.doAction(warriors,fight);List<Wizard>wizards=List.of(newWizard());engine.doAction(wizards,fight);// deze regel mag NIET compileren}
Antwoord
1: C extends Character : acties kunnen enkel uitgevoerd worden door subtypes van Character
2: C: C is het type van Character dat de actie zal uitvoeren
3: CanFight: FightAction is enkel mogelijk voor characters die CanFight implementeren
4: CanFight: aangezien de generische paremeter C van superinterface Action geinitialiseerd werd met CanFight, moet hier ook CanFight gebruikt worden.
5: T extends Character: we noemen T het type van de objecten in de meegeven lijst; we hebben hier een begrenzing nodig (want we willen enkel subtypes van Character toelaten)
6: T: lijst van T’s, zoals verondersteld in 5
7: ? super T: de meegegeven actie moet een actie zijn die door alle T’s uitgevoerd kan worden (dus door T of een van de supertypes van T).
Redenering met behulp van PECS: de meegegeven actie gebruikt/consumeert het character, dus super.
Alternatieve keuze voor 5/6/7:
5: T extends Character: we noemen T het type dat bij de actie hoort; we hebben hier een begrenzing nodig (want we willen enkel subtypes van Character toelaten)
6: ? extends T: lijst van T’s of subtypes ervan.
Redenering met behulp van PECS: de lijst levert/produceert de characters, dus extends.
7: T: het type van de actie, zoals verondersteld in 5
Extra oefeningen
SuccessOrFail
Schrijf een generische klasse (of record) SuccessOrFail die een object voorstelt dat precies één element bevat.
Dat element heeft 1 van 2 mogelijke types (die types zijn generische parameters).
Het eerste type stelt het type van een succesvol resultaat voor; het tweede type is dat van een fout.
Je kan objecten enkel aanmaken via de statische methodes success en fail.
Een voorbeeld van tests voor die klasse vind je hieronder:
@Testpublicvoidsuccess(){SuccessOrFail<String,Exception>result=SuccessOrFail.success("This is the result");assertThat(result.isSuccess()).isTrue();assertThat(result.successValue()).isEqualTo("This is the result");}@Testpublicvoidfailure(){SuccessOrFail<String,Exception>result=SuccessOrFail.fail(newIllegalStateException());assertThat(result.isSuccess()).isFalse();assertThat(result.failValue()).isInstanceOf(IllegalStateException.class);}
Functie compositie
Java bevat een ingebouwde interface java.util.function.Function<T, R>, wat een functie voorstelt met één parameter van type T, en een resultaat van type R. Deze interface voorziet 1 methode R apply(T value) om de functie uit te voeren.
Schrijf nu een generische methode compose die twee functie-objecten als parameters heeft, en als resultaat een nieuwe functie teruggeeft die de compositie voorstelt: eerst wordt de eerste functie uitgevoerd, en dan wordt de tweede functie uitgevoerd op het resultaat van de eerste.
Dus: voor functies
Function<A,B>f1=...Function<B,C>f2=...
moet compose(f1, f2) een Function<A, C> teruggeven, die als resultaat f2.apply(f1.apply(a)) teruggeeft.
Pas de PECS-regel toe om ook functies te kunnen samenstellen die niet exact overeenkomen qua type.
Bijvoorbeeld, volgende code moet compileren en de test moet slagen:
Schrijf een generische klasse (of record) Maybe die een object voorstelt dat nul of één waarde van een bepaald type kan bevatten.
Dat type wordt bepaald door een generische parameter. Je kan Maybe-objecten enkel aanmaken via de statische methodes some en none.
Hieronder vind je twee tests:
Maak de print-methode hieronder ook generisch, zodat deze niet enkel werkt voor een Maybe<String> maar ook voor andere types dan String.
classMaybePrint{publicstaticvoidprint(Maybe<String>maybe){if(maybe.hasValue()){System.out.println("Contains a value: "+maybe.getValue());}else{System.out.println("No value :(");}}publicstaticvoidmain(String[]args){Maybe<String>maybeAString=Maybe.some("yes");Maybe<String>maybeAnotherString=Maybe.none();print(maybeAString);print(maybeAnotherString);}}
Voeg aan Maybe een generische methode map toe die een java.util.function.Function<T, R>-object als parameter heeft, en die een nieuw Maybe-object teruggeeft, met daarin het resultaat van de functie toegepast op het element als er een element is, of een leeg Maybe-object in het andere geval.
Zie de tests hieronder voor een voorbeeld van hoe deze map-functie gebruikt wordt:
(optioneel) Herschrijf Maybe als een sealed interface met twee record-subklassen None en Some.
Geef een voorbeeld van hoe je deze klasse gebruikt met pattern matching.
Kan je ervoor zorgen dat je getValue() nooit kan oproepen als er geen waarde is (compiler error)?
Info
Java bevat een ingebouwd type gelijkaardig aan de Maybe-klasse uit deze oefening, namelijk Optional<T>.
Animal food
Dit is een uitdagende oefening, voor als je je kennis over generics echt wil testen.
Voeg generics (met grenzen/bounds) toe aan de code hieronder, zodat de code (behalve de laatste regel) compileert,
en de compiler enkel toelaat om kattenvoer te geven aan katten, en hondenvoer aan honden:
publicclassAnimalFood{staticclassAnimal{publicvoideat(Foodfood){System.out.println(this.getClass().getSimpleName()+" says 'Yummie!'");}}staticclassMammalextendsAnimal{publicvoiddrink(Milkmilk){this.eat(milk);}}staticclassCatextendsMammal{}staticclassKittenextendsCat{}staticclassDogextendsMammal{}staticclassFood{}staticclassMilkextendsFood{}staticclassMain{publicstaticvoidmain(String[]args){FoodcatFood=newFood();MilkcatMilk=newMilk();FooddogFood=newFood();MilkdogMilk=newMilk();Catcat=newCat();Dogdog=newDog();Kittenkitten=newKitten();cat.eat(catFood);// OK 👍cat.drink(catMilk);// OK 👍dog.eat(dogFood);// OK 👍dog.drink(dogMilk);// OK 👍kitten.eat(catFood);// OK 👍kitten.drink(catMilk);// OK 👍cat.eat(dogFood);// <- moet een compiler error geven! ❌kitten.eat(dogFood);// <- moet een compiler error geven! ❌kitten.drink(dogMilk);// <- moet een compiler error geven! ❌}}}
(Hint: Begin met het type Food te parametriseren met een generische parameter die het Animal-type voorstelt dat dit voedsel eet.)
Self-type
Dit is een uitdagende oefening, voor als je je kennis over generics echt wil testen.
Heb je je al eens afgevraagd hoe assertThat(obj) uit AssertJ werkt?
Afhankelijk van het type van obj dat je meegeeft, worden er andere assertions beschikbaar die door de compiler aanvaard worden:
// een List<String>List<String>someListOfStrings=List.of("hello","there","how","are","you");assertThat(someListOfStrings).isNotNull().hasSize(5).containsItem("hello");// een StringStringsomeString="hello";assertThat(someString).isNotNull().isEqualToIgnoringCase("hello");// een IntegerintsomeInteger=4;assertThat(someInteger).isNotNull().isGreaterThan(4);assertThat(someInteger).isNotNull().isEqualToIgnoringCase("hello");//<=compileertniet❌
Sommige assertions (zoals isNotNull) zijn echter generiek, en wil je slechts op 1 plaats implementeren.
Probeer zelf een assertThat-methode te schrijven die werkt zoals bovenstaande, maar waar isNotNull slechts op 1 plaats geïmplementeerd is.
De assertThat-methode moet een Assertion-object teruggeven, waarop de verschillende methodes gedefinieerd zijn afhankelijk van het type dat meegegeven wordt aan assertThat.
Hint 1: maak verschillende klassen, bijvoorbeeld ListAssertion, StringAssertion, IntegerAssertion die de type-specifieke methodes bevatten. Begin met isNotNull toe te voegen aan elk van die klassen (dus door de implementatie te kopiëren).
Hint 2: in een zogenaamde ‘fluent interface’ geeft elke operatie zoals isNotNull en hasSize het this-object op het einde terug (return this), zodat je oproepen na elkaar kan doen. Bijvoorbeeld .isNotNull().hasSize(5).
Hint 3: maak nu een abstracte klasse GenericAssertion die isNotNull bevat, en waarvan de andere assertions overerven. Verwijder de andere implementaties van isNotNull.
Hint 4: In isNotNull is geen informatie beschikbaar over het type dat gebruikt moet worden als terugkeertype van isNotNull. assertThat(someString).isNotNull() moet bijvoorbeeld opnieuw een StringAssertion teruggeven. Dat kan je oplossen met generics, en een abstracte methode die het juiste object teruggeeft.
Hint 5: Je zal een zogenaamd ‘self-type’ moeten gebruiken. Dat is een generische parameter die wijst naar de (sub)klasse zelf.
Hint 6: op deze pagina wordt uitgelegd hoe AssertJ dit doet. Probeer eerst zelf, zonder dit te lezen!