The Java IAQ - Pytania o Javie z Rzadką Odpowiedzią

napisane przez wiktor, 19:32 05-29-2007

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:

  1. Czym są Pytania z Rzadką Odpowiedzią (ang. Infrequently Answered Question)?
  2. Kod w bloku finally zawsze zostanie wykonany, prawda?
  3. Czy w metodzie m w klasie C wyrażenie this.getClass() zawsze zwraca klasę C?
  4. Przesłoniłem metodę equals, ale Hashtable ignoruje to. Dlaczego?
  5. Próbowałem wywołać metodę z nadklasy, ale czasem nie działa. Dlaczego?
  6. Dziedziczenie wydaję się błędogenne. Jak mogę się przed tymi błędami uchronić?
  7. Jakie są alternatywy dla dziedziczenia?
  8. Dlaczego nie ma zmiennych globalnych w Javie?
  9. Jak duży jest Object? Dlaczego nie ma sizeof?





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 x wywołała super.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:

  1. 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ę equals tak, ż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.get wyglą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 obiektu entery i zdeklarowanego w czasie kompilacji typu zmiennej key. Więc kiedy wywołujesz table.get(new C(...)) to ta metoda wywołuje metodę equals w klasie C, której argumentem jest typ Object. Wtedy zdeklarowana metoda C.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);
       }
    }
    


  2. Mogłeś źle zaimplementować equals. Musi ona być symetryczna, przechodnia i zwrotna. Symetryczna oznacza, że a.equals(b) zwraca taką samą wartość co b.equals(a) (najczęstszy błąd). Przechodniość oznacza, że jeśli a.equals(b) i b.equals(c) zwracają true to a.equals(c) także zwraca true. Zwrotność polega na tym, że a.equals(a) zwraca true. Dobrą praktyką jest stosowanie porównania this == that (jak zostało to w powyższym pokazane), które przy okazji jest wydajniejsze niż inne porównywanie obiektów.

  3. Zapomniałeś zaimplementować metody hashCode. Za każdym razem, kiedy przesłaniasz metodę equals powinieneś także przesłonić metodę hashCode. Musisz się upewnić, że dwa takie same obiekty mają ten sam hashCode. 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óżne hashCode. Niektóre klasy buforują hash code, żeby nie był za każdym razem liczony od początku.

  4. 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 final to 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 klasy C, żeby używała metody C.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);
       }
    }
    

  5. 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.equals nigdy 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ć.

Jeden komentarz

  1. Koziołek napisał(a):

    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

Zostaw komentarz

Możesz używać znaczników do formatowania kodu takich jak: <b>...</b>, <code>...</code> lub dla konkretnych języków programowania: [java]...[/java], [ruby]...[/ruby] itd.


Wiktor Gworek Nazywam się Wiktor Gworek i jestem gospodarzem tego bloga.
Przeczytaj więcej o mnie »