Thursday, February 1, 2007

Generics (Dutch)

Introductie

Generics, oftwel Generieken, die in Java 5.0 is geintroduceerd slaat op de harde typering van de gebruikte objecten in collecties, mappen, klassen en methoden. Het gebruik hiervan is niet verplicht, maar het kan uiteindelijk erg handig zijn. Stel je een ArrayList voor dat (volgens jouw code!) alleen maar Integer objecten kan bevatten:

package test;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class Test {

    public static void main(String[] args) {

        // Declareer en initialiseer een algemene ArrayList.
        List arrayList = new ArrayList();

        // Voeg de Integer waarden toe.
        arrayList.add(new Integer("1"));
        arrayList.add(new Integer("2"));
        arrayList.add(new Integer("3"));

        // Doorloop de waarden.
        for (Iterator iter = arrayList.iterator(); iter.hasNext();) {

            // iter.next() retourneert een Object, je moet deze casten naar Integer.
            Integer integer = (Integer) iter.next();

            // Laat de waarde zien.
            System.out.println(integer);
        }
    }

}

1
2
3

Stel dat je al dan niet onbewust een String object aan de arrayList toevoegt, dan zou die cast naar Integer problemen opleveren: er wordt een ClassCastException afgeworpen. Een String kan namelijk niet gecast worden naar een Integer.

package test;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class Test {

    public static void main(String[] args) {

        // Declareer en initialiseer een algemene ArrayList.
        List arrayList = new ArrayList();

        // Voeg de Integer waarden toe.
        arrayList.add(new Integer("1"));
        arrayList.add(new Integer("2"));
        arrayList.add("3"); // Let op: String ipv Integer.

        // Doorloop de waarden.
        for (Iterator iter = arrayList.iterator(); iter.hasNext();) {

            // iter.next() retourneert een Object, je moet deze casten naar Integer.
            Integer integer = (Integer) iter.next();

            // Laat de waarde zien.
            System.out.println(integer);
        }
    }

}

1
2
Exception in thread "main" java.lang.ClassCastException: java.lang.String
    at test.Test.main(Test.java:23)

De foutmelding spreekt voor zich: je kunt een object van het type java.lang.String niet casten naar een Integer.

Met Generics kun je dit soort problemen dus vroeg afvangen door alvast in de code de ArrayList te aanmerken als een lijst dat alleen maar objecten van het Integer type mag bevatten. De Java compiler zal dat detecteren en zal indien nodig alvast een compilatie fout melden wanneer je een object van het verkeerde type aan de lijst toevoegt. Dit gebeurt in principe dus vóórdat je de code kunt uitvoeren.

Terug naar boven

Generieke collecties

Wanneer je Generics wilt toepassen in collecties, dan moet je ze tussen de vishaken toevoegen aan de declaratie en instantiatie van het object. Hier heb je enkele voorbeelden:

List<Type> arrayList = new ArrayList<Type>();

Set<Type> treeSet = new TreeSet<Type>();

Het Type moet minimaal overeenkomen met het object type van de inhoud van de collectie. Minimaal, dus je kunt ook de supertype opgeven. In het geval van een Integer kun je dus gewoon Integer opgeven, of diens superklasse Number of zelfs heel drastisch de hoofdklasse Object. Wanneer je Integer opgeeft, dan kun je alleen objecten van dezelfde type of objecten van diens (op het moment niet-bestaande) subklasse in de collectie zetten. In het geval van Number kun je dus alleen objecten van de types Number, Integer, Long, Short, etc in de collectie stoppen. De officiële subklassen van Number kun je in de API documentatie vinden onder "Direct Known Subclasses". Wanneer je de type Object opgeeft, dan kun je wel raden dat je élk mogelijk type in de collectie kunt stoppen, wat de collectie eigenlijk ongeneriek zal maken ;)

We passen de oorsronkelijke code even aan met de Generics:

package test;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class Test {

    public static void main(String[] args) {

        // Declareer en initialiseer een ArrayList voor Integers.
        List<Integer> integers = new ArrayList<Integer>();

        // Voeg de Integer waarden toe.
        integers.add(new Integer("1"));
        integers.add(new Integer("2"));
        integers.add(new Integer("3"));

        // Doorloop de waarden.
        for (Iterator<Integer> iter = integers.iterator(); iter.hasNext();) {

            // Dankzij de Generics is een cast niet nodig. De compiler weet in dit 
            // geval immers al van te voren uit welke object type de collectie bestaat.
            Integer integer = iter.next();

            // Laat de waarde zien.
            System.out.println(integer);
        }
    }

}

