SCJP, podejście siódme

Siódmy rozdział podręcznika do SCJP to kolekcje i generyki. Jest to zmora wszystkich programistów, zresztą nie dziwię się – sam rozdział ma koło 100 stron i do lekkich nie należy. Końcowy test wyszedł mi “dobrze-niedobrze” – zasadniczo większość odpowiedzi udzielałem “półpoprawnie”, czyli obejmowałem poprawną odpowiedź, ale też przy okazji wpisywałem czegoś za dużo lub za mało. Jednakże zawziąłem się i postanowiłem przerobić zagadnienie jeszcze raz dość gruntownie, posiłkując się innymi swoimi książkami – Blochem oraz Koffmanem przedstawię kilka rzeczy, które z pewnością się przydadzą. Ten wpis będzie inny i dłuższy niż poprzednie, ale jestem pewny, że jego lektura na pewno nikomu nie zaszkodzi.

Zacznijmy od podstawowej rzeczy, jaką jest wiedza dotycząca metod equals oraz hashCode. Equals służy do porównywania dwóch obiektów i sprawdzenia, czy tym samym są one sobie znaczeniowo równe.

String s1 = new String("chlebik");
String s2 = new String("chlebik");s1.equals(s2); // To daje TRUE

Oczywiście w przypadku klasy String o równości mówić jest łatwo. Podobnie jak w przypadku liczb – albo dwie zmienne reprezentują tę samą liczbę, albo i nie. Co natomiast zrobić w przypadku klas napisanych przez programistę? Ano trzeba by nadpisać tę metodę. Domyślnie w klasie Object metoda ta używa do porównania dwóch obiektów operatora ==, co nie jest raczej dobrym rozwiązaniem dla klas reprezentujących…hmmm…no nigdy nie jest dobre.

Przedstawię wpierw klasę, na których będziemy pracować:

class Bloger { }class WPBloger extends Bloger {
Integer age = 24;
String nick = new String("Chlebik");
}

Załóżmy, że chcemy sprawdzić równość dwóch obiektów klasy WPBloger. Co należy porównać by mieć pewność, że jedna i druga zmienna znaczeniowo reprezentuje ten sam obiekt? Ano jeśli zgadza się wiek, zgadza się nick, no to mamy identyczne obiekty. Super. Oto jak to zaimplementujemy:

public boolean equals( Object o ) {if( (o instanceof WPBloger) &&
( ((WPBloger)o).nick == this.nick ) &&
( ((WPBloger)o).age == this.age ) ) {
return true;
} else {
return false;
}
}

Oto o czym należy pamiętać:

  • parametr metody – musi być typu Object. I kropka.
  • sprawdzanie typu – owszem, parametr jest typu Object, ale jeśli chcemy porównywać nick czy wiek należy potraktować nasz parametr jako obiekt konkretnej klasy. Jednakże zanim to uczynimy, należy zabezpieczyć się przed ewentualnym błędem, jeśli do metody przekazano obiekt innej klasy. Stąd sprawdzenie w warunku instanceof.
  • rzutowanie parametru – po pomyślnym przejściu testu z operatorem instanceof możemy swobodnie (bez obawy o błędy) rzutować parametr na konkretną klasę, a następnie odwoływać się do jego pól/metod.
  • porównywanie wartości – dla zmiennych instancji będącymi obiektami, należy wywoływać ich metody equals. W przypadku typów prostych (prymitywów) rzecz jasna wystarcza operator ==. Wyjątek! W przypadku wartości float i double dobrze jest je wpierw przerobić na inne typy. Float na integer (metoda Float.floatToIntBits), zaś double na long (metoda Double.doubleToLongBits ). Jest to podyktowane tym, że w przypadku tych prymitywów mogą one przyjmować wartości takie jak Float.NaN i parę innych, co przy porównaniu może prowadzić do nieścisłości. Tablice porównujemy w całości, pole po polu (lub za pomocą Arrays.equals)

I to na razie tyle. Czasem warto też zapoznać się z kontraktem tej metody, jednakże jest to dość oczywiste i nie będę przepisywał manuala. Teraz przejdziemy do metody hashCode. Wpierw troche uśmiechu od autorów podręcznika:

