Fehlerhafter Java-Code: Die 10 häufigsten Fehler, die Java-Entwickler machen

Veröffentlicht: 2022-03-11

Java ist eine Programmiersprache, die ursprünglich für interaktives Fernsehen entwickelt wurde, sich aber im Laufe der Zeit überall dort verbreitet hat, wo Software eingesetzt werden kann. Entworfen mit dem Konzept der objektorientierten Programmierung, das die Komplexität anderer Sprachen wie C oder C++, Garbage Collection und eine architekturagnostische virtuelle Maschine abschafft, hat Java eine neue Art der Programmierung geschaffen. Darüber hinaus hat es eine sanfte Lernkurve und scheint sich erfolgreich an sein eigenes Motto zu halten – „Einmal schreiben, überall laufen“, was fast immer zutrifft; aber Java-Probleme sind immer noch vorhanden. Ich werde zehn Java-Probleme ansprechen, die meiner Meinung nach die häufigsten Fehler sind.

Häufiger Fehler Nr. 1: Vernachlässigung vorhandener Bibliotheken

Es ist definitiv ein Fehler für Java-Entwickler, die unzähligen in Java geschriebenen Bibliotheken zu ignorieren. Bevor Sie das Rad neu erfinden, versuchen Sie, nach verfügbaren Bibliotheken zu suchen – viele von ihnen wurden im Laufe der Jahre ihres Bestehens aufpoliert und können kostenlos verwendet werden. Dies können Protokollierungsbibliotheken wie logback und Log4j oder netzwerkbezogene Bibliotheken wie Netty oder Akka sein. Einige der Bibliotheken, wie z. B. Joda-Time, sind zu einem De-facto-Standard geworden.

Das Folgende ist eine persönliche Erfahrung aus einem meiner früheren Projekte. Der Teil des Codes, der für das HTML-Escape verantwortlich ist, wurde von Grund auf neu geschrieben. Es hat jahrelang gut funktioniert, aber schließlich stieß es auf eine Benutzereingabe, die dazu führte, dass es sich in eine Endlosschleife drehte. Der Benutzer, der feststellte, dass der Dienst nicht reagiert, versuchte es mit derselben Eingabe erneut. Schließlich wurden alle CPUs auf dem Server, die dieser Anwendung zugewiesen waren, von dieser Endlosschleife belegt. Wenn der Autor dieses naiven HTML-Escape-Tools sich entschieden hätte, eine der bekannten Bibliotheken für HTML-Escape zu verwenden, wie HtmlEscapers von Google Guava, wäre dies wahrscheinlich nicht passiert. Zumindest gilt für die meisten beliebten Bibliotheken mit einer Community dahinter, dass der Fehler für diese Bibliothek früher von der Community gefunden und behoben worden wäre.

Häufiger Fehler Nr. 2: Das Schlüsselwort „break“ in einem Switch-Case-Block fehlt

Diese Java-Probleme können sehr peinlich sein und bleiben manchmal unentdeckt, bis sie in der Produktion ausgeführt werden. Fallthrough-Verhalten in switch-Anweisungen ist oft nützlich; Das Fehlen eines „Break“-Schlüsselworts, wenn ein solches Verhalten nicht erwünscht ist, kann jedoch zu katastrophalen Ergebnissen führen. Wenn Sie im folgenden Codebeispiel vergessen haben, einen „Break“ in „case 0“ einzufügen, schreibt das Programm „Null“ gefolgt von „Eins“, da der Kontrollfluss hier durch die gesamte „switch“-Anweisung bis geht es kommt zu einer „Pause“. Zum Beispiel:

 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"); } }

In den meisten Fällen wäre die sauberere Lösung, Polymorphismus zu verwenden und Code mit bestimmten Verhaltensweisen in separate Klassen zu verschieben. Java-Fehler wie dieser können mit statischen Code-Analysatoren, zB FindBugs und PMD, erkannt werden.

Häufiger Fehler Nr. 3: Vergessen, Ressourcen freizugeben

Jedes Mal, wenn ein Programm eine Datei oder eine Netzwerkverbindung öffnet, ist es für Java-Anfänger wichtig, die Ressource freizugeben, sobald Sie damit fertig sind. Ähnliche Vorsicht ist geboten, wenn während Operationen auf solchen Ressourcen eine Ausnahme ausgelöst wird. Man könnte argumentieren, dass der FileInputStream einen Finalizer hat, der die Methode close() für ein Garbage-Collection-Ereignis aufruft; Da wir jedoch nicht sicher sein können, wann ein Garbage-Collection-Zyklus beginnt, kann der Eingabestrom Computerressourcen für einen unbestimmten Zeitraum verbrauchen. Tatsächlich gibt es in Java 7 eine wirklich nützliche und nette Anweisung namens try-with-resources, die speziell für diesen Fall eingeführt wurde:

 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(); } } }

