Nasza strona używa cookies. Korzystając ze strony, wyrażasz zgodę na używanie cookies, zgodnie z aktualnymi ustawieniami przeglądarki. Rozumiem

10 częstych błędów popełnianych przez programistów Javy

David Bertoldi Java Technical Leader / openmind
Sprawdź, czy nie popełniasz czasem takich błędów w Javie jak nieużywanie equals(), czy ignorowanie modyfikatorów dostępu.
10 częstych błędów popełnianych przez programistów Javy

Przeprowadziłem rozmowy kwalifikacyjne z dziesiątkami inżynierów oprogramowania, od juniorów po starszych liderów technicznych i w wielu przypadkach kandydaci mieli braki w podstawach. W tym artykule przedstawię listę najczęstszych błędów popełnianych przez programistów Javy, w oparciu o moje doświadczenie zarówno jako lidera technicznego, jak i osoby przeprowadzającej techniczne wywiady z kandydatami.


1. Ignorowanie modyfikatorów dostępu

Nie pytaj dlaczego, ale kandydaci często zapominają o zasięgu chronionego modyfikatora dostępu w Javie. Być może to przez narastające napięcie, ale zazwyczaj wspominają tylko o jednym z dwóch:

  • chronione pola, metody i konstruktory mogą być dostępne z tego samego pakietu
  • chronione pola, metody i konstruktory mogą być dostępne z podklas


Naprawdę nie wiem, którego z nich gorzej jest zapomnieć, ale pominięcie informacji o pakietach ujawnia, że kandydat nigdy nie testował chronionej metody (chronione elementy są dostępne z testowej ścieżki klas, o ile pakiet jest taki sam); metody publiczne i chronione są częścią API zapewnianego przez Twoje oprogramowanie. Zatem zapomnienie o tej właściwości jest równoznaczne z oświadczeniem, że nigdy się nie napisało sensownego testu dla twojego oprogramowania!


2. Nieużywanie equals()

Jeśli użyjesz == (operator porównania) zamiast wywołania equals(), musisz zmienić swoje nawyki, ponieważ wynik może cię zaskoczyć.


Wyjaśnienie

Nigdy nie używaj ==, jeśli chcesz porównać dwa obiekty String (to się właściwie tyczy prawie każdego obiektu). == porównuje referencję do obiektu dwóch operandów (porównanie adresów pamięci), a nie ich zawartości. W powyższym przykładzie ciąg znaków y nie korzysta z internowania łańcuchów: jego adres pamięci jest inny niż adres x.


3. Konkatenacja ciągów znaków

Jeśli pracujesz z dużą liczbą ciągów znaków lub z ogromnymi ciągami znaków, to możesz zmarnować dużo pamięci w procesie konkatenacji.


W powyższym przykładzie tworzymy kilka obiektów StringBuilder oraz String, a dokładniej 10 000 000 obiektów StringBuilder i 10 000 001 obiektów String.


Wyjaśnienie

Aby zrozumieć, co się dzieje, musimy cofnąć się o krok. Gdy używasz operatora + do konkatenacji ciągów znaków, tworzysz obiekt pośredni, który przechowuje wynik konkatenacji przed przypisaniem jego wartości do obiektu docelowego.


W powyższym przykładzie utworzyliśmy w sumie 3 obiekty: 2 dla literałów i 1 dla konkatenacji. Jest to kopia pierwszego ciągu znaków i drugiego "world!". Dzieje się tak, ponieważ ciągi znaków są niemutowalne. Ale kompilator jest wystarczająco inteligentny, aby przekształcić kod w następujący (nie dotyczy Java 9 i dalszych, ponieważ używają one StringContactFacotry, ale wynik jest dość podobny). 


Ta optymalizacja usuwa pośredni obiekt konkatenacji, a pamięć jest zajęta przez 2 literały String oraz 1 StringBuilder. Zasadniczo liczba obiektów String spada z O(n²) do O(n). Powrót do pierwszego przykładu: kompilator optymalizuje kod w ten sposób:


Kompilator po prostu optymalizuje wewnętrzną konkatenację, ale tworzy to wiele obiektów StringBuilder i String. Właściwy sposób na połączenie obiektów String jest następujący i wymaga tylko jednego StringBuilder i jednego String.


Nieźle!


4. Hasła jako ciągi znaków

Przechowywanie haseł dostarczonych przez użytkowników w obiekcie String stanowi problem bezpieczeństwa, ponieważ są one podatne na ataki pamięci. Powinieneś użyć char[], ponieważ JPasswordField i Password4j już tak działają. Jeśli mówimy o aplikacjach webowych, większość kontenerów webowych przekazuje hasło tekstowe w obiekcie HttpServletRequest jako String, więc w tym przypadku prawie nic nie możesz na to poradzić.


Wyjaśnienie

Ciągi znaków są cache’owane przez JVM (interning) i przechowywane w przestrzeni PermGen (przed Java 8) lub w przestrzeni sterty. W obu przypadkach cache’owane wartości są usuwane dopiero po wystąpieniu odśmiecania: oznacza to, że nie wiadomo, kiedy określona wartość zostanie usunięta z puli ciągów znaków, ponieważ Garbage Collector działa w sposób niedeterministyczny.