1
2
3

En nu proberen we een ordinaire String object aan de lijst te toevoegen:

        ...

        // Voeg de Integer waarden toe.
        integers.add(new Integer("1"));
        integers.add(new Integer("2"));
        integers.add("3"); // Let op: String ipv Integer.

        ...

The method add(Integer) in the type List<Integer> is not applicable for the arguments (String)

Deze code zal dus niet compileren en de bovenstaande compilatiefout teruggeven en deze code zal derhalve niet zomaar uitgevoerd kunnen worden. In een goede IDE, zoals Eclipse, krijg je ook een foutmelding te zien bij de add() methode.

Wanneer je de object type Number opgeeft, die zelf ook een tal subklassen heeft, dan kun je zonder problemen verschillende object typen van de superklasse Number in de collectie stoppen.

package test;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class Test {

    public static void main(String[] args) {

        // Declareer en initialiseer een ArrayList voor Numbers.
        List<Number> nummers = new ArrayList<Number>();

        // Voeg de Number waarden toe.
        nummers.add(new Integer("1"));
        nummers.add(new Long("2"));
        nummers.add(new Short("3"));

        // Doorloop de waarden.
        for (Iterator<Number> iter = nummers.iterator(); iter.hasNext();) {

            // Dankzij de Generics is een cast niet nodig. De compiler weet in dit 
            // geval immers al van te voren uit welke object type de collectie bestaat.
            Number nummer = iter.next();

            // Laat de waarde zien.
            System.out.println(nummer);
        }
    }

}

1
2
3

Sinds Java 5.0 is er naast de bestaande Iterator een nieuwe manier om de collectie te doorlopen, namelijk die met de for statement voor array's. We passen de bovenstaande code even hierop aan:

        ...

        // Doorloop de nummers.
        for (Number nummer : nummers) {

            // Laat de nummer zien.
            System.out.println(nummer);
        }

        ...

De in de for statement gedeclareerde type moet minimaal overeenkomen met het object type van de inhoud van de collectie. Je zou boven Number dus ook Object kunnen opgeven. Het is maar net wat je met de doorgelopen waarden wilt doen.

Terug naar boven

Generieke mappen

Je kunt de Generics ook toepassen in mappen, hierbij volg je dezelfde richtlijnen als bij generieke collecties, met het verschil dat je zowel de sleutel als de waarde moet definiëren bij de declaratie en instantiatie van het object. Hier heb je enkele voorbeelden:

Map<SleutelType, WaardeType> hashMap = new HashMap<SleutelType, WaardeType>();

Map<SleutelType, WaardeType> hashtable = new Hashtable<SleutelType, WaardeType>();

Het SleutelType moet dus minimaal overeenkomen met het object type van de sleutels van de map en het WaardeType moet dus minimaal overeenkomen met het object type van de waarden van de map. Voor de rest gelden dezelfde richtlijnen als bij de collecties.

Even wat voorbeeldcode:

package test;

import java.util.LinkedHashMap;
import java.util.Map;

public class Test {

    public static void main(String[] args) {

        // Declareer en initialiseer een LinkedHashMap voor Integers
        // met een String sleutel.
        Map<String, Integer> map = new LinkedHashMap<String, Integer>();

        // Voeg de Integer waarden met String sleutels toe.
        map.put("Een", new Integer("1"));
        map.put("Twee", new Integer("2"));
        map.put("Drie", new Integer("3"));

        // Doorloop de sleutels.
        for (String sleutel : map.keySet()) {

            // Laat de sleutel en de waarde zien.
            System.out.println(sleutel + ", " + map.get(sleutel));
        }
    }

}

Een, 1
Twee, 2
Drie, 3

Terug naar boven

Generieke klassen

Je kunt de Generics ook toepassen in klassen. Dit gaat een stuk verder dan het toepassen van de Generics in collecties en mappen. Er wordt namelijk niet gekeken naar de directe subklassen of superklassen van het opgegeven type. Dit moet je zelf aangeven met behulp van de extends respectievelijk super sleutelwoorden.

Class<Type> c = Klasse van de gegeven type

