Wybrane elementy języka Java – ciąg dalszy Paweł Zdziarski
Wyjątki Reflection Tworzenie i zarządzanie obiektami Garbage Collector i finalize() Nowe elementy Javy 1.5 Typy sparametryzowane Covariant return types „autoboxing” Pętla w stylu foreach Bezpieczne (type-safe) enumeracje Statyczne import Metody ze zmienną liczbą parametrów
Tworzenie i zarządzanie obiektami Garbage Collector i finalize() Wyjątki Reflection Tworzenie i zarządzanie obiektami Garbage Collector i finalize() Nowe elementy Javy 1.5 Typy sparametryzowane Covariant return types „autoboxing” Pętla w stylu foreach Bezpieczne (type-safe) enumeracje Statyczne import Metody ze zmienną liczbą parametrów
Typy sparametryzowane Obecne w np. C++ Używane najczęściej w kontekście różnych operacji na kolekcjach W Javie 1.4 (i wcześniejszych) musimy używać rzutowania List myIntList = new LinkedList(); myIntList.add(new Integer(0)); Integer x = (Integer) myIntList.iterator().next(); aby zapewnić poprawność typów w czasie wykonania. Kompilator wie jedynie, że obiekt typu LinkedList przechowuje elementy typu Object Użycie generics w Javie 1.5 List<Integer> myIntList = new LinkedList<Integer>(); Integer x = myIntList.iterator().next();
Typy sparametryzowane - składnia Deklaracja: używamy formalnego parametru typu, np. public interface List<E> { void add(E x); Iterator<E> iterator(); } Wywołanie: typ sparametryzowany, np. List <Integer> myList; „intuicyjne” rozumienie wywołania: każde wystąpienie formalnego parametru typu w deklaracji typu sparametryzowanego zastępujemy faktycznym parametrem typu, np. zastępujemy we wszystkich metodach List<E> wystąpnienia E typem Integer W rzeczywistości, skompilowany kod typu sparametryzowanego istnieje tylko w jednej kopii (jeden plik klasy .class)
Typy sparametryzowane - wildcards Jak napisać metodę wypisującą wszystkie elementy kolekcji? void printCollection(Collection<Object> c) { for (Object e : c) { System.out.println(e);}} (używamy dodatkowo nowej postaci pętli for) Skoro Collection<Object> nie jest nadtypem innej sparametryzowanej kolekcji, musielibyśmy stworzyć odpowiednie metody do wypisywania elementów Collection<String> etc. Collection<?> jest nadtypem wszystkich kolekcji. ? Jest typem wildcard void printCollection(Collection<?> c) { for (Object e : c) { System.out.println(e);}}
Typy sparametryzowane – bounded wildcards Nawet jeśli Shape jest nadtypem Circle, Rectangle etc., List<Shape> nie jest nadtypem List<Circle>, List<Rectangle> Możemy jawnie zadeklarować tzw. upper bound parametru typu List <? extends Shape>
Typy sparametryzowane – implementacja, erasure Problem używania kodu generics z kodem wcześniejszych wersji Javy public String loophole(Integer x) { List<String> ys = new LinkedList<String>; List xs = ys; xs.add(x); ys.iterator().next(); } Jak we wcześniejszym przykładzie, tworzymy alias xs do zmiennej ys typu spararametryzowanego List<String> Tutaj kod się kompiluje (kompilator daje jedynie ostrzeżenie), natomiast użycie błędnego typu daje błąd wykonania (ClassCastException) return (String) ys.iterator().next(); Implementacja generics działa jako przód (front-end) normalnego kompilatora Javy, kod z użyciem generics jest zamieniany na normalny bytecode Javy w trakcie procesu nazwanego erasure
Typy sparametryzowane – implementacja, erasure Kod sparametryzowany public String loophole(Integer x) { List<String> ys = new LinkedList<String>; List xs = ys; xs.add(x); ys.iterator().next(); } zostanie przetłumaczony do public String loophole(Integer x) { List ys = new LinkedList; return (String) ys.iterator().next(); // run time error Podwójna kontrola typów: translator sprawdza poprawność użycia typów sparametryzowanych (np. nie moglibyśmy zadeklarować List<Object> xs) Do wynikowego kodu „zwykłej” Javy dodawane są rzutowania wszędzie, gdzie może wystąpić niepoprawne (pod wzgl. typów) przypisanie
Typy sparametryzowane - erasure Mechanizm polega na mapowaniu typów sparametryzowanych do typów niesparametryzowanych Erasure klasy niesparametryzowanej nie zmienia definicji tej klasy Typy sparametryzowane tracą typ parametru Tree<T> Tree Tree<Integer> Tree Typ parametru jest mapowany do swojego upper bound T (w klasie Tree) Object ale Bush <T extends Color> T (w klasie Bush) Color Wstawiane są odpowiednie rzutowania
Typy sparametryzowane - erasure Czy skopiluje się class ShoppingCart<T extends DVD>{ // ... } class ShoppingCart<T extends VideoTape>{ ? Nie: po erasure, obie klasy miałyby tą samą nazwę
Typy sparametryzowane - erasure Czy skompiluje się class TwoForOneSpecial<T extends Rentable, W extends Rentable> { public void add(T newRentable) { //... } public void add(W newRentable) { }? Nie: po erasure, obie metody mają te same sygnatury
Typy sparametryzowane - erasure Czy skompiluje się class GetAFreeVideoTape<T extends Rentable, W extends VideoTape> { public void add(T anything) { //... } public void add(W videotape) { ? Tak: po erasure dostaniemy kod odpowiadający class GetAFreeVideoTape { public void add(Rentable anything) { public void add(Videotape videotape) {
Typy sparametryzowane – jeden kod klasy Dlaczego nie jest poprawna konstrukcja class MyClass<T> { static T member; } ? Tworząc wiele instancji typu sparametryzowanego, np. MyClass<Integer> myIntClass; MyClass<String> myStringClass; mielibyśmy tylko jedną kopię member w pamięci. Jakiego typu byłaby ta zmienna?
Typy sparametryzowane – jeden kod klasy Jaki wynik da wykonanie List <String> l1 = new ArrayList<String>(); List<Integer> l2 = new ArrayList<Integer>(); System.out.println(l1.getClass() == l2.getClass()); ? Ponieważ kod z użyciem generics i tak jest tłumaczony do kodu bez typów sparametryzowanych, wszystkie instancje klas sparametryzowanych są tej samej klasy true
Typy sparametryzowane i dziedziczenie Fragment kodu List<String> ls = new ArrayList<String>(); List<Object> lo = ls; daje błąd kompilacji w linii 2 Intuicyjne rozumienie List<Object> jako nadtypu List<String> nie jest poprawne! Rozważając lo.add(new Object()); String s = ls.get(0); Wcześniej stworzyliśmy alias do obiektu lo o nazwie ls W pierwszej linii wstawiamy do listy element typu Object – nie ma już gwarancji, że lista przechowuje elementy wyłącznie typu String Jeśli cały kod kompilowałby się, druga linia nie zachowywałaby poprawności typów Ogólnie, mimo iż A extends B to jednak List<B> nie jest podtypem List<A>
Tworzenie i zarządzanie obiektami Garbage Collector i finalize() Wyjątki Reflection Tworzenie i zarządzanie obiektami Garbage Collector i finalize() Nowe elementy Javy 1.5 Typy sparametryzowane Covariant return types „autoboxing” Pętla w stylu foreach Bezpieczne (type-safe) enumeracje Statyczne import Metody ze zmienną liczbą parametrów
Covariant return types Do wersji 1.4 języka przesłaniająca implementowaną w nadklasie metoda podklasy musiała mieć identyczną sygnaturę – w szczególności, zwracany typ Poniższy kod nie kompiluje się w JRE 1.4.1_02 class Fruit implements Cloneable { Fruit copy() throws CloneNotSupportedException { return (Fruit) clone(); }} class Apple extends Fruit implements Cloneable { Apple copy() throws CloneNotSupportedException { return (Apple) clone(); Wywołując clone() na obiekcie Apple dostajemy obiekt nadklasy Fruit i musimy niepotrzebnie rzutować w dół do Apple Java 1.5 dopuszcza taką konstrukcję
Tworzenie i zarządzanie obiektami Garbage Collector i finalize() Wyjątki Reflection Tworzenie i zarządzanie obiektami Garbage Collector i finalize() Nowe elementy Javy 1.5 Typy sparametryzowane Covariant return types „autoboxing” Pętla w stylu foreach Bezpieczne (type-safe) enumeracje Statyczne import Metody ze zmienną liczbą parametrów
Iterowanie po elementach kolekcji Dotychczas (Java 1.4) używamy konstrukcji typu public void drawAll (Collection c) { Iterator itr = c.iterator(); while (itr.hasNext()) { ((Shape)itr.next()).draw(); } Używając typów parametrycznych, możemy zaoszczędzić sobie kodowania kilku rzutowań public void drawAll (Collection<Shape> c) { Iterator<Shape> itr = c.iterator(); while (itr.hasNext()) { itr.next().draw(); }
Pętla „foreach” + generics Nowa dopuszczalna postać pętli „for” public void drawAll(Collection<Shape> c) { for (Shape s:c) s.draw(); } Rozwijane automatycznie do kodu for (Iterator<Shape> $i = c.iterator(); $i.hasNext();) { Shape s = $i.next();
Tworzenie i zarządzanie obiektami Garbage Collector i finalize() Wyjątki Reflection Tworzenie i zarządzanie obiektami Garbage Collector i finalize() Nowe elementy Javy 1.5 Typy sparametryzowane Covariant return types „autoboxing” Pętla w stylu foreach Bezpieczne (type-safe) enumeracje Statyczne import Metody ze zmienną liczbą parametrów
Bezpieczne (type-safe) typy wyliczeniowe Typ wyliczeniowy – obecny w C, C++, C#, Pascalu Dotychczas public class Karty { public static final int PIK = 0; public static final int TREFL = 1; public static final int KARO = 2; public static final int KIER = 3; } Użycie wzorca (pattern) zamiast konstrukcji języka Potencjalne problemy Metoda oczekująca Karty skompiluje się nawet, jeśli jako parametr przekażemy literał np. 5 Optymalizacja przez kompilator – inlining
Typy wyliczeniowe - inlining Kompilator optymalizuje kod binarny włączając wartości stałych bezpośrednio do każdej klasy, która ich używa Zgodnie ze specyfikacją języka, narzędzia mogą, ale nie muszą wspierać model rozproszonej pracy nad aplikacją i automatycznie rekompilować klasy używane przez program public class Test { public Test() } static public void main(String[] args) System.out.println(Karty.KARO); Po zrekompilowaniu klasy Karty i zmianie wartości stałej KARO uruchomienie klasy Test (poza IDE) da rezutat identyczny jak przed zmianą stałej
Typy wyliczeniowe – wzorzec typesafe enum Zdefiniuj klasę reprezentującą pojedynczy element „typu wyliczeniowego” (UWAGA: system typów Javy 1.4 nie obejmuje typu wyliczeniowego) Nie udostępniaj żadnych publicznych konstruktorów Jedynie pola public static final Nigdy nie stworzymy obiektów tego typu poza tymi udostępnianymi przez pola public static final public class KartyPattern { public final String nazwa; private KartyPattern(String nazwa) {this.nazwa = nazwa;} public String toString() {return nazwa;} public static final KartyPattern PIK = new KartyPattern("pik"); public static final KartyPattern TREFL = new KartyPattern("trefl"); public static final KartyPattern KARO = new KartyPattern("karo"); public static final KartyPattern KIER = new KartyPattern("kier"); }
Typy wyliczeniowe – wzorzec typesafe enum Użycie wzorca typesafe enum gwarantuje poprawność wykonania (runtime) Jeśli zdefiniujemy metodę oczekującą parametru typu KartyPattern mamy pewność, że każda niezerowa (non-null) referencja przekazana do tej metody reprezentuje poprawny kolor karty Stałe mogą być dodane do takiej klasy bez rekompilowania klas-klientów Stałe nigdy nie są wkompilowane do klas ich używających (klientów) Możemy zmienić metodę toString() aby uzyskać sensowną reprezentację stałych na ekranie Porównywanie stałych odbywa się przez dziedziczoną z Object metodę equals wykonującą porównanie referencji, a nie np. kosztowne porównywanie String’ów Wady: Klasy J2SE i API’s produktów opartych na Javie rzadko używają wzorca typesafe enum, jest on generalnie mało popularny Kod znacznie się rozrasta, jeśli chcemy dodać serializację klasy, uporządkowanie wartości etc.
Wzorzec typesafe enum – problemy z serializacją Dodajemy public class KartyPattern implements Serializable Poniższy kod ByteArrayOutputStream bout = new ByteArrayOutputStream (); ObjectOutputStream out = new ObjectOutputStream (bout); KartyPattern kp = KartyPattern.KARO; out.writeObject (kp); out.flush (); ByteArrayInputStream bin = new ByteArrayInputStream (bout.toByteArray ()); ObjectInputStream in = new ObjectInputStream (bin); KartyPattern kp2 = (KartyPattern) in.readObject (); System.out.println ((kp2 == KartyPattern.KARO || kp2 == KartyPattern.KIER || kp2 == KartyPattern.PIK || kp2 == KartyPattern.TREFL)); Wypisuje False W klasie implementującej Serializable musielibyśmy przesłonić metodę readResolve(), żeby upewnić się, że podczas deserializacji nie zostanie stworzona nowa instancja klasy, a użyte będzie stworzone wcześniej statyczne pole..
Typy wyliczeniowy w Java 1.5 Wbudowany w specyfikację języka nowy typ: wyliczeniowy (enum) enum Karta (pik, trefl, karo, kier); W odróżnieniu od klas wzorca typesafe enum, zmienne takiego typu mogą być używane w klauzuli switch Karta karta; switch karta{ case Karta.pik ... } Nie są konieczne zmiany JVM Implementowane przez kompilator Nowa klasa java.lang.Enum, po której dziedziczą wszystkie zmienne typu wyliczeniowego Deklaracja jest nowym rodzajem deklaracji klasy W szczególności, nie jest legalne jawne tworzenie instancji enumeracji przy użyciu new()
Typy wyliczeniowy w Java 1.5 A Typesafe Enum Facility for the Javatm Programming Language http://www.jcp.org/aboutJava/communityprocess/jsr/tiger/enum.html Effective Java Programming: Substitutes for Missing C Constructs http://java.sun.com/developer/Books/shiftintojava/page1.html#replaceenums Typesafe Enum: Using enum in J2SE 1.5 (Tiger) http://www.langrsoft.com/articles/enum.html Beware of Java typesafe enumerations http://www.javaworld.com/javaworld/javatips/jw-javatip122.html
Literatura i URLs Wprowadzenie do użycia Generics http://developer.java.sun.com/developer/technicalArticles/releases/generics/ The Java Language Specification, dostępna z java.sun.com