Generické typy

Generické typy

  • Generické typy = něco obecně použitelného, zobecnění

  • Synonymum: generické typy = generika.

  • Třídy v Javě mají společného předka, třídu Object.

  • Potřebujeme-li pracovat s nějakými objekty, o kterých neznáme typ, můžeme využít společného předka a pracovat s ním.

  • Například do jednoho seznamu prvků Person vložíme prvky Employee i Manager současně.

  • To má ale řadu nevýhod.

Vtip

Generics

Motivační příklad

public class Box {
    private Object object;

    public void set(Object object) { this.object = object; }
    public Object get() { return object; }
}
  • Protože metody akceptují/vrací Object můžeme do krabice vložit libovolný objekt, např. String.

Motivační příklad

  • Použití ale vyžaduje ale přetypování.

  • Během překladu nelze ověřit správnost použití ⇒ může docházet k chybám za běhu

public class Box {
    private Object object;

    public void set(Object object) { this.object = object; }
    public Object get() { return object; }
}

Motivační příklad (pokr.)

Protože metody akceptují/vrací Object můžeme do krabice vložit libovolný objekt, např. String.

...
Box box = new Box();
box.set(new String("Hello"));        // String is always Object
String item = (String) box.get();    // Converting Object back to String is tricky
Integer iItem = (Integer) box.get(); // Will compile, but triggers runtime error
...

Řešení s generiky

  • Namísto konkrétního typu použijeme "volnou proměnnou", za kterou se teprve dosadí konkrétní typ během překladu.

public class Box<T> {
    private T object;

    public void set(T object) { this.object = object; }
    public T get() { return object; }
}

...
Box<String> box = new Box<>();       // Declaration, which type is used
box.set(new String("Hello"));        // String is always Object
String item = box.get();             // No class casting (re-typing) needed
Integer iItem = (Integer) box.get(); // Run-time error, of course
box.set(new Integer(10));            // Compile-time error due to type checking
...

Základní pravidla pro generika

  • Je doporučováno používat velké, jednopísmenné označení typů, zde T

  • Písmeno vystihuje použití — T je type, E je element, apod.

  • Při použití třídy T nahradíme jakoukoliv třídou nebo rozhraním

  • Nelze použít pro primitivní typy. Každý primitivní typ má ale svůj objektový ekvivalent a automatickým převodem (autoboxing/unboxing)

  • Generická třída může definovat více různých typů parametrů (viz např. mapy)

  • Generika se dají použit pro třídy, záznamy i rozhraní

Box<Integer> box = new Box<>();
int x = 2;
box.set(x);        // OK
int y = box.get(); // OK

Základní pravidla pro generika (pokr.)

  • Generická třída může definovat více různých typů parametrů (viz např. mapy)

  • Generika se dají použit pro třídy, záznamy i rozhraní

public interface Pair<K, V> {
    public K getKey();
    public V getValue();
}

public class PairImpl<K, V> implements Pair<K, V> {
    private K key;
    private V value;

    public PairImpl(K key, V value) {
	this.key = key;
	this.value = value;
    }

    public K getKey()	{ return key; }
    public V getValue() { return value; }
}

...
Pair<String, Integer> p1 = new PairImpl<>("Even", 8);
Pair<String, String>  p2 = new PairImpl<>("hello", "world");
...

Generické objekty jsou opět jenom objekty

  • Objekt vytvořený instanciací generické třídy je opět normální objekt

  • Můžu tedy například vytvořit pár, kde klíčem je celé číslo a hodnotou krabice čísel

Pair<String, Box<Integer>> p = new PairImpl<>("primes", new Box<Integer>(...));

Dokumentace generik

/**
 * Generic version of the Box class.
 * @param <T> the type of the value being boxed
 */
public class Box<T> {
...
}

Použití generické třídy bez generického typu

  • Následující kód je přeložitelný, ale přicházíme typovou kontrolu

  • Zavedeno pouze z důvodu zpětné kompatibility (generika byla zavedena až v JDK 5.0)

  • V tomto kurzu je to považováno za chybu

  • Překladač na chybu upozorňuje hláškou "unchecked or unsafe operation"

Box rawBox = new Box();
rawBox.add("Hello");

Generické metody

  • Statické i nestatické

  • Princip je podobný jako u generického typu (třídy), ale zavání generický typ pouze pro jednu metodu třídy

public class Util {
    public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
        return p1.getKey().equals(p2.getKey()) &&
               p1.getValue().equals(p2.getValue());
    }
}

...
Pair<Integer, String> p1 = new PairImpl<>(1, "apple");
Pair<Integer, String> p2 = new PairImpl<>(2, "pear");
boolean same1 = Util.<Integer, String>compare(p1, p2);
boolean same2 = Util.compare(p1, p2); // Also OK thanks to type inference. Preferred way
...

Omezení rozsahu typů

  • Angl.: bounded type parameters

  • Někdy chceme generický typ omezit.

  • Např. metoda, která pracuje s čísly, chce omezit generický typ na Number a její podtřídy (Integer, Double apod.)

  • Syntaxe: <T extends Number>