Class<? extends Type> c = (Sub)klasse van de gegeven type

Class<? super Type> c = (Super)klasse van de gegeven type

Class<?> c = Willekeurige klasse

Hieronder volgen enkele voorbeelden, compleet met gesimuleerde fouten (rood onderstreept):

// Declareer en instantieer een klasse waarvan de type
// strikt gelijk aan de gegeven type is.
Class<Integer> c1 = Number.class; // Number is niet gelijk aan Integer.
Class<Number> c2 = Number.class;
Class<Object> c3 = Number.class; // Number is niet gelijk aan Object.

// Declareer en instantieer een klasse waarvan de type
// gelijk aan of een subklasse van de gegeven type is.
Class<? extends Integer> c4 = Number.class; // Number is geen subklasse van Integer.
Class<? extends Number> c5 = Number.class;
Class<? extends Object> c6 = Number.class;

// Declareer en instantieer een klasse waarvan de type
// gelijk aan of een superklasse van de gegeven type is.
Class<? super Integer> c7 = Number.class;
Class<? super Number> c8 = Number.class;
Class<? super Object> c9 = Number.class; // Number is geen superklasse van Object.

// Declareer en instantieer een klasse waarvan de type
// willekeurig kan zijn.
Class<?> c10 = Number.class;

Type mismatch: cannot convert from Class<Number> to Class<Integer>
Type mismatch: cannot convert from Class<Number> to Class<Object>
Type mismatch: cannot convert from Class<Number> to Class<? extends Integer>
Type mismatch: cannot convert from Class<Number> to Class<? super Object>

Terug naar boven

Generieke methoden

Je kunt de Generics ook toepassen in de parameters en de return type van de methoden. Hierbij kun je dezelfde richtlijnen volgen als bij de generieke klassen. Hier heb je enkele voorbeelden op basis van een List:

package test;

import java.util.ArrayList;
import java.util.List;

public class Test {

    public void methode1(List<Number> nummers) {

        // De parameter van deze methode accepteert dus alleen de volgende lijst:
        List<Number> nummers;
    }

    public void methode2(List<? extends Number> nummers) {

        // De parameter van deze methode accepteert dus o.a. de volgende lijsten:
        List<Number> nummers;
        List<Integer> integers; // Integer is een subklasse van Number.
        List<Long> longs; // Long is een subklasse van Number.
        List<Short> shorts; // Short is een subklasse van Number.
    }

    public void methode3(List<? super Number> nummers) {

        // De parameter van deze methode accepteert dus alleen de volgende lijsten:
        List<Number> nummers;
        List<Object> objecten; // Object is een superklasse van Number.
    }

    public List<Number> methode4() {

        // Deze methode kan dus alleen de volgende lijst retourneren:
        List<Number> nummers = new ArrayList<Number>();

        return nummers;
    }

    public List<? extends Number> methode5() {

        // Deze methode kan dus o.a. de volgende lijsten retourneren:
        List<Number> nummers = new ArrayList<Number>();
        List<Integer> integers; // Integer is een subklasse van Number.
        List<Long> longs; // Long is een subklasse van Number.
        List<Short> shorts; // Short is een subklasse van Number.

        return nummers;
    }

    public List<? super Number> methode6() {

        // Deze methode kan dus alleen de volgende lijsten retourneren:
        List<Number> nummers = new ArrayList<Number>();
        List<Object> objecten; // Object is een superklasse van Number.

        return nummers;
    }

}
Terug naar boven

Generieke typen

Tenslotte kun je ook generieke typen gebruiken in eigengemaakte klassen. Dan kun je jouw klassen op dezelfde manier gebruiken als collecties en mappen. Dit kan vooral handig zijn als je veel bezig bent met abstracte klassen en polymorphisme. Om een generieke type binnen een klasse te kunnen gebruiken, moet je deze in de declaratie van de klasse hebben opgenomen:

public class Klasse<Type*> {}

Je kunt meerdere typen opgeven, gescheiden door een komma. Hieronder volgt een voorbeeld van een klasse met twee generieke typen:

package test;

public class DataTransferObject<T1, T2> {

    private T1 data1;
    private T2 data2;

    public void add(T1 data1, T2 data2) {
        this.data1 = data1;
        this.data2 = data2;
    }

    public T1 getData1() {
        return data1;
    }

