30 kwietnia 2008

SCJP - Implementacja interfejsu

Tym razem to właściwie tylko małe uzupełnienie w kwestii implementacji interfejsów. Uzupełnienie tego, z czym od dłuższego czasu się zmagam a co rozpocząłem artykułem "Przygotowania do SCJP czas zacząć".

Było już o tym, że jeśli klasa implementująca interfejs nie jest abstrakcyjna to musi implementować wszystkie zadeklarowane metody nie zaimplementowane jeszcze przez abstrakcyjne nadklasy. Wreszcie było też o tym, że klasa może implementować wiele interfejsów, choć rozszerzać może tylko jedną klasę, ale nie było o jednym – co to właściwie znaczy zaimplementować metody zadeklarowane w interfejsie, inaczej: jak dokładnie musi wyglądać metoda implementująca tą zadeklarowaną w interfejsie? Zacznijmy od przykładu.

interface SomeInf {
Number doSomething() throws Exception;
}

class SomeClass implements SomeInf {
public Integer doSomething() {
return 1;
}
}

Czy powyższy kod jest poprawny? Tzn. czy metoda doSomething() z klasy SomeClass rzeczywiście implementuje tą zadeklarowaną w interfejsie? Tak, kod ten jest poprawny, albowiem sygnatury nie muszą być identyczne, wystarczy, że są zgodne. Jakby się nad tym dłużej zastanowić, to można by dojść do wniosku, że implementacja metody to jakby jej nadpisanie (ang. overriding), tzn. nadpisanie metody nie posiadającej implementacji, metodą z implementacją. I ma to jakiś sens, jako że w obu tych przypadkach obowiązują dokładnie te same zasady. Metody implementujące interfejsy obowiązują dokładnie te same zasady, co metody nadpisujące metody z nadklasy i są one szczegółowo opisane w artykule "SCJP - Redefiniowanie metod".

SCJP - Rzutowanie referencji

Rzutowanie referencji, czyli streszczenie sekcji "Reference Variable Casting" – kontynuacja działań zapowiedzianych w artykule "Przygotowania do SCJP czas zacząć".

Wiemy już, że typ zmiennej referencyjnej i typ obiektu wskazywanego przez tą zmienną nie koniecznie są tym samym typem – typ obiektu może być także podtypem zmiennej, albo dowolnym typem implementującym, jeśli zmienna ma typ pewnego interfejsu. Zerknijmy na poniższy przykład.

class Parent {
void doSomething() { }
}

class Child extends Parent {
void doMore() { }
}

public class Test {
public static void main(String[] args) {
Parent obj = new Child();

obj.doSomething();
((Child)obj).doMore();
}
}
Zmienna obj jest typu Parent, ale wskazuje na obiekt typu Child. Jak pisałem w artykule "SCJP – Dziedziczenie i wielodziedziczenie" o tym, jakie metody możemy wywołać na danym obiekcie decyduje typ referencji, a nie typ obiektu. Aby więc wywołać metodę doMore() zdefiniowaną w klasie Child trzeba wykonać rzutowanie (ang. casting), tak jak to pokazano w powyższym przykładzie. Jeśli byśmy tego rzutowania nie wykonali, to kod by się nie skompilował, byłby to więc błąd czasu kompilacji. Ale z rzutowaniem związane są także błędy czasu wykonania. Rozważmy następującą modyfikację klasy Test.

public class Test {
public static void main(String[] args) {
Parent obj = new Parent();

((Child)obj).doMore(); // Błąd czasu wykonania!
}
}
W czasie kompilacji wszystko jest w porządku, problem pojawia się po uruchomieniu programu – dostajemy wyjątek java.lang.ClassCastException. Rzeczywiście, obiekt wskazywany przez zmienną obj jest klasy Parent a nie Child. Kompilator zakłada, że wiemy co robimy wykonując rzutowanie i ślepo ufa, że w czasie wykonania zmienna będzie wskazywała na obiekt odpowiedniego typu. Nie ma zresztą innego wyjścia, wykonanie takiego sprawdzenia w czasie kompilacji było by często zbyt skomplikowane, a nawet nie możliwe. Aczkolwiek możliwa jest statyczna weryfikacja w pewnym zakresie. Kompilator zaprotestuje, jeśli wskazane rzutowanie nie ma szans na powodzenie, tj. jeśli typ rzutowania nie jest podtypem albo nadtypem typu zmiennej. Ilustruje to poniższy przykład.

public class Test {
public static void main(String[] args) {
Number num = new Integer(1);

String str = ((String)num); // Błąd kompilacji!
}
}
Obiekt typu Number oraz żaden jego podtyp nigdy nie może być potraktowany jako String, tak więc powyższy kod skutkuje błędem czasu kompilacji.

