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