For the exam you do not need to understand the deep details of how the collection classes that use hashing are implemented, but you do need to know which collections use them (but, um, they all have “hash” in the name so you should be good there)

Generalnie hashe są używane po to, aby polepszyć efektywność składowania i wyszukiwania elementów w kolekcjach. Im nasza klasa będzie generowała bardziej unikalne i różnorodne hashe tym lepiej (wyszukiwanie będzie szybsze). Wartość zwracana przez metodę hashCode to int! Autorzy podręcznika pokazali działanie haszowania na bardzo przystępnym przykładzie.

Załóżmy, że mamy klasę reprezentującą zawodników pewnej drużyny. Hashe są generowanie na takiej zasadzie, że bierzemy po kolei litery z imienia zawodnika i zamieniamy ich wartości na liczby w rosnącej kolejności:

Imię Haszowanie Wynik
Alex A(1)+L(12)+E(5)+X(24) 42
Bob B(2)+O(15)+B(2) 19
Dirk D(4)+I(9)+R(18)+K(11) 42

Jak widać dla imion Alex i Dirk zostaną wygenerowane te same wartości. I teraz na przykładzie koszyków z imionami – oba imiona zostaną włożone do tego samego koszyka. Gdybyśmy próbowali znaleźć imię w takim koszyku, wówczas musielibyśmy przetrząsnąć taki koszyk w poszukiwaniu odpowiedniego. To może zająć sporo czasu! W użyciu mamy wówczas metodę equals, która po kolei przejdze po imionach w koszyku w poszukiwaniu odpowiedniego. Hmmm, trochę to mało efektywne. Na przykładzie 2 imion to może tego nie widać, ale gdybyśmy w takim koszyku mieli owych imion 50, już zacząłby się narzut.

Co zatem zrobić? Najlepiej by było w ogóle zrezygnować z przeszukiwania koszyka. Jak to osiągnąć? Pisząc na tyle oryginalne metody hashCode, aby każde imię miało swój własny koszyk. Najlepiej tak przegrzebać w wartościach, aby były one dość oryginalne (haszowania). Dzięki temu wyszukiwanie naszych obiektów będzie z pewnością szybsze. Metoda hashCode również posiada swój kontrakt, ale podobnie jak przy equals nie zamierzam przepisywać manuala.

Dlaczego piszę o tych metodach? Ano dlatego, że kilka zadań jest w swej idei prosta – mamy dwie kolekcje, coś tam dodamy, coś odejmiemy i mamy linię, w którą możemy wsadzić jakiś kod. Jaki będzie wynik działania programu jeśli ta linia będzie bla bla bla bla. Generalnie bez znajomości działania powyższych metod oraz tego jak zachowają się dodawane elementy będzie dość trudno odpowiedzieć na tego typu pytania. Dla pamięci i na zakończenie tego wątku – wpierw lecimy po hashCode, dopiero potem wyszukiwanie obejmuje equals! (zatem należy uwzględnić sytuacje kiedy np. metoda hashCode w ogóle nie jest nadpisywana).

Jeśli chodzi o samo API kolekcji to raczej nie ma z nim większych problemów. Należy uważać na cztery rzeczy:

  • interfejsySet, Map i List są właśnie interfejsami. Można na nie rzutować, ale nie da się stworzyć ich instancji!.
  • różnice zawartości w zbiorachHashMap może posiadać jeden klucz będący wartością NULL, jak i również wiele wartości NULL jako zawartość, na którą klucze wskazują. Natomiast w przypadku Hashtable (patrz: różnica w wielkości liter) nie ma mowy o jakichkolwiek NULLAch.
  • TreeMap i TreeSet – to są ciekawe kolekcje, gdyż charakteryzują się posiadanym porządkiem i sortowaniem. Nie będę znowu przeklejał manuala( TreeMap i TreeSet ), ale z pewnością wypada zwrócić uwagę na metody, które obejmuje egzamin ( Key w znaczeniu nie parametru ale odmiany tej metody dla Map):
    • ceiling (Key)
    • higher (Key)
    • floor (Key)
    • lower( Key)

    najlepiej sobie wbić do głowy, co i kiedy zostanie zwrócone.

  • “backed collections” – czyli dziwne rozczłonkowanie kolekcji poprzez wywołanie metody subMap (wraz z jej przeciążonymi wersjami).