Diese Anweisung kann mit jedem Objekt verwendet werden, das die AutoClosable-Schnittstelle implementiert. Es stellt sicher, dass jede Ressource am Ende der Anweisung geschlossen wird.

Siehe auch: 8 wichtige Java-Interviewfragen

Häufiger Fehler Nr. 4: Speicherlecks

Java verwendet eine automatische Speicherverwaltung, und obwohl es eine Erleichterung ist, das manuelle Zuweisen und Freigeben von Speicher zu vergessen, bedeutet dies nicht, dass ein beginnender Java-Entwickler nicht wissen sollte, wie Speicher in der Anwendung verwendet wird. Probleme mit Speicherzuweisungen sind weiterhin möglich. Solange ein Programm Verweise auf nicht mehr benötigte Objekte erstellt, wird es nicht freigegeben. In gewisser Weise können wir dieses Speicherleck immer noch nennen. Speicherlecks in Java können auf verschiedene Weise auftreten, aber der häufigste Grund sind ewige Objektreferenzen, da der Garbage Collector keine Objekte aus dem Heap entfernen kann, solange noch Referenzen auf sie vorhanden sind. Man kann eine solche Referenz erstellen, indem man eine Klasse mit einem statischen Feld definiert, das eine Sammlung von Objekten enthält, und vergisst, dieses statische Feld auf null zu setzen, nachdem die Sammlung nicht mehr benötigt wird. Statische Felder gelten als GC-Wurzeln und werden nie erfasst.

Ein weiterer möglicher Grund für solche Speicherlecks ist eine Gruppe von Objekten, die aufeinander verweisen und zirkuläre Abhängigkeiten verursachen, sodass der Garbage Collector nicht entscheiden kann, ob diese Objekte mit Querabhängigkeitsreferenzen benötigt werden oder nicht. Ein weiteres Problem sind Lecks im Nicht-Heap-Speicher, wenn JNI verwendet wird.

Das primitive Leak-Beispiel könnte wie folgt aussehen:

 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(); }

In diesem Beispiel werden zwei geplante Aufgaben erstellt. Die erste Aufgabe nimmt die letzte Zahl aus einer Doppelschlange namens „Zahlen“ und gibt die Zahl und die Größe der Doppelschlange aus, falls die Zahl durch 51 teilbar ist. Die zweite Aufgabe fügt Zahlen in die Doppelschlange ein. Beide Tasks werden mit einer festen Rate geplant und alle 10 ms ausgeführt. Wenn der Code ausgeführt wird, sehen Sie, dass die Größe der Deque permanent zunimmt. Dies führt schließlich dazu, dass die Deque mit Objekten gefüllt wird, die den gesamten verfügbaren Heap-Speicher verbrauchen. Um dies zu verhindern und gleichzeitig die Semantik dieses Programms beizubehalten, können wir eine andere Methode zum Entnehmen von Zahlen aus der Deque verwenden: „pollLast“. Im Gegensatz zur Methode „peekLast“ gibt „pollLast“ das Element zurück und entfernt es aus der Deque, während „peekLast“ nur das letzte Element zurückgibt.

Um mehr über Speicherlecks in Java zu erfahren, lesen Sie bitte unseren Artikel, der dieses Problem entmystifiziert hat.

Häufiger Fehler Nr. 5: Übermäßige Müllzuweisung

Wenn das Programm viele kurzlebige Objekte erstellt, kann es zu einer übermäßigen Garbage-Zuordnung kommen. Der Garbage Collector arbeitet kontinuierlich und entfernt nicht benötigte Objekte aus dem Speicher, was sich negativ auf die Leistung von Anwendungen auswirkt. Ein einfaches Beispiel:

 String oneMillionHello = ""; for (int i = 0; i < 1000000; i++) { oneMillionHello = oneMillionHello + "Hello!"; } System.out.println(oneMillionHello.substring(0, 6));

In der Java-Entwicklung sind Strings unveränderlich. Bei jeder Iteration wird also eine neue Zeichenfolge erstellt. Um dies zu beheben, sollten wir einen veränderlichen StringBuilder verwenden:

 StringBuilder oneMillionHelloSB = new StringBuilder(); for (int i = 0; i < 1000000; i++) { oneMillionHelloSB.append("Hello!"); } System.out.println(oneMillionHelloSB.toString().substring(0, 6));

Während die erste Version ziemlich viel Zeit für die Ausführung benötigt, liefert die Version, die StringBuilder verwendet, ein Ergebnis in deutlich weniger Zeit.

Häufiger Fehler Nr. 6: Verwendung von Nullreferenzen ohne Notwendigkeit

Es empfiehlt sich, die übermäßige Verwendung von null zu vermeiden. Beispielsweise ist es vorzuziehen, leere Arrays oder Auflistungen von Methoden anstelle von Nullen zurückzugeben, da dies dazu beitragen kann, NullPointerException zu verhindern.

Betrachten Sie die folgende Methode, die eine Sammlung durchläuft, die von einer anderen Methode erhalten wurde, wie unten gezeigt:

 List<String> accountIds = person.getAccountIds(); for (String accountId : accountIds) { processAccount(accountId); }

Wenn getAccountIds() null zurückgibt, wenn eine Person kein Konto hat, wird eine NullPointerException ausgelöst. Um dies zu beheben, ist eine Nullprüfung erforderlich. Wenn es jedoch anstelle einer Null eine leere Liste zurückgibt, ist NullPointerException kein Problem mehr. Außerdem ist der Code sauberer, da wir die Variable accountIds nicht auf Null prüfen müssen.

Um mit anderen Fällen fertig zu werden, in denen Nullen vermieden werden sollen, können andere Strategien verwendet werden. Eine dieser Strategien besteht darin, den optionalen Typ zu verwenden, der entweder ein leeres Objekt oder eine Hülle mit einem bestimmten Wert sein kann:

 Optional<String> optionalString = Optional.ofNullable(nullableString); if(optionalString.isPresent()) { System.out.println(optionalString.get()); }

Tatsächlich bietet Java 8 eine prägnantere Lösung:

 Optional<String> optionalString = Optional.ofNullable(nullableString); optionalString.ifPresent(System.out::println);

Der optionale Typ ist seit Version 8 ein Teil von Java, aber in der Welt der funktionalen Programmierung ist er seit langem bekannt. Zuvor war es in Google Guava für frühere Java-Versionen verfügbar.

Häufiger Fehler Nr. 7: Ignorieren von Ausnahmen

Es ist oft verlockend, Ausnahmen unbehandelt zu lassen. Die beste Vorgehensweise für Anfänger und erfahrene Java-Entwickler ist jedoch, mit ihnen umzugehen. Ausnahmen werden absichtlich ausgelöst, daher müssen wir in den meisten Fällen die Probleme beheben, die diese Ausnahmen verursachen. Übersehen Sie diese Ereignisse nicht. Bei Bedarf können Sie es entweder erneut auslösen, dem Benutzer einen Fehlerdialog anzeigen oder dem Protokoll eine Nachricht hinzufügen. Zumindest sollte erklärt werden, warum die Ausnahme nicht behandelt wurde, um andere Entwickler über den Grund zu informieren.

 selfie = person.shootASelfie(); try { selfie.show(); } catch (NullPointerException e) { // Maybe, invisible man. Who cares, anyway? }

Eine klarere Möglichkeit, die Bedeutungslosigkeit einer Ausnahme hervorzuheben, besteht darin, diese Nachricht wie folgt in den Variablennamen der Ausnahme zu codieren:

 try { selfie.delete(); } catch (NullPointerException unimportant) { }

Häufiger Fehler Nr. 8: Ausnahme bei gleichzeitiger Änderung

Diese Ausnahme tritt auf, wenn eine Sammlung geändert wird, während sie mit anderen Methoden als den vom Iteratorobjekt bereitgestellten Methoden durchlaufen wird. Zum Beispiel haben wir eine Liste mit Hüten und wir möchten alle entfernen, die Ohrenklappen haben:

 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); } }

Wenn wir diesen Code ausführen, wird „ConcurrentModificationException“ ausgelöst, da der Code die Sammlung während der Iteration ändert. Dieselbe Ausnahme kann auftreten, wenn einer der mehreren Threads, die mit derselben Liste arbeiten, versucht, die Sammlung zu ändern, während andere darüber iterieren. Die gleichzeitige Änderung von Sammlungen in mehreren Threads ist eine natürliche Sache, sollte aber mit üblichen Werkzeugen aus der Toolbox der gleichzeitigen Programmierung behandelt werden, wie z in Singlethread-Fällen und Multithread-Fällen. Im Folgenden finden Sie eine kurze Erörterung einiger Möglichkeiten, wie dies in einem Single-Thread-Szenario gehandhabt werden kann:

Sammle Objekte und entferne sie in einer weiteren Schleife

Das Sammeln von Hüten mit Ohrenklappen in einer Liste, um sie später aus einer anderen Schleife zu entfernen, ist eine naheliegende Lösung, erfordert jedoch eine zusätzliche Sammlung zum Speichern der zu entfernenden Hüte:

 List<IHat> hatsToRemove = new LinkedList<>(); for (IHat hat : hats) { if (hat.hasEarFlaps()) { hatsToRemove.add(hat); } } for (IHat hat : hatsToRemove) { hats.remove(hat); }

Verwenden Sie die Iterator.remove-Methode

Dieser Ansatz ist prägnanter und es muss keine zusätzliche Sammlung erstellt werden:

 Iterator<IHat> hatIterator = hats.iterator(); while (hatIterator.hasNext()) { IHat hat = hatIterator.next(); if (hat.hasEarFlaps()) { hatIterator.remove(); } }

Verwenden Sie die Methoden von ListIterator

Die Verwendung des Listen-Iterators ist geeignet, wenn die geänderte Sammlung die List-Schnittstelle implementiert. Iteratoren, die die ListIterator-Schnittstelle implementieren, unterstützen nicht nur Entfernungsoperationen, sondern auch Additions- und Set-Operationen. ListIterator implementiert die Iterator-Schnittstelle, sodass das Beispiel fast genauso aussehen würde wie die Iterator-Entfernungsmethode. Der einzige Unterschied ist der Typ des Hut-Iterators und die Art und Weise, wie wir diesen Iterator mit der Methode „listIterator()“ erhalten. Das folgende Snippet zeigt, wie jeder Hut durch Ohrenklappen durch Sombreros mit den Methoden „ListIterator.remove“ und „ListIterator.add“ ersetzt wird:

 IHat sombrero = new Sombrero(); ListIterator<IHat> hatIterator = hats.listIterator(); while (hatIterator.hasNext()) { IHat hat = hatIterator.next(); if (hat.hasEarFlaps()) { hatIterator.remove(); hatIterator.add(sombrero); } }

Mit ListIterator können die Methodenaufrufe remove und add durch einen einzigen Aufruf von set ersetzt werden:

 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 } }

In Java 8 eingeführte Stream-Methoden verwenden Mit Java 8 haben Programmierer die Möglichkeit, eine Sammlung in einen Stream umzuwandeln und diesen Stream nach bestimmten Kriterien zu filtern. Hier ist ein Beispiel dafür, wie die Stream-API uns helfen könnte, Hüte zu filtern und „ConcurrentModificationException“ zu vermeiden.

 hats = hats.stream().filter((hat -> !hat.hasEarFlaps())) .collect(Collectors.toCollection(ArrayList::new));

Die Methode „Collectors.toCollection“ erstellt eine neue ArrayList mit gefilterten Hüten. Dies kann ein Problem sein, wenn die Filterbedingung von einer großen Anzahl von Elementen erfüllt werden muss, was zu einer großen ArrayList führt; Daher sollte es mit Vorsicht verwendet werden. Verwenden Sie die List.removeIf-Methode, die in Java 8 vorgestellt wird. Eine weitere in Java 8 verfügbare und eindeutig prägnanteste Lösung ist die Verwendung der „removeIf“-Methode:

 hats.removeIf(IHat::hasEarFlaps);

Das ist es. Unter der Haube verwendet es „Iterator.remove“, um das Verhalten zu erreichen.

Verwenden Sie spezialisierte Sammlungen

Wenn wir uns ganz am Anfang entschieden hätten, „CopyOnWriteArrayList“ statt „ArrayList“ zu verwenden, dann wäre das überhaupt kein Problem gewesen, da „CopyOnWriteArrayList“ Modifikationsmethoden (wie set, add und remove) bereitstellt, die sich nicht ändern das unterstützende Array der Sammlung, sondern erstellen Sie eine neue modifizierte Version davon. Dies ermöglicht die Iteration über die ursprüngliche Version der Sammlung und gleichzeitige Änderungen daran, ohne das Risiko einer „ConcurrentModificationException“. Der Nachteil dieser Sammlung liegt auf der Hand – die Generierung einer neuen Sammlung mit jeder Änderung.

