Klasy wewnętrzne
Transkrypt
Klasy wewnętrzne
Aplikacje w Javie – wykład 6 Klasy wewnętrzne, klasy anonimowe Typy i metody sparametryzowane (generics) Treści prezentowane w wykładzie zostały oparte o: ● Barteczko, JAVA Programowanie praktyczne od podstaw, PWN, 2014 ● http://docs.oracle.com/javase/8/docs/ ● C. S. Horstmann, G. Cornell, Java. Podstawy, Helion, Gliwice 2008 1 Klasy wewnętrzne Klasa wewnętrzna – to klasa zdefiniowana wewnątrz innej klasy. class A { .... class B { .... } .... } ● Klasa B jest klasą wewnętrzną w klasie A. ● Klasa A jest klasą otaczającą klasy B. ● Klasy wewnętrzne (ang. Nested Classes) dzielą się na dwie kategorie: statyczne (static) i niestatyczne (non-static) class OuterClass { ... static class StaticNestedClass { ... } class InnerClass { ... } } 2 Klasy wewnętrzne Klasa wewnętrzna może: ● ● ● ● ● ● być zadeklarowana ze specyfikatorem private (normalne klasy nie!), uniemożliwiając wszelki dostęp spoza klasy otaczającej, odwoływać się do niestatycznych składowych klasy otaczającej (jeśli nie jest zadeklarowana ze specyfikatorem static), być zadeklarowana ze specyfikatorem static (normalne klasy nie!), co powoduje, że z poziomu tej klasy nie można odwoływać się do składowych niestatycznych klasy otaczającej (takie klasy nazywają się zagnieżdżonymi, ich rola sprowadza się wyłącznie do porządkowania przestrzeni nazw i ew. lepszej strukturyzacji kodu) mieć nazwę (klasa nazwana) lub nie mieć nazwy (wewnętrzna klasa anonimowa), być lokalna – zdefiniowana w bloku (metodzie lub innym bloku np. w bloku po instrukcji if) odwoływać się do zmiennych lokalnych, jeśli jest lokalna, a wartości zmiennych lokalnych, do których się odwołujemy nie mogą się zmieniać (do Javy 7 wymagane było by były ze specyfikatorem final). 3 Klasy wewnętrzne Klasy wewnętrzne tworzymy i używamy ponieważ: ● ● ● ● ● Klasy wewnętrzne mogą być ukryte przed innymi klasami pakietu (względy bezpieczeństwa). Klasy wewnętrzne pozwalają unikać kolizji nazw (np. klasa wewnętrzna nazwana Vector nie koliduje nazwą z klasą zewnętrzną o tej samej nazwie). Klasy wewnętrzne pozwalają (czasami) na lepszą, bardziej klarowną strukturyzację kodu, bo można odwoływać się z nich do składowych (nawet prywatnych) klasy otaczającej, a przy tym zlokalizować pewne działania. Klasy wewnętrzne (w szczególności anonimowe) są intensywnie używane przy implementacji standardowych interfejsów Javy. Anonimowe klasy wewnętrzne pozwalają na traktowanie fragmentów kodu do wykonania (ściślej: metod przedefiniowywanych w tych klasach) jak obiektów, a wobec tego np. umieszczanie ich w tablicach, kolekcjach, czy przekazywanie innym metodom jako argumentów. 4 Klasy wewnętrzne ● Do statycznej klasy wewnętrznej mamy dostęp poprzez nazwę klasy otaczającej OuterClass.StaticNestedClass ● Np. aby stworzyć obiekt statycznej klasy wewnętrznej należy OuterClass.StaticNestedClass nestedObject = new OuterClass.StaticNestedClass(); ● Między obiektami statycznej klasy wewnętrznej a obiektami klasy otaczającej nie zachodzą żadne związki. Rozważmy klasy class OuterClass { ... class InnerClass { ... } } Dla nieprywatnych klas wewnętrznych możliwe jest odwoływanie się do nich spoza kontekstu klasy otaczającej: OuterClass.InnerClass 5 Klasy wewnętrzne ● ● ● Zawarcie klasy wewnętrznej w klasie otaczającej nie oznacza, że obiekty klasy otaczającej zawierają elementy (pola) obiektów klasy wewnętrznej. Obiekt niestatycznej klasy wewnętrznej zawiera referencję do obiektu klasy otaczającej, co umożliwia odwoływanie się do jej wszystkich składowych. Tworzenie obiektu niestatycznej klasy wewnętrznej wymaga zawsze istnienia obiektu klasy otaczającej. Mówi się, że obiekt klasy wewnętrznej opiera się na obiekcie klasy otaczającej. Dlatego tworzymy najpierw obiekt klasy otaczającej OuterClass outerObject = new OuterClass(); OuterClass.InnerClass innerObject = outerObject.new InnerClass(); lub jeśli nie potrzebujemy obiektu klasy zewnętrznej OuterClass.InnerClass innerObject = new OuterClass().new InnerClass(); Wewnątrz klasy otaczającej (OuterClass) tam, gdzie dostępne jest this, można po prostu napisać: InnerClass innerObject = new InnerClass(); 6 Klasy wewnętrzne- przesłanianie zmiennych public class ShadowTest { public int x = 0; // x pole klasy zewnętrznej class FirstLevel { public int x = 1;// x pole klasy wewnętrznej } void methodInFirstLevel(int x) { System.out.println("x = " + x);//x parametr metody System.out.println("this.x = " + this.x); System.out.println("ShadowTest.this.x = " + ShadowTest.this.x); } public static void main(String... args) { ShadowTest st = new ShadowTest(); ShadowTest.FirstLevel fl = st.new FirstLevel(); fl.methodInFirstLevel(23); } } Wyjście: x = 23 this.x = 1 ShadowTest.this.x = 0 7 Anonimowe klasy wewnętrzne Szczególną rolę odgrywają anonimowe klasy wewnętrzne. Klasy te nie mają nazwy. Najczęściej tworzymy je po to, by przedefiniować jakieś metody klasy bazowej przez klasę wewnętrzną bądź zdefiniować metody implementowanego przez nią interfejsu na użytek jednego obiektu. Referencję do tego obiektu chcemy traktować jako typu klasy bazowej lub typu implementowanego interfejsu. Nazwa takiej klasy wewnętrznej jest więc nam niepotrzebna i nie chcemy jej wymyślać. Wtedy stosujemy anonimowe klasy wewnętrzne. Definicja anonimowej klasy wewnętrznej : new NazwaTypu( parametry ) { // pola i metody klasy wewnętrznej } gdzie: ● NazwaTypu – nazwa nadklasy (klasy dziedziczonej w klasie wewnętrznej) lub implementowanego przez klasę wewnętrzną interfejsu, ● parametry – argumenty przekazywane konstruktorowi nadklasy; w przypadku, gdy Typ jest nazwą interfejsu lista parametrów jest oczywiście pusta (bo chodzi o implementację interfejsu). 8 Anonimowe klasy wewnętrzne Uwagi: ● anonimowe klasy wewnętrzne nie mogą mieć konstruktorów (bo nie mają nazwy), ● ● ● za pomocą anonimowej klasy wewnętrznej można stworzyć tylko jeden obiekt, bo definicja klasy podana jest w wyrażeniu new czyli przy tworzeniu obiektu, a nie mając nazwy klasy nie możemy potem tworzyć innych obiektów; jeśli jednak to wyrażenie new umieścimy np. w pętli – to oczywiście stworzone zostanie tyle obiektów ile razy wykona się pętla, definiowanie klas wewnętrznych implementujących interfejsy stanowi jedyny dopuszczalny przypadek użycia nazwy interfejsu w wyrażeniu new anonimowe klasy wewnętrzne są kompilowane do plików .class o nazwach automatycznie nadawanych przez kompilator (nazwa składa się z nazwy klasy otaczającej i jakiegoś automatycznie nadawanego identyfikatora) Klasy wewnętrzne (nazwane i anonimowe) mogą być definiowane w blokach lokalnych (np. w ciele metody). Będziemy je krótko nazywać klasami lokalnymi. Wewnętrzne klasy lokalne są doskonale odseparowane (nie ma do nich żadnego dostępu spoza bloku, w którym są zdefiniowane), a mogą odwoływać się do składowych klasy otaczającej oraz zmiennych lokalnych zadeklarowanych w bloku (pod warunkiem, że są one niezmienne). 9 Anonimowe klasy wewnętrzne - przykład Przykład: Listowanie katalogu z filtrowaniem nazw (pliki .java) Obiekty plikowe (pliki i katalogi) są obiektami klasy File z pakietu java.io. Wobec katalogu można użyć metody list z klasy File, która zwraca tablicę nazw plików (i podkatalogów) w nim zawartych. Używając metody list z argumentem typu FilenameFilter możemy określić kryteria filtrowania wyniku wg nazw (np. otrzymać tylko listę plików o rozszerzeniu .java). FilenameFilter jest interfejsem, w którym zawarto jedną metodę boolean accept(File dir, String filename). Musimy zatem mieć obiekt klasy implementującej FilenameFilter, w której to klasie zdefiniujemy metodę accept i podać referencję do tego obiektu jako argument metody list. Metoda accept będzie wtedy wywoływana dla każdego obiektu plikowego, zawartego w katalogu z argumentami – katalog, nazwa pliku lub podkatalogu. Powinniśmy ją zdefiniować w taki sposób, by zwracała true tylko wtedy, gdy nazwa spełnia wymagane przez nas kryteria, a w przeciwnym razie false. Naturalnym sposobem oprogramowania jest tu umieszczenie definicji anonimowej klasy wewnętrznej implementującej FilenameFilter w wyrażeniu new podanym jako argument metody list. A ponieważ listowanie umieszczamy w jakiejś metodzie, to ta anonimowa klasa będzie lokalną klasą wewnętrzną. 10 Anonimowe klasy wewnętrzne - przykład void listJavaFiles(String dirName) { // argument - nazwa katalogu File dir = new File(dirName); // katalog - obiekt typu File // listowanie z filtrowaniem nazw; kryteria wyboru nazw // podajemy za pomocę implementacji metody accept // w lokalnej anonimowej klasie wewnętrznej String[] fnames = dir.list( new FilenameFilter() { public boolean accept(File directory, String fname) { return fname.endsWith(".java"); } });// for (int i=0; i < fnames.length; i++) { // lista -> stdout System.out.println(fnames[i]); } } 11 Anonimowe klasy wewnętrzne – przykład 2 ● Lista plików z rozszerzeniem .java, które zmodyfikowano po 21 sierpnia. Calendar c=Calendar.getInstance(); c.set(2016,8,22,0,0); long time=c.getTimeInMillis(); File[] files = dir.listFiles( new FileFilter(){ public boolean accept(File file) { return file.isFile() && file.getName().endsWith(".java") && file.lastModified() >= time; } }); //w kodzie anonimowej klasy wewnętrznej odwołujemy się do //zmiennej lokalnej time, od wersji Java 8 nie musimy //takiej zmiennej deklarować jako final, taka zmiena musi //być effectively final - tzn. nie zmieniać swoich wartości, //klasa wewnętrzna otrzymuje jej kopię 12 Anonimowe klasy wewnętrzne – przykład 3 public class HelloWorldAnonymousClasses { interface HelloWorld { public void greet(); public void greetSomeone(String someone); } public void sayHello() { class EnglishGreeting implements HelloWorld { String name = "world"; public void greet() { greetSomeone("world"); } public void greetSomeone(String someone) { name = someone; System.out.println("Hello " + name); } } HelloWorld englishGreeting = new EnglishGreeting(); 13 Anonimowe klasy wewnętrzne – przykład 3 cd } //klasa anonimowa HelloWorld frenchGreeting = new HelloWorld() { String name = "tout le monde"; public void greet() { greetSomeone("tout le monde"); } public void greetSomeone(String someone) { name = someone; System.out.println("Salut " + name); } }; englishGreeting.greet(); frenchGreeting.greetSomeone("Fred"); public static void main(String[] args) { HelloWorldAnonymousClasses myApp = new HelloWorldAnonymousClasses(); myApp.sayHello(); } } 14 Opakowanie typów prostych Często występuje potrzeba traktowania liczb jako obiektów, np. kolekcje mogą zawierać tylko referencje do obiektów. Tymczasem liczby są reprezentowane przez typy proste (pierwotne) (i nie są obiektami). Dlatego w standardowym pakiecie java.lang umieszczono specjalne klasy opakowujące liczby (w ogóle wszystkie typy pierwotne) i czyniące z nich obiekty. Należą do nich następujące klasy: Long ● Integer ● Short ● Byte ● Double ● Float Obiekty tych klas reprezentują (w sposób obiektowy) liczby odpowiednich typów. Mówimy tu o opakowaniu liczby, bowiem liczba ta jest umieszczana "w środku" obiektu odpowiedniej klasy, jako jego element. ● We wcześniejszych wersjach Javy operatory arytmetyczne można było stosować tylko do liczbowych typów prostych. Zatem, gdy potrzebowaliśmy liczby jako obiektu, a jednocześnie typu prostego do wykonywania operacji arytmetycznych, to musieliśmy umieć zapakować liczbę do obiektu klasy opakowującej i wyciągnąć ją stamtąd. 15 Opakowanie typów prostych Typy proste możemy pakować i rozpakowywać "ręcznie", np. obiektowy odpowiednik liczby 5 typu int możemy uzyskać tworząc obiekt klasy Integer: Integer a = new Integer(5); Z obiektu takiej klasy możemy liczbę wyciągnąć za pomocą odpowiednich metod, np. int i = a.intValue(); // zwraca wartość typu int, //"zawartą" w obiekcie a Podobnie: Double dd = new Double(7.1); double d = dd.doubleValue(); // d == 7.1 Aby uprościć powyższe przekształcenia, w Javie 5 wprowadzono mechanizm zwany autoboxingiem. 16 Opakowanie typów prostych - Autoboxing Autoboxing to automatyczne przekształcanie między typami prostymi a typami obiektów klas opakowujących typy proste. Przykład: int n = 1; Integer in = n; //boxing, czyli automatyczne opakowanie // warości typu prostego w nowy obiekt klasy (nie musimy // już pisać new Integer(n), robi to za nas kompilator) n = in + 1 //unboxing - automatyczne pobranie wartości typu // prostego z obiektu klasy opakowującej(nie musimy już // pisać in.intValue() + 1) in += 5; //obiekty typów opakowujących są niemodyfikowalne // – powstaje nowy obiekt Automatyczne przekształcenia – autoboxing – zachodzą nie tylko przy przypisaniach, ale również przy przekazywaniu argumentów metodom i konstruktorom oraz przy zwrocie wyników z metod. Np. metoda Arrays.asList(...), która ma zmienną liczbę argumentów typu referencyjnego, może być zastosowana tak: Arrays.asList(1,2,11,12,23,24), umożliwiając w ten sposób szybką inicjację elementów niemodyfikowalnej listy liczb całkowitych (typ Integer). 17 Opakowanie typów prostych - Autoboxing ● ● Przy posługiwaniu się autoboxingiem należy zwrócić uwagę na możliwe przypadki wypakowania wartości typów prostych z nieistniejących obiektów klas opakowujących (gdy referencja na obiekt ma wartość null) Opakowywanie typów postych zachodzi również przy przypisaniach na zmienne typu Object, np. Object o = 1; spowoduje stworzenie obiektu klasy Integer opakowującego liczbę 1, jednak wypakowanie nie jest już automatyczne, przy próbie podstawienia int x = o;// błąd wystąpi błąd kompilacji, należy użyć rzutowania int x = (Integer) o;// ok ● Autoboxing nie działa też na tablicach, niedopuszczalne jest przypisanie: int[] a = {1,2,3}; Integer[] ia=a;// niedopuszczalne, ale Integer[] iaa={1,2,3}; // jest OK - autoboxing 18 Klasy opakowujące - metody ● ● ● W klasach opakowujących typy pierwotne zdefiniowano statyczne metody: public static ttt parseTtt(String s) zwracające wartości typu ttt reprezentowane przez napis s, gdzie: ttt nazwa typu pierwotnego (np. int, double), Ttt - ta sama nazwa z kapitalizowaną pierwszą literą (np. Int, Double) A więc, po to by przekształcić napis s reprezentujący liczbę rzeczywistą do typu double należy napisać: double d = Double.parseDouble(s); np. String s = "12.4"; double d = Double.parseDouble(s); // d == 12.4 Użytecznych dodatkowych metod dostarcza klasa Character (opakowująca typ char). Należą do nich metody stwierdzania rodzaju znaku: – isDigit() // czy znak jest znakiem cyfry – isLetter() // czy znak jest znakiem litery – isLetterOrDigit() // czy litera lub cyfra – isWhiteSpace() // czy to "biały" znak (spacja, // tabulacja etc.) – isUpperCase() // czy to wielka litera – isLowerCase() // czy to mała litera Metody te zwracają wartości true lub false. 19 Klasy opakowujące - stałe ● ● W klasach opakowujących typy numeryczne zdefiniowano także wiele użytecznych stałych statycznych. Należą do nich stałe zawierające maksymalne i minimalne wartości danego typu. Mają one nazwy MAX_VALUE i MIN_VALUE. Dzięki temu nie musimy pamiętać zakresów wartości danego typu. Możemy np. zsumować wszystkie dodatnie liczby typu short: public class MaxVal { public static void main(String[] args) { long sum = 0; for (int i=1; i <= Short.MAX_VALUE; i++) sum += i; System.out.println(sum); } } //licznik i nie może być typu short, bo po dojściu do //Short.MAX_VALUE i zwiększeniu licznika o 1 i otrzyma //wartość Short.MIN_VALUE (arytmetyczne przepełnienie) //pętla nieskończona 20 Typy i metody sparametryzowane (generics) Często konstrukcje różnych klas oraz metod w klasach są funkcjonalnie podobne (czyli służą wykonaniu tych samych czynności), różnią się natomiast tylko typami danych, na których czynności te są wykonywane. Po to, by nie powielać tego samego kodu dla różnych przypadków do języków programowania wprowadzono szablony (templates) klasy oraz metody parametryzowane typami przetwarzanych danych. W Javie - poczynając od wersji 1.5 - pojawił się odpowiednik szablonów tzw. generics. Przy wprowadzaniu koncepcji generics do Javy w dużo mniejszym stopniu akcent położono na uogólnianie kodu (pierwotny motyw szablonów C++). Dość wysoki stopień uogólniania kodu był i jest bowiem w Javie dostępny poprzez: ● zagwarantowane dziedziczenie klasy Object, ● implementację interfejsów, ● mechanizmy refleksji (czyli np. dynamiczne, w fazie wykonania programu, odwołania do pól i metod klas). 21 Typy i metody sparametryzowane (generics) Ale przy takim podejściu mamy pewne problemy: kompilator nie ma możliwości dokładnego sprawdzenia zgodności typów i błędy związane z użyciem niewłaściwego typu pojawią się dopiero w fazie wykonania, ponadto jesteśmy zmuszeni do stosowania konwersji zawężających, co czasem może być uciążliwe i zmniejsza czytelność kodu. Przykład. Możemy napisać ogólną klasę reprezentującą dowolne pary: class ParaObj { Object first; Object last; public ParaObj(Object f, Object l) {first = f; last = l;} public Object getFirst() { return first; } public Object getLast() { return last; } public void setFirst(Object f) { first = f; } public void setLast(Object l) { last = l; } } 22 Przykład (obrazujacy problemy) Następnie wykorzystać ją w następujący sposób: ParaObj po = new ParaObj("Ala", new Integer(3)); System.out.println(po.getFirst() + " " + po.getLast()); // Problem 1 - konieczne konwersje zawężające String name = (String) po.getFirst(); int nr = (Integer) po.getLast(); po.setFirst(name + " Kowalska"); po.setLast( new Integer(nr + 1)); System.out.println(po.getFirst() + " " + po.getLast()); // Problem 2 - możliwe błędy po.setLast("kot"); System.out.println(po.getFirst() + " " + po.getLast()); // Błąd może być wykryty w fazie wykonania // późno, czasem w innym module Integer n = (Integer) po.getLast(); //ClassCastException 23 Typy i metody sparametryzowane (generics) Zastosowanie generics (poprzez parametryzację typów) do dotychczasowych możliwości uogólniania kodu dodaje rozwiązanie w/w problemów. Parametryzowane mogą być: ● typy tj. klasy lub interfejsy ● metody 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 class | interface ParametrTypuN> { //.... } Nazwa < ParametrTypu1, ParametrTypu2, ... 24 Typy i metody sparametryzowane - przykład class Para<S, T> { S first; T last; public Para(S f, T l) { first = f; last = l; } public S getFirst() { return first; } public T getLast() { return last; } public void setFirst(S f) { first = f; } public void setLast(T l) { last = l; } } Możemy teraz tworzyć różne pary: Para<String, String> p1 = new Para<String, String> ("Jan", "Kowalski"); Para<String, Date> p2 = new Para<String, Date> ("Jan Kowalski", new Date()); Para<Integer, Integer> p = new Para<Integer, Integer>(1,2); 25 Typy i metody sparametryzowane Para<String, String>, Para<String, Date> Para<Integer, Integer> - to konkretyzacje sparametryzowanej klasy Para<S, T>,zaś <String, String> - argumenty typu. Przy tworzeniu konkretnych instancji w wyrażeniu new do wersji Java 7 musieliśmy po obu stronach przypisania podawać typy, np. Para<String, String> p1 = new Para<String, String> ("Jan","Kowalski"); w wersji Java 7 wprowadzono operator <> (diamond operator): Para<String, String> p1 = new Para<> ("Jan","Kowalski"); Argumenty typu potrzebne w new są określane przez kompilator na podstawie typów argumentów konstruktora, a jeśli ich nie ma, to na podstawie typu argumentów podanych z lewej strony przypisania. W Javie 8 konkludowanie typów przy pomocy operatora <> poszerzono. Umożliwiono m.in. określenie ich przez kompilator nawet, gdy wyrażenie new nie ma ani lewej strony, ani argumentów konstruktora: ArrayList<String> metoda(ArrayList<String> list){ list.add("a"); return list; } System.out.println(metoda(new ArrayList<>())) ; //w Javie 8 wyprowadzi [a], wcześniej to był błąd kompilacji 26 Typy sparametryzowane Argumentami typu nie mogą być typy proste, muszą je zastąpić typy opakowujące. Przy podawaniu argumentów konstruktora możemy podawać typy proste, zadziała wówczas autoboxing. Zastosowanie sparametryzowanej klasy Para umożliwia unikanie konwersji zawężających (jak przy dziedziczeniu po Object), bo kompilator zna argumenty typu i dopisuje za nas odpowiednie rzutowania: Para<String, Integer> 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); // autoboxing System.out.println(pg.getFirst() + " " + pg.getLast()); Wyjście: Ala 3 Ala Kowalska 4 27 Typy surowe i czyszczenie typów W Javie - inaczej niż w C++ - po kompilacji dla każdego "szablonu" (typu sparametryzowanego) powstaje tylko jedna klasa (plik klasowy), współdzielona przez wszystkie konkretyzacje tego typu sparametryzowanego. import java.lang.reflect.*; class Para<S, T> { static int nr = 0; S first; T last; public static int getNr() { return nr; } public Para(S f, T l) { first = f; last = l; nr++; } public S getFirst() { return first; } public T getLast() { return last; } public void setFirst(S f) { first = f; } public void setLast(T l) { last = l; } } 28 Typy surowe i czyszczenie typów public class GenTest { public static void main(String[] args) { Para<String, Integer> p1 = new Para<>("Ala", 3); System.out.println(p1.getNr()); //1 Para<String, String> p2 = new Para<>("Ala","Kowalska"); System.out.println(p2.getNr()); //2 // Mamy tylko klasę Para surowego typu "Raw Type" Class p1Class = p1.getClass(); System.out.println(p1Class); //class Para // Metodami refleksji możemy się przekonać, że // w definicji klasy Para typem fazy wykonania dla // parametrów jest Object //"type erasure" - czyszczeniem typów 29 Typy surowe i czyszczenie typów Method[] mets = p1Class.getDeclaredMethods(); // zwraca tablicę metod deklarowanych w klasie for (Method m : mets) System.out.println(m); // Surowego typu ("Raw Type") możemy też używać. Para p = new Para("B", new Double(3.1)); String f = (String) p.getFirst(); double d = (Double) p.getLast(); System.out.println(f + " " + d);//B 3.1 // // // // // // Zwróćmy uwagę, że wykorzystanie typów surowych nie jest bezpieczne - kompilator nie jest w stanie sprawdzić zgodności typów. Dlatego w fazie kompilacji wyda ostrzeżenie. W programie nie korzystamy z typu Para<String, Double>, ale możemy przez pomyłkę stworzyć taki obiekt } } 30 Typy surowe i czyszczenie typów Po jego uruchomieniu uzyskamy następujący wydruk. 1 2 class Para public static int Para.getNr() 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) B 3.1 Wydruk ten oznacza, że: ● jest tylko jedna klasa Para dla wszystkich konkretyzacji klasy sparametryzowanej Para<S, T>; typ wyznaczany przez tę klasę nazywa się typem surowym ("raw type"), ● z definicji klasy Para zniknęły wszystkie parametry typu i zostały zastąpione przez Object; ten mechanizm nazywa się czyszczeniem typów ("type erasure") , ● ponieważ jest tylko jedna klasa Para - zmienne reprezentowane przez pola statyczne są wspólne dla wszystkich instancji typu sparametryzowanego; zmienna nr jest wspólna dla Para<String, Integer> i Para<String, String> - dlatego zwiększa się w sposób ciągły (1, 2). 31 Generics - restrykcje Przyjęte w Javie rozwiązanie (jedna klasa w fazie wykonania, czyszczenie typów) ma swoje zalety i wady: Do zalet zaliczyć można: ● ● mniejszą liczbę klas po kompilacji, zgodność kodu binarnego z kodem nie używającym "generics" ("czyszczenie typów" stanowi właśnie o kompatybilności kodów używających generics z kodami nie używającymi ich). Do wad zaliczymy ograniczenia na możliwości użycia parametrów typu i parametryzacji typów. Nie można parametryzować: ● enumeracji (bo generalnie są to typy statyczne, a parametrów typu nie można używać w kontekstach statycznych), ● klas anonimowych (bo nie można tworzyć ich obiektów przez new, a zatem nie można podac konkretnych typów, które zastąpią parametry), ● klas wyjątków (bo mechanizm wyjątków jest mechanizmem fazy wykonania, a JVM nie wie nic o generics). 32 Generics - restrykcje Ze sposobu kompilacji "generics" wynika, że parametry typu są symbolicznymi oznaczeniami typów, ale przy definiowaniu szablonów - inaczej niż w C++ - nie możemy traktować ich dokładnie tak samo jak normalne typy. Argumenty typu Nie możemy używać jako argumentów typu typów prostych (int, double itp.), co jest jednak łagodzone (ale tylko na poziomie wykorzystania instancji klas sparametryzowanych) autoboxingiem. Możemy używać jako argumentów typu: ● – nazw klas, w tym enumeracji (enum), – nazw interfejsów, – nazw typów sparametryzowanych. Parametry typu 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 (ale to będzie tylko ważne na etapie kompilacji, po to by uniknąć błędów niezgodności typów, natomiast nie uzyskamy w fazie wykonania faktycznych konwersji np. zawężających), wywoływać na rzecz zmiennych oznaczanych typami sparametryzowanymi metody klasy Object (i ew. właściwe dla klas i interfejsów, które stanowią tzw. górne ograniczenia danego parametru typu). 33 Generics - restrykcje Nie możemy (w definicjach sparametryzowanych klas i metod): tworzyć obiektów typów sparametryzowanych (new T() jest niedozwolone, no bo na poziomie definicji generics nie wiadomo co to konkretnie jest T), ● używać operatora instanceof ( z powodu j.w.), ● używać ich w statycznych kontekstach (bo statyczny kontekst jest jeden dla wszystkich różnych instancji typu sparametryzowanego), ● wywoływać metod z konkretnych klas i interfejsów, które nie są zaznaczone jako górne ograniczenia parametru typu (w najprostszym przypadku tą górną granicą jest Object, wtedy możemy używać tylko metod klasy Object). Ograniczenia te powodują, że np. funkcjonalność szablonu Para<S, T> nie może być zbyt wysoka. Na pewno możemy dodać metody toString(), hashCode() i equals() - bo występują w klasie Object i kompilator nie będzie protestował. ● public boolean equals(Para<S,T> p) { return first.equals(p.first) && last.equals(p.last); } public String toString() {return first + " " + last;} Większą (ogólną) funkcjonalność, np. operacje kopiowania par, dodawania ich do siebie można osiągnąć korzystając z refleksji lub ograniczeń typów. 34 Generics - restrykcje Również użycie typów sparametryzowanych obarczone jest restrykcjami. Nie wolno używać typów sparametryzowanych np.: ● w obsłudze wyjątków (bo jest to mechanizm fazy wykonania). ● przy tworzeniu tablic (podając je jako typ elementu tablicy). Wynika to z istoty pojęcia tablicy oraz ze sposobu kompilacji generics. Tablica jest zestawem elementów tego samego typu (albo jego podtypu). Informacja o typie elementów tablicy jest przechowywana i JVM korzysta z niej w fazie wykonania, aby zapewnić, że do tablicy nie jest wstawiany element niewłaściwego typu (wtedy generowany jest wyjątek ArrayStoreException).Gdyby dopuścić tablice elementów typów sparametryzowanych nie można by zapewnić odpowiedniej dynamicznej kontroli typów, bowiem w fazie wykonania nic nie wiadomo o konkretnych instancjach typów sparametryzowanych. Para<String, Integer>[] pArr = new Para<>[5]; //niedozwolone! Oczywiście, to ograniczenie można obejść, stosując następujące rozwiązania: – tablice typów surowych (niebezpieczne), – tablice uniwersalnych instancji typów sparametryzowanych wprowadzanych z użyciem parametru typu ? (co oznacza dowolny typ); ale one też nie są dobrym rozwiązaniem - choć semantycznie są zbliżone do typów surowych, to składniowa różnica powoduje, że są przez kompilator traktowane inaczej niż typy surowe i w wielu przypadkach zamiast ostrzeżeń "unchecked cast" dostaniemy raczej błędy w kompilacji, – i rozwiązanie najlepsze - zastosowanie kolekcji (list) konkretnych instancji typu sparametryzowanego. 35 Ograniczenia parametrów typu ● Jednym ze sposobów zwiększania funkcjonalności generics Javy (poza wykorzystaniem refleksji) jest użycie (jawnych) ograniczeń parametrów typu. Dzięki temu w klasach i metodach sparametryzowanych możemy korzystać z metod, specyficznych dla podanych ograniczeń. Ograniczenie parametru typu określa zestaw typów, które mogą być używane jako argumenty typu (i podstawiane w szablonie w miejscu parametrów typu), a w konsekwencji zestaw metod, które mogą być wywoływane na rzecz zmiennych oznaczanych parametrami typu ● ● Ograniczenia parametru typu wprowadzamy za pomocą składni: ParametrTypu extends Typ1 & Typ2 & Typ3 & ... & TypN gdzie: – Typ1 - nazwa klasy lub interfejsu – Typ2-TypN - nazwy interfejsów Uwaga: – typy Typ1-TypN mogą być sparametryzowane, – typy ograniczające nie mogą się powtarzać, w tym nie mogą występować powtórzenia dla typów sparametryzowanych TP<X> TP<Y> (ze względu na czyszczenie typów) 36 Ograniczenia parametrów typu ● ● W przypadku ograniczanych parametrów typu "type erasure" daje typ pierwszego ograniczenia. Np. w fazie wykonania, w kontekście class A <T extends Appendable>, T staje się Appendable. Przykład public class SpecialNumber<T extends Number> { private T n; public SpecialNumber(T n) { this.n = n; } public boolean isEven() { return n.intValue() % 2 == 0; //intValue - metoda klasy Number } // ... } 37 Parametry uniwersalne (wildcards) ● W Javie ArrayList<Integer> i ArrayList<String> nie są podtypami ArrayList<Object>. public class TestGenerics { public static void main(String[] args) { Integer i =5; Object o = i; //OK ArrayList<Integer> ai = new ArrayList<>(); ai.add(i); } ● } ArrayList<Object> ao = ai; //błąd, bo to by dopuściło: ao.add("test"); // czyli ai.add("test"); W Javie pomiędzy typami sparametryzowanymi za pomocą konkretnych parametrów nie zachodzą żadne relacje w rodzaju dziedziczenia (typ-nadtyp itp.). A jednak takie relacje są czasem potrzebne. Jeśli List<Integer> i List<String> nie są podtypami List<Object>, to jak stworzyć metodę wypisującą zawartośc dowolnej listy? 38 Parametry uniwersalne (wildcards) Do tego służą parametry uniwersalne (wildcards) - oznaczenie "?". ● ● ● ● Są trzy typy takich parametrów: – ograniczone z góry <? extends X> - oznacza "wszystkie podtypy X" – ograniczone z dołu <? super X> - oznacza "wszystkie nadtypy X" – nieograniczone <?> - oznacza "wszystkie typy" Notacja ta wprowadza do Javy wariancję typów sparametryzowanych. Typ sparametryzowany C<T> jest kowariantny względem parametru T, jeśli dla dowolnych typów A i B, takich, że B jest podtypem A, typ sparametryzowany C<B> jest podtypem C<A> (kowariancja - bo kierunek dziedziczenia typów sparametryzowanych jest zgodny z kierunkiem dziedziczenia parametrów typu) Kowariancję uzyskujemy za pomocą symbolu <? extends X>, co oznacza np. że List<? extends Number> jest nadtypem wszystkich typów sparametryzowanych, gdzie parametrem typu jest Number albo typ pochodny od Number. 39 Parametry uniwersalne (wildcards) ● ● Typ sparametryzowany C<T> jest kontrawariantny względem parametru T, jeżeli dla dowolnych typów A i B, takich że B jest podtypem A, typ sparametryzowany C<A> jest podtypem typu sparametryzowanego C<B> (kontra - bo kierunek dziedziczenia jest przeciwny). Kontrawariancję uzyskujemy za pomocą symbolu <? super X>. Np. Integer jest podtypem Number, a List<Number> jest podtypem List<? super Integer>, wobec czego możemy podstawiać: List<? super Integer> list = new ArrayList<Number>(); ● ● Biwariancja oznacza równoczesną kowariancję i kontrawariancję typu sparametryzowanego Biwariancję uzyskujemy za pomocą symbolu <?>, który oznacza wszystkie typy. Faktycznie List<?> oznacza wszystkie możliwe listy z dowolnym parametrem T. Czyli List<?> jest nadtypem List<? extends Integer> i nadtypem dla List<? super Integer>. 40 Parametry uniwersalne (wildcards) ● Kowariancja typów sparametryzowanych umożliwia pisanie uniwersalnych metod (w rodzaju "wypisz dowolną kolekcję" albo "pokaż dowolną listę", Np. void showEmployee(ArrayList <? extends Pracownik> a) { // ... } Bez tego nie moglibyśmy np. jako argumentów przekazywać listy dyrektorów, kierowników, asystentów etc. ( bo między ArrayList<Pracownik> i ArrayList<Asystent> nie ma relacji nadtyp – podtyp). ● ● Ale jeśli mamy gdzieś dostęp do typu sparametryzowanego <? extends X>, to zabronione jest podstawianie na ten typ konkretniejszych podtypów. Inaczej mielibyśmy sytuację, w której do przekazanej listy dyrektorów dopisywani mogliby być np. asystenci. Nie możemy "podstawiać", ale możemy pobierać (dostajemy coś całkiem bezpiecznego typu - np. typu wyznaczanego przez dolną granicę). 41 Wildcards- przykład public class TestGenerics { static class A { } static class B extends A { } static class C extends B { } static class D extends B { } public static void main(String[] args) { ArrayList<A> aa = new ArrayList<>(); List<? super B> list = aa; C c = new C(); list.add(c); //B g = list.get(0);//blad Object g1 = list.get(0); } } ArrayList<C> ae = new ArrayList<>(); List<? extends B> liste = ae; C ce = new C(); //liste.add(ce);//blad B g2 = liste.get(0); 42 Metody sparametryzowane i konkludowanie typów Parametryzacji mogą podlegać nie tylko klasy czy interfejsy, ale również metody. ● Definicja metody sparametryzowanej ma postać: specyfikatorDostępu [static] <ParametryTypu> typWyniku nazwa(lista parametrów) { } ● ● // … 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). PRZYKŁAD. Poniższy program zawiera przykład sparametryzowanej metody wyznaczającej maksimum z tablicy elementów dowolnego typu pochodnego od Comparable. Konkretne argumenty typu (odpowiadające parametrowi T użytemu zarówno na liście parametrów metody, jak i jako typ jej wyniku) są konkludowane z wywołań. 43 Metody sparametryzowane i konkludowanie typów public class Metoda { public static <T extends Comparable<T>> T max(T[] arr) { T max = arr[0]; for (int i=1; i<arr.length; i++) if (arr[i].compareTo(max) > 0) max = arr[i]; return max; } public static void main(String[] args) { Integer[] ia = { 1, 2, 77 }; int imax = max(ia); // w wyniku konkluzji T staje się Integer Double[] da = {1.5, 231.7 }; double dmax = max(da); // w wyniku konkluzji T staje się Double } System.out.println(imax + " " + dmax); } 44