Miesiąc temu skończyłem czytać książkę Effective Java Joshuy Blocha. Dzisiaj podzielę się setką krótkich myśli, które z niej wyniosłem. Ich kolejność jest najczęściej zgodna z kolejnością przedstawionych w książce tematów.
Gorąco polecam książkę tym, którzy jeszcze książki nie przeczytali – jest to najlepsza książka, jaką przeczytałem. Nie jest to just another book on Java z opisem klas i ich API. Jest ona przeznaczona dla programistów Javy, którzy chcą pisać lepszy kod. A poprzez “dobry kod” rozumiem czytelny, przekazujący intencje, testowalny, łatwy w utrzymaniu oraz zgodny ze specyfiką całej platformy.
Tworzenie i niszczenie obiektów (item 1-7)
- Jeśli obiekt ma zbyt dużo konstruktorów, przemyśl użycie statycznych metod wytwórczych. Statyczna metoda factory ma nazwę (w przeciwieństwie do konstruktora) i lepiej oddaje intencje. Ponadto, w przyszłości można wymienić implementację metody wytwórczej, aby nie tworzyła nowych obiektów, tylko reużywała istniejący, jeśli ten jest niezmienny.
- Używaj kolekcji ze słabymi referencjami do przechowywania listenerów (np. WeakHashMap). Inaczej garbage collector nie zwolni pamięci obiektu, jeśli go jawnie nie wyrejestrujesz.
- Unikaj finalizatorów, ponieważ nie ma gwarancji ich wykonania, są powolne oraz nie dowiesz się o żadnym wyjątku, gdyby taki został rzucony. Lepiej dodać własną metodę finalizującą i wymagać jej wywołania przez użytkownika klasy – sekcja try-finally idealnie się do tego nadaje. Jeśli jednak musisz użyć finalizatora, dla swojego bezpieczeństwa użyj wartownika finalizatora.
- Preferuj obiekty niezmienne, ponieważ nie wymagają żadnej synchronizacji w środowisku wielowątkowym. Obiekty niezmienne to takie, których wszystkie “znaczące” pola są final oraz nie można zmienić stanu wewnętrznego tych obiektów. Użyj budowniczego, jeśli obiekt ma dużo opcjonalnych pól, a nie chcesz używać setterów. Zobacz przykład z książki.
- Jednoelementowy typ enum jest najlepszym sposobem na implementację singletonu.
- Singletony trudno się testuje. Użyj zwykłych instancji obiektów - Twoje testy jednostkowe na tym tylko zyskają. I będziesz mógł użyć Mockito. :-)
- Klasy pomocnicze typu Utils powinny mieć prywatny konstruktor. W ciele konstruktora można dodatkowo rzucać wyjątek, aby zablokować możliwość przypadkowego utworzenia obiektu, jeśli konstruktor zostanie wywołany bezpośrednio z wewnątrz tej klasy.
- Jeśli piszesz new String(“myString”), to dzisiaj z tym skończ. Uważaj również na autoboxing i unboxing.
- Jeśli sam zarządzasz pamięcią, pamiętaj o usuwaniu obiektów (object = null). Jest to jednak raczej wyjątek, niż norma – w standardowych sytuacjach obiekty powinny ginąć wraz z wyjściem poza zasięg.
Cały kompletny rozdział możesz przeczytać tutaj: Effective Java - chapter 1 - creating and destroying objects in Java.
Metody wspólne wszystkich obiektów (item 8-12)
- Nie ma sensu, aby niezmienne klasy udostępniały metodę clone.
- Zachowuj kontrakt metod equals, hashCode i Comparable.compareTo. Niedopełnienie tego obowiązku może się ujawnić w najmniej zręcznych momentach i być bardzo trudne do zidentyfikowania.
- Napisz dobrą metodę hashCode – w przeciwnym razie Twoje programy będą miały kwadratową złożoność obliczeniową zamiast liniowej podczas pracy np. z haszmapami.
- Przedefiniowanie metody toString umila życie użytkownikowi klasy.
- Jeśli nie jest to absolutnie konieczne, nie implementuj interfejsu Cloneable – użyj zwykłego konstruktora, który przyjmuje obiekt tej samej klasy jako parametr. Nie zapominaj przy okazji o tworzeniu defensywnych kopii obiektów, które nie są niezmienne.
- Dobrze, gdy implementacja Comparable.compareTo jest spójna z implementacją Object.equals. Antyprzykład: new BigDecimal(“1.0”).equals(new BigDecimal(“1.00”)) = false, new BigDecimal(“1.0”).compareTo(new BigDecimal(“1.00”)) = true. W efekcie, w kolekcji HashSet występować będą dwa elementy (implementacja korzysta z equals), a w kolekcji TreeSet – jeden (compareTo).
- Użyj Float.compare i Double.compare zamiast operatorów < i >.
Klasy i interfejsy (item 13-22)
- Klasa jest bezpieczna do rozszerzenia, gdy żadna metoda, którą można przesłonić, nie jest wywoływana wewnętrznie przez klasę.
- Ukrywaj wszystko, co się da. Nadawaj klasom, metodom i składowym najmniejszą możliwą widoczność.
- Dopuszcza się zmianę widoczności z private na domyślną (package private), aby móc testować metodę. Należy się jednak zastanowić dwukrotnie, czy na pewno warto ją testować – interesowanie się szczegółami implementacyjnymi jest niedobre.
- Jeżeli jakieś składowe klasy mają być dostępne, nie ulegnij pokusie deklarowania składowych jako public. Jeśli tak zrobisz, nigdy nie będziesz miał możliwości zmiany implementacji na lepszą.
- Implementując klasy niezmienne, pamiętaj o tworzeniu defensywnych kopii oraz o zwracaniu kopii obiektów, które nie są niezmienne. W przeciwnym wypadku można będzie zmodyfikować wewnętrzny stan Twojego obiektu z zewnątrz. Nie pozwalaj na dziedziczenie po Twojej klasie i oznacz wszystkie pola jako private i final.
- Zanim użyjesz słowa kluczowego extends, zastanów się, czy kompozycja nie będzie lepszym rozwiązaniem. Kompozycja eliminuje niebezpieczny problem ułomnej klasy bazowej. Przeczytaj o wzorcu projektowym decorator.
- Dokumentuj klasy przeznaczone do dziedziczenia.
- Konstruktorom nie wolno wywoływać metod, które można przesłonić. Ryzykujesz możliwością uchwycenia składowych finalnych w dwóch różnych stanach (!) i/lub otrzymaniem NullPointerException. Metody clone oraz readObject logicznie działają jak konstruktory, więc to również ich dotyczy.
- Jeśli klasa nie jest przeznaczona do dziedziczenia (np. klasa Utils), to jawnie zabroń dziedziczenia poprzez nadanie modyfikatora final klasie i/lub oznaczenie konstruktorów jako prywatne.
- Preferuj interfejsy ponad klasy abstrakcyjne. Można przygotować dla użytkowników naszych klas bardziej zaawansowane, przykładowe klasy abstrakcyjne, ale zawsze powinna być możliwość własnej implementacji z użyciem wyłącznie interfejsów.
- Pamiętaj, że definiując raz interfejs, przywiązujesz siebie i klientów do niego. Nie jest możliwa jego łatwa modyfikacja.
- Nie twórz interfejsów, których jedynym celem jest udostępnianie stałych.
- Preferuj hierarchię klas ponad klasy oznaczane. W szybkim skrócie, klasa oznaczana to taka, w którą można by rozdzielić na kilka rozłącznych klas – np. klasa Figure z wewnętrznym przełącznikiem typu Shape, który określa, czy figura jest prostokątem, czy kwadratem.
- Jeśli nie musisz odwoływać się z klasy wewnętrznej do metod i pól klasy zawierającej, zadeklaruj klasę wewnętrzną jako statyczną. Wewnętrzne klasy statyczne nie trzymają referencji do obiektu klasy zawierającej – nie możesz więc użyć konstrukcji OuterClass.this. Miejsce dla klas statycznych jest wszędzie tam, gdzie obiekt klasy wewnętrznej może istnieć niezależnie od klasy zawierającej.
Generics (item 23-29)
- Nigdy nie korzystaj z surowych typów. Niech Twoje IDE podkreśla Ci te złe miejsca. Tylko kod bez żadnych ostrzeżeń da Ci bezpieczeństwo typów. Korzystaniem z surowych typów ryzykujesz możliwością otrzymania ClassCastException.
- Jeśli chcesz coś robić z kolekcją, której zawartość Cię nie interesuje, użyj Collection<?> zamiast Collection. Unikniesz w ten sposób nieopatrznego umieszczenia obiektu o niewłaściwym typie w kolekcji. W Collection<?> możesz umieścić tylko wartości null.
- @SuppressWarnings(“unchecked”) używaj tylko wtedy, gdy jesteś w stanie udowodnić poprawność kodu. Adnotacji należy używać w najmniejszym wymaganym zasięgu.
- Tablice nie zapewniają bezpieczeństwa typów. String[] dziedziczy po Object[], a do Object[] możesz już wrzucić wszystko. Typy ogólne działają inaczej – List<String> nie dziedziczy po List<Object>, co właśnie gwarantuje bezpieczeństwo typów.
- Zapis class List<E extends Component> oznacza deklarację klasy, w której typ E jest pochodny do Component. Może to być więc List<Component> lub List<ExtendedComponent>, pod warunkiem, że class ExtendedComponent extends Component.
- Deklaracja pojedynczej metody, która ma parametr typu ogólnego powinna wyglądać mniej więcej tak: protected <T> T lookupBean(final Class<T> beanClass) throws NamingException { return (T) new InitialContext().lookup(“java:comp/env/” + beanClass.getSimpleName()); }
- Dzięki typom ogólnym można skrócić niewygodny zapis Map<String, List<String>> map = new HashMap<String, List<String>>() do * Map<String, List<String>> map = newHashMap(), poprzez wprowadzenie prostej statycznej metody *newHashMap. Można też czekać na Javę 1.7. ;-)
- W celu zapewnienia elastyczności Twojej klasy, będziesz czasami musiał użyć typów szablonowych – <? extends E> i <? super E>. W dużym skrócie, jeśli metoda zwraca E, należy użyć <? extends E>, zaś jeśli metoda przyjmuje E, należy użyć <? super E>. Ta zasada jest nazywana PECS – producer extends, consumer super.
- Typy szablonowe powinny być prawie niezauważalne dla użytkownika klasy. Mają mu pomagać np. poprzez wskazywanie miejsc, w których użytkownik próbuje użyć zmiennej o nieprawidłowym typie jako parametr. Jeśli typy szablonowe są uciążliwe, API klasy może być słabe.
Typy wyliczeniowe i adnotacje (item 30-37)
- Używaj enumów zamiast stałych typu int. Typy wyliczeniowe są obiektami i mają metodę toString(), co ułatwia debugowanie. Ułatwia również programowanie - Twoje IDE będzie mogło dużo więcej podpowiedzieć. Gdyby tak Swing korzystał z typów wyliczeniowych, a nie stałych typu int…
- Wartości specyficzne dla każdego elementu typ wyliczeniowego zapewnij poprzez konstruktor.
- Preferuj rozszerzanie metody dla każdego elementu ponad pojedynczą metodę z instrukcją switch. Jeśli dodasz nowy element do typu wyliczeniowego w przyszłości, nie zapomnisz o dodaniu odpowiedniej metody.
- Wydajność typów wyliczeniowych jest zbliżona do liczb int. Jedyny narzut pojawia się przy ładowaniu klas.
- Jeśli elementy typu wyliczeniowego mają logiczną kolejność, przypisz wartość odpowiadającą kolejności na stałe poprzez konstruktor. Nie korzystaj z metody ordinal().
- Jeśli korzystasz z wzorca typu wyliczeniowego int, aby w łatwy sposób obsługiwać wiele parametrów, pora zainteresować się kolekcją EnumSet. Kod, który wyglądał kiedyś tak: item.setLayout(LAYOUTSHRINK | LAYOUTNEWLINEAFTER | LAYOUTRIGHT), teraz będzie wyglądał: item.setLayout(EnumSet.of(LAYOUTSHRINK | LAYOUTNEWLINEAFTER | LAYOUTRIGHT)). Pomoże Ci to zwłaszcza przy debugowaniu – EnumSet.toString() powie Ci więcej, niż liczba.
- Użyj EnumMap<YourEnumClass, Object> zamiast tablicy o indeksach pochodzących z wywołania metody ordinal().
- Brak możliwości dziedziczenia w typach wyliczeniowych można zrekompensować poprzez implementację dowolnych interfejsów.
- Specjalne metody oznaczaj adnotacjami. Nie korzystaj z wzorców nazw. W JUnit 3.x testem stawała się każda metoda, której nazwa rozpoczynała się od test – w wersji 4.x używa się adnotacji.
- Używaj adnotacji @Override zawsze, gdy nadpisujesz metodę lub implementujesz intefejs. Kod staje się bardziej czytelny, zwłaszcza z poziomu przeglądarek poza IDE. Ty zaś zyskujesz dodatkowe bezpieczeństwo – jeśli z klasy, którą rozszerzasz w przyszłości zostanie usunięta jakaś metoda, dowiesz się o tym od razu. Wyłapiesz również literówki, np. haszCode zamiast hashCode.
- Czasami przydatne są typy znacznikowe, czyli interfejsy bez żadnych metod. Zastanów się jednak, czy adnotacja nie będzie lepsza. Adnotacja może ewoluować z czasem – można dodawać nowe pola i nadawać im wartości domyślne. Nie można z kolei wymusić na etapie kompilacji, aby przekazywany obiekt posiadał jakąś adnotację.
Metody (item 38-44)
- Sprawdzaj poprawność otrzymywanych parametrów. Lepiej rzucić InvalidArgumentException, niż wprowadzić do systemu nieprawidłowe dane. Korzystaj z gotowych wyjątków z Javy: NullPointerException, InvalidArgumentException, ArithmericException, ParseException. Poprawność metod prywatnych sprawdzaj zwykłymi assertami.
- Nie wolno używać metody clone() do wykonywania defensywnych kopii klas, które można rozszerzać.
- Wybieraj dobre nazwy metod. Konieczność napisania komentarza metody może wskazywać na jej nieodpowiednią nazwę.
- Metody nie powinny mieć zbyt dużo parametrów.
- Korzystaj z typu wyliczeniowego z dwoma elementami zamiast przełącznika typu boolean. Zyska na tym czytelność, a przy okazji będziesz mógł dodać w przyszłości dodatkową stałą.
- Przeciążanie metod jest statyczne. Dysponując dwoma metodami – doIt(Object) i doIt(String) oraz zmienną zmienna typu String zrzutowaną na Object, wywołanie doIt(zmienna) wywoła doIt(Object). Nie udostępniaj dwóch metod o takiej samej liczbie parametrów, gdy różnią się najwyżej jednym parametrem i jeden z nich jest podtypem drugiego.
- Jeśli metoda może przyjmować dowolną liczbę argumentów, ale wymaga co najmniej jednego, deklaracja powinna wyglądać int myMethod(int firstParam, int… args).
- Do wyświetlania zawartości tablic używaj Arrays.toString(array). Nie korzystaj z Arrays.asList.
- Zawsze zwracaj pustą tablicę lub kolekcję – nigdy null.
- Jeśli potrzebna jest dokumentacja, korzystaj z JavaDoc.
Programowanie (item 45-56)
- Zmienne lokalne powinny mieć najmniejszy możliwy zakres widzialności. Minimalizujesz dzięki temu niebezpieczeństwo przypadkowego nieopatrznego użycia tej zmiennej w innym miejscu.
- Gdy tylko możesz, korzystaj z pętli for each zamiast for. Przy zagnieżdżonych iteracjach nie użyjesz przypadkowo zewnętrznego iteratora.
- Poznaj biblioteki oferowane przez środowisko Java i korzystaj z nich. Nie odkrywaj koła na nowo, zwłaszcza, że Twoje koło będzie najprawdopodobniej gorsze. Obowiązuje Cię znajomość co najmniej java.lang, java.util oraz część java.io. ;-)
- Unikaj typów float i double, gdy dokładność obliczeń jest krytyczna. Użyj BigDecimal lub zwykłych int lub long.
- Uważaj na operator == przy porównywaniu typów opakowanych (np. Integer), bo porównujesz referencje, a nie wartości. Równie mylące mogą być wyjątki NullPointerException, gdy liczba jest nullem, a zajdzie potrzeba odpakowania wartości (unboxing).
- Dane powinny mieć odpowiedni typ – String nie nadaje się do wszystkiego. Masz do wyboru również typy wyliczeniowe, kolekcje, własne klasy.
- Obiekty typu String są niezmienne, a każda konkatenacja tworzy nowy obiekt. Unikaj wielokrotnego złączania Stringów operatorem + – użyj StringBuilder lub StringBuffer. Nie warto jednak stosować tego do stosunkowo krótkich złączeń, ponieważ czytelność kodu drastycznie maleje.
- Odwołuj się do obiektów przez ich interfejs, a nie klasę, jeśli istnieje interfejs. Takie rozwiązanie jest bardziej elastyczne i możliwe, że zaoszczędzisz kłopotów w przyszłości, gdy będziesz musiał rozszerzyć swój kod.
- Generalnie, nie powinieneś korzystać z refleksji. Jeśli jednak refleksja jest potrzebna, to spróbuj ograniczyć ją wyłącznie do tworzenia obiektów klas nieznanych w trakcie pisania programu (Class.forName(“net.nowaker.MyClass”)). Potem zrzutuj obiekt na odpowiedni interfejs i wołaj metody w sposób standardowy.
- Niezmiernie rzadko występuje konieczność stosowania metod natywnych w celu optymalizacji aplikacji. Nowoczesne JVM są naprawdę wydajne.
- Wykonywanie optymalizacji przed stwierdzeniem, że dany fragment programu stanowi wąskie gardło to niewybaczalny grzech. Zwykle optymalizacja niczego nie zmieni – oprócz tego, że kod będzie trudniejszy w utrzymaniu. Aplikacja musi być przede wszystkim poprawna, a dopiero potem wydajna. Nie znaczy to oczywiście, że masz w pętli for korzystać z Integer zamiast int.
- Zwłaszcza niebezpieczne jest optymalizowanie API. Implementację można wymienić, smród zostanie.
- Jeśli już wykonujesz optymalizację, kontroluj wydajność przed i po. Jeśli nie stwierdzisz znaczącego przyspieszenia, koniecznie wycofaj zmiany.
- Korzystaj z ogólnie przyjętych konwencji nazewnictwa. Pakiety powinny rozpoczynać się od nazwy domeny firmy pisanej od tyłu (net.nowaker). Skróty i akronimy pisz w ten sposób: EjbBean, HttpUrl, JndiSqlResource, ponieważ są czytelniejsze od EJBBean, HTTPURL, JNDISQLResource. Więcej zasad znajdziesz w Code Conventions for the Java Programming Language.
Wyjątki (item 57-65)
- Wyjątki rzuca się w sytuacjach wyjątkowych i żadnych innych. Nigdy wyjątek nie powinien oznaczać zakończenia się operacji sukcesem.
- Błąd programisty (użytkownika klasy) wyrażaj wyjątkami pochodnymi do RuntimeException, jak np. IllegalArgumentException. Jeśli użytkownik klasy jest w stanie przywrócić prawidłowe działanie aplikacji, należy raczej użyć wyjątku przechwytywalnego (“zwykłe” Exception). Nie powinno się stosować klas Error i pochodnych, ponieważ przyjęło się, że błędy są zarezerwowane dla JVM.
- Zbyt dużo wyłapywalnych wyjątków zaśmieca API. Przemyśl umieszczenie dodatkowej metody w stylu boolean isPermitted(), którą należy odpytać przed wywołaniem właściwej metody. Wtedy bez obaw możesz zamienić wyjątek przechwytywalny na nieprzechwytywalny.
- Korzystaj z wyjątków dostarczonych w środowisku Java. Najważniejsze to NullPointerException, IllegalArgumentException, UnsupportedOperationException, IllegalStateException, IndexOutOfBoundException, ConcurrentModificationException, ArithmeticException, NumberFormatException.
- Nie przekazuj warstwie wyższej wyjątków warstwy niższej, tylko tłumacz wyjątki. throw new HighLevelException(message, lowLevelException)
- Konstrukcja catch(Exception e) { logger.log(e); throw e; } powinna być zakazana. Albo loguj wyjątek albo rzuć go dalej – nigdy obu naraz.
- Dokładnie precyzuj warunki, przy jakich rzucane są poszczególne wyjątki z pomocą @throws w JavaDoc. @throws NullPointerException when string given is null, @throws IllegalArgumentException when amount is larger than 20.
- Opis wyjątku powinien jak najdokładniej opisywać przyczynę błędu oraz zawierać możliwie wszystkie dane, które pozwolą na zorientowanie się, co jest przyczyną. Attachment by given name not found jest słabsze od Attachment named MyFile.txt not found. Available attachments: MyFile.odt, picture.png, movie.avi.
- Wywołanie metody zakończone wyjątkiem nie powinno pozostawiać obiektu w nieprawidłowym stanie. Zachowaj atomowość – przed rzuceniem wyjątku wycofaj ewentualne zmiany.
- Nie umieszczaj pustych bloków catch. Jeśli jednak musisz tak zrobić, umieść komentarz z wyjaśnieniem sytuacji.
Cały kompletny rozdział po polsku znajdziesz tutaj: Effective Java - rozdział 9 - wyjątki.
Współbieżność (item 66-73)
- Synchronizowanie obiektów uniemożliwia ujrzenie obiektu w stanie niespójnym.
- Nie ulegnij pokusie, aby zwiększyć wydajność poprzez unikanie synchronizacji. Błędy współbieżności to chyba najtrudniejsze błędy do wykrycia. Rzadko idzie je zreprodukować w testach jednostkowych.
- Nie używaj Thread.stop(). Ryzykujesz pozostawienie danych w stanie niespójnym.
- Masz gwarancję, że jeden wątek zobaczy zmianę wartości zmiennej dokonanej przez drugi wątek tylko przez synchronizację pola lub oznaczenie go jako volatile.
- Synchronizacja nie ma sensu, gdy występuje wyłącznie na setterze. Trzeba synchronizować również setter i getter lub wcale.
- Operator inkrementacji var++ nie jest atomowy. Nie można więc generować kolejnych wartości bez synchronizacji metody zwracającej. Jednak zamiast synchronizować tą metodę, użyj gotowca – klasy AtomicLong.
- …A najlepiej mieć wyłącznie niezmienne obiekty – ich nie trzeba synchronizować.
- Nigdy nie wywołuj “obcych” akcji w sekcji synchronizowanej. Obcą akcją może być np. wywołanie metody, która nie jest final – użytkownik może ją rozszerzyć, umieszczając w niej operacje prowadzące do zakleszczenia. Powiadamianie obserwatorów również.
- Wykonuj jak najmniej operacji w sekcji synchronizowanej, ale nie za mało.
- Pakiet java.util.concurrent zawiera bardzo dużo znakomitych klas. Programowanie współbieżne jest trudne, dlatego nie próbuj pisać kolejek wątków na własną rękę.
- Nie rozszerzaj klasy Thread. Używaj interfejsów Runnable i Callable. Jeśli chcesz wystartować nowy wątek, użyj: new Thread(new Runnable { @Override… }).start();
- Korzystaj z narzędzi udostępnianych przez pakiet java.util.concurrent zamiast z metod wait i notify.
- Współbieżne kolekcje działają lepiej od synchronizowanych kolekcji, bo… są współbieżne. :-) Kolekcje współbieżne same zarządzają swoim stanem i blokadami, przez co oferuję o wiele lepszą szybkość działania.
- Do obliczania różnic czasu pomiędzy dwoma momentami korzystaj z System.nanoTime zamiast System.currentTimeMillis.
- Jeśli korzystamy z “natywnych” metod wait i notify, wywoływanie wait powinno odbywać się w pętli while. Preferuj notifyAll ponad notify, ponieważ to pierwsze zawsze gwarantuje poprawność.
- Istnienie modyfikatora synchronized przy metodzie nie oznacza automatycznego bezpieczeństwa klasy dla wątków. Poza tym, modyfikator ten jest szczegółem implementacyjnym metody, a nie częścią publicznego API. Bezpieczeństwo dla wątków należy dokumentować przy pomocy JavaDoc. Najczęściej spotykane poziomy bezpieczeństwa to niezmienne, bezpieczne dla wątków, warunkowo bezpieczne (dokumentacja mówi pod jakimi warunkami), niezgodne z wątkami (trzeba zewnętrznie synchronizować dostęp do nich) oraz niebezpieczne dla wątków (niemożliwe do użycia przez wiele wątków - gdy np. wywołania metod modyfikują wspólne dane statyczne).
- Idiom prywatnego blokowania obiektu można używać, aby zapobiegać próbom ataków typu denial of service na klasy.
- Raczej unikaj późnej inicjalizacji. Wprowadź ją dopiero, gdy profiler wykaże problemy wydajnościowe związane z niepotrzebną inicjalizacją pola, a jej wprowadzenie ją rozwiąże. Gdy już ją wprowadzasz, dla pól statycznych użyj statycznej klasy wewnętrznej - JVM gwarantuje, że inicjalizacja klasy i jej składowych odłożona jest na pierwsze użycie klasy.
- Nie ufaj harmonogramowi wątków. Program, którego poprawność zależy np. od priorytetów wątków, prędzej czy później przestanie działać.
- Nie używaj ThreadGroup. Klasa jest, paradoksalnie, niebezpieczna dla wątków.
Serializacja (item 74-78)
- Implementuj interfejs Serializable tylko wtedy, gdy jest to absolutnie potrzebne. Wiąże się z tym interfejsem wiele niedogodności - brak możliwości zmiany implementacji w przyszłości, utrudnienie testowania oraz łatwość powstania błędów.
- Najczęściej domyślna postać serializacji nie będzie odpowiednia. Wtedy musisz zadbać o oznaczenie odpowiednich pól jako transient i przygotowanie własnych metod readObject i writeObject.
- Własna metoda readObject pod względem logicznym to nic innego, jak pozajęzykowy konstruktor z szeregiem wad, np. brakiem możliwości nadania wartości polom final. Jest jednak lepszy od domyślnej implementacji, która nie radzi sobie dobrze z grafami obiektów (np. listami dwukierunkowymi).
- Metoda writeObject powinna być synchronizowana.
- Zawsze jawnie definiuj serialVersionUID klas serializowalnych.
- Nie zapomnij o wykonywaniu defensywnych kopii podczas deserializacji.
Słowo końcowe ;-)
Kontynuuję osobistą krucjatę mającą na celu pisanie lepszego kodu. Niedawno zacząłem czytać Design Patterns – Elements of Reusable Object-Oriented Software Bandy Czterech. A Clean Code Roberta Martina zostało zakolejkowane. ;-) Jeśli nasunęły Ci się jakieś własne myśli po przeczytaniu Effective Java, zostaw komentarz – dołączę je do wpisu.
Damian NowakCEO & Ruby Developer