    public T2 getData2() {
        return data2;
    }

}

Binnen eigen klassen kun je de typen inderdaad een willekeurige naam geven, zoals T1, T2 of eventueel Type1, Type2 of zelfs A, B, C, etcetera. Het maakt niet uit hoeveel typen je opgeeft. Let wel, wanneer je de naam van een bestaande klasse gebruikt, zoals Integer, String, etc, dan wordt de bestaande klasse overgeschreven met de gegeven type!

Zo'n klasse kan dan bijvoorbeeld als volgt worden gebruikt, compleet met een gesimuleerde foutmelding:

public void test() {

    // Declareer en initialiseer een DTO dat een String en een Integer kan bevatten.
    DataTransferObject<String, Integer> dto1 = new DataTransferObject<String, Integer>();
    dto1.add("Een", new Integer("1"));

    // Declareer en initialiseer een DTO dat een String en een Integer kan bevatten.
    DataTransferObject<String, Integer> dto2 = new DataTransferObject<String, Integer>();
    dto2.add(1, "Een"); // Fout: de types komen niet overeen.
}

The method add(String, Integer) in the type DataTransferObject<String,Integer> is not applicable for the arguments (int, String)

Je kunt net als bij generieke klassen en methoden ook subklassen toestaan in de typering, gebruikmakend van de extends bepaling in de typering. Je mag volgens de specificatie echter geen superklassen (super) toestaan in de typering. Je kunt ook geen geen wildcard (?) gebruiken om een willekeurige object type mee aan te geven. Da's immers zinloos, aangezien je je eigen typen een eigen naam kunt meegeven.

Hieronder volgt een voorbeeld van de extends bepaling in de typering:

public class DataTransferObject<T1 extends Number, T2> {

}

Je kunt deze op dezelfde manier gebruiken als bij generieke klassen en methoden (let ook op de gesimuleerde fouten):

public void test() {

    // Declareer en initialiseer enkele DTO's.
    DataTransferObject<Number, Object> dto1 = new DataTransferObject<Number, Object>();
    DataTransferObject<Integer, String> dto2 = new DataTransferObject<Integer, String>();
    DataTransferObject<String, Integer> dto3 = new DataTransferObject<String, Integer>();

}

Bound mismatch: The type String is not a valid substitute for the bounded parameter <T1 extends Number> of the type DataTransferObject<T1,T2>
Bound mismatch: The type String is not a valid substitute for the bounded parameter <T1 extends Number> of the type DataTransferObject<T1,T2>

Ja, deze foutmelding krijg je in dit geval inderdaad 2x ;)

Wanneer je de generieke typering per methode wilt declareren, dan moet je deze vóór het return type in de declaratie vaststellen. Ook hier mag je geen superklassen toestaan in de typering.

public <Type> void methode() {}

Hier heb je enkele voorbeelden op basis van een List:

package test;

import java.util.ArrayList;
import java.util.List;

public class Test {

    public <T extends Number> void methode7(List<T> nummers) {

        // De parameter van deze methode accepteert dus alleen de volgende lijst:
        List<T> nummerTypes;

        // Binnen de methode kun je de T verder gebruiken als subklasse van Number.
    }

    public <T extends Number> List<T> methode8() {

        // Deze methode kan dus alleen de volgende lijst retourneren:
        List<T> nummerTypes = new ArrayList<T>();

        return nummerTypes;

        // Binnen de methode kun je de T verder gebruiken als subklasse van Number.
    }

    public <T extends Number> List<T> methode9(Class<T> klasse) {

        // De parameter van deze methode accepteert dus alleen de volgende klasse:
        Class<T> c;

        // Deze methode kan dus alleen de volgende lijst retourneren:
        List<T> nummerTypes = new ArrayList<T>();

        return nummerTypes;

        // Binnen de methode kun je de T verder gebruiken als subklasse van Number.
    }

}

Wanneer je de definitie van de type uit de declaratie weglaat, terwijl de type ook niet in de klasse is gedeclareerd, dan zul je de volgende foutmelding krijgen:

package test;

import java.util.List;

public class Test {

    public void methode10(List<T> nummers) {

    }

}

T cannot be resolved to a type

Terug naar boven

Copyright - Er is geen copyright op de code. Je kunt het naar believen overnemen, aanpassen danwel verspreiden.

(C) Januari 2007, BalusC

No comments: