Błędny kod Java: 10 najczęstszych błędów popełnianych przez programistów Java
Opublikowany: 2022-03-11Java to język programowania, który początkowo został opracowany dla telewizji interaktywnej, ale z czasem stał się powszechny wszędzie tam, gdzie można używać oprogramowania. Zaprojektowana z myślą o programowaniu obiektowym, znosząca złożoność innych języków, takich jak C lub C++, wyrzucanie śmieci i agnostyczna pod względem architektonicznym maszyna wirtualna, Java stworzyła nowy sposób programowania. Co więcej, ma łagodną krzywą uczenia się i wydaje się, że z powodzeniem stosuje się do własnego moto - „Napisz raz, biegnij wszędzie”, co prawie zawsze jest prawdą; ale problemy z Javą są nadal obecne. Zajmę się dziesięcioma problemami Javy, które moim zdaniem są najczęstszymi błędami.
Powszechny błąd nr 1: zaniedbywanie istniejących bibliotek
Zdecydowanie błędem dla programistów Java jest ignorowanie niezliczonej ilości bibliotek napisanych w Javie. Przed wynalezieniem koła na nowo postaraj się poszukać dostępnych bibliotek – wiele z nich zostało dopracowanych przez lata ich istnienia i można z nich korzystać bezpłatnie. Mogą to być biblioteki rejestrujące, takie jak logback i Log4j, lub biblioteki związane z siecią, takie jak Netty lub Akka. Niektóre z bibliotek, jak np. Joda-Time, stały się de facto standardem.
Poniżej znajduje się osobiste doświadczenie z jednego z moich poprzednich projektów. Część kodu odpowiedzialna za ucieczkę HTML została napisana od podstaw. Działał dobrze przez lata, ale w końcu napotkał dane wejściowe użytkownika, które spowodowały, że zakręcił się w nieskończoną pętlę. Użytkownik, stwierdzając, że usługa nie odpowiada, próbował ponowić próbę z tymi samymi danymi wejściowymi. Ostatecznie wszystkie procesory na serwerze przydzielonym dla tej aplikacji zostały zajęte przez tę nieskończoną pętlę. Gdyby autor tego naiwnego narzędzia do ucieczki HTML zdecydował się użyć jednej z dobrze znanych bibliotek dostępnych do ucieczki HTML, takich jak HtmlEscapers z Google Guava, prawdopodobnie by się nie stało. Przynajmniej, w przypadku większości popularnych bibliotek, za którymi stoi społeczność, błąd zostałby wcześniej znaleziony i naprawiony przez społeczność dla tej biblioteki.
Częsty błąd nr 2: brak słowa kluczowego „break” w bloku rozdzielnicy
Te problemy z Javą mogą być bardzo krępujące i czasami pozostają nieodkryte, dopóki nie zostaną uruchomione w środowisku produkcyjnym. Zachowanie opadowe w instrukcjach switch jest często przydatne; jednak pominięcie słowa kluczowego „przerwa”, gdy takie zachowanie nie jest pożądane, może prowadzić do katastrofalnych rezultatów. Jeśli zapomniałeś umieścić „przerwę” w „przypadku 0” w poniższym przykładzie, program napisze „Zero”, a następnie „Jeden”, ponieważ przepływ sterowania w tym miejscu przejdzie przez całą instrukcję „switch”, aż dochodzi do „przerwy”. Na przykład:
public static void switchCasePrimer() { int caseIndex = 0; switch (caseIndex) { case 0: System.out.println("Zero"); case 1: System.out.println("One"); break; case 2: System.out.println("Two"); break; default: System.out.println("Default"); } }
W większości przypadków czystszym rozwiązaniem byłoby użycie polimorfizmu i przeniesienie kodu o określonych zachowaniach do oddzielnych klas. Takie błędy Java można wykryć za pomocą statycznych analizatorów kodu, np. FindBugs i PMD.
Powszechny błąd nr 3: Zapomnienie o wolnych zasobach
Za każdym razem, gdy program otwiera plik lub połączenie sieciowe, ważne jest, aby początkujący użytkownicy języka Java zwolnili zasób po zakończeniu korzystania z niego. Podobną ostrożność należy zachować w przypadku zgłaszania wyjątków podczas operacji na takich zasobach. Można argumentować, że FileInputStream ma finalizator, który wywołuje metodę close() w zdarzeniu garbage collection; jednak ponieważ nie możemy być pewni, kiedy rozpocznie się cykl wyrzucania śmieci, strumień wejściowy może zużywać zasoby komputera przez nieokreślony czas. W rzeczywistości w Javie 7 wprowadzono bardzo przydatne i zgrabne oświadczenie, szczególnie w tym przypadku, zwane try-with-resources:
private static void printFileJava7() throws IOException { try(FileInputStream input = new FileInputStream("file.txt")) { int data = input.read(); while(data != -1){ System.out.print((char) data); data = input.read(); } } }
Ta instrukcja może być używana z dowolnym obiektem, który implementuje interfejs AutoClosable. Gwarantuje, że każdy zasób zostanie zamknięty do końca wyciągu.
Powszechny błąd nr 4: wycieki pamięci
Java korzysta z automatycznego zarządzania pamięcią i choć z ulgą zapomina się o ręcznym przydzielaniu i zwalnianiu pamięci, nie oznacza to, że początkujący programista Java nie powinien być świadomy tego, jak pamięć jest wykorzystywana w aplikacji. Nadal możliwe są problemy z alokacją pamięci. Dopóki program tworzy odniesienia do obiektów, które nie są już potrzebne, nie zostanie zwolniony. W pewnym sensie nadal możemy nazwać ten wyciek pamięci. Wycieki pamięci w Javie mogą wystąpić na różne sposoby, ale najczęstszym powodem są wieczne odniesienia do obiektów, ponieważ garbage collector nie może usunąć obiektów ze sterty, gdy wciąż istnieją do nich odniesienia. Można stworzyć taką referencję, definiując klasę z polem statycznym zawierającym pewną kolekcję obiektów i zapominając o ustawieniu tego pola statycznego na null, gdy kolekcja nie jest już potrzebna. Pola statyczne są uważane za korzenie GC i nigdy nie są zbierane.
Inną potencjalną przyczyną takich wycieków pamięci jest grupa obiektów, które odwołują się do siebie nawzajem, powodując zależności kołowe, tak że garbage collector nie może zdecydować, czy te obiekty z odniesieniami między zależnościami są potrzebne, czy nie. Innym problemem są przecieki w pamięci nie sterty, gdy używane jest JNI.
Przykład pierwotnego wycieku może wyglądać następująco:
final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1); final Deque<BigDecimal> numbers = new LinkedBlockingDeque<>(); final BigDecimal divisor = new BigDecimal(51); scheduledExecutorService.scheduleAtFixedRate(() -> { BigDecimal number = numbers.peekLast(); if (number != null && number.remainder(divisor).byteValue() == 0) { System.out.println("Number: " + number); System.out.println("Deque size: " + numbers.size()); } }, 10, 10, TimeUnit.MILLISECONDS); scheduledExecutorService.scheduleAtFixedRate(() -> { numbers.add(new BigDecimal(System.currentTimeMillis())); }, 10, 10, TimeUnit.MILLISECONDS); try { scheduledExecutorService.awaitTermination(1, TimeUnit.DAYS); } catch (InterruptedException e) { e.printStackTrace(); }
Ten przykład tworzy dwa zaplanowane zadania. Pierwsze zadanie pobiera ostatnią liczbę z deque o nazwie „liczby” i drukuje liczbę i rozmiar deque w przypadku, gdy liczba jest podzielna przez 51. Drugie zadanie umieszcza liczby w deque. Oba zadania są zaplanowane ze stałą częstotliwością i uruchamiane co 10 ms. Jeśli kod zostanie wykonany, zobaczysz, że rozmiar deque stale się zwiększa. Spowoduje to w końcu wypełnienie deki obiektami zużywającymi całą dostępną pamięć sterty. Aby temu zapobiec, zachowując semantykę tego programu, możemy zastosować inną metodę pobierania liczb z deque: „pollLast”. W przeciwieństwie do metody „peekLast”, „pollLast” zwraca element i usuwa go z deque, podczas gdy „peekLast” zwraca tylko ostatni element.
Aby dowiedzieć się więcej o wyciekach pamięci w Javie, zapoznaj się z naszym artykułem wyjaśniającym ten problem.
Powszechny błąd nr 5: nadmierne przydzielanie śmieci
Nadmierna alokacja śmieci może się zdarzyć, gdy program tworzy wiele obiektów krótko żyjących. Odśmiecacz działa w sposób ciągły, usuwając niepotrzebne obiekty z pamięci, co negatywnie wpływa na wydajność aplikacji. Jeden prosty przykład:
String oneMillionHello = ""; for (int i = 0; i < 1000000; i++) { oneMillionHello = oneMillionHello + "Hello!"; } System.out.println(oneMillionHello.substring(0, 6));
W programowaniu Java łańcuchy są niezmienne. Tak więc w każdej iteracji tworzony jest nowy ciąg. Aby rozwiązać ten problem, powinniśmy użyć zmiennej StringBuilder:
StringBuilder oneMillionHelloSB = new StringBuilder(); for (int i = 0; i < 1000000; i++) { oneMillionHelloSB.append("Hello!"); } System.out.println(oneMillionHelloSB.toString().substring(0, 6));
Podczas gdy pierwsza wersja wymaga trochę czasu na wykonanie, wersja używająca StringBuilder generuje wynik w znacznie krótszym czasie.
Częsty błąd nr 6: Używanie zerowych odniesień bez potrzeby
Dobrą praktyką jest unikanie nadmiernego używania wartości null. Na przykład lepiej jest zwracać puste tablice lub kolekcje z metod zamiast wartości null, ponieważ może to pomóc w zapobieganiu NullPointerException.
Rozważ następującą metodę, która przemierza kolekcję uzyskaną z innej metody, jak pokazano poniżej:
List<String> accountIds = person.getAccountIds(); for (String accountId : accountIds) { processAccount(accountId); }
Jeśli getAccountIds() zwraca null, gdy osoba nie ma konta, zostanie zgłoszony wyjątek NullPointerException. Aby to naprawić, konieczne będzie sprawdzenie wartości null. Jeśli jednak zamiast null zwraca pustą listę, NullPointerException nie stanowi już problemu. Co więcej, kod jest bardziej przejrzysty, ponieważ nie musimy sprawdzać wartości null w zmiennej accountIds.
Aby poradzić sobie z innymi przypadkami, gdy chce się uniknąć wartości zerowych, można zastosować różne strategie. Jedną z tych strategii jest użycie typu opcjonalnego, który może być pustym obiektem lub opakowaniem o pewnej wartości:
Optional<String> optionalString = Optional.ofNullable(nullableString); if(optionalString.isPresent()) { System.out.println(optionalString.get()); }
W rzeczywistości Java 8 zapewnia bardziej zwięzłe rozwiązanie:
Optional<String> optionalString = Optional.ofNullable(nullableString); optionalString.ifPresent(System.out::println);
Typ opcjonalny jest częścią Javy od wersji 8, ale jest dobrze znany w świecie programowania funkcjonalnego od dawna. Wcześniej był dostępny w Google Guava dla wcześniejszych wersji Javy.
Powszechny błąd nr 7: ignorowanie wyjątków
Często kuszące jest pozostawienie nieobsługiwanych wyjątków. Jednak najlepszą praktyką zarówno dla początkujących, jak i doświadczonych programistów Java jest ich obsługa. Wyjątki są rzucane celowo, więc w większości przypadków musimy rozwiązać problemy powodujące te wyjątki. Nie przeocz tych wydarzeń. W razie potrzeby możesz go ponownie zgłosić, wyświetlić użytkownikowi okno dialogowe błędu lub dodać wiadomość do dziennika. Przynajmniej należy wyjaśnić, dlaczego wyjątek nie został obsłużony, aby inni programiści znali przyczynę.
selfie = person.shootASelfie(); try { selfie.show(); } catch (NullPointerException e) { // Maybe, invisible man. Who cares, anyway? }
Jaśniejszym sposobem podkreślenia nieistotności wyjątków jest zakodowanie tej wiadomości w nazwie zmiennej wyjątków, na przykład:
try { selfie.delete(); } catch (NullPointerException unimportant) { }
Powszechny błąd nr 8: wyjątek współbieżnej modyfikacji
Ten wyjątek występuje, gdy kolekcja jest modyfikowana podczas iteracji przy użyciu metod innych niż te, które zapewnia obiekt iteratora. Na przykład mamy listę czapek i chcemy usunąć wszystkie te, które mają nauszniki:
List<IHat> hats = new ArrayList<>(); hats.add(new Ushanka()); // that one has ear flaps hats.add(new Fedora()); hats.add(new Sombrero()); for (IHat hat : hats) { if (hat.hasEarFlaps()) { hats.remove(hat); } }
Jeśli uruchomimy ten kod, zostanie zgłoszony „ConcurrentModificationException”, ponieważ kod modyfikuje kolekcję podczas jej iteracji. Ten sam wyjątek może wystąpić, jeśli jeden z wielu wątków pracujących z tą samą listą próbuje zmodyfikować kolekcję, podczas gdy inne ją iterują. Współbieżna modyfikacja kolekcji w wielu wątkach jest rzeczą naturalną, ale należy ją traktować za pomocą zwykłych narzędzi z zestawu narzędzi do programowania współbieżnego, takich jak blokady synchronizacji, specjalne kolekcje przyjęte do jednoczesnej modyfikacji itp. Istnieją subtelne różnice w sposobie rozwiązania tego problemu z Javą w przypadkach jednowątkowych i wielowątkowych. Poniżej znajduje się krótkie omówienie niektórych sposobów, w jakie można to obsłużyć w scenariuszu z jednym wątkiem:

Zbieraj przedmioty i usuwaj je w kolejnej pętli
Zbieranie czapek z nausznikami w listę w celu ich późniejszego usunięcia z innej pętli jest oczywistym rozwiązaniem, ale wymaga dodatkowej kolekcji na przechowywanie czapek do zdjęcia:
List<IHat> hatsToRemove = new LinkedList<>(); for (IHat hat : hats) { if (hat.hasEarFlaps()) { hatsToRemove.add(hat); } } for (IHat hat : hatsToRemove) { hats.remove(hat); }
Użyj metody Iterator.remove
To podejście jest bardziej zwięzłe i nie wymaga tworzenia dodatkowej kolekcji:
Iterator<IHat> hatIterator = hats.iterator(); while (hatIterator.hasNext()) { IHat hat = hatIterator.next(); if (hat.hasEarFlaps()) { hatIterator.remove(); } }
Użyj metod ListIterator
Użycie iteratora listy jest odpowiednie, gdy zmodyfikowana kolekcja implementuje interfejs List. Iteratory implementujące interfejs ListIterator obsługują nie tylko operacje usuwania, ale także operacje dodawania i ustawiania. ListIterator implementuje interfejs Iteratora, więc przykład wygląda prawie tak samo, jak metoda usuwania Iteratora. Jedyną różnicą jest typ iteratora kapelusza i sposób, w jaki uzyskujemy go za pomocą metody „listIterator()”. Poniższy fragment pokazuje, jak zastąpić każdy kapelusz z nausznikami sombrero przy użyciu metod „ListIterator.remove” i „ListIterator.add”:
IHat sombrero = new Sombrero(); ListIterator<IHat> hatIterator = hats.listIterator(); while (hatIterator.hasNext()) { IHat hat = hatIterator.next(); if (hat.hasEarFlaps()) { hatIterator.remove(); hatIterator.add(sombrero); } }
Dzięki ListIteratorowi wywołania metod usuwania i dodawania można zastąpić pojedynczym wywołaniem do ustawienia:
IHat sombrero = new Sombrero(); ListIterator<IHat> hatIterator = hats.listIterator(); while (hatIterator.hasNext()) { IHat hat = hatIterator.next(); if (hat.hasEarFlaps()) { hatIterator.set(sombrero); // set instead of remove and add } }
Używaj metod strumieniowych wprowadzonych w Javie 8 W Javie 8 programiści mają możliwość przekształcania kolekcji w strumień i filtrowania tego strumienia według pewnych kryteriów. Oto przykład, w jaki sposób api strumienia może pomóc nam filtrować kapelusze i unikać „ConcurrentModificationException”.
hats = hats.stream().filter((hat -> !hat.hasEarFlaps())) .collect(Collectors.toCollection(ArrayList::new));
Metoda „Collectors.toCollection” utworzy nową ArrayList z filtrowanymi kapeluszami. Może to stanowić problem, jeśli warunek filtrowania ma być spełniony przez dużą liczbę elementów, co skutkuje dużą ArrayList; dlatego należy go używać ostrożnie. Użyj metody List.removeIf przedstawionej w Javie 8 Kolejnym rozwiązaniem dostępnym w Javie 8 i oczywiście najbardziej zwięzłym jest zastosowanie metody „removeIf”:
hats.removeIf(IHat::hasEarFlaps);
Otóż to. Pod maską używa "Iterator.remove", aby wykonać zachowanie.
Korzystaj ze specjalistycznych kolekcji
Gdybyśmy na samym początku zdecydowali się na użycie „CopyOnWriteArrayList” zamiast „ArrayList”, to nie byłoby żadnego problemu, ponieważ „CopyOnWriteArrayList” udostępnia metody modyfikacji (takie jak set, add i remove), które się nie zmieniają podstawowa tablica kolekcji, ale raczej utwórz jej nową zmodyfikowaną wersję. Pozwala to na iterację oryginalnej wersji kolekcji i jednoczesne jej modyfikacje bez ryzyka „ConcurrentModificationException”. Wada tej kolekcji jest oczywista - generowanie nowej kolekcji z każdą modyfikacją.
Istnieją inne kolekcje dostosowane do różnych przypadków, np. „CopyOnWriteSet” i „ConcurrentHashMap”.
Innym możliwym błędem przy równoczesnych modyfikacjach kolekcji jest utworzenie strumienia z kolekcji, a podczas iteracji strumienia zmodyfikowanie kolekcji kopii zapasowej. Ogólna zasada dotycząca strumieni polega na unikaniu modyfikacji podstawowej kolekcji podczas wykonywania zapytań o strumienie. Poniższy przykład pokaże niepoprawny sposób obsługi strumienia:
List<IHat> filteredHats = hats.stream().peek(hat -> { if (hat.hasEarFlaps()) { hats.remove(hat); } }).collect(Collectors.toCollection(ArrayList::new));
Metoda peek zbiera wszystkie elementy i wykonuje na każdym z nich podaną akcję. W tym przypadku akcja próbuje usunąć elementy z podstawowej listy, co jest błędem. Aby tego uniknąć, wypróbuj niektóre z metod opisanych powyżej.
Powszechny błąd nr 9: zerwanie umów
Czasami kod dostarczany przez standardową bibliotekę lub przez zewnętrznego dostawcę opiera się na regułach, których należy przestrzegać, aby wszystko działało. Na przykład może to być kontrakt hashCode i equals, który po wykonaniu gwarantuje pracę dla zestawu kolekcji ze struktury kolekcji Java oraz dla innych klas, które używają metod hashCode i equals. Nieprzestrzeganie umów nie jest rodzajem błędu, który zawsze prowadzi do wyjątków lub zepsucia kompilacji kodu; jest to trudniejsze, ponieważ czasami zmienia zachowanie aplikacji bez żadnych oznak zagrożenia. Błędny kod może wślizgnąć się do wersji produkcyjnej i spowodować całą masę niepożądanych efektów. Może to obejmować złe zachowanie interfejsu użytkownika, nieprawidłowe raporty danych, słabą wydajność aplikacji, utratę danych i inne. Na szczęście te katastrofalne błędy nie zdarzają się zbyt często. Wspomniałem już o hashCode i oznacza kontrakt. Jest używany w kolekcjach, które opierają się na mieszaniu i porównywaniu obiektów, takich jak HashMap i HashSet. Mówiąc najprościej, umowa zawiera dwie zasady:
- Jeśli dwa obiekty są równe, to ich kody skrótu powinny być takie same.
- Jeśli dwa obiekty mają ten sam kod skrótu, mogą, ale nie muszą być równe.
Złamanie pierwszej zasady kontraktu prowadzi do problemów podczas próby pobrania obiektów z hashmapy. Druga reguła oznacza, że obiekty z tym samym kodem skrótu niekoniecznie są równe. Przyjrzyjmy się skutkom złamania pierwszej zasady:
public static class Boat { private String name; Boat(String name) { this.name = name; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Boat boat = (Boat) o; return !(name != null ? !name.equals(boat.name) : boat.name != null); } @Override public int hashCode() { return (int) (Math.random() * 5000); } }
Jak widać, klasa Boat przesłoniła metody równości i hashCode. Jednak zerwał kontrakt, ponieważ hashCode zwraca losowe wartości dla tego samego obiektu przy każdym wywołaniu. Poniższy kod najprawdopodobniej nie znajdzie łodzi o nazwie „Enterprise” w hashset, mimo że dodaliśmy wcześniej tego rodzaju łódź:
public static void main(String[] args) { Set<Boat> boats = new HashSet<>(); boats.add(new Boat("Enterprise")); System.out.printf("We have a boat named 'Enterprise' : %b\n", boats.contains(new Boat("Enterprise"))); }
Inny przykład kontraktu dotyczy metody finalizacji. Oto cytat z oficjalnej dokumentacji javy opisujący jej funkcję:
Ogólna umowa finalizacji polega na tym, że jest on wywoływany, jeśli i kiedy maszyna wirtualna JavaTM ustali, że nie ma już żadnych środków, za pomocą których można uzyskać dostęp do tego obiektu przez dowolny wątek (który jeszcze nie umarł), chyba że w wyniku działanie podjęte przez sfinalizowanie innego obiektu lub klasy, która jest gotowa do sfinalizowania. Metoda finalize może podjąć dowolną akcję, w tym ponowne udostępnienie tego obiektu innym wątkom; jednak zwykłym celem finalizacji jest wykonanie czynności porządkowych, zanim obiekt zostanie nieodwołalnie odrzucony. Na przykład metoda finalize dla obiektu, który reprezentuje połączenie wejścia/wyjścia, może wykonać jawne transakcje we/wy, aby przerwać połączenie, zanim obiekt zostanie trwale odrzucony.
Można by zdecydować się na użycie metody finalize do zwalniania zasobów, takich jak programy obsługi plików, ale byłby to zły pomysł. Dzieje się tak, ponieważ nie ma gwarancji czasu, kiedy zostanie wywołane finalize, ponieważ jest ono wywoływane podczas zbierania śmieci, a czas GC jest nieokreślony.
Powszechny błąd nr 10: używanie typu surowego zamiast sparametryzowanego
Typy surowe, zgodnie ze specyfikacjami Javy, są typami, które albo nie są sparametryzowane, albo niestatycznymi członkami klasy R, które nie są dziedziczone z nadklasy lub superinterfejsu R. Nie było alternatyw dla typów surowych, dopóki typy generyczne nie zostały wprowadzone w Javie . Obsługuje programowanie generyczne od wersji 1.5, a generyki były niewątpliwie znaczącym ulepszeniem. Jednak ze względu na kompatybilność wsteczną pozostawiono pułapkę, która może potencjalnie uszkodzić system typów. Spójrzmy na następujący przykład:
List listOfNumbers = new ArrayList(); listOfNumbers.add(10); listOfNumbers.add("Twenty"); listOfNumbers.forEach(n -> System.out.println((int) n * 2));
Tutaj mamy listę liczb zdefiniowaną jako surowa ArrayList. Ponieważ jego typ nie jest określony parametrem type, możemy dodać do niego dowolny obiekt. Ale w ostatniej linii rzutujemy elementy na int, podwajamy je i wypisujemy podwojoną liczbę na standardowe wyjście. Ten kod skompiluje się bez błędów, ale po uruchomieniu zgłosi wyjątek w czasie wykonywania, ponieważ próbowaliśmy rzutować łańcuch na liczbę całkowitą. Oczywiście system typów nie jest w stanie pomóc nam napisać bezpiecznego kodu, jeśli ukryjemy przed nim niezbędne informacje. Aby rozwiązać ten problem, musimy określić typ obiektów, które zamierzamy przechowywać w kolekcji:
List<Integer> listOfNumbers = new ArrayList<>(); listOfNumbers.add(10); listOfNumbers.add("Twenty"); listOfNumbers.forEach(n -> System.out.println((int) n * 2));
Jedyną różnicą w stosunku do oryginału jest linia określająca kolekcję:
List<Integer> listOfNumbers = new ArrayList<>();
Poprawiony kod nie skompilowałby się, ponieważ próbujemy dodać ciąg do kolekcji, która powinna przechowywać tylko liczby całkowite. Kompilator wyświetli błąd i wskaże wiersz, w którym próbujemy dodać do listy ciąg „Dwadzieścia”. Zawsze dobrze jest sparametryzować typy generyczne. W ten sposób kompilator jest w stanie wykonać wszystkie możliwe sprawdzenia typów, a szanse na wyjątki środowiska uruchomieniowego spowodowane przez niespójności systemu typów są zminimalizowane.
Wniosek
Java jako platforma upraszcza wiele rzeczy w tworzeniu oprogramowania, opierając się zarówno na wyrafinowanej JVM, jak i samym języku. Jednak jego funkcje, takie jak usuwanie ręcznego zarządzania pamięcią lub przyzwoite narzędzia OOP, nie eliminują wszystkich problemów, z którymi boryka się zwykły programista Java. Jak zawsze, wiedza, praktyka i samouczki Java, takie jak ta, są najlepszym sposobem na uniknięcie i naprawienie błędów aplikacji - więc poznaj swoje biblioteki, czytaj Javę, czytaj dokumentację JVM i pisz programy. Nie zapomnij również o statycznych analizatorach kodu, ponieważ mogą one wskazywać rzeczywiste błędy i wskazywać potencjalne błędy.