Es gibt andere Sammlungen, die auf andere Fälle abgestimmt sind, zB „CopyOnWriteSet“ und „ConcurrentHashMap“.

Ein weiterer möglicher Fehler bei gleichzeitigen Sammlungsänderungen besteht darin, einen Stream aus einer Sammlung zu erstellen und während der Stream-Iteration die zugrunde liegende Sammlung zu ändern. Die allgemeine Regel für Streams besteht darin, eine Änderung der zugrunde liegenden Sammlung während der Stream-Abfrage zu vermeiden. Das folgende Beispiel zeigt eine falsche Handhabung eines Streams:

 List<IHat> filteredHats = hats.stream().peek(hat -> { if (hat.hasEarFlaps()) { hats.remove(hat); } }).collect(Collectors.toCollection(ArrayList::new));

Die Methode peek sammelt alle Elemente und führt die bereitgestellte Aktion für jedes von ihnen aus. Hier versucht die Aktion, Elemente aus der zugrunde liegenden Liste zu entfernen, was fehlerhaft ist. Um dies zu vermeiden, versuchen Sie einige der oben beschriebenen Methoden.

Häufiger Fehler Nr. 9: Verträge brechen

Manchmal ist Code, der von der Standardbibliothek oder von einem Drittanbieter bereitgestellt wird, auf Regeln angewiesen, die befolgt werden sollten, damit die Dinge funktionieren. Beispielsweise könnte es sich um einen hashCode-and-equals-Vertrag handeln, der bei Befolgung das Funktionieren für eine Reihe von Sammlungen aus dem Java-Collection-Framework und für andere Klassen garantiert, die hashCode- und equals-Methoden verwenden. Das Nichteinhalten von Verträgen ist nicht die Art von Fehler, die immer zu Ausnahmen führt oder die Codekompilierung unterbricht; es ist kniffliger, weil es manchmal das Anwendungsverhalten ohne Anzeichen von Gefahr ändert. Fehlerhafter Code könnte in die Produktionsfreigabe schlüpfen und eine ganze Reihe unerwünschter Effekte verursachen. Dies kann schlechtes UI-Verhalten, falsche Datenberichte, schlechte Anwendungsleistung, Datenverlust und mehr umfassen. Glücklicherweise treten diese katastrophalen Fehler nicht sehr oft auf. Ich habe bereits den Vertrag hashCode and equals erwähnt. Es wird in Sammlungen verwendet, die auf Hashing und Vergleichen von Objekten angewiesen sind, wie HashMap und HashSet. Einfach ausgedrückt enthält der Vertrag zwei Regeln:

  • Wenn zwei Objekte gleich sind, sollten ihre Hash-Codes gleich sein.
  • Wenn zwei Objekte denselben Hashcode haben, können sie gleich sein oder nicht.

Das Brechen der ersten Regel des Vertrags führt zu Problemen beim Versuch, Objekte aus einer Hashmap abzurufen. Die zweite Regel bedeutet, dass Objekte mit demselben Hashcode nicht unbedingt gleich sind. Untersuchen wir die Auswirkungen des Verstoßes gegen die erste Regel:

 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); } }

Wie Sie sehen können, hat die Klasse Boat die Methoden equals und hashCode überschrieben. Es hat jedoch den Vertrag gebrochen, da hashCode bei jedem Aufruf zufällige Werte für dasselbe Objekt zurückgibt. Der folgende Code wird höchstwahrscheinlich kein Boot namens „Enterprise“ im Hashset finden, obwohl wir diese Art von Boot früher hinzugefügt haben:

 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"))); }

Ein weiteres Vertragsbeispiel betrifft die Finalize-Methode. Hier ist ein Zitat aus der offiziellen Java-Dokumentation, das seine Funktion beschreibt:

Der allgemeine Abschlussvertrag besteht darin, dass er aufgerufen wird, wenn und wenn die virtuelle JavaTM-Maschine festgestellt hat, dass kein (noch nicht beendeter) Thread mehr auf dieses Objekt zugreifen kann, außer als Ergebnis einer Aktion, die durch die Finalisierung eines anderen Objekts oder einer anderen Klasse ausgeführt wird, die bereit ist, finalisiert zu werden. Die Finalize-Methode kann jede Aktion ausführen, einschließlich der erneuten Bereitstellung dieses Objekts für andere Threads; Der übliche Zweck von finalize besteht jedoch darin, Aufräumaktionen durchzuführen, bevor das Objekt unwiderruflich verworfen wird. Beispielsweise kann die finalize-Methode für ein Objekt, das eine Eingabe-/Ausgabeverbindung darstellt, explizite E/A-Transaktionen ausführen, um die Verbindung zu unterbrechen, bevor das Objekt dauerhaft verworfen wird.