No i przyszedł czas na generyki. Wpierw trochę historii. Otóż obsługa generyków została dodana w Javie 1.5 i w związku z tym (albo przede wszystkim dlatego, że to była wersja 1.5) musiała w racjonalny sposób umożliwić działanie starego kodu, który był napisany bez istnienia czegoś takiego jak typy generyczne. Dlatego też całe to zagadnienie jest trochę “walnięte”, gdyż jest kompromisem pomiędzy nowoczesnością, a przenośnością kodu. No i dlatego mamy taki bajzel.

Po kilku słowach krytyki czas na konkrety. Standardowo w wersjach przed wprowadzeniem generyków istniała możliwość zrobienia czegoś takiego:

List lista = new ArrayList();lista.add(new Integer(23));
lista.add(new String("chlebik"));

I to nawet działało. Problemem było oczywiście porównywanie elementów, a także konieczność ciągłego rzutowania wyciąganych obiektów z listy (metody dostępowe zwracały Object). I głównie po to powstały generyki – by umożliwić kontrolę typów na poziomie samej struktury danych, bez konieczności dokonywania ciągłych rzutowań i obawy o ClassCastException. Gdyby zagadnienie kończyło się w tym miejscu, byłoby ono jednym z najłatwiejszych w certyfikacji SCJP. Oczywiście tak różowo nie jest.

Generalnie kod napisany w wersjach sprzed generyków daje spore szanse na działanie. Oto przykład:

public void zrobCos() {
List lista = new ArrayList();
lista.add(23); // zwroc uwage na autoboxing
lista.add(44);
List drugaLista = zrobCosZLista( lista );
}public List zrobCosZLista( List lista ) {
// dodajemy cos do listy i tak dalej
}

Jeżeli w metodzie zrobCosZLista grzecznie będzie istniał kod sprawdzający czy elementy listy są Integerami, wówczas problemu nie ma. Dodatkowe rzutowania nikomu nigdy nie zaszkodziły (poza pewnie narzutem na wydajności). Co jednakże się stanie jeśli postanowimy coś dodać do takiej listy? Ano mamy problem, gdyż oficjalnie dla kompilatora mamy doczynienia ze zwykłą listą, która może przyjmować dowolne wartości! Co z tego, że “piętro wyżej” nasz argument nie był zwykłą listą, ale listą z predefiniowanym typem?

public List zrobCosZLista( List lista ) {
lista.add( new Integer(29) );
}

Działa! Oj jak fajno. Zwracamy listę, ale z elegancko dodaną kolejną wartością Integer. Kompilator jest szczęśliwy, programista też. Co jednakże jeśli do listy dodamy np. łańcuch tekstowy?

public List zrobCosZLista( List lista ) {
lista.add( new String("chlebik") );
}

Dodać możemy, w końcu to zwykła lista. I co? Ano wszystko dobrze – taki kod skompiluje się i nawet uruchomi. Nie będzie błędów podczas działania programu!!!. Jedyną rzeczą będzie wygenerowanie ostrzeżenia przez kompilator (w poprzednim przypadku dodawania Integera takie ostrzeżenie również zostanie wygenerowane). Przy dodawaniu trefnego elementu do kolekcji nic się nie stanie! Problem najczęściej pojawia się wówczas, gdy próbujemy potraktować nasz element o typie String jako Integer. Co się dzieje w takiej sytuacji chyba nie muszę tłumaczyć.

Powyższe przykłady nie są tragiczne i da się je zrozumieć. Najgorszą rzeczą w przypadku generyków jest polimorfizm i różne jego odcienie. To na tym zagadnieniu polega większość zdających (no i oczywiście ja :). Problemu z polimorfizmem w przypadku “typu kolekcji” nie ma. Czyli możemy rzutować ArrayList na List i wszystko będzie działało. Co zaś z takim kodem (kalka z podręcznika)?

class Parent { }
class Child extends Parent { }
List<Parent> myList = new ArrayList<Child>();