Myśląc o rzutowaniu zwykle mamy na myśli rzutowanie uszczegóławiające (ang. downcasting), tj. rzutowanie zmiennej do pewnego typu pochodnego, ale możliwe jest też rzutowanie uogólniające (ang. upcasting) do typu nadrzędnego, tak jak to pokazano poniżej, choć nie ma ono większego sensu.

public class Test {
public static void main(String[] args) {
Number num = new Integer(1);

((Object)num).toString();
}
}

29 kwietnia 2008

SCJP - Przeciążanie metod

Kolejny zastrzyk wiedzy przed egzaminem SCJP – kontynuacja dzieła zapowiedzianego w artykule "Przygotowania do SCJP czas zacząć". Dziś będzie o przeciążaniu metod (ang. overloading).

Przeciążanie metod to definiowanie wielu metod o tej samej nazwie a różniących się listą argumentów. W zasadzie o metodzie przeciążającej – tj. używającej tej samej nazwy, co już istniejąca metoda – można myśleć jak o metodzie zupełnie nowej. Nie ma więc żadnych zasad ograniczających zwracany typ, deklarowane wyjątki czy modyfikatory dostępu, jako to ma miejsce w przypadku redefiniowania (ang. overriding). W zasadzie można by to sformułować następująco: jeśli chcemy zdefiniować metodę która będzie się nazywała tak samo jak inna, już istniejąca metoda to musimy określić inną listę argumentów. To tyle.

Mamy więc w naszej klasie zdefiniowanych kilka metod o tej samej nazwie, skąd więc wiadomo, którą z nich wywołujemy w danej chwili? Naturalnie, na podstawie listy parametrów, tyle, że nie zawsze sprawa jest bardzo prosta. Weźmy dla przykładu następujący program.

class Test {
public static void main(String[] args) {
Test obj = new Test();

System.out.println(obj.getString(1));
}

String getString(float f) {
return "float value: " + f;
}

String getString(long l) {
return "long value: " + l;
}
}
Jaki jest efekt jego działania? Która z metod getString() zostanie wywołana? W tym wypadku efektem działania programu będzie napis "long value: 1". Autorzy książki nie podali w rozdziale drugim precyzyjnych reguł, ale intuicyjnie, wywoływana jest ta metoda, której argumenty "bardziej odpowiadają" przekazywanym parametrom. Szczegóły mają się pojawić w dalszych rozdziałach i póki co nie będę wybiegał przed szereg – poczekam. Popatrzmy jednak na jeszcze jeden przykład.

class Parent { }

class Child extends Parent { }

class Test {
public static void main(String[] args) {
Parent obj = new Child();

test(obj);
}

static void test(Parent p) {
System.out.println("method for Parent");
}

static void test(Child c) {
System.out.println("method for Child");
}
}
Efektem działania będzie napis "method for Parent", mimo, że obiekt, dla którego wywołano metodę test() jest klasy Child. Powód jest prosty – to, która z przeciążonych metod zostanie wywołana jest określane w czasie kompilacji, kiedy to nie wiadomo jeszcze, jaki będzie rzeczywisty obiekt, a więc o wywoływanej metodzie decyduje typ referencji nie zaś typ obiektu.

25 kwietnia 2008

SCJP - Redefiniowanie metod

Jeszcze trochę i zakończymy przerabianie drugiego rozdziału książki "SCJP Sun Certified Programmer for Java 5 Study Guide (Exam 310-055)" – patrz artykuł "Przygotowania do SCJP czas zacząć". Dziś będzie o przedefiniowywaniu metod (ang. overriding).

Redefiniowanie metody to implementacja na nowo metody, którą odziedziczyliśmy z klasy nadrzędnej. Jeśli implementujemy interfejs czy dziedziczymy z klasy abstrakcyjnej to nie ma wyjścia, metody trzeba zaimplementować, jeśli natomiast dziedziczymy metody wraz z implementacją to mamy wybór: możemy zaakceptować tą implementację, lub dostarczyć nową, odpowiednią dla danej podklasy. Metoda redefiniująca metodę odziedziczoną musi być z nią zgodna co do listy argumentów, modyfikatorów dostępu, deklarowanych wyjątków i zwracanego typu, jednak zgodna to nie oznacza identyczna. Reguły są następujące:

• Modyfikator dostępu dla metody redefiniującej nie może być bardziej restrykcyjny niż metody redefiniowanej. Musi być taki sam albo mniej restrykcyjny.
• Argumenty metody redefiniującej muszą być dokładnie takie same jak argumenty metody redefiniowanej.
• Zwracany typ metody redefiniującej musi być albo taki sam, albo być podtypem typu zwracanego przez metodę redefiniowaną – musi być z nim kompatybilny pod względem przypisania.
• Metoda redefiniująca nie może deklarować wyjątków, które nie były zadeklarowane w metodzie redefiniowanej jednak może deklarować ich mniej – zbiór wyjątków deklarowanych przez metodę redefiniującą musi być podzbiorem wyjątków deklarowanych przez metodę redefiniowaną. Ale uwaga, tutaj znowu nie chodzi o to żeby były to dokładnie te same wyjątki. Mogą być także wyjątki będące podtypami tych zadeklarowanych w metodzie przedefiniowywanej. Dla przykładu, poniższy kod jest poprawny, ponieważ zarówno IOException jak i SQLException są podklasami klasy Exception.

class Parent {
void test() throws Exception { }
}

class Child extends Parent {
@Override // dzięki tej adnotacji mamy pewność, że redefiniujemy
void test() throws IOException, SQLException { }
}

• Naturalnie, nie można przedefiniować metody, która została oznaczona jako finalna, tj. została oznaczona słówkiem kluczowym final – właśnie do tego, by redefiniowania zabronić to słówko kluczowe służy.
• Nie można przedefiniować metody statycznej, tj. oznaczonej słówkiem kluczowym static. Metody takie nie podlegają polimorfizmowi, więc zwyczajnie nie miałoby to sensu.

Ale co to właściwie oznacza, że metoda redefiniująca musi spełniać określone wymagania? Pod jakim rygorem? Jaki jest skutek nie przestrzegania tych zasad? Rozważmy następujące klasy.

class Parent {
static String getDescr() {
return "descr for Parent class";
}
}

class Child extends Parent {
static String getDescr() {
return "descr for Child class";
}
}

Czy metoda getDescr() została prawidłowo przedefiniowana? Nie! Czy powyższy kod się skompiluje, tj. czy jest poprawny? Tak! O co więc chodzi? Ano o to, że metoda getDescr() w klasie Child jest zupełnie inną metodą niż metoda getDescr() w klasie Parent. Obie metody są zdefiniowane poprawnie tyle, że jedna nie przedefiniowuje drugiej, po prostu, są to zupełnie dwie różne metody. Metody statyczne nie podlegają przedefiniowaniu. Zerknijmy na kolejny przykład.

class Parent {
String getDescr(String str) {
return str;
}
}

class Child extends Parent {
String getDescr(Object obj) {
return obj.toString();
}
}

Tym razem metoda getDescr() z klasy Parent nie jest statyczna, więc można ją przedefiniować, ale czy powyższy kod jest poprawnym przedefiniowaniem? Nie! Rzeczywiście, nie zgadza się lista argumentów, nie jest ona identyczna. Czy kod się kompiluje? Tak! W jakim sensie zatem przedefiniowanie to jest niepoprawne? Otóż kod jest poprawny, ale nie jest to przedefiniowanie (ang. overriding) tylko przeciążenie (ang. overloading). Skutek jest więc taki, że w klasie Child zdefiniowane są dwie metody o tej samej nazwie, ale różniące się argumentami. Więcej o przeciążaniu w kolejnym artykule. I jeszcze ostatni przykład.

class Parent {
String getDescr() {
return "descr for Parent class";
}
}

class Child extends Parent {
String getDescr() throws Exception {
return "descr for Child class";
}
}

Czy jest to poprawne przedefiniowanie? Nie! Czy kod się kompiluje? Niespodzianka – też nie! Metoda w klasie Child deklaruje wyjątek nie zadeklarowany w metodzie z klasy Parent, więc nie jest to poprawne przedefiniowanie. Nie jest to też przeciążenie, jako że metody nie różnią się listą argumentów, zatem nie pozostaje nic innego jak tylko zgłosić błąd kompilacji. W ostatnim przykładzie nieudana próba przedefiniowania metody skutkuje brakiem możliwości kompilacji kodu, wydawałoby się więc, że jest to przypadek najgorszy, ale czy na pewno? To chyba dobrze, że wiemy, że nasz kod nie robi tego, co się nam wydaje, że robi. Jeśli piszemy jakąś metodę z tym zamiarem, aby była ona przedefiniowaniem metody z nadklasy to chcielibyśmy, żeby kompilator powiedział nam, że popełniliśmy błąd i w istocie jest inaczej. Właśnie po to jest adnotacja @Override. Jeśli umieścimy ją nad jakąś metodą to kompilator sprawdzi, czy rzeczywiście przedefiniowuje ona jakąś metodę z nadklasy i zgłosi błąd kompilacji, jeśli jest inaczej. Nie jest to coś, o co mogą nas zapytać na egzaminie SCJP 5 – bo nie pytają na nim o adnotacje – ale warto wiedzieć.

Implementując metodę redefiniującą możemy wywołać metodę redefiniowaną, tj. z nadklasy, wywołując ją ze słówkiem kluczowym super. Przykład poniżej.

class Parent {
String getDescr() {
return "descr for Parent class";
}
}

class Child extends Parent {
String getDescr() {
return "descr for Child class and " + super.getDescr();
}
}

Na zakończenie odnotujmy jeszcze jeden fakt dotyczący zadeklarowanej listy wyjątków. Spójrzmy na poniższy kod.

class Parent {
String getDescr() throws Exception {
return "descr for Parent class";
}
}

class Child extends Parent {
String getDescr() {
return "descr for Child class";
}
}

class Other {
void test() {
Parent obj = new Child();

obj.getDescr(); // Nie obsługiwany wyjątek!
}
}

Kod ten nie kopiluje się. Powodem jest nie obsługiwany wyjątek. Zmienna obj wskazuje na obiekt klasy Child którego metoda getDescr() nie deklaruje wyjątków, ale nie ma to znaczenia. Istotny jest typ zmiennej, a jest to Parent. Metoda getDescr() w klasie Parent deklaruje wyjątek i musi on być obsługiwany, jeśli posługujemy się zmienną tego typu.

24 kwietnia 2008

SCJP – Hermetyzacja, zależność i spójność

Dziś parę słów o terminach, w których wyraża się jakość programu obiektowego – kontynuacja akcji zapowiedzianej w artykule "Przygotowania do SCJP czas zacząć". Nie było łatwo dobrać tłumaczenia do anglojęzycznych zwrotów, ale myślę, że ostatecznie się udało. Będzie więc o hermetyzacji (ang. encapsulation), zależności (ang. coupling), która im prostsza tym lepsza (ang. loose coupling) i spójności (ang. cohesion) która z kolei powinna być możliwie duża (ang. high cohesion).

Na początku zadajmy sobie pytanie – czemu te pojęcia są ważne? Ludzkość nie bez przyczyny przestawiła się na programowanie obiektowe, tą przyczyną nie była też li tylko moda. Programowanie obiektowe ma ułatwiać tworzenie oprogramowania i rzeczywiście ułatwia, ale tylko pod warunkiem, że programy obiektowe są napisane – nie wymagajmy zbyt wiele – chociaż przyzwoicie. I właśnie temu służą omawiane dziś pojęcia – określeniu, jak należy programować, żeby uzyskać z obiektowości maksymalne korzyści. Program obiektowy składa się z klas a pojęcia te określają, jakie te klasy powinny być.

Klasy powinny być hermetyczne. Oznacza to, że klasy powinny ukrywać swoje wewnętrzne struktury i operacje a udostępniać tylko te, dla których udostępnienia zostały powołane, tj. swoje dobrze określone interfejsy. Dla przykładu, jeśli dla implementacji wymaganych mechanizmów posłużyliśmy się pewną strukturą danych, albo kilkoma strukturami to nie powinny być one "widoczne z zewnątrz". Jest to bowiem wewnętrzna sprawa klasy jak jej mechanizmy zostały zaimplementowane i poprzez nie ujawnianie tych szczegółów chcemy zagwarantować, że sposób implementacji będzie można w przyszłości zmienić bez obawy o konieczność modyfikacji innych klas. Tutaj uwaga! Niska hermetyczność klas to tylko potencjalne ryzyko pojawienia się nieprzewidzianych i niepożądanych zależności, natomiast stan faktyczny istnienia takowych to zupełnie inna sprawa. W języku Java hermetyzację osiągamy poprzez odpowiednie stosowanie modyfikatorów dostępu – w miarę możliwości jak najbardziej restrykcyjne. Przejawem dążenia do wysokiej hermetyzacji są też właściwości (ang. properties) znane ze specyfikacji JavaBeans – wszystkie zmienne instancyjne powinny być prywatne i "z zewnątrz" dostępne poprzez metody typu get i set. Klasy powinny być hermetyczne także dlatego, aby nie było możliwe tworzenie instancji reprezentujących niespójny, niepożądany stan. Rozważmy poniższy przykład, klasę reprezentującą wniosek kredytowy.

class LoanApplication {
public float profitMargin;

public long amount;

public LoanApplication(long amount) {
if(amount < Config.AMOUNT_LOW) {
profitMargin = Config.PROFIT_HIGH;
} else {
profitMargin = Config.PROFIT_LOW;
}
}
}