Man könnte sich entscheiden, die finalize-Methode zum Freigeben von Ressourcen wie Dateihandlern zu verwenden, aber das wäre eine schlechte Idee. Dies liegt daran, dass es keine zeitlichen Garantien dafür gibt, wann finalize aufgerufen wird, da es während der Garbage Collection aufgerufen wird und die Zeit von GC unbestimmbar ist.

Häufiger Fehler Nr. 10: Verwenden von Raw-Typen anstelle eines parametrisierten Typs

Raw-Typen sind laut Java-Spezifikation Typen, die entweder nicht parametrisiert oder nicht statische Member der Klasse R sind, die nicht von der Superklasse oder Superschnittstelle von R geerbt werden. Bis zur Einführung generischer Typen in Java gab es keine Alternativen zu Raw-Typen . Es unterstützt die generische Programmierung seit Version 1.5, und Generika waren zweifellos eine signifikante Verbesserung. Aus Gründen der Abwärtskompatibilität wurde jedoch eine Falle gelassen, die das Typsystem möglicherweise beschädigen könnte. Schauen wir uns das folgende Beispiel an:

 List listOfNumbers = new ArrayList(); listOfNumbers.add(10); listOfNumbers.add("Twenty"); listOfNumbers.forEach(n -> System.out.println((int) n * 2));

Hier haben wir eine Liste von Zahlen, die als rohe ArrayList definiert ist. Da sein Typ nicht mit dem Typparameter angegeben wird, können wir ihm jedes beliebige Objekt hinzufügen. Aber in der letzten Zeile wandeln wir Elemente in int um, verdoppeln sie und geben die verdoppelte Zahl auf der Standardausgabe aus. Dieser Code wird ohne Fehler kompiliert, aber sobald er ausgeführt wird, wird eine Laufzeitausnahme ausgelöst, da wir versucht haben, eine Zeichenfolge in eine Ganzzahl umzuwandeln. Offensichtlich ist das Typsystem nicht in der Lage, uns beim Schreiben von sicherem Code zu helfen, wenn wir notwendige Informationen davor verbergen. Um das Problem zu beheben, müssen wir die Art der Objekte angeben, die wir in der Sammlung speichern werden:

 List<Integer> listOfNumbers = new ArrayList<>(); listOfNumbers.add(10); listOfNumbers.add("Twenty"); listOfNumbers.forEach(n -> System.out.println((int) n * 2));

Der einzige Unterschied zum Original ist die Linie, die die Sammlung definiert:

 List<Integer> listOfNumbers = new ArrayList<>();

Der feste Code lässt sich nicht kompilieren, da wir versuchen, eine Zeichenfolge zu einer Sammlung hinzuzufügen, von der erwartet wird, dass sie nur Ganzzahlen speichert. Der Compiler zeigt einen Fehler an und zeigt auf die Zeile, in der wir versuchen, die Zeichenfolge „Twenty“ zur Liste hinzuzufügen. Es ist immer eine gute Idee, generische Typen zu parametrisieren. Auf diese Weise ist der Compiler in der Lage, alle möglichen Typprüfungen durchzuführen, und die Wahrscheinlichkeit von Laufzeitausnahmen, die durch Inkonsistenzen des Typsystems verursacht werden, wird minimiert.

Fazit

Java als Plattform vereinfacht viele Dinge in der Softwareentwicklung und stützt sich sowohl auf eine ausgeklügelte JVM als auch auf die Sprache selbst. Seine Funktionen, wie das Entfernen der manuellen Speicherverwaltung oder anständige OOP-Tools, beseitigen jedoch nicht alle Probleme und Probleme, mit denen ein normaler Java-Entwickler konfrontiert ist. Wie immer sind Wissen, Praxis und Java-Tutorials wie dieses die besten Mittel, um Anwendungsfehler zu vermeiden und zu beheben - also kennen Sie Ihre Bibliotheken, lesen Sie Java, lesen Sie die JVM-Dokumentation und schreiben Sie Programme. Vergessen Sie auch nicht die statischen Code-Analysatoren, da sie auf die tatsächlichen Fehler hinweisen und potenzielle Fehler hervorheben könnten.

Siehe auch: Fortgeschrittenes Java-Klassen-Tutorial: Eine Anleitung zum Neuladen von Klassen