Takie coś nie zadziała. Zadziała zaś z kolei zasada mówiąca o tym, że zadeklarowany typ generyczny jest jedynym typem, który dana kolekcja może przyjąć. Koniec kropka. Czyli w powyższym przypadku tylko Child, albo tylko Parent. Próby napisania innego kodu skończą się błędem kompilacji.
Podobna sytuacja ma również miejsce w momencie, kiedy np ArrayList jest przekazywana jako parametr do metody, która przyjmuje np: List. Nie, nie i jeszcze raz nie. Tak się nie da.Co zatem zrobić w takim przypadku? Zrezygnować z polimorfizmu się raczej nie da, pisanie przeciążonych metod dla każdej z podklas Parent też nie wchodzi w grę. Z pomocą w takich przypadkach przychodzi specjalny “operator” (tzw. wildcard) – <? extends Parent>. Znaczy on mniej więcej tyle – pozwalaj na przekazywanie obiektów, które rozszerzają podaną klasę (no i obiekty tej klasy też) i operowanie na takiej kolekcji. Obiecuję, że nie będę tam niczego wsadzał. Dzięki takiej konstrukcji znaczna część istniejącego już kodu mogła zadziałać – wystarczyło tylko nie wsadzać niczego do kolekcji w ten sposób przekazanej.

W tym miejscu wypada wskazać, że podobnie jak w kilku innych przypadkach w Javie – parametryzowanie w przypadku powyższym oznacza nie tylko klasę, ale również implementację interfejsu. Czyli taki kod jest jak najbardziej poprawny:

public zrobCos( List<? extends Runnable> ) {}

Istnieje też odwrotność tego zapisu. Jak już wspomniałem <? extends Class> dotyczy konkretnej klasy/interfejsu oraz jej dzieci/implementacji. A co jeśli chcemy rzutować w górę? Ależ proszę bardzo:

public zrobCos( List<? super Class>) {}

Taki zapis mówi kompilatorowi – przyjmij jako argument listę z obiektami typu Class i wszystkimi jego rodzicami. Dzięki temu mamy możliwość ograniczenia potencjalnych obiektów, które można przekazać jako parametr, ale z drugiej strony jesteśmy w stanie doprowadzić stary kod do stanu używalności z generykami.

Ostatnią rzeczą, na którą wypada zwrócić uwagę są deklaracje z użyciem generyków. Jak podkreślają sami autorzy podręcznika jest to dla generyków zastosowanie bardzo rzadkie, jednakże wypada się z nim zapoznać (bo w sumie tylko z takich zagadnień układają pytania :). Uogólnione bowiem typy możemy stosować w przypadku klas:

public class mojaKlasa<T extends jakasKlase> {
T[] tabela;
T innaZmienna;
// i reszta kodu klasy, gdzie pod T zostanie podłożona nazwa klasy
}

Albo też i metod:

public <T> int zwrocInt( T obiekt ) {
// owo T reprezentuje jeszcze nam nieznany typ obiektu
}

To tyle, co postanowiłem pokrótce przedstawić. Zagadnienie kolekcji jako takich jest olbrzymie – bo nie tylko rzecz w powyższej wiedzy – w ogóle bowiem nie dotknąłem zagadnienia efektywności kolekcji (jaką i do czego najlepiej wykorzystać), własnych implementacji, bardziej rozbudowanego API. Ale w tej serii postów skupiam się na zagadnieniach poruszanych na egzaminie – zainteresowani niech poszukają dla siebie odpowiedniej literatury 🙂

Advertisements

2 thoughts on “SCJP, podejście siódme

  1. Marcin

    witam,
    moj pierwszy koment na tym zacnym blogu, wielki szacunek za wytrwalosc i chec podzielenia sie swoimi przemysleniami. Zamierzam zaczac przygotowania do egzamu wiec pewnie bede czestym gosciem na Twojej stronie:)

    a teraz pytanie:
    ” Przedstawię wpierw klasę, na których będziemy pracować:
    class Bloger { }class WPBloger extends Bloger { ”

    w jakim celu wystepuje tutaj klasa Bloger?;)

  2. chlebik Post author

    Jak znam zycie byl tam pewnie zamysl by napisac o czyms jeszcze w kontekscie dziedziczenia i relacji rodzic-dziecko. I jakos pewnie wyszlo, ze ostatecznie nic z tego nie wyszlo 🙂 Ale w czytelnosci raczej nie przeszkadza.

    Przymierzam sie do tego coby przepatrzec calego bloga i poprawic formatowanie kodu, ale to moze niedlugo przy okazji migracji z serwerow wordpressa.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s