Typy i metody sparametryzowane (generics) (c) Krzysztof Barteczko 2014
Definicja Typ sparametryzowany - to typ (wyznaczany przez nazwę klasy lub interfejsu) z dołączonym jednym lub większą liczbą parametrów. Definicję typu sparametryzowanego wprowadzamy słowem kluczowym class lub interface podając po nazwie (klasy lub interfejsu) parametry w nawiasach kątowych. Parametrów tych następnie używamy w ciele klasy (interfejsu) w miejscu "normalnych" typów
(c) Krzysztof Barteczko 2014 Przykład klasy sparametryzowanej class Para { private S first; private T last; public Para(S f, T l) { first = f; last = l; } public Para() {} public S getFirst() { return first; } public T getLast() { return last; } public void setFirst(S f) { first = f; } public void setLast(T l) { last = l; } public String toString() { return first + " " + last; } }
(c) Krzysztof Barteczko 2014 Tworzenie instancji typów sparametryzowanych public static void main(String[] args) { class Data { String data; Data(String s) { data = s; } public String toString() { return data; } Para p1 = new Para<> ("Jan", "Kowalski"); System.out.println(p1); Para p2 = new Para<> ("Jan Kowalski", new Data(" ")); System.out.println(p2); Para p3 = new Para<>("Ala",2); System.out.println(p3); } Jan Kowalski Jan Kowalski Ala 2 argumenty typu Diamond operator
(c) Krzysztof Barteczko 2014 Diamond operator i konkludowanie typów Do wersji 7: Para p = new Para ("Jan", "Kowalski"); Od wersji 7: Para p = new Para<>("Jan", "Kowalski"); // arg typu konkludowane (infer) Od wersji 8: - rozwinięty inferring static void metoda(List list) { list.add("a"); list.add("b"); list.add("c"); System.out.println(list); } public static void main(String[] args) { metoda(new ArrayList<>()); // tu nic nie mówi o argumencie typu // w Javie 7 - błąd kompilacji, w 8 - ok } [a, b, c]
(c) Krzysztof Barteczko 2014 Korzyści generics – prostszy kod Para pg = new Para<>("Ala", 3); //autoboxing System.out.println(pg.getFirst() + " " + pg.getLast()); String name = pg.getFirst(); // bez konwersji! int m = pg.getLast(); // bez konwersji! pg.setFirst(name + " Kowalska"); pg.setLast(m+1); // bez konwersji, autoboxing System.out.println(pg.getFirst() + " " + pg.getLast()); Uwaga Argumenty typów mogą być tylko typami referencyjnymi. Nie możemy użyć typów prostych. Ale przy tworzeniu obiektów i wywołaniu metod działa autoboxing
(c) Krzysztof Barteczko 2014 Korzyści generics – wykrywanie błędów
(c) Krzysztof Barteczko 2014 Implementacja: typy surowe i czyszczenie typów class Para { private static int count; private S first; private T last; public Para(S f, T l) { count++; first = f; last = l; } public Para() {} public S getFirst() { return first; } public T getLast() { return last; } public void setFirst(S f) { first = f; } public void setLast(T l) { last = l; } public String toString() { return first + " " + last; public static void main(String[] args) throws Exception { Para p1 = new Para<>("Ala",2); Para p2 = new Para<>("Ala", "Ala"); System.out.println( p1.getClass() + " " + p2.getClass()); // raw type for (Method m : p1.getClass().getDeclaredMethods()) { if (!m.getName().equals("main")) System.out.println(m); // type erasure } System.out.println(p1.count + " " + p2.count); } } class Para class Para // raw type public java.lang.String Para.toString() // type erasure public java.lang.Object Para.getFirst() public java.lang.Object Para.getLast() public void Para.setFirst(java.lang.Object) public void Para.setLast(java.lang.Object) 2 2 // tylko jedna klasa
(c) Krzysztof Barteczko 2014 Możliwości i restrykcje Ze względu na sposób kompilacji (w fazie wykonania mamy jedną klasę "raw type" oraz zachodzi czyszczenie typów) w definicjach klas (i metod) sparametryzowanych nie do końca możemy traktować parametry typu tak jak zwykłe typy. Możemy: podawać je jako typy pól i zmiennych lokalnych, podawać je jako typy parametrów i wyników metod, dokonywać jawnych konwersji do typów oznaczanych przez nie (np. (T) object), wywoływać na rzecz zmiennych oznaczanych typami sparametryzowanymi metody klasy Object (i ew. właściwe dla klas i interfejsów, które stanowią górne ograniczenia danego parametru typu).
(c) Krzysztof Barteczko 2014 Restrykcje
(c) Krzysztof Barteczko 2014 Dlaczego nie wolno tworzyć tablic? Para [] pArr = new Para [5]; // (1) niedozwolone Object[] objArr = p; objArr[0] = new Para ("A", "B"); // przejdzie, jeśli dopuścimy (1) a błąd pojawił by się (jako ClassCastException) kiedyś później. Rozwiązanie: zamiast tablic używajmy kolekcji
(c) Krzysztof Barteczko 2014 Ograniczenia parametrów typu Sposób na zwiększenie funkcjonalności!
(c) Krzysztof Barteczko 2014 Użyteczność ograniczania typów... public abstract class Zwierz { private String name = "bez imienia"; public Zwierz() {} public Zwierz(String s) { name = s; } public abstract String getTyp(); public String getName() { return name; } } public interface Speakable { int QUIET = 0; int LOUD = 1; String getVoice(int voice); } public interface Moveable { Moveable start(); Moveable stop(); } public class Pies extends Zwierz implements Speakable, Moveable { public Pies() {} public Pies(String s) { super(s); } public String getTyp() { return "Pies"; } public String getVoice(int voice) { if (voice == LOUD) return "HAU... HAU... HAU... "; else return "Hau... Hau..."; } public Pies start() { System.out.println("Pies " + getName() + " biegnie"); return this; } public Pies stop() { System.out.println("Pies " + getName() + " stan �� "); return this; } public Pies merda() { System.out.println("Merda ogonem"); return this; }
(c) Krzysztof Barteczko 2014 Użyteczność ograniczania typów - przykład public class NaszeZwierze { private T z; public NaszeZwierze(T zwierz) { z = zwierz; } public void speak() { System.out.println( z.getTyp()+" "+z.getName() + " mówi " + z.getVoice(Speakable.LOUD) ); } T get() { return z; } public void startSpeakAndStop() { z.start(); speak(); z.stop(); } public static void main(String[] args) { NaszeZwierze p = new NaszeZwierze<>(new Pies("kuba")); for (Method m : p.getClass().getDeclaredMethods()) System.out.println(m); p.startSpeakAndStop(); p.get().merda(); //NaszeZwierze ryba; // błąd w kompilacji, bo Ryba nie jest Speakable } ///.... public Zwierz NaszeZwierze.get() public void NaszeZwierze.speak() public void NaszeZwierze.startSpeakAndStop() Pies kuba biegnie Pies kuba mówi HAU... HAU... HAU... Pies kuba stan �� Merda ogonem
(c) Krzysztof Barteczko 2014 Przykład 2 – generyczna tablica public class GenArr > { private T[] arr; private Para minMax; // na przechowanie warto � ci min i max public GenArr(T[] a) { init(a); } public void init(T[] a) { if (a == null || a.length == 0) throw new IllegalArgumentException("Invalid array init"); minMax = null; arr = a; } public T max() { return evaluate("MAX").getFirst(); } public T min() { return evaluate("MIN").getLast(); } private Para evaluate(String kind) { // w klasie Para jest konstruktor bezparametrowy if (minMax == null) minMax = new Para (); T v = arr[0]; switch (kind) { case "MAX": { // liczymy tylko, gdy dotąd nie policzone if (minMax.getFirst() == null) { // możemy to napisac � dzieki temu, � ze T extends Comparable for (T e : arr) if (e.compareTo(v) > 0) v = e; minMax.setFirst(v); } case "MIN": { if (minMax.getLast() == null) { for (T e : arr) if (e.compareTo(v) < 0) v = e; minMax.setLast(v); } return minMax; } Tablica elementów dowolnego typu pochodnego od Comparable z metodami max() i min().
(c) Krzysztof Barteczko 2014 Generyczna tablica - użycie public static void main(String[] args) { Integer[] arr1 = { 1, 2, 7, -3 }; Integer[] arr2 = { 1, 7, 8, -10 }; String[] napisy = { "A", "Z", "C" }; GenArr gai = new GenArr<>(arr1); System.out.println(gai.max() + " " + gai.min()); gai.init(arr2); System.out.println(gai.max() + " " + gai.min()); GenArr gas = new GenArr<>(napisy); System.out.println(gas.max() + " " + gas.min()); } Z A
(c) Krzysztof Barteczko 2014 Metody sparametryzowane Poniższy fragment prezentuje przykład definicji sparametryzowanej metody, zwracającej ostatni element przekazanej tablicy dowolnych obiektów: class A { static T last(T[] elts) { return elts[elts.length-1]; }
(c) Krzysztof Barteczko 2014 Wywołanie metody sparametryzowanej Przy wywołaniu metody sparametryzowanej można podać aktualne argumenty typu: ref. nazwaMetody(ListaArgumentów) gdzie: ref – referencja do obiektu (klasy) na rzecz której jest wołanie Np. wywołanie metody statycznej z klasy A Integer[] arr; //.... Integer lastElt = A. last(arr); Ale tu i w większości przypadkow nie jest to konieczne, ponieważ kompilator może określić aktualne argumenty typu z kontekstu wywołania.
(c) Krzysztof Barteczko 2014 Konkludowanie typów (type inferring) Argumenty typów (podstawiane w fazie kompilacji w miejsce parametrów, choćby po to by zapewnić zgodność typów oraz automatyczne konwersje zawężające) są określane na podstawie faktycznych typów użytych przy wywołaniu metody. Proces wyznaczania aktualnych argumentów typów nazywa się konkludowaniem typów (ang. type inferring). static T last(T[] elts) { return elts[elts.length-1]; } //.... Integer n = last(new Integer[] { 1, 4, 7 }); // konkludowanie typu System.out.println(n + 1); String s = last(new String[] {"a", "b", "ccc"}); // konkludowanie System.out.println(s + s.length()); 8 ccc3
(c) Krzysztof Barteczko 2014 Relacje dziedziczenia typów sparametryzowanych Czy ArrayList jest nadtypem dla typu ArrayList ? Jeśli tak, to: ArrayList list1 = new ArrayList (); i wobec tego: ArrayList list2 = list1; // hipotetyczna konwersja rozszerzająca a wtedy kompilator nie mógłby protestować przeciwko czemuś takiemu: list2.add(new Object()); co jednak doprowadziłoby do katastrofy: Integer n = list1.get(0); // ClassCastException Pomiędzy typami sparametryzowanymi z użyciem konkretnych argumentów nie zachodzą relacje dziedziczenia.
(c) Krzysztof Barteczko 2014 Uniwersalne argumenty typu (wildcards) Wypisz zawartość ArrayList elementów dowolnego typu: void show(ArrayList list) { System.out.println(list); }
(c) Krzysztof Barteczko 2014 Jak stosować wildcards ? (1) class Person { String name; public Person(String n) { name = n; } public String toString() { return "Person [name=" + name + "]"; } class Employee extends Person { Employee(String n) { super(n); } public String toString() { return "Employee [name=" + name + "]"; } class Manager extends Employee { Manager(String n) { super(n); } public String toString() { return "Manager [name=" + name + "]"; } Załóżmy, że mamy następującą hierarchię dziedziczenia:
(c) Krzysztof Barteczko 2014 Jak stosować wildcards ? (2) Metodą process chcemy „przetwarzać” dowolne pary obiektów w/w klas. Np. (Person, Employee), (Manager, Employee), (Manager, Manager). Biwariancja nie będzie tu zbyt użyteczna. W kontekście: void process(Para p) { //... } w ciele metody process(): A) odwołania p.getFirst() i p.getLast() mają nawet w fazie kompilacji typ wyniku Object B) nie mamy żadnych możliwości modyfikacji elementów pary (każde setFirst(..) lub setLast(...) z dowolnym argumentem, oprócz null będzie skutkowało błędem kompilacji. Parametr "?" oznacza przecież dowolny, nieznany typ, więc, oprócz mało ciekawej referencji null kompilator zabroni wszelkich podstawień, a jedyne na co pozwoli przy pobieraniu wartosci to Object, bo jest wspólnym typem dla wszystkich możliwych obiektów.
(c) Krzysztof Barteczko 2014 Jak stosować wildcards ? (3) Trzeba zastosować ograniczenia typów uniwersalnych (bounded wildcards), bo dadzą one kompilatorowi dodatkową informację. Co nam da kowariancja? static void process(Para p) { Person pers = p.getFirst(); Employee emp = (Employee) p.getFirst(); // ok, ale możliwe CCE p.setFirst(new Person("a")); // błąd w kompilacji p.setFirst(null); // tylko to możliwe } Przy kowariancji (? extends X): możemy pobierać wartości (bezpiecznie tylko typów = górne ograniczenie), w żaden sposób nie możemy ustalać (zmieniać) wartości.
(c) Krzysztof Barteczko 2014 Jak stosować wildcards ? (4) Kontrawariancja – pobieranie: static void process(Para p) { ??? first = p.getFirst(); } czym może być ???: Manager - NIE – błąd w kompilacji Person – NIE – błąd w kompilacji Tylko Object! Wyjaśnienie? Jest proste (ścisła kontrola zgodności typów). Przy kontrawariancji (? super X): nie możemy pobierać wartości (tylko na Object, co nie ma sensu)
(c) Krzysztof Barteczko 2014 Jak stosować wildcards ? (5) Kontrawariancja – zmiany wartości. static void process3(Para p) { p.setFirst( new ???("a")); } ??? może być tylko Manager (to oczywiste). Ważne, że można zmieniać!. Przy kontrawariancji (? super X): możemy ustalać wartości (tylko na typ == dolne ograniczenie)
(c) Krzysztof Barteczko 2014 Jak stosować wildcards - zasada Ważna zasada aby pobierać wartości stosuj "? extends T” aby ustalać (zmieniać) wartości - "? super T”
(c) Krzysztof Barteczko 2014 Użyteczność wildcards (1) Napiszmy metodę „kopiowania” (płytkiego) jednej pary na drugą. Taka metoda: static Para copy(Para src, Para dest) { dest.setFirst(src.getFirst()); dest.setLast(src.getLast()); return dest; } nie nada się w takiej sytuacji (menedżerowie to też pracownicy): Employee emp1 = new Employee("emp1"), emp2 = new Employee("emp2"); Manager man1 = new Manager("man1"), man2 = new Manager("man2"); Para epar = new Para<>(emp1, emp1); Para mpar = new Para<>(man1, man2); copy(mpar, epar); // Błąd w kompilacji System.out.println(epar); System.out.println(mpar);
(c) Krzysztof Barteczko 2014 Użyteczność wildcards (2) Na pomoc przychodzą bounded wildcards: chcemy pobierac wartości z src – więc tu będzie „? extends” chcemy ustalać wartości w dest - więc tu będzie „? super” static Para copy(Para src, Para dest) { dest.setFirst((Manager) src.getFirst()); dest.setLast((Manager) src.getLast()); return dest; } Kod nadaje się do „kopiowania” obiektów typu Manager i każdego jego nadtypu (np. Director) na obiekty typu Employee. Aby się zabezpieczyć przed wywołaniem z argumentem src mającym elementy typów nie będących nadtypem Manager: (getFirst i getLast) instanceof Manager