Innym problemem jest to, że ciągi znaków są niemutowalne, więc nie można ich wyczyścić. char[] nie jest jednak niemutowalny i może zostać skasowany (np. zamień każdy element na 0) po przetworzeniu. Dzięki tej prostej sztuczce atakujący znalazłby w pamięci tylko wyzerowane tablice zamiast haseł w postaci tekstu.


5. Zwracanie null

Dużo razy zdarzyło mi się widzieć takie metody, jak ta:

Problem ze zwróceniem wartości null polega na tym, że zmuszasz osobę wywołującą do sprawdzenia, czy wynik nie jest nullem. W takim przypadku osoba wywołująca oczekuje, że jeśli nie będzie elementu, zostanie zwrócona pusta lista. Zawsze zwracaj wyjątek lub obiekt specjalny (np. pustą listę), bo w przeciwnym wypadku aplikacja używająca Twojego kodu będzie musiała sobie radzić z NullPointerException.


6. Przekazywanie null

Z drugiej strony, przekazanie wartości null oznacza, że nie kwestionujesz tego, że kod, który wywołujesz, może zarządzać taką wartością. Jeśli nie jest to prawda, aplikacja na pewno zgłosi wyjątek NullPointerException. Ponadto tworzysz zamieszanie w kodzie, gdy jawnie przekazujesz null. Poniżej znajduje się klasyczny przykład:


Po wywołaniu init() nie ma dostępnego obiektu User. Dlaczego więc wywoływać metodę działającą z User, jeśli nie masz nawet singleUser? Jeśli potrzebujesz logiki zawartej w grantAccessToUser(), powinieneś wyodrębnić ją do innej metody, której użyjesz zamiast przekazywania wartości null.


7. Ciężkie metody

Poniższy przykład może spowodować zmniejszenie wydajności w systemie:

Pattern.compile() jest ciężką metodą i nie należy jej wywoływać za każdym razem, gdy trzeba sprawdzić, czy dany ciąg znaków pasuje do tego samego wzorca.


Wyjaśnienie

Pattern.compile() wstępnie kompiluje wzorzec, dzięki czemu używana jest szybsza reprezentacja w pamięci. Ta operacja wymaga dużej mocy obliczeniowej w porównaniu do pojedynczego dopasowania. Klasycznym sposobem na zwiększenie wydajności jest zapamiętanie obiektu Pattern w polu statycznym, takim jak to:


Z tego rozwiązania należy korzystać za każdym razem, gdy ponownie używasz tego samego ciężkiego do obliczenia, bezstanowego obiektu.


8. Zwracanie „kodów” zamiast zgłaszania wyjątków

Programiści nie do końca lubią wyjątki, dlatego mają tendencję do pisania metod, które zwracają dziwne wartości, na przykład -1 lub C_ERR.


Jest to typowy przypadek, w którym warto utworzyć niestandardowy wyjątek. Przykład można zapisać w następujący sposób:


Jak widać, czytelność i łatwość utrzymania są większe. Wywołujący nie musi obsługiwać każdego pojedynczego kodu, a może jedynie odczytać treść wyjątku DeviceStartException.


9. Zmienianie kolekcji podczas iteracji


Ten kod zgłosi wyjątek ConcurrentModificationException.


Wyjaśnienie

Usunięcie elementu z listy podczas iteracji spowoduje, że iterator listy źle się zachowuje, np. pomija, czy powtarza elementy, indeksuje koniec tablicy itp. Dlatego wiele kolekcji woli zgłaszać wyjątek ConcurrentModificationException. Zamiast tego polecam użycie iteratora podstawowej tablicy:


10. Używanie StringBuffer


Przykład ten generuje dużo narzutu, bo StringBuffer jest klasą synchronizowaną. W bardziej skomplikowanych kontekstach czytelnicy mogą być przekonani, że synchronizacja jest wymagana tam, gdzie tak naprawdę nie jest. Jeśli znajdziesz StringBuffer w projekcie, może być to spowodowane tym, że jest on wymagany przez niektóre legacy API (a dokładniej te przed Javą 5). Dzieje się to jednak bardzo rzadko, ponieważ Twój kod próbuje dołączyć ciągi znaków w równoległym kontekście. Zamiast tego użyj StringBuilder: wprowadzono go w Javie 5 i wszystkie jego operacje nie są zsynchronizowane.


Podsumowanie

To tylko część błędów, które widzę podczas rozmów kwalifikacyjnych i wielu projektów. Nie wspomniałem nawet o pułapkach OOP, wzorcach projektowych, over-engineeringu, wyciekach pamięci itp. Jeśli popełnisz te błędy, to nadszedł czas, aby zmienić styl kodowania i sprawić, że Twoja aplikacja będzie łatwiejsza w utrzymaniu i bezpieczniejsza. Nie jest to takie trudne, a unikanie tych pułapek sprawi, że będziesz lepszym programistą, który bardziej się sprawdzi na kolejnej rozmowie.  

Badaj teorię, która stoi za każdym językiem programowania, a nie tylko jego składnię. Dużo koduj i używaj statycznych analizatorów kodów, takich jak SonarQube, ponieważ mogą one wskazać istniejące błędy i przewidzieć przyszłe. 

Lubisz dzielić się wiedzą i chcesz zostać autorem?

Podziel się wiedzą z 160 tysiącami naszych czytelników

Dowiedz się więcej