public class Box {
private Object object;
public void set(Object object) { this.object = object; }
public Object get() { return object; }
}
Tomáš Pitner, Radek Ošlejšek, Marek Šabo, Jakub Čecháček pitner@muni.cz
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.
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
.
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; }
}
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
...
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
...
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
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");
...
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>(...));
/**
* Generic version of the Box class.
* @param <T> the type of the value being boxed
*/
public class Box<T> {
...
}
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");
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
...
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
...
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) { /* ... */ }
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.
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
...
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
}
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 |
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. |
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];
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!
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í.
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ů
Generics in the Java Programming Language od Gilada Brachy