Intencją powyższego kodu jest, aby wnioski kredytowe opiewające na małą sumę skutkowały wysoką marżą a dopiero powyżej pewnej kwoty marża byłaby odpowiednio niższa. Jednak, ponieważ zmienna ‘profitMargin’ jest publiczna, możliwa jest dowolna zmiana jej wartości zaraz po utworzeniu obiektu. Wbrew intencjom legalny jest więc poniższy kod.

class GoodTeller {
private LoanRegistry loanRegistry;

public LoanID submitApplication(long amount, Customer cust) {
LoanApplication loanApplication = new LoanApplication(amount);

loanApplication.profitMargin = 0; // Ups!

if(cust.isTrustworthy()) {
return loanRegistry.grantLoan(loanApplication, cust);
} else {
return null;
}
}
}
Klasy nie powinny być we wzajemnych skomplikowanych zależnościach. Kod jednej klasy nie powinien wykorzystywać cech innej klasy, które wynikają z jej konkretnej implementacji a nie są elementem zaprojektowanego interfejsu. Zagadnienie to jest ściśle powiązane z hermetyzacją w tym sensie, że skomplikowane zależności mogą wystąpić tylko wtedy, kiedy tej hermetyzacji brakuje. W kontekście egzaminu SCJP, ale nie tylko ważne jest, aby zrozumieć różnicę – różnicę między stanem faktycznym istnienia ścisłych zależności a brakiem hermetyzacji, który jedynie umożliwia istnienie tych zależności, ale niczego nie implikuje.

Wymóg spójności oznacza, że klasy powinny implementować dobrze określony, wąski zakres funkcjonalności. Oznacza to, że tam gdzie tylko ma to sens należy używać delegacji odpowiedzialności. Wspominałem już o tym w artykule "SCJP - Związki typu IS-A oraz HAS-A" i tam też znajduje się dobry przykład. Dla ułatwienia lektury powtórzę go tutaj. Zerknijmy na poniższy kod.

class Company {
private Boss boss;

int requestPayRise(Employee emp, int amount) {
return boss.requestPayRise(emp, amount);
}
}

class Boss extends Employee {
int requestPayRise(Employee emp, int amount) {
if(emp instanceof Boss) {
return amount;
} else {
return 0;
}
}
}

class Employee {
private Company company;

private int salary;

void requestPayRise(int amount) {
salary += company.requestPayRise(this, amount);
}
}
Zwróćmy uwagę na klasę Employee. Jej metoda requestPayRise() deleguje wykonanie właściwych operacji do klasy Company a ta z kolei dalej do klasy Boss. I jest to słuszne, to w końcu nasz szef decyduje, czy dostaniemy podwyżkę czy nie.

16 kwietnia 2008

SCJP – Polimorfizm

Kolejny artykuł w wielkim maratonie po SCJP – kontynuacja działań zapowiedzianych w artykule "Przygotowania do SCJP czas zacząć". Tym razem będzie o polimorfizmie.

Zacznę od szybkiego wyjaśnienia, czym właściwie jest polimorfizm a w tym celu posłużę się przykładem. Zerknijmy na poniższy kod.

class A {
void test() {
System.out.println("A");
}
}

class B extends A {
void test() {
System.out.println("B");
}
}

public class Test {
public static void main(String[] args) {
A var = new A();
var.test();

var = new B();
var.test();
}
}

Uruchomienie powyższego programu spowoduje wypisanie litery A a następnie litery B. I właśnie to jest polimorfizm. Że co (ang. say what)? Ano, że to, która metoda zostanie wykonana zależy nie od typu zmiennej tylko od typu faktycznego obiektu wskazywanego przez tą zmienną. W powyższym przykładzie dwu krotnie wywołaliśmy metodę test() na zmiennej var typu A a jednak za drugim razem wywołała się metoda zdefiniowana w klasie B. Właśnie dlatego, że za drugim razem zmienna var wskazywała na obiekt klasy B. A teraz uwaga! Polimorfizm dotyczy tylko metod i to tylko metod instancji, tj. nie statycznych. Spróbujmy dopisać słówko kluczowe static w deklaracjach metod test() w klasach A i B. Polimorfizm znika – program wypisuje dwu krotnie literę A, przestaje się bowiem liczyć typ obiektu, ważny jest typ zmiennej. I jeszcze jeden przykład. Zmodyfikujmy klasy A i B tak jak pokazano poniżej.

class A {
String id = "A";

void test() {
System.out.println(id);
}
}

class B extends A {
String id = "B";
}

Jaki będzie teraz efekt działania programu? Będzie to dwu krotnie litera A. Na koniec jeszcze jeden przykład. Zastąpmy klasy A i B następującymi.

class A {
void test() {
System.out.println(getId());
}

String getId() {
return "A";
}
}

class B extends A {
String getId() {
return "B";
}
}

Tym razem program wypisze A a potem B. Metoda getId() wykonuje się przecież dla obiektu this, a obiektem tym za drugim razem jest obiekt klasy B.

SCJP – Dziedziczenie i wielodziedziczenie

Streszczenie drugiego rozdziału Książki do SCJP kontynuujemy! Jakie streszczenie? Patrz artykuł "SCJP - Związki typu IS-A oraz HAS-A". Dziś będzie o dziedziczeniu i wielodziedziczeniu, a także jego braku.

Zacznijmy od paru prostych konstatacji. Obiekty to są instancje klas. Do obiektów tych odwołujemy się za pomocą zmiennych, które są de facto referencjami. Każda referencja, tj. zmienna ma swój typ. Raz zadeklarowana zmienna nie może później zmieniać swego typu, ale może wskazywać różne obiekty, także obiekty różnych typów – tych typów, które są zgodne pod względem przypisania (ang. assignment compatible) z typem zmiennej. Typ zmiennej może być określony poprzez klasę, interfejs lub typ wyliczeniowy. Jeśli A jest pewną klasą, to zgodne pod względem przypisania ze zmienną typu A są obiekty klasy A oraz jej klas pochodnych a jeśli A jest interfejsem to obiekty klas implementujących ten interfejs. Dla przykładu ‘A a = new B()’ jest ok, jeśli np. ‘B extends A’ albo ‘B implements A’, albo jeśli B rozszerza czy też implementuje A nie bezpośrednio. Typy wyliczeniowe są tutaj jak klasy, z dokładnością do tego, że nie mogą one rozszerzać innych typów wyliczeniowych – patrz artykuł "SCJP - Typy wyliczeniowe". I jeszcze jedno – to typ zmiennej, a nie typ faktycznego obiektu określa API. Zerknijmy na poniższy przykład.

class Parent {
void testFromParent() {}
}

class Child extends Parent {
void testFromChild() {}
}

class Test {
void test() {
Parent p = new Child();

p.testFromParent();
p.testFromChild(); // Błąd!
}
}

Zmienna p w powyższym przykładzie jest typu Parent a więc nie możemy na niej wywołać metody testFromChild(), mimo że rzeczywisty obiekt wskazywany przez zmienną p jest typu Child i posiada implementację tej metody. Gdybyśmy natomiast powiedzieli kompilatorowi, że zmienna p wskazuje na obiekt klasy Child stosując rzutowanie, to byłoby ok. Instrukcje ‘p.testFromChild()’ trzeba więc w powyższym przykładzie zastąpić przez ‘((Child)p).testFromChild()’. Zwróćmy jeszcze uwagę na nawiasy przy rzutowaniu – wszystkie są konieczne, a może ich zabraknąć w godzinie próby, tj. w czasie egzaminu na SCJP.

Przejdźmy do wielodziedziczenia. Jak wiadomo w języku Java (inaczej niż np. w C++) nie dysponujemy wielodziedziczeniem klas, tj. klasa może rozszerzać co najwyżej jedną klasę. Powód jest prosty – chęć uniknięcia problemów. Wyobraźmy sobie, że zarówno klasa A jak i B implementują metodę anyOp(). Jeśliby teraz dopuścić wielodziedziczenie, to moglibyśmy zadeklarować klasę C, która rozszerza zarówno A i B co prowadzi do pytania – którą implementację metody anyOp() mamy w klasie C? Tą z klasy A czy z B? Jak widać, problem wynika z konieczności wyboru implementacji, a więc nie dotyczy interfejsów. Tak, język Java dopuszcza wielodziedziczenie interfejsów, interfejs może rozszerzać dowolną liczbę innych interfejsów. Klasa może rozszerzać co najwyżej jedną klasę ale jeśli chodzi o implementację interfejsów nie ma ograniczeń. Klasy mogą implementować dowolną liczbę interfejsów.

13 kwietnia 2008

SCJP - Związki typu IS-A oraz HAS-A

Niniejszym rozpoczynam streszczenie drugiego rozdziału książki "SCJP Sun Certified Programmer for Java 5 Study Guide (Exam 310-055)" – kontynuuję dzieło zapowiedziane w artykule "Przygotowania do SCJP czas zacząć". Rozdział drugi ma inną naturę niż pierwszy – więcej tu rozważań o stylu a mniej konkretów – dlatego do zadania podejdę nieco inaczej. Nie będzie już opracowań krok po kroku, sekcja za sekcją, bo też nie czuje żeby to było celowe i potrzebne. W zamian pojawią się artykuły, w których na wyrywki przedstawię główne zagadnienia. Powiem wprost – autorzy w rozdziale drugim zwyczajnie przynudzają. Nie to żebym krytykował Książkę, po prostu, jest ona dedykowana dla osób początkujących, które jeszcze nie pewnie posługują się językiem Java i nie do końca rozumieją zagadnienia programowania obiektowego. Zdecydowałem się na częściowo anglojęzyczny tytuł dzisiejszego artykułu, ponieważ zasadniczym jego celem jest wytłumaczenie znaczenia właśnie tych anglojęzycznych terminów, tj. co to znaczy, że klasy są w związku (ang. relationship) IS-A albo HAS-A.

IS-A oznacza "jest". Warzywo jest rośliną, marchewka jest warzywem, ale rośliną również. Oprócz tego warzywo jest jadalne, ale roślina już nie koniecznie. O czym ja tu piszę? Naturalnie, o Javie i egzaminie SCJP. Rozważmy następujące klasy i interfejsy:


class Plant { }

interface Eatable {}

class Vegetable extends Plant implements Eatable { }
// Vegetable IS-A Plant, Vegetable IS-A Eatable

class Carrot extends Vegetable { }
// Carrot IS-A Vegetable, Carrot IS-A Eatable, Carrot IS-A Plant

Związki IS-A są oparte na dziedziczeniu i implementacji interfejsów. Klasa A jest w relacji IS-A z klasą B, jeśli A jest klasą pochodną – bezpośrednio lub pośrednio – klasy B, ewentualnie klasa A IS-A B jeśli B jest interfejsem i A implementuje B. Mówiąc jeszcze inaczej, A IS-A B, jeśli wyrażenie ‘(new A()) instanceof B’ ma wartość ‘true’.

HAS-A oznacza "ma". Firma ma szefa, który jest pracownikiem. Jeśli chcemy czegoś od firmy, np. podwyżki to niby uruchamiamy procedury w firmie, ale decyzję podejmuje jednak szef, firma deleguje tego typu zadania właśnie szefom. W języku Javy wygląda to tak:


class Company {
private Boss boss; // Company HAS-A Boss

int requestPayRise(Employee emp, int amount) {
return boss.requestPayRise(emp, amount);
}
}

class Boss extends Employee { // Boss IS-A Employee
int requestPayRise(Employee emp, int amount) {
if(emp instanceof Boss) {
return amount;
} else {
return 0;
}
}
}

class Employee {
private Company company; // Employee HAS-A Company

private int salary;

void requestPayRise(int amount) {
salary += company.requestPayRise(this, amount);
}
}

Związek HAS-A oparty jest na kompozycji. Klasa A jest w relacji HAS-A z klasą B, tj. A HAS-A B, jeśli klasa A posiada referencję do obiektów klasy B. Z relacjami typu HAS-A związany jest kolejny ważny aspekt – delegacja odpowiedzialności. W powyższym przykładzie klasa Company posiada metodę requestPayRise(), jednak jest ona w całości oddelegowana do powiązanego obiektu klasy Boss. To, jaki jest rezultat działania metody requestPayRise() w klasie Company zależy więc w pełni od implementacji w klasie Boss.

4 kwietnia 2008

Mechanizm pojedynczego uwierzytelnienia, czyli Single Sign-On

Mechanizm pojedynczego uwierzytelnienia – czyli Single Sign-On, w skrócie SSO – to obszerny temat. Wyobraźmy sobie, że udostępniamy użytkownikom wiele odrębnych aplikacji, z których każda wymaga uwierzytelnienia. Czy nie byłoby dobrze, gdyby dało się używać ich wszystkich podając swój identyfikator i hasło (albo inne dane uwierzytelniające) tylko jednokrotnie? Z pewnością wygodniej niż gdybyśmy mieli to robić dla każdej aplikacji z osobna.

W dzisiejszym artykule opiszę swą wizję SSO dla środowiska złożonego z aplikacji WWW. SSO nie żyje jednak w próżni i same dla siebie, przeciwnie, jest częścią ogólnej architektury bezpieczeństwa dla systemów klasy korporacyjnej (ang. enterprise). Przynajmniej ja tak to widzę i tak też o SSO będę pisał. Zacznijmy od spojrzenia na poniższy diagram ilustrujący proces uwierzytelnienia i pokazujący kluczowe komponenty.




Na szczególną uwagę zasługuje komponent nazwany na powyższym diagramie serwerem bezpieczeństwa. Otóż mój pomysł na implementacje kompleksowego systemu bezpieczeństwa dla przedsiębiorstwa polega na tym, że wszystkie decyzje dotyczące bezpieczeństwa, tj. autoryzacja i uwierzytelnienie podejmowane są przez wyspecjalizowany komponent/serwer, nie przez każdą z aplikacji z osobna. Nie jest więc tak, że na potrzeby autoryzacji aplikacje pobierają listę ról przypisanych do bieżącej tożsamości i konfrontują tą listę z rolami przypisanymi do danego zasobu. Aplikacje wysyłają do serwera bezpieczeństwa zapytanie autoryzacyjne podając identyfikatory zasobów i identyfikator kontekstu a to serwer podejmuje decyzje na podstawie sobie tylko znanych kryteriów. Przede wszystkim, nie wydaje mi się być dobrym pomysłem zaszywanie w aplikacji informacji o związku zasobów z rolami. Ale jest to dyskusja na oddzielny artykuł. Wróćmy do SSO.

Filtr weryfikacji tożsamości to filtr servletowy, który sprawdza czy w żądaniu HTTP zostało przekazane ciasteczko o odpowiedniej nazwie. Jeśli ciasteczko jest dostępne to pobiera z niego identyfikator kontekstu bezpieczeństwa i przekazuje go do aplikacji, np. jako atrybut żądania HTTP (ang. HTTP request). Ewentualnie, filtr może uprzednio wywołać funkcję weryfikacji identyfikatora udostępnianą przez serwer bezpieczeństwa. Aplikacja (a dokładnie jej moduł autoryzacji) wysyła do serwera bezpieczeństwa zapytanie o autoryzację dostępu dla żądanego zasobu i identyfikatora kontekstu. Na podstawie wyniku podejmuje odpowiednie kroki – zwraca żądany zasób lub komunikat błędu. Serwer bezpieczeństwa pobiera z kontekstu dane o uwierzytelnionej tożsamości i przypisanych jej rolach. Zasoby opisane są odpowiednimi identyfikatorami. Jeśli role przypisane do uwierzytelnionej tożsamości zezwalają na uzyskanie dostępu do opisanego zasobu serwer udziela odpowiedzi pozytywnej. Oczywiście, jeśli przekazany do serwera bezpieczeństwa identyfikator kontekstu nie identyfikuje poprawnie żadnego kontekstu zwracany jest komunikat błędu. Serwer bezpieczeństwa może również odpowiedzieć, że użytkownik ma prawo do uzyskania dostępu, ale pod jakimś określonym warunkiem, np. podania poprawnego hasła jednorazowego.

Jeśli filtr weryfikacji tożsamości nie znajdzie w żądaniu odpowiedniego ciasteczka HTTP, to uruchomiona zostaje procedura uwierzytelniania. Rozpoczyna się ona przekierowaniem do aplikacji uwierzytelniającej. Żądania skierowane do tej aplikacji są filtrowane przez kolejny filtr servletowy – filtr uwierzytelniający. To właśnie ten filtr wykonuje główną pracę. Sprawdza on, czy w żądaniu HTTP znajdują się odpowiednie parametry, tj. identyfikator użytkownika i hasło. Jeśli tak, to są one weryfikowane, jeśli nie, to żądanie jest przekazywane do aplikacji uwierzytelniającej – wyświetlana jest w przeglądarce strona z formularzem uwierzytelniania. Po wypełnieniu i wysłaniu formularza żądanie znowu trafia do filtra. Identyfikator użytkownika i hasło są weryfikowane z użyciem serwera bezpieczeństwa. Poprawna weryfikacja powoduje utworzenie na tym serwerze kontekstu (sesji) bezpieczeństwa. Odsyłany jest identyfikator tego kontekstu. Filtr uwierzytelniający przekierowuje z powrotem do oryginalnie żądanego zasobu ustawiając jednocześnie ciasteczko z identyfikatorem utworzonego kontekstu. Od tej pory filtr weryfikacji tożsamości, niezależnie od tego, jaką aplikację zabezpiecza będzie posiadał prawidłowe informacje o uwierzytelnionym użytkowniku. Tym oto sposobem otrzymujemy efekt uwierzytelnienia do pracy z przeglądarką, niezależnie od tego, jaką aplikację w tej przeglądarce aktualnie użytkujemy – oczywiście pod warunkiem, że wszystkie one używają tej samej infrastruktury bezpieczeństwa. Implementacja opisanych tu mechanizmów już wkrótce – oczywiście, jak nie zabraknie czasu i zapału.