The Java IAQ – Pytania o Javie z Rzadką Odpowiedzią
Autorem Java IAQ jest Peter Norvig (tutaj jest oryginał), a polskie tłumaczenie zostało napisane przeze mnie, czyli Wiktora Gworka. Tylko wybrane pytnia zostały przetłumaczone.
Pytnia:
- Czym są Pytania z Rzadką Odpowiedzią (ang. Infrequently Answered Question)?
- Kod w bloku
finallyzawsze zostanie wykonany, prawda? - Czy w metodzie
mw klasieCwyrażeniethis.getClass()zawsze zwraca klasęC? - Przesłoniłem metodę
equals, aleHashtableignoruje to. Dlaczego? - Próbowałem wywołać metodę z nadklasy, ale czasem nie działa. Dlaczego?
- Dziedziczenie wydaję się błędogenne. Jak mogę się przed tymi błędami uchronić?
- Jakie są alternatywy dla dziedziczenia?
- Dlaczego nie ma zmiennych globalnych w Javie?
- Jak duży jest
Object? Dlaczego nie masizeof?
Q: Czym są Pytania z Rzadką Odpowiedzią (ang. Infrequently Answered Question)?
Na pytanie rzadko dostajemy odpowiedź ponieważ niewielu ludzi wie, jak na nie odpowiedzieć albo ponieważ jest to mało znany, subtelny temat (ale temat, który może być istotny dla ciebie). Myślę, że wymyśliłem ten zwrot. Jest dużo Java FAQ (często zadawanych pytań) wokoło, ale to jest jedyne Java IAQ. (Są także parę Pytań Rzadko Zadawanych wliczając także satyryczną listę pytań dotyczących języka C).
Q: Kod w bloku finally zawsze zostanie wykonany, prawda?
Nie można robić takie założenia. Spójrzmy na przykład, gdzie kod w bloku finally nie zostanie wykonany obojętnie jaka będzie wartość logiczna w wyrażeniu warunkowym:
try { if (choice) while (true) ; else System.exit(1); } finally { code.to.cleanup(); } |
Q: Czy w metodzie m w klasie C wyrażenie this.getClass() zawsze zwraca klasę C?
Nie. Załóżmy, że mamy referencję do jakiegoś obiektu x, który jest egzemplarzem klasy C1, która jest podklasą klasy C. Możliwe są następujące sytuacje:
- nie ma metody
C1.m(), - któraś metoda w
xwywołałasuper.m().
W żadnym z tych przypadków wyrażenie this.getClass() nie zwróci C w metodzie C.m(). Jeśli klasa C zostałaby oznaczona jako final to takie założenie można byłoby przyjąć.
Q: Przesłoniłem metodę equals, ale Hashtable ignoruje to. Dlaczego?
Metody equals nie są proste, żeby dobrze działały. Poniżej opisane są miejsca, gdzie można szukać potencjalnych błędów:
- Przesłoniłeś złą metodę
equals. Np. napisałeś:public class C { public boolean equals(C that) { return id(this) == id(that); } }
Żeby wyrażenie
table.get(c)działało poprawnie musisz przesłonić metodęequalstak, żeby jej argumentem był Object, a nie C:public class C { public boolean equals(Object that) { return (that instanceof C) && id(this) == id((C)that); } }
Dlaczego? Ponieważ
Hashtable.getwygląda jakoś tak:public class Hashtable { public Object get(Object key) { Object entry; ... if (entry.equals(key)) ... } }
Wybór wywoływanej metody
entry.equals(key)zależy od typu w czasie wykonania obiektuenteryi zdeklarowanego w czasie kompilacji typu zmiennejkey. Więc kiedy wywołujesztable.get(new C(...))to ta metoda wywołuje metodęequalsw klasieC, której argumentem jest typObject. Wtedy zdeklarowana metodaC.equals(C that)jest pomijana i ignorowana. Kiedy chcesz przesłonić metodę to typy argumentów muszą być takie same. Możesz chcieć mieć natomiast dwie metody:public class C { public boolean equals(Object that) { return (this == that) || ((that instanceof C) && this.equals((C)that)); } public boolean equals(C that) { return id(this) == id(that); } }
- Mogłeś źle zaimplementować
equals. Musi ona być symetryczna, przechodnia i zwrotna. Symetryczna oznacza, żea.equals(b)zwraca taką samą wartość cob.equals(a)(najczęstszy błąd). Przechodniość oznacza, że jeślia.equals(b)ib.equals(c)zwracajątruetoa.equals(c)także zwracatrue. Zwrotność polega na tym, żea.equals(a)zwracatrue. Dobrą praktyką jest stosowanie porównaniathis == that(jak zostało to w powyższym pokazane), które przy okazji jest wydajniejsze niż inne porównywanie obiektów. - Zapomniałeś zaimplementować metody
hashCode. Za każdym razem, kiedy przesłaniasz metodęequalspowinieneś także przesłonić metodęhashCode. Musisz się upewnić, że dwa takie same obiekty mają ten samhashCode. Jeśli chcesz, żeby tablice haszujące działały lepiej to powinieneś postarać się, żeby dwa różne obiekty miały także różnehashCode. Niektóre klasy buforują hash code, żeby nie był za każdym razem liczony od początku. - Mogłeś źle poradzić sobie z dziedziczeniem. Wyobraź sobie dwa obiekty różnych klas, które mogą być różne. Zanim powiesz “NIE! Oczywiście, że to niemożliwe!” wyobraź sobie klasę Prostokąt, która ma pola długość i szerokość oraz klasę Pudełko, która ma te dwa pola i jeszcze głębokość. Czy kiedy Pudełko z głębokością równą 0 jest podobne do Prostokąta. Możesz chcieć odpowiedzieć na to pytanie tak. Jeśli klasy nie są oznaczone jako
finalto znaczy, że mogą mieć podklasy, a ty chcesz być dobrym człowiekiem i szanować swoje podklasy. W szczególności możesz chcieć pozwolić podklasie klasyC, żeby używała metodyC.equals:public class C2 extends C { int newField = 0; public boolean equals(Object that) { if (this == that) return true; else if (!(that instanceof C2)) return false; else return this.newField == ((C2)that).newField && super.equals(that); } }
- Nie uporałeś się odpowiednio z referencjami cyklicznymi. Rozważ przypadek:
public class LinkedList { Object contents; LinkedList next = null; public boolean equals(Object that) { return (this == that) || ((that instanceof LinkedList) && this.equals((LinkedList)that)); } public boolean equals(LinkedList that) { // Błędne! return Util.equals(this.contents, that.contents) && Util.equals(this.next, that.next); } } class Util { public static boolean equals(Object x, Object y) { return (x == y) || (x != null && x.equals(y)); } }
LinkedList.equalsnigdy nic nie zwróci, jeśli zostanie wywołana.
Q: Próbowałem wywołać metodę z nadklasy, ale czasem nie działa. Dlaczego?
Spójrzmy na uproszczony przypadek:
/** Ta wersja Hashtable pozwala na zrobienie czegoś takiego: * table.put("dog", "canine");, a poźniej * table.get("dogs") zwraca "canine". **/ public class HashtableWithPlurals extends Hashtable { public Object put(Object key, Object value) { super.put(key + "s", value); return super.put(key, value); } } |
Musisz dokładnie rozumieć, co metoda w nadklasie robi, kiedy się do niej odwołujesz. W tym przypadku Hashtable zapamiętuje pary (klucz, wartość) w tablicy. Jeśli tablica się przepełni to Hashtable zaalokuje nową, większą tablicę, do której skopiuje stare obiekty wywołując jeszcze raz Hashtable.put. Ponieważ metody są wywoływane w czasie działania programu i zależą od typu obiektu, to w naszym przypadku wywołanie ponowne HashtableWithPlurals.put(key, value) może spowodować wpisy dla “dogss”, “dogs” jak i “dog” (kiedy tablica przepełni się w niewłaściwym czasie). Niestety w dokumentacji nic nie jest napisane na ten temat, ale w razie czego można zawsze zajrzeć do kodów źródłowych JDK.
Q: Dziedziczenie wydaję się błędogenne. Jak mogę się przed tymi błędami uchronić?
Programiści muszą być bardzo uważni, kiedy dziedziczą po jakiejś klasie dodając nową funkcjonalność. Można przytoczyć słowa Johna Ousterhouta: “Wprowadzenie dziedziczenia powoduje ten sam splot i kruchość kodu co nadużywanie wyrażenia goto. W wyniku systemy OO często cierpią z powodu braku możliwości ponownego użycia” (Scripting, IEEE Computer, Marzec 1998) i Edsger Dijkstra rzekomo powiedział: “Programowanie zorientowane obiektowo jest wyjątkowo złym pomysłem, które mogło tylko narodzić się w Kalifornii”. Nie uważam, że istnieje sposób, żeby zabezpieczyć się przed pułapkami OO, ale jest kilka rzeczy, na które trzeba zwracać uwagę”
- Dziedziczenie po klasie, do której nie masz kodu źródłowego, jest zawsze ryzykowne. Dokumentacja może być niekompletna w tych miejscach, w których to trudno przewidzieć.
- Wywoływanie metod z nadklasy często prawadzi do niespodziewanych problemów.
- Musisz brać pod uwagę metody, których nie przesłaniasz, a do których się odwołujesz. Jedną z największych mitów projektowania zorientowanego obiektowo, kiedy dziedziczenie jest użwane. Jest to prawa, że dziedziczenie pozwala pisać mniej kodu, ale nadal musisz myśleć o kodzie, którego nie piszesz.
- Sam szukasz problemu, kiedy twoja podklasa zmienia kontrakt, sposób działania jakiejkolwiek metody lub całej klasy. Nie jest prosto powiedzieć, kiedy kontrakt się zmienił, gdyż są one niejawne (mogą być natomiast opisane w komentarzach).
Q: Jakie są alternatywy dla dziedziczenia?
Delegacja jest alternatywą dla dziedziczenia. Delegacja oznacza, że w klasie posiadasz referencję do egzemplarza innej klasy i przekazujesz wiadomości/sterowanie do tego obiektu. Używanie delegacji jest przeważnie bezpieczniejsze niż dziedziczenie, ponieważ zmusza programistę do myślenia nad tym, co właściwie robi. Dzieje się tak ze względu na to, że mamy referencję do egzemplarza znanej nam klasy i nie musimy akceptować wszystkich metod z nadklasy. Co więcej, udostępniamy tylko te metody, które są niezbędne i mają sens. Z drugiej strony tym sposobem napiszemy więcej kodu i trudniej będzie go ponownie użyć.
Dla przykładu napiszmy klasę HashtableWithPlurals, w której wykorzystamy delegację (uwaga: od JDK 1.2 Dictionary jest oznaczona jako przestarzała):
/** Ta wersja Hashtable pozwala na następujące operacje: * table.put("dog", "canine");, and then have * table.get("dogs") return "canine". **/ public class HashtableWithPlurals extends Dictionary { Hashtable table = new Hashtable(); public Object put(Object key, Object value) { table.put(key + "s", value); return table.put(key, value); } ... // inne metody } |
Dobrym przykładem, że zespół pracujący nad JDK spieszył się i popełnił błąd jest klasa Properties. Dziedziczy ono po Hashtable, co rodzi problemy (patrz metody get() oraz getProperty).
Q: Dlaczego nie ma zmiennych globalnych w Javie?
Zmienne globalne są uznawane za złe z różnych względów:
- dodawanie zmiennych określających stan aplikacji niszczy przejrzystość kodu (nie można zrozumieć wyrażenia, jeśli nie zna się kontekstu, który jest określoną przez zmienną globalną),
- powyższe zmienne zmniejszają spójność aplikacji: musisz wiedzieć więcej, żeby dowiedzieć się, jak coś działają. Głównym punktem OOP jest przełamanie globalnych zmiennych określających stan na łatwiejsze do zrozumienia kolekcje lokalnych stanów,
- jeśli dodasz jedną zmienną globalną to ograniczasz swoją aplikację do jednego egzemplarza. To, co ty myślisz, że jest globalne, ktoś inny może pomyśleć jako lokalne i może uruchomić dwie kopie aplikacji i może być niemiło.
Z tych względów projektanci Javy zdecydowali zabronić używania zmiennych globalnych. Oczywiście można je mieć w inny sposób: wzorzec Singleton, zmienne klasowe, itp.
Q: Jak duży jest Object? Dlaczego nie ma sizeof?
C ma operator sizeof i musi go mieć, ponieważ programista musi zarządzać wywołaniami do malloc i ponieważ rozmiary typów podstawowych nie jest ustandaryzowane. Java nie potrzebuje takiego operatora, ale dla geeków byłby on ciekawym narzędziem. Można zrobić coś takiego:
static Runtime runtime = Runtime.getRuntime(); ... long start, end; Object obj; runtime.gc(); start = runtime.freememory(); obj = new Object(); // Or whatever you want to look at end = runtime.freememory(); System.out.println("That took " + (start-end) + " bytes."); |
Ten sposób nie jest niezawodny ze względu na odśmieciarkę.
Możesz być zdziwiony, że Object zajmuje 16 bajtów (4 słowa) na maszynie wirtualnej Sun’a. Dwa słowa to nagłówek: jedno słowo to wskaźnik do obiektu klasy, a drugi to wskaźnik do instancji. Pusty new String() zajmuje 40 bajtów, a dodanie do mapy klucza i wartości typu Integer zajmuje 64 bajty.
Sensowność takich pomiarów jest raczej niska, gdyż nie można porównywać tego z np. C. Dodatkowych warstw abstrakcji (np. dla GC) nie sposób porównywać.

Nazywam się
Wiktor Gworek
i jestem gospodarzem tego bloga.
Sensowność pomiarów zajętości pamięci w przedstawiony sposób rzeczywiście sensu nie ma, ale jest szybka i pozwala na estymację ilości potrzebnych zasobów. Nieźle sprawdzi się w środowiskach o małych zasobach typu MDA/PDA.
Lepszą, w sensie precyzyjniejszą, metodą jest użycie Java Memory Profiler.
pozdrawiam