Generiky Aplikační programování v Javě (BI-APJ) - 10 Ing. Jiří Daněček Katedra softwarového inženýrství Fakulta informačních technologií ČVUT Praha
Evropský sociální fond Praha & EU: Investujeme do vaší budoucnosti
Generiky ● Generické (tj. parametrizovatelné) konstrukce (zkráceně generiky) umožňují některé části programu, který se liší pouze typem, zapsat v programu pouze jednou a opakovaně je použít ("volat"). ● Generiky lze volat s různými, ale pouze referenčními typy (Pozn.: C++ umí generiky i s primitivními typy). ● Hrubou analogií generik jsou tedy metody, které lze také volat opakovaně s různými parametry. Avšak zatímco metody se volají při běhu programu, generiky se volají při kompilaci! ● Generické mohou být: ○ třídy, ○ rozhraní, ○ konstruktory, ○ metody.
Příklad: kontrolovaná reference public class Box { private Object ref; public Box(Object ref) { this.ref = ref; } public Object getRef() throws MyNullRefException { if (ref == null) { throw new MyNullRefException(); } return ref; } public static void main(String[] args) { try { Box sp = new Box("Ahoj"); // při přístupu vyžaduje přetypování: String s = (String) sp.getRef(); } catch (MyNullRefException ex) { . . . } } }
Řešení s pomocí generické třídy public class Box
{ private T ref; public Box(T ref) { this.ref = ref; } public T getRef() throws MyNullRefException { if (ref == null) { throw new MyNullRefException(); } return ref; } public static void main(String[] args) { try { // parametrizování ("vyvolání") generické třídy // pro typ String: Box<String> stringBox = new Box<String>("Ahoj"); String s = stringBox.getRef(); } catch (MyNullRefException ex) { . . . } } }
Opakování ● Typ je entita, která existuje v době překladu (udržuje si ji kompilátor). ● V Javě je o každém proměnné a každém výrazu znám typ v době překladu. ● Typ definuje množinu hodnot, které mohou být uloženy v proměnných a význam operací, které se provádějí s operandy výrazů. ● Typy jsou primitivní a referenční. ● Referenční typy jsou: ○ typ třída (může být parametrizovaný), ○ typ interface (může být parametrizovaný), ○ typ pole.
Referenční typy ● ReferenceType: ○ ClassOrInterfaceType ○ TypeVariable ○ ArrayType ● ClassOrInterfaceType: ○ ClassType ○ InterfaceType ● ClassType: ○ TypeDeclSpecifier TypeArgumentsopt ● InterfaceType: ○ TypeDeclSpecifier TypeArgumentsopt
Typové proměnné ● Identifikátor v parametrové části deklarace generické třídy nebo rozhraní definuje tzv. typovou proměnnou: class Box { ...
● Typová proměnná je podle definice referenční typ: 1. je možno deklarovat proměnné resp. parametry typu T: T ref;
2. lze ji použít jako argument v jiném parametrickém typu: List
3. je možno ji použít v přetypování (avšak s unchecked warning): (T)x
4. nelze ji použít pro vytváření objektů: new T() // error new T[10]; // error
5. Nelze ji použít v operátoru instanceof: obj instanceof T // error
Typové parametry (1/2) ● Za deklarací typové proměnné může nepovinně následovat tzv. omezení (bounds) : class Box {
● Omezení je tvořeno jedním nebo více referenčními typy. V případě více typů má formát: extends B1 & B2 & ... Bn
● Pozn.: První prvek omezení (B1) může být libovolný refereční typ, ostatní pouze typy rozhraní ● Omezení omezuje možné refereční typy dosazené za typovou proměnnou a je tedy analogií typu u "normálních" proměnných. ● Typová proměnná spolu s omezením se nazývá typový (formální) parametr. ● Pokud omezení není uvedeno předpokládá se implicitně Object.
Typové parametry (2/2) ● TypeParameter: ○ TypeVariable TypeBoundopt ● TypeBound: ○ extends ClassOrInterfaceType AdditionalBoundListopt ● AdditionalBoundList: ○ AdditionalBound AdditionalBoundList ○ AdditionalBound ● AdditionalBound: ○ & InterfaceType
Průnikový typ ● Průnikový (intersection) typ se zapisuje ve formě T1 & ... & Tn, n>0, kde Ti je typový výraz. Průnikové typy nelze zapsat přímo (nemají syntaxi), ale vznikají v procesu typové inference a při zpracování chytací konverze. ● Pokud jsou některé z typů Ti třídní, pak jeden z nich musí být podtypem všech ostatních: String & Number - chybný průnikový typ ● Členy průnikového typu jsou všechny členy typů Ti. ● Členy typové proměnné jsou členy průnikového typu tvořeného jednotlivými omezeními.
Příklad typového parametru public class NumberBox { ... public static void main(String[] args) { try { NumberBox sri = new NumberBox(33); NumberBox<String> srs = new NumberBox<String>("33"); // error: type argument String is not within its bound Integer i = sp.getRef(); } catch (MyNullRefException ex) { ... } } }
Parametrizované typy ● Generická konstrukce (třída, rozhraní, konstruktor, metoda) uvedená s typovými argumenty (skutečnými parametry) se nazývá volání generické konstrukce - analogie volání metody. ● Speciálně pro třídy a rozhraní vzniká voláním nový referenční typ, který se nazývá parametrizovaný. ● Parametrizovaný typ je možno použít (s některými výjimkami) tam, kde obyčejný referenční typ. ● Hlavní použití je v deklaraci proměnných a parametrů a při vytváření objektů: Box> box = new Box>(new ArrayList<String>());
Členy parametrizovaných typů ● Jesliže C je deklarace třídy nebo rozhraní s typovým parametrem F,..., a C je parametrizovaný typ, kde T není supertyp. Potom: ● Jestliže m je člen nebo konstruktor v C, jehož typ je S. Potom typ m v parametrizovan0m typu C je supertype of C který koresponduje s D. Potom typ m v C je typ m v D ● Jestliže parametrizovaný typ má jako typový argument supertyp, jsou jeho členy nedefinované. Členy jsou přístupné až po provedení chytací konverze.
Mechanizmus vyčištění ● Všechny objekty parametrizovaných typů, které vznikly "voláním" stejné generiky, jsou stejné tzv. runtimové třídy. ● Tvar této třídy je dán mechanismem vyčištění (erasure): ○ třída se jmenuje stejně jako původní generická třída ○ nemá žádné typové parametry, ○ její nadtřída a nadrozhraní jsou vyčištěné, ○ v jejím těle jsou použité typové proměnné nahrazeny svými nejlevějšími omezeními, ○ generické metody a konstruktory jsou vyčištěny. ● Při běhu programu je generická třída reprezentována (implemetována) tzv. runtimovou třídou, která je dána vyčištěním generické třídy.
Důsledky mechanizmu vyčištění ● Všechny objekty parametrizovaných typů, které vznikly voláním stejné generiky, jsou stejné tzv. runtimeové třídy. ● Při výpočtu nelze zjistit od kterého parametrizovaného typu byl objekt vytvořen: Box<String> stringBox = new Box<String>("Ahoj"); Box intBox = new Box(33); System.out.println(stringBox.getClass() == intBox.getClass()); // vrátí true boolean jeTypu = stringBox instanceof Box<String>; // illegal generic type for instanceof
Důsledky zavedení generik ● Je potřeba rozlišovat mezi referenčním typem, deklarací třídy a runtimovou třídou (v Java 1.4 byly tyto pojmy zaměnitelné). Deklarace generická třídy generuje množinu referenčních typů. ● Nadtřída (resp. nadrozhraní) v deklaraci třídy je referenční typ (nikoliv třída resp. rozhraní). ● Nadtřída (resp. nadrozhraní) runtimové třídy je runtimová třída. ● Výrazy jsou referenčních typů zatímco objekty jsou rutimových tříd. ● Je potřeba definovat vlastnosti nových referenčních typů: 1. parametrizovaných typů, 2. typových proměnných. ● Pro nové referenční typy je potřeba definovat: ○ jaké mají členy, ○ jaká je jejich kompatibilita (tj. relace nadtypu).
Členy parametrizovaných typů ● Pro každý parametrizovaný typ je vytvořena fiktivní parametrizovaná třída, jejíž složky jsou určeny složkami původní generické třídy, ve kterých je za typovou proměnnou dosazen typový argument: Box intBox = new Box(10); int i = sri.getRef().intValue(); ● Proměnná intBox (která je parametrizovaného typu)
obsahuje referenci na objekt (fiktivní) parametrizované třídy. Tato třída pak obsahuje: ○ metodu: Integer getRef() { . . . } ○ proměnnou: Integer ref; ○ konstruktor: Box(Integer ref) { . . . }
Další vlastnosti typových proměnných ● Je označena identifikátorem, který platí v celé oblasti třídy (resp. metody nebo konstruktoru) včetně deklarační části. ● Existuje analogie mezi typovou proměnnou a "normální" proměnnou s těmito odchylkami: ○ Typová proměnná je v podstatě proměnná kompilátoru, která neexistuje v době výpočtu, ale pouze v době překladu. ○ Do typové proměnné nelze přiřazovat - hodnotu získává tzv. vyvoláním generické konstrukce (resp. chytací konverzí): Box<String>
● Pozn.:Typová proměnná tak svými vlastnostmi odpovídá proměnným ve funkcionálních jazycích.
Generická třída jako nadtřída ● Typová proměnná může být použita jako typový argument nadtřídy: public class VarBox extends Box { public VarBox() { } public VarBox(T ref) { super(ref); } public void setRef(T ref) { this.ref = ref; } public static void main(String[] args) { try { VarBox<String> stringVarBox = new VarBox<String>(); stringVarBox.setRef("Hello"); String s = stringVarBox.getRef(); } catch (MyNullRefException ex) { . . . } } }
● Může být také použita jako typový argument omezení: class X>
Omezení parametrických typů ● Omezující vlastnost parametrických typů: z kompatibility typových argumentů neplyne kompatibilita parametrizovaných typů (tzv. nejsou kovariantní) ● Příklad: Number n = new Integer(20); // ok VarBox numberBox = new VarBox(20);// error
● Důvod: parametrizovaný typ VarBox neposkytuje službu setRef(Number ref), takže přiřazením by se do numberBox uložila reference na objekt porušující kontrakt typu VarBox. ● Srovnej: Number[] an = new Integer[10]; // ok, protože pole jsou kovariantní an[0] = new Number(1) // error
Surové typy ● Standardně musí počet argumentů v parametrizovaném typu odpovídat počtu formálních parametrů generické třídy (resp. rozhraní). ● Výjimečně je možno argumenty vynechat - takový typ se pak nazývá surový (raw). ● Surové typy umožňují, aby program navržený pro Javu 1.4 byl přeložitelný v Javě 1.5 a tak byla zachována kompatibilita. ● Surové typy jsou oboustraně kompatibilní s parametrizovanými typy: Box rawBox = new Box(); ... Box<String> stringBox = rawBox ; // unchecked warning
Typový parametr supertyp (wildcard) ● Řešení: zavedení "supertypu" označeného znakem ? (wildcard) ● Použití supertypu jako typového argumentu dělá parametrizované typy kompatibilní: Box> sr = new Box(20); // ok
● Supertyp si můžeme představit jako typ tvořený množinou objektů vzniklou sjednocením všech referenčních typů. ● O typu parametrizovaném supertypem má kompilátor pouze omezené informace: VarBox> varBox = new VarBox(); Integer i = (Integer)varBox.getRef(); varBox.setRef(new Integer(20)); //error: setRef cannot be applied to Integer
○ návratový typ getRef je Object, ○ typ parametru setRef je nedefinovaný.
Použití supertypu ● Hlavní význam je při deklaraci parametru metody, která může pracovat s různými parametrickými typy: public static boolean containInteger (Box> box) { try { return box.getRef() instanceof Integer; } catch (MyNullRefException ex) { return false; } } ... containInteger(new Box(3.14)) // false containInteger(new Box(10)) // true
● Příklad z třídy java.util.Collections: public static int indexOfSubList (List> source, List> target)
Omezený supertyp ● Supertyp lze omezovat podobně jako typovou proměnnou a tak zpřesnit typovou informaci známou při překladu: VarBox extends Number> numVarBox; if (. . .) numVarBox = new VarBox(20.0); else numVarBox = new VarBox(20); ... numVarBox.getRef().doubleValue()); // vrati 20.0
● Typ parametrizovaný supertypem se velmi podobá surovému typu. Není však kompatibilní s jinými parametrizovanými typy: VarBox> varBox= . . . VarBox intVarBox = (VarBox) varBox; // nutné přetypování
Dolním omezením ● Supertyp lze omezit také zdola, tzv. dolním omezením: static void setToBox(Integer i, VarBox super Integer> box) { box.setRef(i) }
● Supertyp omezený shora reprezentuje množinu referenčních typů, které jsou nadtypy dolního omezení. V uvedeném příkladu: Integer, Number, Object.
Kompatibilita parametrizovaných typů ● Opakování: mezi typy existuje relace nadtyp-podtyp (supertype-subtype). Tato relace je tranzitivním uzávěrem relace přímý nadtyp-přímý podtyp (direct supertype-directsubtype) označená >1. ● Relace přímého nadtypu mezi primitivními typy: double >1 float float >1 long long >1 int int >1 char int >1 short short >1 byte ● Nadtyp parametrizovaného typu je: ○ příslušně parametrizovaná nadtřída, ○ příslušně parametrizovaná implementovaná rozhraní, ○ třída Object, ○ surový typ.
Přímý nadtyp ● Jestliže existuje deklarace parametrického typu C, s nadtřídou resp. nadrozhraním D, pak přímý nadtyp parametrizovaného typu C (T není supertyp): ○ Object, jestliže C je interface be nadrozhraní, ○ surový typ C, ○ přímá nadtřída where D, kde theta je substituce U[F := T], ○ C<S>, kde S obsahuje T. ● Příklad: VarBox<String> <1 Object VarBox<String> <1 VarBox VarBox<String> <1 Box<String> VarBox <1 VarBox extends String> VarBox <1 VarBox super String>
Ostatní přímé nadtypy ● Přímé nadtypy typu C , kde R je supertyp jsou přímé nadtypy C<X>, kde C<X je výsledek capture conversion na C. ● Přímé nadtypy průnikového (intersection) typu T1 & ... & Tn, jsou Ti, .... ● Přímé nadtypy typové proměnní jsou typy z jejího omezení. ● Přímý nadtyp null typu jsou všechny referenční typy s výjimkou samotného null typu. ● Typová proměnná je přímý nadtyp svého dolního omezení
Přímé nadtypy typu pole ● Jestliže S a T jsou oba referenční typy, pak: ○ S[] >1 T[] jestliže S >1 T pole na rozdíl od tříd jsou kovariantní !!! ● Dále: ○ Object >1 Object[] ○ Cloneable >1 Object[] ○ java.io.Serializable >1 Object[] ● Jestliže p je primitivní typ, pak: ○ Object >1 p[] ○ Cloneable >1 p[] ○ java.io.Serializable >1 p[]
Znečištění heapu ● Přiřazení hodnoty surového typu do proměnné parametrizovaného typu může způsobit tzv. znečištění heapu (heap pollution). Jedná se o stav, kdy proměnná neobsahuje objekt deklarovaného typu. Tato situace je při překladu hlášena varováním: unchecked warning. ● Varování znamená, že program není při překladu striktně typově zkontrolován a mohou se vyskytnout neočekávané výjimky za běhu: Box rawBox = new Box<String>("Ahoj"); Box intBox = rawBox; // warning: [unchecked] unchecked conversion System.out.println(intBox.getRef().intValue()); // java.lang.ClassCastException: ...
Důsledky znečištění heapu ● Důsledkem možného znečištění heapu je, že při použití parametrizovaných typů se musí v run-timu provádět stejné kontroly jako u typů neparametrizovaných. Fragment kódu: intBox.getRef().intValue()
se ve skutečnost přeloží: ((Integer)intBox.getRef()).intValue()
● Obdobný problém vniká i tehdy, pokud je v přetypování použita typová proměnná nebo parametrizovaný typ: private void uncheckedCasts(Object o) { T ref = (T) o; // warning: [unchecked] unchecked cast Box sr = (Box) o; // warning: [unchecked] unchecked cast }
Dolní omezení supertypu ● Supertyp je možno omezit také zdola: ? super T
● V tomto případě je typový argument tvořen sjednocením všech typů, které jsou nadtypem typu T. ● Dolní omezení se používá pro vyjádření vztahu mezi parametry metod: // Collections public static int binarySearch (List extends Comparable super T>> list, T key)
○ parametr list musí být seznam prvků, které jsou provnatelné, ○ porovnávací metoda prvků seznamu (compareTo) musí jako parameter akceptovat typ, který je nadtypem T.
Relace obsažení ● Typový argument TA1 obsahuje (je nadmnožina) typového argumentu TA2 (psáno TA2 <= TA1), jestliže množina typů TA2 je podmnožinou typů TA1: ○ ? extends T <= ? extends S jestliže T <: S ○ ? super T <= ? super S jestliže S <: T ○ T <= T ○ T <= ? extends T ○ T <= ? super T
Konverze ● Pokud se v určitém kontextu objeví dva typy dochází ke konverzi. V Javě existují následující konverze: 1. identická, 2. rozšiřující (widening) primitivní, 3. zužující (narowing) primitivní, 4. rozšiřující referenční, 5. užující referenční, 6. zabalovací (boxing), 7. rozbalovací (unboxing), 8. nekontrolovaná (unchecked), 9. chytací (capture), 10. řetězová, 11. Value set konverze. ● Konverzní kontexty jsou: přiřazení, volání metody, přetypování, spojování řetězu a číselné povýšení.
Chytací konverze ● Chytací (capture) konverze je automaticky aplikována v místě výskytu referenčního typu parametrizovaného supertypem. Jestliže G je generická deklarace, pak existuje chytací konverze z parametrizovaného typu G ...> na parametrizovaný typ G<S>, kde S je nová (fresh) typová proměnná s následujícími vlastnostmi: Parametrizovaný typ G>
Dolní mez S typ null typ null
Horní mez S U[A := S] UB & U[A := S]
G extends UB> G super := S]musí být LB ● Pokud jsouLB> ve druhém případě B a U typU[A třída, jedna podtřídou druhé: NumberBox super String> ns; // error
Nekontrolovaná konverze ● Nekontrolovaná (unchecked) je konverze ze surového typu G na parametrizovaný typ G. ● Pokud nejsou všechny typové argumenty cílového typu neohraničené supertypy, generuje nekontrolovaná konverze při kompilaci povinně varování (unchecked warning), protože může způsobit znečištění heapu: Box box = new Box<String>("Ahoj"); Box iBox = box; // warning: [unchecked] unchecked conversion Box> uBox = box;
Volací konverze ● V kontextu volací konverze (method invocation conversion) se typ argumentu konvertuje na typ parametru. Kontext připouští tyto konverze: ○ identickou konverzi, ○ rozšiřující primitivní konverzi, ○ rozšiřující referenční konverzi, ○ zabalovací konverzi volitelně následovanou rozšiřující referenční konverzí, ○ rozbalovací konverzi volitelně následovanou rozšiřující primitivní konverzí. ● Jestliže po aplikaci uvedených konverzí výsledný typ surový, může být na konec aplikována nekontrolovaná konverze. Chyba nastane, pokud se v řetězu konverzí vyskytnou parametrizované typy, které nejsou v relaci podtypu: Integer => Comparable => Comparable => Comparable<String>
Přiřazovací konverze ● Kontext přiřazovací konverze (assignment conversion) se chová stejně jako volací konverze a navíc připouští zužující primitivní konverzi, pokud: ○ typ proměnné je byte, short, nebo char ○ hodnota výrazu je reprezentovatelná v typu proměnné.
Přetypování ● V kontextu přetypování (typecasting) je možno použít: ○ identickou konverzi, ○ rozšiřující primitivní konverzi, ○ zužující primitivní konverzi, ○ rozšiřující referenční konverzi volitelně následovanou nekontrolovanou konverzí, ○ a zužující referenční konverzi volitelně následovanou nekontrolovanou konverzí, ○ zabalovací konverzi, ○ rozbalovací konverzi. ● V přetypování může být aplikována každá konverze z výjimkou řetězové a chytací.
Přetypování referenčních typů ● Přetypování z typu S na typ T je staticky korektní (statically known to be correct) jestliže S <: T. ● Přetypování z typu S na parameterizovaný typ T je nekontrolované (unchecked) pokud neplatí ani jedna z podmínek: ○ S <: T, ○ všechny typové argumenty T jsou supertypy bez omezení, ○ T <: S a S nemá žádný podtyp X != T, takový že |X| = |T|. ● Přetypování na typovou proměnnou je vždy nekontrolované. ● Nekontrolované přetypování z S na T je kompletně nekontrolovatelné, pokud přetypování z |S| na |T| je staticky korektní, jinak je částečně nekontrolované. ● Přetypování je kontrolované (checed) pokud není staticky korektní a není nekontrolované.
Přetypování v runtimu ● Chování přetypování S na T v runtimu: žádná runtime kontrola runtime kontrola bez znečištění heapu staticky korektní
kontrolované
možné znečištění heapu
částečně nekontrolované -
kompletně nekontrolované
● Částečně nekontrolované přetypování - v runtimu je prováděna stejná kontrola jako u kontrolovaného přetypování avšak mezi |S| and |T|, ● Kontrolované přetypování: ○ hodnotu null je možno vždy přetypovat, ○ třída objektu reference je kompatibilní pro přiřazení s typem |T|.
Generické metody a konstruktory ● Typové parametry generických metod umožňují vyjádřit vztah mezi typy skutečných parametrů a výsledku metody: public static boolean replaceAll // Collections (List list, T oldVal, T newVal) public static List asList(T... a) // Arrays
● Při volání generické metody je udání typových argumentů nepovinné. Hodnoty typových argumentů se v tomto případě odvodí z typů skutečných parametrů metody: List<String> ls = Arrays.asList("x", "b", "x"); Collections.replaceAll(ls, "x", "c");
nebo Collections.<String>replaceAll(ls, "x", "c");
● Pozn.: volání metody nelze začínat typovými argumenty. Pokud jsou vyjádřeny musí být před nimi uvedno jméno třídy, this atd.
Generická metoda versus supertyp ● Při práci s generickými třídami je možno použít buď generickou metodu nebo parametr se supertypem. Generická metoda je doporučena pouze při potřebě vyjádřit vztahy mezi parametry: public static void reverse(List> list) { rev(list); } private static void rev(List list) { List tmp = new ArrayList(list); for (int i = 0; i < list.size(); i++) { list.set(i, tmp.get(list.size() - i - 1)); } } public static boolean contains(Box x, T y) throws MyNullRefException { return x.getRef().equals(y); }
Omezení metod ● Je chyba jestliže v deklaraci typu C je deklarovaná metoda m1 a existuje metoda m2 deklarovaná v C nebo v nadtypu C taková že platí: ○ m1 and m2 mají stejné jméno, ○ signatura m1 se liší od signatury of m2, ○ m1 má stejné vyčištění jako m2. class C { T id (T x) {...} } class D extends C<String> { Object id(Object x) {...} }
Přemosťovací metody class C { abstract T id(T x); } class D extends C<String> { String id(String x) { return x; } } C c = new D(); c.id(new Object()); // nedovolený argument pro C<String>.id()
● Signatura aktuálně volané metody: D.id(String) se liší od signatury kompilované metody: C.id (Object). ● Chyba může být detekována vytvořením přemosťovací metody ve třídě D: Object id(Object x) { return id((String) x); }