public <T extends Number> void inspect(T t){
   ...
}

...
obj.inspect(new Integer(10));     // OK
obj.inspect(new Double(10.1));    // OK
obj.inspect(new String("text"));  // Error: String is not subclass of Number
...

generika, dědičnost a podtypy

  • Mějme následující metodu. Dalo by s očekávat, že jako vstupní argument můžeme použít třeba Box<Integer>. To ale nelze, protože Box<Integer> není podtypem Box<Number>!

  • Ve skutečnosti jsou Box<Integer> i Box<Number> podtřídami Object

  • Řešením je použít tzv. žolíky (wildcards)

public void boxTest(Box<Number> box) { /* ... */ }

Žolíci s horním ohraničení typu

  • Angl.: upper bounded wildcards

  • Generika poskytují nástroj zvaný žolík (wildcard) , který se zapisuje jako <?>.

  • Předchozí situaci pomůže vyřešit žolík, který ohraničuje typ shora: <? extends SomeClass>

public void boxTest(Box<? extends Number> box) {
    Number n = box.get();
}
  • Interpretace: Jako vstupní argument může být použitý jakýkoliv Box, jehož generický typ je podtype Number, tedy např. Box<Number>, Box<Integer>, atd.

Neomezení žolíci

  • Angl.: unbounded wildcards

  • Žolík se dá použít bez stanovení rozšiřujícího typu. Pak je tím typem Object

public void boxTest(Box<?> box) {
    Number n = box.get(); // error
    Object o = box.get();
}
...
obj.boxTest(new Box<Integer>(...)); // OK
obj.boxTest(new Box<Object>(...));  // OK
...

Neomezení žolíci: limity

  • Box<Object> a Box<?> není totéž

  • Jelikož nevíme, jakého typu jsou prvky při použití neomezeného žolíku, nelze objekt přetypovat na tento "neznámý typ"

  • Jedinou výjimkou je žádný prvek null, který lze použít kdykoliv.

public void boxTest(Box<Object> box) {
     Object o = box.get(); // OK
     box.set(null);        // OK
     box.set(anyObject);   // OK because anything can be casted to Object
}

public void boxTest(Box<?> box) {
     Object o = box.get(); // OK because anything can be casted to Object
     box.set(null);        // OK
     box.set(anyObject);   // error because the type is uknown
}

Žolíci se spodním ohraničení typu

  • Angl.: lower bounded wildcards

  • Syntaxe: <? super SomeClass>

  • Interpretace: Dodaný typ musí být SomeClass nebo její nadtřída (nebo implementované rozhraní)

public void boxTest(Box<? super Number> box) { ... }

...
obj.boxTest(new Box<Number>(...));  // OK
obj.boxTest(new Box<Object>(...));  // OK
obj.boxTest(new Box<Integer>(...)); // error
...
Lze specifikovat horní ohraničení nebo spodní ohraničení, nelze oboje

Generics metody vs. wildcards

  • Chceme, aby typ u generické metody spojoval parametry nebo parametr a návratovou hodnotu.

  • Ne úplně správné (funkční) použití generické metody:

static <T, S extends T> void copy(List<T> destination, List<S> source);
  • Lepší zápis, T spojuje dva parametry metody a přebytečné S je nahrazené žolíkem:

static <T> void copy(List<T> destination, List<? extends T> source);
Metody jsou public, viditelnost je vynechána kvůli lepší přehlednosti.

Pole

  • Pro pole nelze použít parametrizovanou třídu.

  • Při vkládání prvků do pole runtime systém kontroluje pouze typ vkládaného prvku.

  • Do pole řetězců bychom pak mohli vložit pole čísel a pod.

// generic array creation error
public <T> T[] returnArray() {
  return new T[10];
}
  • Jde však použít třídu s žolíkem, který není vázaný:

List<?>[] pole = new List<?>[10];

Vícenásobná vazba generik I

  • Uvažujme následující metodu, která vyhledává maximální prvek kolekce.

static Object max(Collection<T> c);
  • Prvky kolekce musí implementovat rozhraní Comparable, což není syntaxí vůbec podchyceno.

    • Zavolání této metody proto může vyvolat výjimku ClassCastException!

  • Chceme, aby prvky kolekce implementovali rozhraní Comparable.

static <T extends Comparable<? super T>> T max(Collection<T> c);
// if generics are removed
static Comparable max(Collection c); // does not return Object!

Vícenásobná vazba generik II

  • Signatura metody se změnila — má vracet Object, ale vrací Comparable!

    • Metoda musí vracet Object kvůli zpětné kompatibilitě.

  • Využijeme tedy vícenásobnou vazbu:

static <T extends Object & Comparable<? super T>> T max (Collection<T> c);
  • Po výmazu má metoda správnou signaturu, protože v úvahu se bere první zmíněná třída.

  • Obecně lze použít více vazeb, například když je obecný prvek implementací více rozhraní.

Závěr

  • Generiky mají i další využití, například u reflexe.

  • Tohle však již překračuje rámec začátečnického seznamování s Javou.

  • Slidy vychází z materiálů