1.4-Software-Development-Principles

Generics advanced

Leerdoelen:

Voorwaarde: Je moet bekend zijn met het gebruik van generics in het algemeen.

Static methods en generieke types

Bij het aanmaken van static methods wil je vaak een generiek type kunnen accepteren:

public static <Q> void myStaticMethod(List<Q> list) {
  // ... do something with the list.
}

Wat hier opvalt is de vreemde plaatsing van het generieke type <Q>, ergens tussen public static and void. Dit betekent dat deze methode ergens gebruik maakt van het generieke type Q in de definitie die volgt.

Een tweede belangrijk punt: deze static method kan gedefinieerd zijn in een klasse die een generiek type accepteert, maar daar niets mee van doen hoeft. Een completer voorbeeld staat hieronder:

public class MyGenericClass<T> {
  private List<T> list;
  // insert additional stuff here.
  
  public static <Q> void myStaticMethod(List<Q> list) {
    // ... do something with the list.	  
  }
}

De static method kan gebruikt worden binnen de klasse zelf, maar ook van buitenaf. Daarom moet je altijd een ander generiek type (= andere type parameter) meegeven als je zo’n static method definieert.

De klasse waarin de methdoe gedefinieerd wordt, hoeft niet eens generic te zijn:

public class MyUtilityClass {
  // insert additional stuff here.
  
  public static <Q> void myStaticMethod(List<Q> list) {
    // ... do something with the list.	  
  }
}

Wildcard types

Met andere woorden: Het maakt me echt niet uit wat er in de lijst wordt opgeslagen. Zeker weten? Dit komt niet vaak voor, maar het kan…

Bekijk deze functie eens:

public String toString(ArrayList<?> myList) { 
	/* ... */ 
}

Deze functie zou alle elementen in de lijst op een nette manier moeten omzetten in een enkele String.

Deze syntax, met het ?, is toegestaan. Daarmee zeg je effectief: “Ik accepteer elke lijst, het maakt me niet uit welk type de elementen hebben.

Op deze manier worden alle elementen toegewezen aan Object:

Object item = myList.get(i);

Omdat alle referentietypes in Java overerven van Object werkt dat. Middels polymorfisme kunnen we zelfs de toString() aanroepen op elk element en een correcte representatie krijgen. Dit terwijl deze functie het actuele type niet hoeft te weten!

Als de functie op onderstaande manier gedefinieerd zou zijn, waren er problemen geweest:

public static String toString(List<Object> myList) { ... }

Dit vereist dat de lijst daadwerkelijk objecten bevat, Dus een lijst van Strings, personen of een ander type zou niet werken…

Generieke types inperken

Soms willen we de mogelijkheid hebben om een bepaald soort persoon op te slaan, zie onderstaand diagram:

classDiagram
    Person <|-- Student
    Person <|-- Teacher

We kunnen nu natuurlijk een lijst gebruiken die elk van deze subclasses kan opslaan: List<Teacher> or List<Student>.

Het enige alternatief dat we tot nu toe gezien hebben om beide subclasses op te slaan, is List<Person>.

Bekijk deze definitie:

class MyClass<T extends Person> {
	private List<T> list;
	public T getPerson(int index);
}

We kunnen deze klasse op drie verschillende manieren instantiëren:

De restrictie T extends Person staat je niet toe om zoiets te doen: new MyClass<Chicken> omdat Chicken zeer waarschijnlijk niet overerft van Person.

Deze klasse kan werken met of alleen Students of alleen Teachers. De mogelijkheid om met alle mogelijke soorten personen te werken blijft ook beschikbaar.

Wildcards inperken

<? extends Person>

Deze situatie wordt gebruikt wanneer je geen mening hebt over het precieze type, aangezien je geen gebruik zult maken van methoden die aangeboden worden door de extensies van de Person klasse. (In dit geval.)

Merk op: Als je op enig moment geneigd bent om instance of te gebruiken in je implementatie, dan had je blijkbaar geen generics moeten gebruiken.

Het verschild met gewoon een <?> is dat in dit geval alle Person methoden beschikbaar zijn, niet alleen de methoden uit Object.

De gebruiker van deze methode kan nu een instantie aanbieden van Person of een afgeleide daarvan, maar nooit een String or een ander referentie type.

Verwijzen naar eerder genoemde types

Deze constructie wordt gebruikt wanneer je in je code gebruikt maakt van een definitie die afhankelijk is van een ander type.

Verwijzen naar “jezelf”

<T extends Comparable<T>>

In dit voorbeeld accepteren we ieder type T, zolang dat type maar vergeleken kan worden met anderen van hetzelfde type. Het moet dus de compareTo(T other) methode implementeren.

Om het type Person te accepteren moet deze uitwerking gebruikt worden:

public class Person implements Comparable<Person> {
    // ...
    @Override
    public boolean compareTo(Person other) {
        // ...
    }
}

Zonder deze implementatie van de Comparable<Person> interface, mag Person niet gebruikt worden als een type voor T in deze situatie.

Verwijzen naar een eerder type

Deze constructie wordt gebruik wanneer de implementatie twee types wil gebruiken waarbij de ene afhankelijk is van de andere:

<T, L extends Iterable<T>>

Dit voorbeeld verwacht een willekeurig type T en een tweede type dat de Iterable<T> interface implementeerd. Ik kan bijvoorbeeld deze generieke methode schrijven:

public static <T, L extends Iterable<T>> boolean contains(T needle, L haystack) {
    for (T item:haystack) {
        if (item.equals(needle)) {
            return true;
        }
    }
    return false;
}

Merk op dat ook dit voorbeeld weer wat ver gezocht is, aangezien het ook makkelijker kan:

public static boolean contains(Object obj, Iterable<?> iterable) {
	// ...
}

En we hebben geleerd dat een makkelijke oplossing (bijna) altijd beter is.