Richtlinien für Java
Einleitung
Allen Kapiteln wurde eine eindeutige Nummerierung, der Richtliniennummer, hinzugefügt, um eine eindeutige Identifikation zu ermöglichen. Jede Richtliniennummer besteht aus dem Buchstaben J(Java) gefolgt von einer Nummer, die den Abschnitt identifiziert. Damit kann eine Regel eindeutig identifiziert werden, z.B. für ein Code-Review.
INFO
Alle Beispiele sind mit 2 Leerzeichen eingerückt, da dies in Markdown die beste Darstellung bietet.
J1 Allgemeine Regeln
Es gelten die Allgemeinen Regeln für Sprachen.
J2 Einheitliche Namensgebung
Es gelten die Regeln für eine Einheitliche Namensgebung, wenn für Java anwendbar.
J3 Abstraktionsschichten
Zugriffe auf unterliegende Schichten (Vergleich mit GL3 Abstraktionsschichten) sollen in Java vermieden werden.
J3 Problem
Der direkte Zugriff auf unterliegende Schichten wie Datenbanken, APIs oder andere externe Dienste kann zu einer starken Kopplung und Abhängigkeit führen. Dies ist insbesondere problematisch, wenn der Zugriff direkt in den Anwendungscode (Business-Logik) eingebettet ist und somit eine Mischung von unterschiedlichen Code-Schichten entsteht.
public void onClick() {
try {
const connection = this.db.connect();
const data = connection.query('SELECT * FROM business');
this.api.sendData(data, { options: "abc;charset=utf-8" });
this.ui.editText.setText(data);
} catch (e) {
System.out.println(e);
}
}
J3 Lösung
- Um die Abhängigkeit von unterliegenden Schichten zu reduzieren, sollen Zugriffe auf diese Schichten in separate Klassen oder Module ausgelagert werden.
- Dependency Injection (DI) soll verwendet werden, um die Abhängigkeiten zwischen den Schichten zu verwalten und den Zugriff auf unterliegende Schichten zu ermöglichen.
- Die Business-Logik soll von der Implementierungsdetails getrennt werden, um eine klare Trennung der Verantwortlichkeiten zu gewährleisten.
- Fehlerbehandlung soll in der UI-Schicht über einen allgemeinen Fehlermechanismus erfolgen, um die Business-Logik nicht mit Fehlerbehandlung zu belasten. Nur behandelbare Fehler sollen selbst behandelt werden.
- Der Einsatz eines Modellansatzes wie MVC, MVP oder MVVM kann helfen, die Business-Logik von der UI zu trennen.
- Architekturansätze wie Domain-Driven Design (DDD), Model-Driver Architecture (MDA) oder Clean Architecture können ebenfalls helfen, die Abhängigkeiten zu reduzieren und die Business-Logik zu isolieren.
public void onClick() {
const data = this.dbDi.getBusinessData();
this.apiDi.sendDataToFoo(data);
this.uiDi.updateBusinessData(data);
}
J3 Vorteile
- Simple und klare Struktur in den Methoden/Funktionen
- Reduzierung der Abhängigkeiten zwischen den Schichten
- Klare Trennung der Verantwortlichkeiten
- Verbesserte Wartbarkeit und Erweiterbarkeit des Codes
- Verbesserte Testbarkeit durch die Möglichkeit, die unterliegenden Schichten zu mocken
J3 Nachteile
- Erhöhter Aufwand durch die Notwendigkeit, zusätzliche Klassen oder Module zu erstellen
- Erhöhter Aufwand durch die Notwendigkeit, Dependency Injection zu verwenden
- Überblick über die Abhängigkeiten und Struktur des Codes kann schwieriger sein
- Verhalten zur Laufzeit ist nicht direkt aus dem Code ersichtlich
J3 Ausnahmen
- In Prototypen kann der direkte Zugriff auf unterliegende Schichten akzeptabel sein. Der Prototyp muss jedoch nachträglich dahingehend refaktorisiert werden, dass die Zugriffe auf unterliegende Schichten in separate Klassen oder Module ausgelagert werden.
- In kleinen Anwendungen oder Tools kann der direkte Zugriff auf unterliegende Schichten akzeptabel sein.
J4 Trennung von operationalem und integrativem Code
Nach dem Integration Operation Segregation Principle soll Code entweder Operations-Logik oder Integration-Logik enthalten, aber nicht beides.
Operation vs. Integration
Eine Operations-Logik enthält Bedingungen, Schleifen, etc., die die Geschäftsregeln implementieren. Auch API-Aufrufe oder andere I/O-Operationen gehören zur Operations-Logik.
Eine Integration-Logik enthält Code, der andere Code verwendet, um die Operations-Logik zu implementieren.
Eine Hybrid-Logik enthält sowohl Operations- als auch Integrationslogik.
J4 Problem
Funktionale Abhängigkeiten sin ein Symptom von sich schlecht änderbaren Codes. Durch die Vermischung von Operations- und Integrationslogik wird der Code unübersichtlich und schwer verständlich und lässt sich nur schwer automatisiert testen oder refaktorisieren.
Wenn in Methoden oder Funktionen verhaltenserzeugende Anweisungen (if, while, for, etc.) mit Aufrufen anderer Methoden derselben Codebasis gemischt sind, ist nicht mehr klar erkennbar, wie das Gesamtverhalten entsteht, da viel zu viel in der Methode oder Funktion passiert.
Solche Methoden tendieren oftmals dazu unbegrenzt zu wachsen.
Im folgenden Code besteht keine klare Trennung zwischen Operations- und Integration-Logik. Es wurde einfach die Lösung "herunter-programmiert" und die Logik in einer Methode zusammengefasst. Als Beispiel ist eine kleine Funktion gegeben, die in der Praxis oftmals deutlich größer und komplexer ist.
public void onClick(String input) {
String value = myInput.getText();
if (input == null) {
return;
}
final Connection connection = db.connect();
ResultSet data = connection.query("SELECT * FROM Business");
if (data.foo == 0) {
data = connection.query("SELECT * FROM FooFoo");
}
api.sendData(data, "abc;charset=utf-8");
myDiv.setText(data);
}
J4 Lösung
Die Trennung kann durch die Verwendung von mehreren Zwischenmethoden erreicht werden, die die Operations- und Integrationslogik trennen.
Guard Clause
Strenggenommen ist die Guard Clause eine Operations-Logik, welche die Methode nach IOSP auch zu einer Operations-Logik, statt einer Integration-Logik macht.
Folge dem Prinzip nicht blind
Beachte: Es steht die Lesbarkeit und Verständlichkeit des Codes im Vordergrund!
Wie stark eine Trennung durchgeführt werden soll, wird durch die Größe und Komplexität der Methode bestimmt.
Konzentriere dich daher darauf, die Methoden und Funktionen soweit zu trennen, dass sie leicht verständlich und testbar sind.
Es ist nicht erforderlich, dass jede Methode entweder nur Operations- oder Integrationslogik enthält, nur damit das Prinzip zu 100% eingehalten wird.
public void onClick(String input) {
// Integration-Logik
// und Trennung von UI und Business-Logik
final String value = myInput.getText();
processData(input);
}
// Hybrid-Logik, wenn guard clause mitgezählt wird
void processData(String input) {
if (input == null) {
// Guard clause, macht die Methode zu einer Operations-Logik
return;
}
// Integration-Logik
final Connection connection = db.connect();
final ResultSet data = getDataFromBusinessOrFooTable(connection);
sendData(data);
setElementData(data);
}
ResultSet getDataFromBusinessTable(Connection connection) {
// Operation-Logik, da Datenbankabfrage
return connection.query("SELECT * FROM Business");
}
ResultSet getDataFromFooTable(Connection connection) {
// Operation-Logik, da Datenbankabfrage
return connection.query("SELECT * FROM FooFoo");
}
// Hybrid-Logik könnte weiter aufgeteilt werden
// aber die Trennung von Operations- und Integration-Logik ist bereits deutlich
ResultSet getDataFromBusinessOrFooTable(Connection connection) {
final ResultSet data = getDataFromBusinessTable(connection);
// Beispiel-If-Abfrage
if (data.foo == 0) {
return getDataFromFooTable(connection);
}
return data;
}
void sendData(ResultSet data) {
// Operation-Logik, da API-Aufruf
api.sendData(data, "abc;charset=utf-8");
}
void setElementData(ResultSet data) {
// Operation-Logik, da UI-Aufruf
myDiv.setText(data);
}
Code-Größe
Im Allgemeinen führt IOSP zu kürzeren Methoden, da die Operations- und Integrationslogik getrennt sind. Jedoch wird insgesamt mehr Code geschrieben, da die Trennung zu mehr Methoden führt, welche neue Zeilen hinzufügen.
J4 Vorteile
- Durch die strikte Trennung von Operations- und Integration-Logik wird der Code übersichtlicher und leichter verständlich.
- Methoden/Funktionen sind einzeln einfacher zu lesen, da sie kurz sind.
- Methoden/Funktionen sind einzeln einfacher zu testen.
- Korrektheit von Integrationen lässt sich leicht durch Augenscheinnahme prüfen.
- Es gibt oftmals eine Haupteinstiegs-Methode, die die Integration-Logik koordiniert und die Operations-Logik in separaten Methoden aufruft.
- Integrations-Methoden/Funktionen lassen sich leicht erweitern, indem neue Methoden hinzugefügt werden, um neue Anforderungen zu erfüllen.
J4 Nachteile
- Die Trennung von Operations- und Integration-Logik kann zu mehr Code führen, da mehr Methoden/Funktionen erstellt werden müssen.
J4 Ausnahmen
- In kleinen Anwendungen oder Prototypen kann die Trennung von Operations- und Integration-Logik übertrieben sein.
- Die strikte Trennung kann in manchen Fällen unnötigen Overhead verursachen (siehe Trennung von
getOrCreateUser
undgetUser
).
class UserService {
private List<User> database;
public UserService(List<User> database) {
this.database = database;
}
// Operation: Benutzer suchen (nur Leseoperation)
public User findUser(int id) {
for (User user : database) {
if (user.getId() == id) {
return user;
}
}
return null;
}
// Operation: Benutzer erstellen (nur Schreiboperation)
public User createUser(int id) {
User newUser = new User(id, "New User");
database.add(newUser);
return newUser;
}
// Operation: Benutzer holen oder erstellen (Logik zur Entscheidung, ob ein Benutzer erstellt werden muss)
public User getOrCreateUser(int id) {
User user = findUser(id);
return user != null ? user : createUser(id);
}
// Integration: Koordiniert nur den Aufruf von getOrCreateUser
public User getUser(int id) {
return getOrCreateUser(id);
}
// Alternative Methode mit Hybrid aus Operations- und Integrationslogik
public User getUserAlternative(int id) {
User user = findUser(id);
if (user == null) {
user = createUser(id);
}
return user;
}
}
J5 Anwendung von neuen Java-Sprachfeatures
- Der Einsatz von neuen Java-Sprachfeatures gegenüber alten Sprachfeatures soll bevorzugt werden.
- Alte Sprachfeatures, insbesondere die als veraltet markierten, sollen vermieden und im Zuge von Refaktorisierungen durch neue Sprachfeatures ersetzt werden.
J5 Problem
Java wird ständig weiterentwickelt und neue Sprachfeatures werden eingeführt, um die Sprache zu verbessern und die Entwicklung zu erleichtern. Der Einsatz von veralteten Sprachfeatures kann zu schlechterer Lesbarkeit, Wartbarkeit und Performance führen. Letztendlich kann der Code nicht mehr mit neuen Compiler-Versionen kompiliert werden, wenn veraltete Sprachfeatures entfernt werden.
J5 Lösung
Neue Sprachfeatures wie Lambda-Ausdrücke, Streams, Optionals, Records, Pattern Matching, Sealed Classes, Text Blocks, etc. sollen bevorzugt verwendet werden, um den Code zu verbessern und die Entwicklung zu erleichtern.
Folgende Sprachfeatures sind als veraltet markiert und sollen nicht mehr verwendet werden:
- Generics mit Raw Types (z.B.
List list = new ArrayList();
). Vector
undHashtable
sollen durchArrayList
undHashMap
ersetzt werden.Enumeration
soll durchIterator
ersetzt werden.StringBuffer
soll durchStringBuilder
ersetzt werden.Date
undCalendar
sollen durchLocalDate
,LocalTime
,LocalDateTime
,ZonedDateTime
oderInstant
ersetzt werden.SimpleDateFormat
soll durchDateTimeFormatter
ersetzt werden.Random
soll durchThreadLocalRandom
ersetzt werden.System.out
undSystem.err
sollen durchLogger
und einem Framework wie SLF4J ersetzt werden.
Neue Sprachfeatures sind zum Beispiel:
- Lambda-Ausdrücke für die Implementierung von Funktions-Interfaces.
var
für lokale Variablen, um den Code zu vereinfachen.- Streams für die Verarbeitung von Daten.
- Optionals für die Behandlung von Null-Werten.
- Records für die Definition von Datenklassen.
- Pattern Matching für die Vereinfachung von
instanceof
-Abfragen. - Sealed Classes für die Definition von geschlossenen Klassen.
- Text Blocks für die Definition von mehrzeiligen Strings.
- Switch Expressions für die Vereinfachung von
switch
-Anweisungen.
final List<String> list = Arrays.asList("a", "b", "c");
// Alte Schreibweise
list.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
// Neue Schreibweise
list.forEach(s -> System.out.println(s));
// Methodenreferenz
list.forEach(System.out::println);
// Alte Schreibweise
List<String> list = new ArrayList<>();
// Neue mögliche Schreibweise
var list = new ArrayList<String>();
List<String> list = Arrays.asList("a", "b", "c");
// Alte Schreibweise
List<String> newList = new ArrayList<>();
for (String s : list) {
if (s.startsWith("a")) {
newList.add(s);
}
}
// Neue Schreibweise
List<String> newList = list.stream()
.filter(s -> s.startsWith("a"))
.collect(Collectors.toList());
String str = "123";
// Alte Schreibweise
Integer i = null;
try {
i = Integer.parseInt(str);
} catch (NumberFormatException e) {
i = 0;
}
// Neue Schreibweise
Integer i = Optional.ofNullable(str)
.map(Integer::parseInt)
.orElse(0);
// Alte Schreibweise
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
// Neue Schreibweise
record Person(String name, int age) {}
// sealed ermöglicht die Definition von Klassen, deren Unterklassen
// von vorneherein auf bestimmte Unterklassen beschränkt sind.
// Alte Schreibweise
abstract class Shape {
public abstract double area();
}
class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
// Neue Schreibweise
sealed abstract class Shape permits Circle, Square {
public abstract double area();
}
final class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
// und so weiter für Square
// Alte Schreibweise
String html = "<html>\n" +
" <body>\n" +
" <p>Hello, World!</p>\n" +
" </body>\n" +
"</html>\n";
// Neue Schreibweise
// Text Blocks
String html = """
<html>
<body>
<p>Hello, World!</p>
</body>
</html>
""";
//switch expression ermöglicht die Verwendung von switch als Ausdruck
//Das Schlüsselwort break fehlt, da es nicht mehr notwendig ist.
// Alte Schreibweise
String result;
switch (day) {
case MONDAY:
case TUESDAY:
case WEDNESDAY:
case THURSDAY:
case FRIDAY:
result = "Weekday";
break;
case SATURDAY:
case SUNDAY:
result = "Weekend";
break;
}
// Neue Schreibweise
String result = switch (day) {
case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "Weekday";
case SATURDAY, SUNDAY -> "Weekend";
};
J6 Benennung von Variablen, Funktionen, Klassen und mehr
- Variablen sind im camelCase
zu benennen:
myVariable
. - Funktionen oder Methoden sind im camelCase zu benennen
myFunction()
odermyMethod()
. - Klassen sind im PascalCase zu benennen
MyClass
. - Globale Konstanten sind in UPPER_SNAKE_CASE zu benennen
MY_CONSTANT
. - Statische Klassen-Konstanten sind in UPPER_SNAKE_CASE zu benennen
MY_CLASS_CONSTANT
. - Lokale Konstanten können in camelCase sein
myConstant
oder in UPPER_SNAKE_CASEMY_CONSTANT
, wenn sie z.B. einen einzigen Wert definieren. - Parameter sind im camelCase zu benennen
myParameter
. - Exceptions sind in PascalCase zu benennen
MyException
und enden mitException
. - Typen sind in PascalCase zu benennen
MyType
. - Interfaces sind in PascalCase zu benennen
MyInterface
. - Enumerations sind in PascalCase zu benennen
MyEnum
. - Objekt-Instanzen sind wie Variablen zu benennen
myObject
.
public class GlobalAnyClass {
// Konstanten
private static final int THE_ANSWER = 42;
}
public class Example {
public static void myFunction(int myParameter) throws MyException {
final int myConstant = THE_ANSWER;
final MyObject myObject = new MyObject("value");
if (myObject.getKey().equals(String.valueOf(myConstant))) {
throw new MyException("Error");
}
}
}
// Klasse MyClass
class MyClass {
public static final int MY_CLASS_CONSTANT = 42;
// Enum als Ersatz für JS Symbol
public enum MyEnum {
RED,
GREEN,
BLUE
}
public void myMethod() {
MyEnum myEnum = MyEnum.RED;
// Nutzung von myEnum...
}
}
Schein-Konstanten
Objekte oder Array-Inhalte sind immer veränderbar, auch wenn sie mit final
deklariert werden. Nur die Zuweisung der Variable ist konstant, nicht der Wert.
In Java gibt es keine Möglichkeit, den Inhalt eines Objekts oder Arrays zu sperren. Alternativen sind die Software Prinzipien Tell, don't ask und Information Hiding.
J7 Reihenfolge der Deklarationen
Die Reihenfolge der Deklarationen soll konsistent sein und die Lesbarkeit des Codes verbessern.
J7 Reihenfolge in Funktionen und Methoden
Die Deklaration von Variablen und Konstanten innerhalb von Scope-Blöcken soll in folgender Reihenfolge erfolgen:
- Umschließender Funktions- oder Block-Scope
- Konstanten
- Variablen
- Funktionen
public void myFunction() {
final int myConstant = 42;
int myVariable = 42;
void myInnerFunction() {
final int innerConstant = 42;
int innerVariable = 42;
// Code
}
}
J7 Reihenfolge in Klassen
In Klassen sollen die Deklarationen in folgender Reihenfolge erfolgen:
- Statische Klassen-Konstanten
- Statische Klassen-Methoden
- Klassen-Konstanten sind in der Regel statisch, daher keine zusätzlichen Klassen-Konstanten.
- Klassen-Attribute
- Konstruktoren
- Klassen-Methoden
- Getter und Setter (vermeiden, siehe Tell, don't ask)
- Methoden für
toString
,equals
,hashCode
,compareTo
undcompare
- Methoden für
clone
undcopy
für die InterfacenCloneable
undCopyable
public class MyClass implements Cloneable {
// 1. Statische Klassen-Konstanten
public static final int MY_CLASS_CONSTANT = 42;
// 2. Statische Klassen-Methoden
public static void myStaticMethod()
// 3. Klassen-Konstanten (keine zusätzlichen, da MY_CLASS_CONSTANT schon statisch ist)
// 4. Klassen-Attribute
private int myAttribute = 42;
// 5. Konstruktoren
public MyClass() {
// Konstruktorlogik
}
public void myMethod() {
// Normale Methode
System.out.println("Instance method called.");
}
// 7. Getter und Setter (vermeiden, aber falls benötigt)
public int getMyAttribute() {
return myAttribute;
}
public void setMyAttribute(int value) {
this.myAttribute = value;
}
// 8. Methoden für toString, equals, hashCode, compareTo und compare
@Override
public String toString()
@Override
public boolean equals(Object other)
@Override
public int hashCode()
public int compareTo(MyClass other)
@Override
protected MyClass clone()
}
J7 Ausnahmen
- Zwischenberechnungen für Konstanten oder Variablen können vor der Verwendung deklariert werden, wenn es nicht anders geht.
- In Fällen, in der eine besser Verständlichkeit des Codes durch eine andere Reihenfolge erreicht wird, kann von der oben genannten Reihenfolge abgewichen werden.
J8 Verwendung von final
für alle Variablen und Methoden/Funktions-Parameter Kennzeichnung von Nicht-Konstanten
Variablen enthalten für gewöhnlich Werte, die sich während der Laufzeit des Programms nicht ändern. Eine erneute Zuweisung von Werten zu Variablen kann zu unerwartetem Verhalten führen, weil sich der Wert plötzlich ändert oder versehentlich undefiniert wird.
- Variablen und Parameter sollen daher mit
final
deklariert werden, um sicherzustellen, dass sie nicht versehentlich geändert werden. - Eine erneute Zuweisung von Werten zu Variablen soll vermieden werden, um unerwartetes Verhalten zu vermeiden. Stattdessen sollen neue Variablen deklariert werden, wenn ein neuer Wert benötigt wird.
- Ist eine erneute Zuweisung von Werten notwendig, soll ein Kommentar mit dem Inhalt
/*nonfinal*/
hinzugefügt werden, um darauf hinzuweisen und dies auch einem Code-Review zu signalisieren, dass der Entwickler sich der Änderung bewusst ist.
J8 Problem
Die Verwendung von final
sorgt dafür, dass Variablen nicht versehentlich geändert werden. Ohne die Verwendung von final
besteht die Gefahr, dass Variablen unbeabsichtigt überschrieben werden. Dies kann dazu führen, dass sich der Wert von Variablen, Attributen oder Parametern unerwartet ändert und dadurch unerwünschte Nebeneffekte auftreten können. Dies passiert beispielsweise dann, wenn die Variable, das Attribut oder der Parameter in einem anderen Teil des Codes nachträglich und von einer anderen Person unerwartet geändert wird. Dadurch wird die Lesbarkeit und Nachvollziehbarkeit des Codes erschwert.
String name = "John";
int age = 30;
// ...
name = "Jane"; // Unbeabsichtigte Änderung der Variable
// schlimmer
name = null;
void myFunction(int count) {
count++;
// originaler Wert von count ist verloren
}
J8 Lösung
Um unbeabsichtigtes Ändern von Variablen zu vermeiden, sollen alle Variablen mit final
deklariert werden. In Fällen, in denen die Verwendung von final
nicht möglich ist (z. B. bei Variablen, die sich ändern müssen), soll ein Kommentar mit dem Inhalt /*nonfinal*/
hinzugefügt werden, um darauf hinzuweisen.
final String name = "John";
final int age = 30;
// ...
/*nonfinal*/ int count = 0;
// oder
/*nonfinal*/
int count = 0;
count++;
void myFunction(final int count) {
// count kann nicht verändert werden
final int increasedByOneCount = count + 1;
}
J8 Vorteile
- Vermeidung unbeabsichtigter Änderungen von Variablen
- Klarheit in Bezug auf die Veränderlichkeit von Variablen
- Verbesserte Code-Qualität und Verständlichkeit
- Entspricht dem Prinzip Least Astonishment
J8 Nachteile
Es gibt Situationen, in denen die Verwendung von final
nicht möglich oder sinnvoll ist, z. B. bei Variablen, die sich ändern müssen oder in komplexen Legacy-Code. In solchen Fällen kann die Kennzeichnung mit einem Kommentar /nonconst*/
helfen, auf die Ausnahme hinzuweisen.
J9 Einsatz von Linter und Formatter
Tools wie Checkstyle, PMD, SpotBugs und SonarLint oder SonarQube sollen verwendet werden, um sicherzustellen, dass der Code den definierten Richtlinien entspricht und potenzielle Fehler und Probleme im Code identifiziert werden.
Formatierungs-Tools wie Eclipse, IntelliJ IDEA, VS Code oder Eclipse sollen verwendet werden, um den Code automatisch zu formatieren und die Einhaltung der Formatierungsrichtlinien zu gewährleisten.
Formatierer-Konfiguration
Formatierer müssen so konfiguriert werden, dass sie für alle Entwickler im Team die gleichen Einstellungen verwenden. Andernfalls kann es zu Konflikten bei der Versionierung des Codes kommen.
J10 Verwende CompletableFuture für kurze asynchrone Operationen
CompletableFuture soll für kurze asynchrone Operationen verwendet werden, um die Lesbarkeit und Wartbarkeit des Codes zu verbessern.
J10 Problem
Der Einsatz von Thread
oder ExecutorService
für asynchrone Operationen kann zu komplexem und schwer verständlichem Code führen. Die Verwendung von Thread
oder ExecutorService
erfordert die explizite Verwaltung von Threads und die Synchronisation von Threads, was zu Fehlern und unerwartetem Verhalten führen kann. Diese Thread müssen explizit gestartet und beendet werden, was zu zusätzlichem Code führt. Für kurze Operationen kann der Start einen Threads im Betriebssystem teurer sein, als die Operation selbst, was zu Warterzeiten und Problemen mit Performance führen kann.
J10 Lösung
CompletableFuture soll für kurze asynchrone Operationen verwendet werden, um die Lesbarkeit und Wartbarkeit des Codes zu verbessern.
public CompletableFuture<String> myFunction() {
return CompletableFuture.supplyAsync(() -> {
// Operation Beispiel
Thread.sleep(1000);
return "Hello, World!";
});
}
final CompletableFuture<String> future = myFunction();
// Abbruch der Operation
future.cancel(true);
public CompletableFuture<String> myFunction() {
return CompletableFuture.supplyAsync(() -> {
// Operation Beispiel
if (condition) {
throw new RuntimeException("Error");
}
return "Hello, World!";
});
}
final CompletableFuture<String> future = myFunction();
future.exceptionally(ex -> {
System.out.println("Error: " + ex.getMessage());
return "Error";
});
// thenapply führt eine Operation auf dem Ergebnis aus
final CompletableFuture<String> future = myFunction();
final CompletableFuture<Integer> result = future.thenApply(s -> s.length());
// thenaccept führt eine Operation auf dem Ergebnis aus, gibt aber kein Ergebnis zurück
final CompletableFuture<String> future = myFunction();
future.thenAccept(s -> System.out.println(s));
// thenrun führt eine Operation aus, ohne auf das Ergebnis zu warten
final CompletableFuture<String> future = myFunction();
future.thenRun(() -> System.out.println("Operation completed"));
// thencombine kombiniert zwei Ergebnisse, wenn beide abgeschlossen sind
final CompletableFuture<String> future1 = myFunction();
final CompletableFuture<String> future2 = myFunction();
final CompletableFuture<String> result = future1.thenCombine(future2, (s1, s2) -> s1 + s2);
// thencompose führt eine Operation auf dem Ergebnis aus und gibt ein neues CompletableFuture zurück
final CompletableFuture<String> future = myFunction();
final CompletableFuture<Integer> result = future.thenCompose(s -> CompletableFuture.supplyAsync(() -> s.length()));
// thenacceptboth führt eine Operation auf beiden Ergebnissen aus
final CompletableFuture<String> future1 = myFunction();
final CompletableFuture<String> future2 = myFunction();
future1.thenAcceptBoth(future2, (s1, s2) -> System.out.println(s1 + s2));
// allof wartet auf das Ergebnis aller CompletableFuture
final CompletableFuture<String> future1 = myFunction();
final CompletableFuture<String> future2 = myFunction();
final CompletableFuture<String> future3 = myFunction();
final CompletableFuture<Void> all = CompletableFuture.allOf(future1, future2, future3);
// anyof wartet auf das Ergebnis eines CompletableFuture
final CompletableFuture<String> future1 = myFunction();
final CompletableFuture<String> future2 = myFunction();
final CompletableFuture<String> future3 = myFunction();
final CompletableFuture<Object> any = CompletableFuture.anyOf(future1, future2, future3);
J10 Weiterführende Links
J11 Begrenzte Zeilenanzahl in Methoden/Funktionen
Codezeilen in Methoden und Funktionen sollen auf eine begrenzte Anzahl beschränkt werden, um die Lesbarkeit und Wartbarkeit des Codes zu verbessern.
Die Begrenzung ist nicht in Stein gemeißelt, aber eine Methode soll nicht mehr als eine Bildschirmseite umfassen bei einer normalen Bildschirmauflösung und Schriftgröße.
Eine Quantisierung von Zeilen ist schwierig, aber eine Möglichkeit ist die Millersche Zahl 7 ± 2 oder die zyklomatische Komplexität.
Millersche Zahl 7 ± 2
Die Millersche Zahl besagt, dass Menschen in der Lage sind, sich an etwa 7 ± 2 Einheiten von Information zu erinnern. Man kann dies auf die Anzahl der Codezeilen übertragen, die ein Entwickler in einer Methode oder Funktion verarbeiten kann.
Siehe auch The Magical Number Seven, Plus or Minus Two
Beachte jedoch, dass diese Zahl umstritten ist und es viele Faktoren gibt, die die Lesbarkeit und das Verständnis von Code beeinflussen.
Zyklomatische Komplexität
Die zyklomatische Komplexität ist eine Softwaremetrik, die die Anzahl der unabhängigen Pfade durch den Quellcode misst. Sie ist gegeben durch die Formel M = E - N + 2P
, wobei E
die Anzahl der Kanten, N
die Anzahl der Knoten und P
die Anzahl der Zusammenhangskomponenten ist.
Siehe auch Cyclomatic Complexity oder McCabe-Metrik
Viele Entwicklungsumgebungen bieten eine Möglichkeit, die zyklomatische Komplexität zu berechnen und pro Methode anzuzeigen.
Beachte jedoch auch hier, dass die zyklomatische Komplexität nur ein Indikator für die Komplexität eines Codes ist und nicht alle Aspekte der Lesbarkeit und Wartbarkeit abdeckt.
J11 Problem
Methoden oder Funktionen mit einer großen Anzahl von Codezeilen können schwer zu lesen, zu verstehen und zu warten sein. Lange Methoden können verschiedene Aufgaben vermischen und die Einhaltung des Single Responsibility Principle erschweren.
public class UserProcessor {
public void processUserData(User user) {
// Schritt 1: Validierung der Benutzerdaten
if (user != null) {
if (validateName(user.getName())) {
if (validateEmail(user.getEmail())) {
if (validateAge(user.getAge())) {
// Weitere Logik...
System.out.println("Benutzerdaten erfolgreich validiert.");
}
}
}
}
// Schritt 2: Speichern der Benutzerdaten
if (user != null) {
if (saveUserData(user)) {
// Weitere Logik...
System.out.println("Benutzerdaten erfolgreich gespeichert.");
}
}
// Schritt 3: Senden einer Bestätigungs-E-Mail
if (user != null) {
if (sendConfirmationEmail(user.getEmail())) {
// Weitere Logik...
System.out.println("Bestätigungs-E-Mail gesendet.");
}
}
// Schritt 4: Aktualisierung des Benutzerstatus
if (user != null) {
if (updateUserStatus(user)) {
// Weitere Logik...
System.out.println("Benutzerstatus erfolgreich aktualisiert.");
}
}
// Weitere Logik...
}
}
J11 Lösung
Um die Lesbarkeit und Verständlichkeit des Codes zu verbessern, sollen Methoden und Funktionen auf eine begrenzte Anzahl von Zeilen beschränkt sein. Komplexe Aufgaben sollen in kleinere Teilfunktionen ausgelagert werden, um die Verantwortlichkeiten klarer zu trennen.
Die Anzahl von Zeilen soll allgemein so klein wie möglich gehalten werden. Sie soll allerdings nie über eine Bildschirmhöhe hinausgehen, d.h. mehr als 25 Zeilen sollen vermieden werden.
Allgemeine Code-Refactorings sind:
- Code-Blöcke oder Scopes (durch geschweifte Klammern separiert) können in Methoden ausgelagert werden.
- Kommentare, die eine Sektion kommentieren können im Allgemeinen in eine Methode ausgelagert werden.
- For-Schleifen, welche If-Bedingungen beinhalten, können als Methode geschrieben werden.
- Mehrdimensionale For-Schleifen können in Methoden ausgelagert werden,
- If-Bedingungen innerhalb einer Methode können als Methode geschrieben werden.
public class UserProcessor {
public void processUserData(final User user) {
validateUser(user);
saveUser(user);
sendConfirmationEmail(user.getEmail());
updateUserStatus(user);
}
void validateUser(final User user) {
if (user == null) {
throw new IllegalArgumentException("User cannot be null");
}
if (!validateName(user.getName())) {
throw new IllegalArgumentException("Invalid name");
}
if (!validateEmail(user.getEmail())) {
throw new IllegalArgumentException("Invalid email");
}
if (!validateAge(user.getAge())) {
throw new IllegalArgumentException("Age must be 18 or older");
}
// Weitere Validierungen...
}
private boolean saveUser(final User user) {
if (user == null) {
throw new IllegalArgumentException("User cannot be null");
}
if (!saveUserData(user)) {
throw new RuntimeException("Failed to save user data");
}
// Weitere Speicheroperationen...
}
}
J11 Vorteile
- Verbesserte Lesbarkeit und Verständlichkeit des Codes durch kleinere und fokussierte Methoden/Funktionen
- Einfachere Wartbarkeit und Testbarkeit durch klar abgegrenzte Verantwortlichkeiten
- Bessere Übersichtlichkeit und Strukturierung des Codes
- Bessere Testbarkeit des Codes, da kleinere Methoden leichter isoliert und getestet werden können
Weitere Gründe für kleine Methoden
KISS-Prinzip: Das KISS-Prinzip kann leichter eingehalten werden, wenn Methoden und Funktionen auf eine begrenzte Anzahl von Zeilen beschränkt sind. Der Entwickler kommt nicht dazu überkomplexe Methoden zu schreiben, da er sich an die Zeilenbeschränkung halten muss.
Bessere Isolierung: Kleine Methoden behandeln normalerweise nur eine spezifische Aufgabe oder Verantwortlichkeit. Dadurch können sie isoliert und unabhängig von anderen Teilen des Codes getestet werden. Durch die Fokussierung auf eine spezifische Funktion können Tests effektiver und einfacher geschrieben werden.
Lesbarkeit: Kleine Methoden sind in der Regel einfacher zu verstehen, da sie nur eine begrenzte Anzahl von Zeilen umfassen und sich auf eine bestimmte Funktionalität konzentrieren. Dadurch wird die Lesbarkeit des Codes verbessert und es ist einfacher, den Zweck und das Verhalten der Methode zu erfassen.
Wiederverwendbarkeit: Kleine Methoden können leichter wiederverwendet werden. Da sie in der Regel spezifische Aufgaben erfüllen, können sie in verschiedenen Teilen des Codes wiederverwendet werden, wenn ähnliche Funktionalität benötigt wird. Dies fördert die Modularität und reduziert die Duplizierung von Code.
Einfache Wartbarkeit: Kleine Methoden sind einfacher zu warten, da sie in sich abgeschlossen sind und Änderungen an einer Methode weniger Auswirkungen auf andere Teile des Codes haben. Bei einem Fehler oder einer gewünschten Änderung ist es einfacher, den relevanten Code zu lokalisieren und anzupassen, ohne den gesamten Kontext berücksichtigen zu müssen.
Bessere Testabdeckung: Durch die Aufteilung des Codes in kleine Methoden wird die Testabdeckung verbessert, da jede Methode spezifische Tests erhalten kann. Dadurch können verschiedene Szenarien und Randbedingungen gezielt getestet werden, um die Fehleranfälligkeit zu reduzieren.
Einfacheres Mocking: Darüber hinaus ist das Mocking in Tests einfacher, wenn der Code in kleine Methoden aufgeteilt ist. Beim Schreiben von Unit-Tests ist es häufig erforderlich, externe Abhängigkeiten zu mocken oder zu fälschen, um den Fokus auf die zu testende Methode zu legen. Mit kleinen Methoden ist es einfacher, diese Abhängigkeiten zu identifizieren und zu isolieren, da sie in der Regel explizit als Parameter an die Methode übergeben werden. Das Mocking-Setup ist zudem kleiner und einfacher, weil aufgespaltete Methoden einfach diese Methoden mocken können, statt die fremde (externe) API, die darin verwendet wird.
Förderung des Single Responsibility Principle: Kleine Methoden unterstützen das Single Responsibility Principle, das besagt, dass eine Methode oder Funktion nur eine einzige Verantwortlichkeit haben soll. Durch die Aufteilung des Codes in kleine Methoden wird die Verantwortlichkeit klarer definiert und das Prinzip der klaren Trennung von Aufgaben eingehalten.
Die Verwendung kleiner Methoden verbessert die Qualität und Wartbarkeit des Codes, indem sie die Testbarkeit, Lesbarkeit, Wiederverwendbarkeit und Modularität fördern. Es ist jedoch wichtig, ein Gleichgewicht zu finden, um eine übermäßige Fragmentierung des Codes zu vermeiden und die Lesbarkeit nicht zu beeinträchtigen.
INFO
Siehe zu Prinzipien und Vorteilen auch Prinzipien der Softwareentwicklung.
J11 Nachteile
Die strikte Begrenzung der Zeilenanzahl kann zu einer übermäßigen Fragmentierung des Codes führen, wenn kleinere Methoden oder Funktionen unnötig erstellt werden.
J11 Ausnahmen
Die Anzahl der Codezeilen in einer Methode oder Funktion kann je nach Kontext und Komplexität des Codes variieren.
J12 Attribute komplexer Typen sollten nicht mit Getter und Setter veröffentlicht werden
Klassen sollen interne Daten nicht direkt über Getter- und Setter-Methoden veröffentlichen, um unerwartetes Verhalten und Inkonsistenzen zu vermeiden.
J12 Problem
Wenn Attribute von Klassen einfach von außen manipuliert werden können, kann die Integrität und Konsistenz der Daten gefährdet sein und die Klasse wird anfällig für Fehler und unerwartete Veränderungen. Das Prinzip Tell, don't ask und Information Hiding werden hier verletzt, da die Klasse nicht mehr die Kontrolle über ihre eigenen Daten hat.
Die folgende Klasse erwartet, dass es ein Element in der Liste gibt, aber wenn ein Benutzer die Liste manipuliert, kann es zu einer NullPointerException
kommen.
public class MyClass {
private List<String> myList;
public MyClass() {
this.myList = List.of("Hello", "World");
}
public List<String> getMyList() {
return myList;
}
public void setMyList(List<String> myList) {
this.myList = myList;
}
public void sayHello() {
// Fehler IndexOutOfBoundsException oder NullPointerException
System.out.println(this.myList.get(0));
}
}
// ...
MyClass obj1 = new MyClass();
obj1.getMyList().clear();
// oder
obj1.setMyList(null);
obj.sayHello(); // NullPointerException
J12 Lösung
- Es sollen spezielle Methoden bereitgestellt werden, um den inneren Zustand des Attributs zu verändern oder darauf zuzugreifen.
- Werte sollen als Kopie oder unveränderbare Objekte zurückgegeben werden, um die Inkonsistenz und unerwartete Veränderungen zu verhindern.
public class MyClass {
private List<String> myList;
public MyClass() {
this.reset();
}
public void addToList(String item) {
this.myList.add(item);
}
public List<String> getList() {
return Collections.unmodifiableList(myList);
}
public void reset() {
this.myList = List.of("Hello", "World");
}
public void sayHello() {
System.out.println(this.myList.get(0));
}
}
// Keine Möglichkeit, die Liste zu manipulieren
J12 Vorteile
Indem die Verwendung von Getter- und Setter-Methoden für Attribute komplexer Typen vermieden wird, können Inkonsistenzen bei der Verwendung von Referenzen auf dieselben Objekte verhindert werden. Stattdessen sollten notwendige Methoden über das Parent-Objekt bereitgestellt werden, um sicherzustellen, dass das Attribut konsistent und korrekt verwendet wird.
J12 Ausnahmen
Es kann jedoch Fälle geben, in denen die Verwendung von Getter- und Setter-Methoden sinnvoll ist, z.B. wenn das Attribut nicht referenzierbare Objekte enthält oder wenn das Attribut geändert werden kann, ohne dass Inkonsistenzen entstehen. Es ist daher wichtig, die Verwendung von Getter- und Setter-Methoden sorgfältig zu prüfen und nur dann zu verwenden, wenn es notwendig und sinnvoll ist.
J13 Auf null prüfen
Objekt-Variablen, die null
sein können, sollen auf null
geprüft werden, um keine Nullpointer-Exceptions zu verursachen.
Anwendungseinsatz und Alternativen
Diese Regel gilt nur für bereits bestehenden Methoden und Funktionen, die null
zurückgeben können. Optional
Soll für neue Methoden/Funktionen verwendet werden, um diese spezielle Fälle (null
etc.) zu repräsentieren.
Soll ein neues Klassen- oder Objektmodell erstellt werden, sollen direkt spezielle Objekte verwendet werden.
Siehe auch Null-Objekte.
Alternative Namensgebung
Ist es nicht möglich null
zu vermeiden, sollen die Variablen entsprechend der Namensgebung benannt werden.
J13 Problem
Viele Methoden liefern direkt ein Objekt zurück, welches null
sein kann. Jedoch ist es oftmals nicht ersichtlich, ob die Methode null
zurückgibt oder nicht und ein Blick in den Quellcode ist notwendig. Doch das birgt die Gefahr, wenn der Quellcode geändert wird, dass die Methode plötzlich null
zurückgibt und dadurch eine Nullpointer-Exception ausgelöst wird.
public interface Data {
String name();
}
public void myFunction() {
final String name = data.name();
// mögliche Nullpointer-Exception
if (name.equals("John")) {
// Code
}
}
J13 Lösung
Um sicherzustellen, dass keine Nullpointer-Exceptions auftreten, soll auf null
geprüft werden, bevor auf die Methode zugegriffen wird.
public void myFunction() {
final String name = data.name();
if (name != null && name.equals("John")) {
// Code
}
}
J13 Refactoring
Im Beispiel kann nach der Namensgebung die Variable umbenannt werden, um zu signalisieren, dass die Methode null
zurückgeben kann.
Die Methode name
sollte nicht umbenannt und stattdessen Optional
verwendet werden.
public void myFunction() {
final String nameOrNull = data.name();
...
}
J13 Vorteile
- Vermeidung von Nullpointer-Exceptions
- Vermeidung von unerwarteten Fehlern durch falsche Erkennung von falsy-Werten
J13 Nachteile
- Werte wie NaN werden nicht erkannt
- ESLint muss entsprechend konfiguriert werden, um die Verwendung von
==
bei null Vergleich zu erlauben. Dies ist möglich, indem die Regeleqeqeq
auf smart umgestellt wird.
J14 Methoden/Funktionen, die Mengen zurückgeben sollen niemals null zurückgeben
Methoden oder Funktionen, die Mengen wie Arrays zurückgeben, sollen nie null
zurückgeben, sondern leere Mengen oder Objekte.
J14 Problem
Das Zurückgeben von null als Ergebnis einer Methode/Funktion, die eine Liste, HashMap oder ein Array zurückgibt, kann zu Zugriffsfehlern (undefined) und unerwartetem Verhalten führen. Es erfordert zusätzliche Überprüfungen auf null und erhöht die Komplexität des Aufrufercodes.
public ArrayList<String> getNames() {
if (condition) {
return null;
}
// ...
}
J14 Lösung
Um Zugriffsfehler und unerwartetes Verhalten zu vermeiden, sollen Methoden/Funktionen stattdessen ein leeres Objekt oder Array zurückgeben.
public ArrayList<String> getNames() {
if (condition) {
return new ArrayList<>();
}
// ...
}
J14 Vorteile
- Vermeidung von Zugriffsfehlern und unerwartetem Verhalten
- Einfachere Handhabung und weniger Überprüfungen auf null im Aufrufercode
- Verbesserte Robustheit und Stabilität des Codes
J14 Ausnahmen
Es kann Situationen geben, in denen die Rückgabe von null sinnvoll ist, z. B. wenn null einen speziellen Zustand oder eine Bedeutung hat. In solchen Fällen ist es wichtig, die Dokumentation klar zu kommunizieren und sicherzustellen, dass der Aufrufer angemessen darauf reagiert.
J14 Weiterführende Literatur/Links
- Effective Java: Item 54 - Return Empty Arrays or Collections, Not Nulls
- Null or Empty Collection in Java (für Java)
J15 Verwendung von Optional
in bei Rückgabewerte in Funktionen
Eine Funktion oder Methode, die dennoch null
zurückgeben muss, soll stattdessen die Optional
-Klasse verwenden, um den Status des Ergebnisses zu kennzeichnen.
Anwendungseinsatz und Alternativen
Diese Regel gilt nur für bereits bestehenden Klassenstrukturen, die nicht veränderbar oder erweiterbar sind.
Soll ein neues Klassen- oder Objektmodell erstellt werden, sollen direkt spezielle Objekte verwendet werden.
J15 Problem
Das Zurückgeben von null
aus einer Funktion kann zu Fehlern führen, insbesondere wenn nicht überprüft wird, ob das Ergebnis vorhanden ist oder nicht.
public Integer divide() {
if (b != 0) {
return a / b;
}
return null;
}
J15 Lösung
Die Verwendung des Optional
-Objekts ermöglicht es, den Status des Ergebnisses klar zu kennzeichnen, anstatt null
zurückzugeben.
public Optional<Integer> divide() {
if (b != 0) {
return Optional.of(a / b);
}
return Optional.empty();
}
J15 Vorteile
- Klarere Kennzeichnung des Zustands des Ergebnisses durch Verwendung von
Optional
- Bessere Fehlervermeidung durch explizite Behandlung von leeren Ergebnissen
- Verbesserte Lesbarkeit des Codes durch den Verzicht auf
null
J15 Nachteile
- Zusätzlicher Overhead durch die Verwendung von
Optional
- Potenziell erhöhter Komplexitätsgrad in der Verwendung des
Optional
-Objekts - Abhängigkeit von der Implementierung der
Optional
-Klasse
J16 If-Bedingungen ohne Else und mit Return
If-Bedingungen, die ein Return enthalten, sollen kein else
enthalten, um die Lesbarkeit des Codes zu verbessern und die Verschachtelung von Bedingungen zu reduzieren.
Im Folgenden sind sich widersprechende Regeln aufgeführt, die bei der Reihenfolge der Bedingungen in If-Statements zu beachten sind:
- Die Bedingung, welche am wenigsten Code enthält, sollte zuerst geprüft werden.
- Die Bedingung, welche die Funktion/Methode am schnellsten verlässt, sollte zuerst geprüft werden.
- Die Bedingung, welche eine Exception wirft, sollte zuerst geprüft werden.
- Die Bedingung, welche eine positive Bedingung prüft, sollte zuerst geprüft werden.
- Die Bedingung, welche am häufigsten zutrifft, sollte zuerst geprüft werden.
J16 Problem
If-Bedingungen mit einem Return und einem dazugehörigen else-Block können die Lesbarkeit des Codes beeinträchtigen und zu unnötiger Verschachtelung führen.
public int calculate(int x) {
if (x > 0) {
return x * 2;
} else if (x < 0) {
return x;
} else {
return 42;
}
}
J16 Lösung
Durch Entfernen des else-Blocks und direktes Rückgabestatement wird der Code lesbarer und die Verschachtelung von Bedingungen reduziert.
public int calculate(int x) {
if (x > 0) {
return x * 2;
}
if (x < 0) {
return x;
}
return 42;
}
J16 Vorteile
- Verbesserte Lesbarkeit und Klarheit des Codes
- Pfade durch die Funktion sind klarer und leichter nachvollziehbar
- Reduzierung der Verschachtelung von Bedingungen
- Vereinfachte Struktur und Fluss des Codes
J16 Weiterführende Literatur/Links
J17 Guard Pattern
Guard-Klauseln sollen verwendet werden, um unerwünschte Ausführungszweige frühzeitig zu beenden und die Lesbarkeit des Codes zu verbessern.
Im Folgenden sind sich widersprechende Regeln aufgeführt, die bei der Reihenfolge der Bedingungen in If-Statements zu beachten sind:
- Die Bedingung, welche am wenigsten Code enthält, sollte zuerst geprüft werden.
- Die Bedingung, welche die Funktion/Methode am schnellsten verlässt, sollte zuerst geprüft werden.
- Die Bedingung, welche eine Exception wirft, sollte zuerst geprüft werden.
- Die Bedingung, welche eine positive Bedingung prüft, sollte zuerst geprüft werden.
- Die Bedingung, welche am häufigsten zutrifft, sollte zuerst geprüft werden.
J17 Problem
In Java müssen oft komplexe Bedingungen geprüft werden, um unerwünschte Ausführungszweige zu verhindern oder ungültige Eingaben abzufangen. Dies kann zu verschachteltem Code führen, der schwer zu lesen und zu warten ist.
public void processInput(Integer input) {
if (input != null && input > 0) {
// Code zur Verarbeitung des Eingabewerts
}
}
J17 Lösung
Das Guard Pattern ermöglicht es, Bedingungsprüfungen klarer und lesbarer zu gestalten, indem wir unerwünschte Fälle frühzeitig abfangen und beenden.
public void processInput(Integer input) {
if (input == null || input <= 0) {
return;
}
// Code zur Verarbeitung des Eingabewerts
}
J17 Vorteile
- Verbesserte Lesbarkeit des Codes durch eine klare und frühzeitige Abhandlung unerwünschter Fälle
- Reduzierung der Verschachtelung von Bedingungsprüfungen
- Einfache Erweiterbarkeit und Wartbarkeit des Codes
J17 Weiterführende Literatur/Links
J18 Positiv formulierte If-Bedingungen und Auslagerung komplexer Bedingungen
If-Bedingungen sollen positiv formuliert werden und komplexe Bedingungen sollen in temporäre Variablen ausgelagert werden, um die Lesbarkeit und Wartbarkeit des Codes zu verbessern.
Beachte
Die Aufsplittung von If-Bedingungen ist sehr abhängig vom Verständnis des Entwicklers und soll mit Sinn und Verstand eingesetzt werden.
Generell ist die KISS-Regel (Keep It Simple, Stupid) zu beachten.
J18 Problem
Komplexe Bedingungen in If-Anweisungen können den Code schwer verständlich machen, insbesondere wenn sie negativ formuliert sind. Lange und verschachtelte Bedingungen erschweren die Lesbarkeit und können zu Fehlern führen.
if (!(name.isEmpty() || age < 18 || !isAuthorized)) {
// Code ausführen
}
J18 Lösung
Durch die positive Formulierung der Bedingungen und die Auslagerung komplexer Ausdrücke in temporäre Variablen wird der Code lesbarer und verständlicher.
final boolean isNameEmpty = name.isEmpty();
final boolean isUnderAge = age < 18;
final boolean isNotAuthorized = !isAuthorized;
if (!isNameEmpty && !isUnderAge && isNotAuthorized) {
// Code ausführen
}
J18 Vorteile
- Verbesserte Lesbarkeit des Codes durch positiv formulierte Bedingungen
- Reduzierung der Verschachtelung und Komplexität von If-Anweisungen
- Bessere Wartbarkeit des Codes durch klare und beschreibende Variablen
J18 Nachteile
- Alternativ kann ein Kommentar die If-Bedingung beschreiben, aber bei einer Änderung muss daran gedacht werden auch den Kommentar anzupassen.
- Das Auslagern von Bedingungen in temporäre Variablen kann zu einem erhöhten Speicherverbrauch führen, insbesondere bei komplexen Ausdrücken. Dies ist normalerweise vernachlässigbar, kann jedoch in speziellen Situationen berücksichtigt werden.
J18 Ausnahmen
Es gibt Fälle, in denen das Auslagern von Bedingungen in temporäre Variablen nicht sinnvoll ist, z. B. wenn die Bedingung nur an einer Stelle verwendet wird und keine weitere Klarheit oder Wartbarkeit gewonnen wird.
J18 Weiterführende Literatur/Links
- The Art of Readable Code - Simple Conditionals
- Clean Code: A Handbook of Agile Software Craftsmanship
J19 Verwendung von Exceptions
Exceptions sollten in Java nur für unerwartete Situationen verwendet werden, um eine saubere Trennung von Fehlerbehandlung und regulärem Code zu ermöglichen.
J19 Problem
Wenn Exceptions unangemessen verwendet werden, kann dies zu schlechter Leistung, inkonsistentem Verhalten und schwer zu findenden Fehlern führen. Eine übermäßige Verwendung von Exceptions kann auch die Lesbarkeit des Codes beeinträchtigen und dazu führen, dass der Code schwer verständlich ist.
Im folgenden Code kann nicht zwischen den beiden Ausnahmen unterschieden werden, die geworfen werden. Es ist erforderlich, dass der Aufrufer den Exception-Text analysiert, um zu verstehen, was schief gelaufen ist.
public void calculatePrice(int quantity) throws Exception {
if (quantity < 0) {
throw new Exception("Invalid quantity");
}
// do something
// more code
// throw another exception
throw new Exception("Another error occurred");
}
Exceptions in Schleifen können auch zu schlechter Leistung führen, da das Erstellen und Werfen von Ausnahmen teuer sein kann.
public void foo() {
if (aCondition) {
throw new Exception("An error occurred");
}
}
public void calculatePrice(int quantity) throws Exception {
for (int i = 0; i < quantity; i++) {
try {
foo();
} catch (Exception e) {
// und weiter
}
}
}
J19 Lösung
Um die Verwendung von Exceptions zu verbessern, sollte man sie nur für unerwartete Situationen verwenden, wie zum Beispiel unerwartete Eingaben, Netzwerkprobleme oder Systemfehler. Für erwartete Situationen sollte man eine andere Methode der Fehlerbehandlung verwenden, wie zum Beispiel die Rückgabe eines Fehlercodes oder die Verwendung von booleschen Rückgabewerten.
public void calculatePrice(int quantity) throws InvalidQuantityException {
if (quantity < 0) {
throw new InvalidQuantityException("Invalid quantity: {}. Must be greater or equal than zero.", quantity);
}
// do something
// more code
throw new AnotherException("Another error occurred");
}
boolean foo() throws Exception {
if (aCondition) {
return false;
}
}
public void calculatePrice(int quantity) throws Exception {
for (int i = 0; i < quantity; i++) {
if (!foo()) {
// und weiter
}
}
}
J19 Vorteile
- Eine angemessene Verwendung von Exceptions führt zu einem saubereren Code, der einfacher zu verstehen und zu warten ist.
- Exceptions ermöglichen eine saubere Trennung von Fehlerbehandlung und regulärem Code.
- Durch eine bessere Strukturierung des Codes kann die Leistung verbessert werden.
J19 Nachteile
- Eine übermäßige Verwendung von Exceptions kann die Leistung beeinträchtigen und die Lesbarkeit des Codes erschweren (Exceptions innerhalb von for-Schleifen.).
- Es kann schwierig sein, zwischen erwarteten und unerwarteten Situationen zu unterscheiden, was zu Fehlern führen kann, wenn Ausnahmen falsch verwendet werden.
J20 Eigene Exceptions für Fehlerfälle erstellen
Es ist eine bewährte Praxis, eigene Exceptions für spezifische Fehlerfälle zu erstellen, um eine klare und konsistente Fehlerbehandlung zu ermöglichen.
Es sollen für Fehlerfälle eigene Exceptions erstellt werden, um eine klare und konsistente Fehlerbehandlung zu ermöglichen.
J20 Problem
Die Verwendung von allgemeinen Exceptions wie Exception
oder RuntimeException
kann zu unklaren Fehlermeldungen und unzureichender Fehlerbehandlung führen. Insbesondere wenn mehrere gleichnamige Exceptions geworfen werden, kann es schwierig sein, den Ursprung des Fehlers zu identifizieren. Wenn ein Fehlerfall abgefangen werden soll, ist es wichtig, dass der Aufrufer genau weiß, welcher Fehler aufgetreten ist und wie darauf reagiert werden soll.
public void sendOrder(Order order) {
if (order.isInvalid()) {
throw new Exception("Invalid order");
}
if (order.unpaid()) {
throw new Exception("Unpaid order");
}
if (order.notAvailable()) {
throw new Exception("Product not available");
}
}
public void processOrder(Order order) {
try {
// Code zur Verarbeitung der Bestellung
} catch (Exception e) {
// RunTimeException wie NullPointerException leitet auch von Exception ab
// und sollte/kann hier nicht behandelt werden
// Unklare Fehlerbehandlung
logger.error("Fehler bei der Verarbeitung der Bestellung", e);
if (e.getMessage().equals("Invalid order")) {
// Behandlung von ungültigen Bestellungen
} else if (e.getMessage().equals("Unpaid order")) {
// Behandlung von unbezahlten Bestellungen
} else if (e.getMessage().equals("Product not available")) {
// Behandlung von nicht verfügbaren Produkten
}
}
}
J20 Lösung
Durch die Erstellung eigener Exceptions für spezifische Fehlerfälle kann eine klare und konsistente Fehlerbehandlung ermöglicht werden. Wenn eine eigene Hierarchie von Exceptions erstellt wird, kann der Aufrufer genau wissen, welcher Fehler aufgetreten ist und wie darauf reagiert werden soll.
public class OrdersException extends Exception {
public OrdersException(String message) {
super(message);
}
}
public class InvalidOrderException extends OrdersException {
public InvalidOrderException(String message) {
super(message);
}
}
public class UnpaidOrderException extends OrdersException {
public UnpaidOrderException(String message) {
super(message);
}
}
public void sendOrder(Order order) throws OrdersException {
if (order.isInvalid()) {
throw new InvalidOrderException("Invalid order");
}
if (order.unpaid()) {
throw new UnpaidOrderException("Unpaid order");
}
if (order.notAvailable()) {
throw new OrdersException("Product not available");
}
}
public void processOrder(Order order) {
try {
sendOrder(order);
} catch (InvalidOrderException e) {
// Behandlung von ungültigen Bestellungen
logger.error("Ungültige Bestellung", e);
}
catch (OrdersException e) {
// Restliche Fehlerbehandlung: UnpaidOrderException und Rest
logger.error("Fehler bei der Verarbeitung der Bestellung", e);
}
}
J20 Vorteile
- Klare und konsistente Fehlerbehandlung durch spezifische Exceptions
- Bessere Identifizierung und Behandlung von Fehlern
- Verbesserte Lesbarkeit und Wartbarkeit des Codes
J20 Nachteile
- Erhöhter Aufwand bei der Erstellung und Verwaltung von eigenen Exceptions
- Möglicher Overhead bei der Verwendung von Exceptions
- Es kann schwierig sein, die richtige Hierarchie von Exceptions zu erstellen
J21 Exceptions dürfen nur geloggt werden, wenn sie nicht geworfen werden
Exceptions sollten nur dann geloggt werden, wenn sie nicht an höhere Ebenen weitergegeben werden und keine Auswirkungen auf den weiteren Ablauf des Programms haben.
Stattdessen ist es wichtig, Exceptions sinnvoll zu behandeln und angemessene Maßnahmen zu ergreifen.
Wichtig
Entweder wird die Exception geloggt und behandelt ODER in eine andere Form umgewandelt und geworfen.
Aber nicht beides.
J21 Problem
Das Loggen von Exceptions in Methoden, in denen sie bereits abgefangen werden, führt zu einer unnötigen Vermehrung von Exception-Stacktraces in den Logs. Dies erschwert das Lesen der Logs und kann zu einer höheren Belastung des Speichers führen.
Ein Beispiel für ungeprüftes Weiterschicken von Exceptions:
public void readFromFile(String filePath) {
try {
BufferedReader reader = new BufferedReader(new FileReader(filePath));
String line = reader.readLine();
while (line != null) {
// do something
line = reader.readLine();
}
} catch (IOException e) {
logger.error("Unknown error occurred", e);
throw e;
}
}
J21 Lösung
Um Exceptions richtig zu behandeln, sollten sie entweder in der aktuellen Methode behandelt oder an eine höhere Ebene weitergegeben werden. Nur im ersten Fall sollte ein Logging ausgeben werden.
public void readFromFile(String filePath) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
String line = reader.readLine();
while (line != null) {
// do something
line = reader.readLine();
}
} catch (IOException e) {
throw new MySpecificException("The file {} could not be read.", filePath, e);
}
}
J21 Vorteile
- Klare Behandlung und Reaktion auf Exceptions
- Verbesserte Fehlerbehandlung und Debugging-Möglichkeiten
- Besseres Verständnis der Ursachen von Fehlern
- Logs beinhalten keine doppelten Fehlermeldungen.
- Logs werden kleiner.
J21 Ausnahmen
In einigen Fällen kann es sinnvoll sein, Exceptions zu loggen und unverändert wieder zu werfen. Dies ist jedoch eher die Ausnahme und soll gut begründet sein, z.B. wenn der Code in einem bestimmten Kontext läuft, der spezielle Anforderungen hat.
J22 Benennung von Methoden mit verschiedenen Präfixen für Synchronität und Ergebnisverhalten
Es ist eine bewährte Praxis bei der Benennung von Methoden in Java, unterschiedliche Präfixe zu verwenden, um die Synchronität und das Ergebnisverhalten der Methode zu kennzeichnen. Das Präfix "get" soll für synchronen Zugriff verwendet werden und immer einen Wert zurückgeben, während die Präfixe "fetch" oder "request" für asynchronen Zugriff stehen, der länger dauern und auch fehlschlagen kann.
J22 Problem
Bei der Benennung von Methoden ist es wichtig, klare Hinweise auf die Synchronität und das Ergebnisverhalten der Methode zu geben. Unklare oder inkonsistente Benennung kann zu Missverständnissen und unerwartetem Verhalten führen.
// Unklare Benennung ohne klare Angabe zur Synchronität und zum Ergebnisverhalten
public DataResult getData() {
// ...
}
// Unklare Benennung ohne klare Angabe zur Synchronität und zum Ergebnisverhalten
public DataResult getAsyncData() {
// ...
}
J22 Lösung
Um die Synchronität und das Ergebnisverhalten einer Methode klarer zu kennzeichnen, sollen unterschiedliche Präfixe verwendet werden. Das Präfix "get" soll für synchronen Zugriff verwendet werden und immer einen Wert zurückgeben. Die Präfixe "fetch" oder "request" sollen für asynchronen Zugriff stehen, der länger dauern und auch fehlschlagen kann.
get-Präfixe sollen nie async sein, dagegen sollen fetch- oder request- Präfixe immer async sein.
// Synchroner Zugriff mit Wert-Rückgabe
public CompletableFuture<DataResult> fetchData() {
// ...
return CompletableFuture.completedFuture(data);
}
// Asynchroner Zugriff mit Möglichkeit eines Fehlschlags
public CompletableFuture<DataResult> fetchAsyncData() {
// ...
}
J22 Vorteile
- Klare und eindeutige Benennung, die die Synchronität und das Ergebnisverhalten einer Methode widerspiegelt
- Verbesserte Lesbarkeit und Verständlichkeit des Codes
- Einfachere Fehlersuche und Debugging-Möglichkeiten
J22 Ausnahmen
Es kann Situationen geben, in denen die Verwendung von anderen Präfixen angemessen ist, abhängig von den spezifischen Anforderungen und Konventionen des Projekts. Es ist wichtig, einheitliche Namen innerhalb des Projekts festzulegen und zu dokumentieren.
J22 Weiterführende Literatur/Links
J23 Einsatz von JavaDoc
Methoden, Objekte, Typen und Pakete in Java sollen mit JavaDoc annotiert werden, um eine klare Dokumentation der Objekte, Methoden, Parameter, Rückgabewerts und Pakete zu ermöglichen.
J23 Problem
Es ist aufgrund der Benennung und der Signatur einer Methode oder eines Objekts nicht immer klar, wie sie verwendet werden sollen und welche Parameter und Rückgabewerte sie erwarten. Auch in welchen Situationen Ausnahmen geworfen werden können und wie sie behandelt werden sollen, ist oft unklar.
Pakete haben oft keine klare Dokumentation, was sie enthalten und wie sie verwendet werden sollen.
J23 Lösung
Die Verwendung von JavaDoc ermöglicht es, eine klare und konsistente Dokumentation von Methoden, Objekten, Typen und Paketen bereitzustellen.
Weiterhin kann eine Dokumentation automatisch generiert werden, die Entwicklern hilft, den Code besser zu verstehen und zu verwenden.
INFO
Moderne Entwicklungsumgebungen und Tools wie Visual Studio Code, WebStorm und ESLint unterstützen JavaDoc und bieten Funktionen wie Autovervollständigung und Anzeige der Dokumentation, wenn mit dem Mauszeiger über den Code gefahren wird.
J23 Beispiele
J23 Methoden und Funktionen
Beachte!
JavaDoc-Kommentare beginnen mit /**
und enden mit */
. Jede Zeile innerhalb des Kommentars beginnt mit *
. Der Kommentar muss direkt vor der Deklaration der Entität stehen.
/**
* Berechnet die Summe von zwei Zahlen.
* Es kann auch ein Link mit {@link com.example.user.User} eingefügt werden.
* @param x Die erste Zahl.
* @param y Die zweite Zahl.
* @param <T> Der Typ der Zahlen. Generischer Typ.
* @return Die Summe der beiden Zahlen.
* @throws IllegalArgumentException Wenn einer der Parameter null ist.
*/
// Erstelle eine Datei package-info.java im Paket mit folgendem Inhalt
/**
* Enthält Klassen zur Verarbeitung von Benutzerdaten.
* @since 1.0
* @see com.example.user.User
*/
package com.example.user;
/**
* Dokumentation mit einem Code-Beispiel.
* Damit werden < und > in der Dokumentation korrekt dargestellt.
* Im Text selbst kann z.B. für < auch {@code <} verwendet werden.
*
* <code>{@code
* if (x == null || y == null) {
* }</code>
* <pre>{@code
* if (x == null || y == null) {
* }</pre>
*/
J23 Weiterführende Literatur/Links
J24 Variable Parameter in Methoden vermeiden
Variable Parameter in Funktionen oder Methoden sollen vermieden werden, wenn bereits Parameter mit spezifischen Typen oder Strukturen definiert sind.
J24 Problem
Variable Parameter in Funktionen oder Methoden in Kombination mit weiteren vorangestellten unterschiedlichen Parametern können zu Verwirrung und unerwartetem Verhalten führen.
public void fetchData(String url, Headers headers, Options options, Object... params) {
// ...
}
J24 Lösung
Verwende stattdessen spezifische Parameter oder separate Funktionen/Methoden, um das Verhalten klarer zu kennzeichnen.
public void fetchData(String url, Headers headers, Options options) {
// ...
}
public void fetchDataWithParams(String url, Object... params) {
// ...
}
J24 Ausnahmen
Wenn die Funktion oder Methode nur ein vorangestellten Parameter besitzt, kann der Restparameter ...params
verwendet werden, um eine variable Anzahl von Argumenten zu akzeptieren. Eine Verwechslung mit den vorangestellten Parametern ist in diesem Fall unwahrscheinlich.
public void formatString(String template, Object... params) {
// ...
}
Viele spezifische Parameter mit variablem Parameter
Variable Parameter kombiniert mit vielen spezifischen Parametern kann zu Verwirrung führen, ab welchem Parameter die variablen Parameter beginnen. Es ist daher besser wenige Parameter zu verwenden und in mehrere Methoden aufzuteilen, die jeweils einen spezifischen Zweck erfüllen.
J25 Boolean-Parameter in Methoden vermeiden
Boolean als Parameter in Methoden sollen nicht verwendet werden. Stattdessen sollen eigene Methoden mit entsprechenden Namen und Parametern erstellt werden, weil damit das Verhalten der Funktion oder Methode klarer wird.
J25 Problem
Boolean-Parameter in Methoden können zu Verwirrung und unerwartetem Verhalten führen, da der Aufrufer den Zweck des Parameters erraten muss.
public void fetchData(String url, boolean async) {
if (async) {
// Asynchroner Aufruf
} else {
// Synchroner Aufruf
}
}
J25 Lösung
Verwende stattdessen spezifische Parameter oder separate Funktionen/Methoden, um das Verhalten klarer zu kennzeichnen.
public void fetchAsyncData(String url) {
// Asynchroner Aufruf
}
public void fetchData(String url) {
// Synchroner Aufruf
}
J26 Lambda-Ausdrücke statt Funktionsdeklarationen
Lambda-Ausdrücke sollen verwendet werden, um Methoden in Java zu deklarieren, wenn sie kurz und prägnant sind.
Methodenreferenzen
Methodenreferenzen sind eine spezielle Form von Lambda-Ausdrücken, die es ermöglichen, eine Methode als Lambda-Ausdruck zu übergeben. Sie sollen eingesetzt werden, wenn ein Lambda-Ausdruck nur eine Methode aufruft, ohne zusätzliche Logik zu enthalten und dabei die Parameter unverändert weitergibt.
// Lambda-Ausdruck
list.forEach(e -> System.out.println(e));
// Methodenreferenz
list.forEach(System.out::println);
J27 Ternärer Operator
Der ternäre Operator (condition ? expression1 : expression2
) soll verwendet werden, um einfache Bedingungen in einer einzigen Zeile zu schreiben. Er ist einfach zu lesen und zu schreiben. Er soll jedoch nicht geschachtelt werden, um die Lesbarkeit zu erhalten. Verwende dann lieber if...else
.
final var result = condition ? expression1 : expression2;
Hinweis
Der ternäre Operator ist auch bekannt als bedingter Operator oder Elvis Operator
.
Ternär
bedeutet, dass der Operator drei Operanden hat: die Bedingung, den Ausdruck, der ausgeführt wird, wenn die Bedingung wahr ist, und den Ausdruck, der ausgeführt wird, wenn die Bedingung falsch ist.
Komplexität
- Der ternäre Operator soll nicht verschachtelt werden, um die Lesbarkeit zu erhalten.
- Der ternäre Operator soll nur für kurze Ausdrücke verwendet werden.
- Bei komplexeren Bedingungen oder Ausdrücken ist es besser,
if...else
zu verwenden. - Bei komplexeren Bedingungen oder Ausdrücken kann auch eine separate Funktion verwendet werden.
J28 Verwendung von Streams
Java unterstützt Streams, die eine Reihe von Elementen in einer sequenziellen oder parallelen Weise verarbeiten können. Streams sind eine leistungsstarke Möglichkeit, Daten zu filtern, zu transformieren und zu aggregieren, ohne Schleifen oder Iteratoren verwenden zu müssen.
Streams sollen verwendet werden, um Code klarer, lesbarer und kompatkter zu machen.
J28 Problem
Die Verwendung von Schleifen und Iteratoren kann zu unleserlichem und unübersichtlichem Code führen, insbesondere wenn komplexe Filter- oder Transformationsoperationen durchgeführt werden müssen. Darüber hinaus kann es zu Fehlern kommen, wenn Schleifen und Iteratoren nicht korrekt implementiert oder angewendet werden.
final List<Integer> myList = Arrays.asList(1, 2, 3, 4, 5);
int sum = 0;
for (Integer num : myList) {
if (num % 2 == 0) {
sum += num;
}
}
System.out.println(sum); // Output: 6
J28 Lösung
Streams ersetzt die for-Schleife.
List<Integer> myList = Arrays.asList(1, 2, 3, 4, 5);
int sum = myList.stream()
.filter(num -> num % 2 == 0)
.mapToInt(Integer::intValue)
.sum();
System.out.println(sum); // Output: 6
J28 Operationen für Streams
Methode | Erklärung | Beispiel |
---|---|---|
filter() | Filtert Elemente, die einer Bedingung entsprechen | stream.filter(e -> e > 5) |
map() | Transformiert Elemente in andere Elemente | stream.map(e -> e * 2) |
mapToInt() | Transformiert Elemente in Integer | stream.mapToInt(e -> e.intValue()) |
mapToDouble() | Transformiert Elemente in Double | stream.mapToDouble(e -> e.doubleValue()) |
mapToLong() | Transformiert Elemente in Long | stream.mapToLong(e -> e.longValue()) |
flatMap() | Transformiert und flacht verschachtelte Listen | stream.flatMap(e -> e.stream()) |
distinct() | Entfernt Duplikate | stream.distinct() |
sorted() | Sortiert Elemente | stream.sorted() |
limit() | Begrenzt die Anzahl der Elemente | stream.limit(5) |
skip() | Überspringt die ersten n Elemente | stream.skip(5) |
reduce() | Reduziert Elemente zu einem einzigen Wert | stream.reduce(0, (a, b) -> a + b) |
collect() | Sammelt Elemente in eine Sammlung | stream.collect(Collectors.toList()) |
toList() | Sammelt Elemente in eine Liste, ersetzt collect(Collectors.toList()) | stream.toList() |
toSet() | Sammelt Elemente in ein Set | stream.toSet() |
toMap() | Sammelt Elemente in eine Map | stream.toMap(e -> e, e -> e * 2) |
groupingBy() | Gruppiert Elemente nach einem Schlüssel | stream.collect(Collectors.groupingBy(e -> e % 2)) |
forEach() | Führt eine Aktion für jedes Element aus | stream.forEach(e -> System.out.println(e)) |
anyMatch() | Überprüft, ob ein Element einer Bedingung entspricht | stream.anyMatch(e -> e > 5) |
allMatch() | Überprüft, ob alle Elemente einer Bedingung entsprechen | stream.allMatch(e -> e > 5) |
noneMatch() | Überprüft, ob kein Element einer Bedingung entspricht | stream.noneMatch(e -> e > 5) |
count() | Zählt die Anzahl der Elemente | stream.count() |
min() | Findet das kleinste Element | stream.min(Comparator.naturalOrder()) |
max() | Findet das größte Element | stream.max(Comparator.naturalOrder()) |
findFirst() | Findet das erste Element | stream.findFirst() |
findAny() | Findet ein beliebiges Element | stream.findAny() |
parallel() | Führt die Operationen parallel aus | stream.parallel() |
peek() | Führt eine Aktion für jedes Element aus, ohne den Stream zu verändern | stream.peek(System.out::println) |
J28 Oft verwendete Operationen
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Integer[] arr = list.stream().toArray(Integer[]::new);
Integer[] arr = {1, 2, 3, 4, 5};
List<Integer> list = Arrays.stream(arr).collect(Collectors.toList());
// Einfaches Mapping
List<String> list = Arrays.asList("Java", "Kotlin", "Scala");
Map<Integer, String> map = list.stream().collect(Collectors.toMap(String::length, e -> e));
// Gruppiert nach Genre
Map<String, List<Book>> groupedByGenre = items.stream().collect(Collectors.groupingBy(Book::genre));
// Neue Objekte erstellen
Map<Integer, PersonInfo> personInfoMap = personList.stream()
.collect(Collectors.toMap(
Person::getId,
person -> new PersonInfo(person.getName(), person.getName().length())
));
// Map für Transformation
List<PersonInfo> personInfoList = personList.stream()
.map(person -> new PersonInfo(person.getName(), person.getName().length()))
.toLisT();
Map<Integer, String> map = new HashMap<>();
List<String> list = map.entrySet().stream()
.map(e -> e.getKey() + ": " + e.getValue())
.toLisT();
// Ausgabe: ["1: Java", "2: Kotlin", "3: Scala"]
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> evenNumbers = list.stream()
.filter(e -> e % 2 == 0)
.toLisT();
// Ausgabe: [2, 4]
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
int sum = list.stream()
.reduce(0, (a, b) -> a + b);
List<List<Integer>> list = Arrays.asList(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5, 6),
Arrays.asList(7, 8, 9)
);
list.stream()
.flatMap(Collection::stream)
.forEach(System.out::println);
// Ausgabe: 1, 2, 3, 4, 5, 6, 7, 8, 9
Fallstricke
- Die Quelldaten darf während den Stream-Operationen nicht verändert werden (durch Hinzufügen, Entfernen oder Ändern von Elementen).
- Eine Stream Operation wird erst ausgeführt, wenn ein Terminal-Operation (z.B.
collect
,sum
,forEach
) aufgerufen wird. D.h. Log-Ausgaben innerhalb von Stream-Operationen werden nicht ausgeführt, wenn keine Terminal-Operation aufgerufen wird. toList()
undtoSet()
sind nicht die gleichen wiecollect(Collectors.toList())
undcollect(Collectors.toSet())
.toList
undtoSet
liefern eine immutable Liste bzw. ein immutable Set zurück, das nicht verändert werden kann. Der Zugriff darauf führt zu einerUnsupportedOperationException
.toMap()
kann eineIllegalStateException
werfen, wenn Schlüssel dupliziert werden. D.h. die Schlüssel müssen eindeutig sein.groupingBy()
kann eineNullPointerException
werfen, wenn der Schlüsselnull
ist.collect()
kann eineNullPointerException
werfen, wenn das Sammeln in eine Liste oder ein Set erfolgt und ein Elementnull
ist.collect()
kann eineIllegalStateException
werfen, wenn das Sammeln in eine Map erfolgt und Schlüssel dupliziert werden.- Boxing und Unboxing kann ein Performance-Problem sein, wenn primitive Datentypen in Wrapper-Klassen umgewandelt werden. Siehe Autoboxing und Unboxing für weitere Informationen.
Methodenreferenzen
Für Stream-Operationen mit nur einem Parameter soll ein Methodenreferenz verwendet werden, um den Code zu vereinfachen und zu verkürzen.
Siehe Methodenreferenzen Info für weitere Informationen.
J28 Vorteile
- Die Verwendung der Stream-API kann zu einem einfacheren, übersichtlicheren und fehlersichereren Code führen.
- Durch die Verwendung von Stream-Operationen wie
filter
,map
,reduce
,distinct
usw. können komplexe Filter- und Transformationsoperationen auf eine klare und konsistente Weise durchgeführt werden. - Darüber hinaus kann die Stream-API auch Parallelverarbeitung unterstützen, um die Leistung von Multi-Core-Systemen voll auszuschöpfen.
J28 Nachteile
- Potentielle Performance-Probleme
- Komplizierte Verkettung von Befehlen
- Keine Zwischenergebnisse
- Schwierige Fehlerbehandlung (kann durch Stream*Debugger in IntelliJ entgegengewirkt werden)
- Komplexität
J28 Ausnahmen
Es kann Fälle geben, in denen die Verwendung von Schleifen und Iteratoren sinnvoller ist, z.B. wenn es sich um eine einfache Iteration ohne komplexe Filter- oder Transformationsoperationen handelt oder wenn es notwendig ist, auf Elemente in einer bestimmten Reihenfolge zuzugreifen. Es ist daher wichtig, die Verwendung von Stream-Operationen sorgfältig zu prüfen und nur dann zu verwenden, wenn es notwendig und sinnvoll ist.
J28 Weiterführende Literatur
J29 Namen von Paketen
- Paketen beginnen immer mit
com.company
für Enterprise-Anwendungen oderorg.project
für Open-Source-Projekte. - Paketen und Packages sollen mit einem Nomen benannt werden, das den Inhalt beschreibt
com.company
,com.company.project
,com.company.project.module
. - Implementierungen sollen
impl
als Teil des Namens enthaltencom.company.project.impl
,com.company.project.user.impl
. - Zusammengesetzte Nomen innerhalb eines Teils des Namensraums sollen mit Punkten getrennt werden. Statt
com.company.projectmodule
sollcom.company.project.module
verwendet werden. Ausnahmen sind Situationen, in denen ein semantisches Problem oder Unverständnis entsteht, wenn der Name durch Punkt getrennt wird. In diesem Fall kann ein Bindstrich oder Unterstrich verwendet werdencom.company.project-module
. - Die einzelne Tile eines Namensraums werden mit tieferliegenden Teilen immer konkreter.
com.company.project.entities
,com.company.project.entities.user
,com.company.project.entities.user.impl
.
J30 Paket-Importe
Paket-Importe sollen in einer bestimmten Reihenfolge durch ein Formatierungs-Tool automatisch angeordnet werden, denn der Entwickler soll sich nicht um die Reihenfolge kümmern müssen. Die Reihenfolge kann wie folgt sein, sie soll jedoch immer über ein Projekt konsistent sein.
- Java-Standardbibliotheken:
java.*
,javax.*
- Drittanbieter-Bibliotheken:
org.*
,com.*
- Eigene Pakete:
com.example.myproject
- Statische Importe:
import static com.example.myproject.MyClass.MyStaticMethod
- Wildcard-Importe:
import com.example.myproject.*
- Wildcard-Static-Importe:
import static com.example.myproject.MyClass.*
Wildcard-Importe
Wildcard-Importe können zu Namenskonflikten führen, wenn mehrere Klassen mit dem gleichen Namen in verschiedenen Paketen vorhanden sind.
J31 Vermeide automatisches Boxing und Unboxing
Das automatische Boxing oder Unboxing von primitive Datentypen soll vermieden werden, um keine ungewollte Performance-Einbußen zu verursachen.
Boxing vs. Unboxing
- Boxing: Konvertiert einen primitiven Datentyp in ein entsprechendes Wrapper-Objekt. Aus
int
wirdInteger
, ausdouble
wirdDouble
, usw. - Unboxing: Konvertiert ein Wrapper-Objekt in einen primitiven Datentyp. Aus
Integer
wirdint
, ausDouble
wirddouble
, usw.
Integer myFoo(Integer input) {
int i = input 5; // Unboxing
return i; // Boxing
}
J31 Problem
Das automatische Boxing und Unboxing von primitiven Datentypen kann zu unerwünschten Performance-Einbußen führen, insbesondere in Schleifen oder bei häufigen Operationen.
Wo möglich soll auf primitive Datentypen zurückgegriffen werden, um das Boxing und Unboxing zu vermeiden.
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 1000000000; i++) {
list.add(i); // Boxing von int zu Integer
}
Ein weiteres Beispiel ist die Stream-API, die häufig verwendet wird, um Daten zu filtern, zu transformieren und zu aggregieren.
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
int sum = list.stream()
.filter(num -> num % 2 == 0) // Boxing von int zu Integer
.mapToInt(Integer::intValue) // Unboxing von Integer zu int
.sum();
Aber auch Optional kann zu Boxing führen.
Optional<Integer> optional = Optional.of(1); // Boxing von int zu Integer
J31 Lösung
J31 Primitive Datentypen
Für bekannte große Datenmengen oder Schleifen sollen primitive Datentypen verwendet werden, um das Boxing und Unboxing zu vermeiden.
int[] array = new int[1000000000];
for (Integer i = 0; i < array.length; i++) {
array[i] = i;
}
Generics und primitive Datentypen
Generics unterstützen keine primitiven Datentypen, daher müssen Wrapper-Klassen wie Integer
, Double
, Long
usw. verwendet werden.
J31 Stream-API
Für die Stream-API sollen spezielle Methoden wie mapToInt
, mapToDouble
, mapToLong
, flatMapToInt
, flatMapToDouble
, flatMapToLong
verwendet werden, um das Boxing und Unboxing zu vermeiden.
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
int sum = list.stream()
.filter(num -> num % 2 == 0)
.mapToInt(Integer::intValue)
.sum();
// oder
IntStream intStream = list.stream()
.mapToInt(Integer::intValue); // Unboxing von Integer zu int
// liefert Statistiken wie Summe, Durchschnitt, Minimum, Maximum, Anzahl
IntSummaryStatistics summaryStatistics = IntStream.of(1, 2, 3, 4, 5).summaryStatistics();
summaryStatistics.getAverage() // 3.0
summaryStatistics.getCount() // 5
summaryStatistics.getMax() // 5
summaryStatistics.getMin() // 1
summaryStatistics.getSum() // 15
J31 Optional
Für Optional
sollen die folgenden Klassen für primitive Datentypen verwendet werden, um das Boxing und Unboxing zu vermeiden.
OptionalInt optionalInt = OptionalInt.of(1);
OptionalDouble optionalDouble = OptionalDouble.of(1.0);
OptionalLong optionalLong = OptionalLong.of(1L);
J31 Functional Interfaces
Für primitive Datentypen gibt es neben den generischen Functional Interfaces auch spezielle Functional Interfaces, die primitiven Datentypen entsprechen. Diese sollen verwendet werden, um das Boxing und Unboxing zu vermeiden.
IntBinaryOperator
stattBinaryOperator<Integer>
LongBinaryOperator
stattBinaryOperator<Long>
DoubleBinaryOperator
stattBinaryOperator<Double>
IntConsumer
stattConsumer<Integer>
LongConsumer
stattConsumer<Long>
DoubleConsumer
stattConsumer<Double>
ObjIntConsumer
stattBiConsumer<T, Integer>
ObjLongConsumer
stattBiConsumer<T, Long>
ObjDoubleConsumer
stattBiConsumer<T, Double>
IntFunction
stattFunction<Integer, R>
LongFunction
stattFunction<Long, R>
DoubleFunction
stattFunction<Double, R>
IntUnaryOperator
stattFunction<Integer, Integer>
LongUnaryOperator
stattFunction<Long, Long>
DoubleUnaryOperator
stattFunction<Double, Double>
IntToDoubleFunction
stattFunction<Integer, Double>
IntToLongFunction
stattFunction<Integer, Long>
LongToIntFunction
stattFunction<Long, Integer>
LongToDoubleFunction
stattFunction<Long, Double>
DoubleToIntFunction
stattFunction<Double, Integer>
DoubleToLongFunction
stattFunction<Double, Long>
ToIntFunction
stattFunction<T, Integer>
ToLongFunction
stattFunction<T, Long>
ToDoubleFunction
stattFunction<T, Double>
ToIntBiFunction<T, U>
stattBiFunction<T, U, Integer>
ToLongBiFunction<T, U>
stattBiFunction<T, U, Long>
ToDoubleBiFunction<T, U>
stattBiFunction<T, U, Double>
IntPredicate
stattPredicate<Integer>
LongPredicate
stattPredicate<Long>
DoublePredicate
stattPredicate<Double>
IntSupplier
stattSupplier<Integer>
LongSupplier
stattSupplier<Long>
DoubleSupplier
stattSupplier<Double>
J31 Vorteile
- Die Verwendung von primitiven Datentypen kann zu einer besseren Leistung und Effizienz führen, insbesondere bei großen Datenmengen oder Schleifen.
- Das Erstellen von Wrapper-Objekten benötigt zusätzlichen Speicherplatz.
J31 Weiterführende Literatur
J32 for, Array.forEach, Stream.forEach
Lange Zeit wurden for
-Schleifen genutzt, um über Arrays oder Listen zu iterieren. Mit der Einführung von Array.forEach
wurde die Syntax vereinfacht und die Lesbarkeit verbessert. Seit Java 8 ermöglicht die Stream-API eine deklarative Verarbeitung von Daten, sowohl sequenziell als auch parallel.
Jede dieser Iterationsmethoden hat ihre Vor- und Nachteile, abhängig vom Anwendungsfall.
J32 for-Schleife mit Index
Eine for
-Schleife mit einem Index wird verwendet, wenn der Index zur Verarbeitung der Elemente benötigt wird:
final int[] numbers = {1, 2, 3, 4, 5};
for (final int i = 0; i < numbers.length; i++) {
System.out.println(numbers[i]);
}
Alternative: Wenn der Index nicht benötigt wird, sollte Array.forEach
oder Stream.forEach
bevorzugt werden, um die Lesbarkeit zu erhöhen:
J32 for-Schleife für Arrays und iterierbare Objekte
Arrays und Objekte, die das Iterable
-Interface implementieren, können mit einer erweiterten for
-Schleife durchlaufen werden.
final List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
for (Integer num : list) {
System.out.println(num);
}
class MyIterable implements Iterable<Integer> {
private final List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
@Override
public Iterator<Integer> iterator() {
return list.iterator();
}
}
final MyIterable myIterable = new MyIterable();
for (Integer num : myIterable) {
System.out.println(num);
}
J32 Array.forEach
Die Methode Array.forEach
ermöglicht eine elegantere Iteration über Listen und Arrays mit Lambda-Ausdrücken.
final List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.forEach(name -> System.out.println(name));
// oder mit Referenz auf eine Methode
names.forEach(System.out::println);
Vorteile:
- Kürzere und lesbarere Syntax (Referenz auf Methode oder Lambda-Ausdruck)
- Kein expliziter Iterator oder Index erforderlich
Nachteile:
- Keine Möglichkeit, die Iteration vorzeitig zu beenden (kein
break
odercontinue
) - Kann Performance-Nachteile haben im Vergleich zu einer simplen
for
-Schleife (Autoboxing, Methodenaufrufe) - Keine Weitergabe von Exceptions
Exceptions-Handling in Array.forEach
kann durch die Verwendung von com.machinezoo.noexception:Exceptions
vereinfacht werden:
names.forEach(com.machinezoo.noexception.Exceptions.wrap().run(name -> {
System.out.println(name);
}));
J32 Stream.forEach
Die Stream.forEach
-Methode gehört zur Java Stream API und wird meist in Verbindung mit Filtern und Transformationen verwendet.
final List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream().forEach(num -> System.out.println(num));
Vorteile:
- Gut kombinierbar mit Filter- und Mapping-Operationen
- Ermöglicht parallele Verarbeitung durch
parallelStream()
- Fördert eine deklarative Programmierung
Nachteile:
- Kann Performance-Probleme verursachen, wenn unnötig Streams verwendet werden
- Höhere kognitive Last, da Streams oft für Entwickler ungewohnt sind
- Schwerer zu debuggen, da Streams in Pipelines verarbeitet werden (wenn nicht IntelliJ IDEA verwendet wird)
Array.forEach vs. Stream.forEach
Array.forEach
sollte bevorzugt werden, wenn keine Transformation oder Filterung notwendig ist. Stream.forEach
eignet sich besser für komplexe Datenverarbeitungen.
J32 Vor- und Nachteile im Vergleich
Methode | Vorteile | Nachteile |
---|---|---|
for -Schleife (Index) | Hohe Performance, direkte Kontrolle | Unleserlich für große Operationen, manuelle Fehler möglich |
for -Schleife (Iterable ) | Lesbarer als Index-basierte for -Schleife | Keine direkte Kontrolle über Index- Keine vorzeitige Beendigung möglich |
Array.forEach | Kürzere und lesbarere Syntax | Keine vorzeitige Beendigung möglich, Performance-Probleme, keine Weitergabe von Exceptions |
Stream.forEach | Ideal für komplexe Operationen, parallele Verarbeitung möglich | Höhere kognitive Last, schwieriges Debugging |
J32 Fazit
Die Wahl der Iterationsmethode hängt von mehreren Faktoren ab, darunter Lesbarkeit, Performance und Debugging-Aufwand:
- Verwende eine
for
-Schleife, wenn höchste Performance und direkte Kontrolle erforderlich sind. - Nutze
Array.forEach
, wenn eine einfache Iteration ohne Index bevorzugt wird und keine vorzeitige Beendigung notwendig ist. - Setze
Stream.forEach
ein, wenn eine deklarative Verarbeitung mit Filtern oder Transformationen gewünscht ist und die Vorteile der Stream-API genutzt werden sollen. - Vermeide
Stream.forEach
für simple Iterationen (z.B. ohne Filterung), da es unnötige Performance-Kosten verursachen kann und schwieriger zu debuggen ist.
Generell sollte die Iterationsmethode so gewählt werden, dass sie die Lesbarkeit des Codes erhöht und die Anforderungen des Anwendungsfalls erfüllt. Performance-Optimierungen sollten nur bei Bedarf und nach Messung vorgenommen werden.
J33 Generics einsetzen
Generics sollen verwendet werden, um die Typsicherheit in Java zu erhöhen und die Wiederverwendbarkeit von Klassen und Methoden zu verbessern.
J33 Problem
Oftmals müssen Objekte eines Typs in einer Liste oder Map gespeichert werden, ohne dass der Typ zur Laufzeit bekannt ist.
Der Compiler kann jedoch bei diesen allgemeinen Listen nicht überprüfen, ob die Typen korrekt sind, was zu Laufzeitfehlern führen kann.
List list = new ArrayList();
list.add("Java");
Integer value = (Integer) list.get(0); // ClassCastException
J33 Lösung
Generics ermöglichen es, den Typ eines Objekts zur Compile-Zeit zu überprüfen und sicherzustellen, dass der Typ zur Laufzeit korrekt ist.
final List<String> list = new ArrayList<>();
list.add("Java");
final Integer value = list.get(0); // Compiler-Fehler
Erstellen von generischen Instanzen
Beim Erzeugen einer Instanz sollte der Typ nicht explizit angegeben werden, um die Typsicherheit zu gewährleisten. Der Diamond-Operator <>
soll verwendet werden, um den Typ automatisch zu inferieren.
// Falsch (zwei Typen String angegeben)
List<String> list = new ArrayList<String>();
// Richtig
List<String> list = new ArrayList<>();
J34 Type Erasure bei Generics
Generics in Java sind zur Compile-Zeit und nicht zur Laufzeit verfügbar. Das bedeutet, dass der Compiler die Typen zur Compile-Zeit überprüft und dann die Typen entfernt.
J34 Problem
Zur Laufzeit kann nicht auf den Typ eines generischen Typs wie z.B. einer Liste mit einem bestimmten Typ überprüfen.
if (someList instanceof List<String>) { // Compiler-Fehler
// ...
}
J34 Vorteile
- Da keine Typprüfung zur Laufzeit durchgeführt wird, wird die Leistung nicht beeinträchtigt.
- Kompatibel zu älteren Binärcode-Versionen von Java.
J34 Nachteile
- Einschränkungen bei der Verwendung von Reflexion und Typprüfung zur Laufzeit.
- Zur Laufzeit kann nicht auf den Typ eines generischen Typs überprüft werden.
J35 Methoden-Verkettung
Die Methoden-Verkettung soll verwendet werden, um Methodenaufrufe auf einem Objekt in einer einzigen Anweisung zu verkettet.
Methoden-Verkettung ist eine Technik, bei der mehrere Methodenaufrufe auf einem Objekt in einer einzigen Anweisung verkettet werden. Dies wird beispielsweise bei Array-Methoden wie map()
, filter()
, reduce()
und anderen verwendet.
Verwende Methoden-Verkettung, um den Code kompakter und lesbarer zu machen.
J35 Beispiel
final var numbers = List.of(1, 2, 3, 4, 5);
final var sum = numbers.stream()
.filter(x -> x % 2 == 1)
.map(x -> x * 2)
.reduce(0, Integer::sum);
J35 Regeln
- Jeder Methodenaufruf wird auf einer neuen Zeile eingerückt (entsprechend den ESLint-Regeln).
- Jeder Methodenaufruf wird durch einen Punkt (
.
) vorangehend zum Methodennamen getrennt. - Verschachtelung werden vermieden, um die Lesbarkeit zu erhalten, ggf. durch Methoden-Referenzen.
final var sum = numbers.stream()
.filter(this::divisibleByTwo)
.map(this::doubleIt)
.reduce(this::addSum)
.orElse(0);
J35 Vorteile
- Kompakter und lesbarer Code
- Einfache Verkettung von Methodenaufrufen
- Bessere Performance durch Vermeidung von Zwischenvariablen
- Einfache Wiederverwendung von Methodenketten
J35 Ausnahmen
- Übermäßige Verkettung von Methoden kann die Lesbarkeit beeinträchtigen.
- Bei komplexen Operationen oder Bedingungen ist es besser, die Methodenaufrufe aufzuteilen.
- Bei der Verkettung von Methoden ist darauf zu achten, dass die Reihenfolge der Methodenaufrufe korrekt ist.
J36 Unbenutzte Variablen und Parameter
Java-Version
Das Feature ist erst ab Java 22 verfügbar (März 2024)
Es sollen keine unbenutzten Variablen und Parameter im Code vorhanden sein.
- Wenn die Funktionsdeklaration die Parameter vorschreibt, kann
_
als Platzhalter für unbenutzte Parameter verwendet werden.
J36 Problem
Unbenutzte Variablen und Parameter sind oft als Deklaration notwendig, um den Code zu kompilieren, jedoch sieht es so aus, als würden sie im Code verwendet werden, obwohl das nicht der Fall ist.
J36 Lösung
Verwende _
als Platzhalter, um den Code sauber zu halten.
Underline _
als Platzhalter kann für Parameter, Pattern-Matching (switch), Schleifenvariablen, Exceptionvariablen in try-catch
und try-with-resources
verwendet werden.
public void sum(a, b)
public void sum(_, _)
J36 Vorteile
- Sauberer und wartbarer Code
- Vermeidung von Verwirrung und unerwartetem Verhalten
- Bessere Lesbarkeit und Verständlichkeit des Codes
J36 Nachteile
- Der Unterstrich kann zu Verwirrung führen, wenn er nicht als Platzhalter für unbenutzte Variablen oder Parameter verwendet wird.
- Spätere Erweiterungen der Funktion oder Methode lassen den Namen des originalen Parameters vermissen, wenn der Unterstrich verwendet wird. Bitte beachten, dass eine Erweiterung einer vorhandenen Methode gegen das OCP Prinzip verstößt.
J36 Weiterführende Literatur/Links
J37 Verwende spezielle Objekte statt spezielle Werte
Wenn Objekte, wie User
oder jede andere Art von Entität verwendet werden, und es spezielle Fälle gibt wie nicht gefunden, ungültig, leer, fehlerhaft, etc., dann sollen spezielle abgeleitete Objekte verwendet werden, um diese Fälle zu repräsentieren.
J37 Problem
Spezielle Fälle wie nicht gefunden, ungültig, leer, fehlerhaft, etc. werden oft durch spezielle Werte wie null
, -1
, 0
, ''
, false
, etc. repräsentiert. Dies führt dazu, dass im Code ständig überprüft werden muss, ob der Wert speziell ist und entsprechend behandelt werden muss.
Wird diese Prüfung nicht gemacht und vergessen, kommt es zu Fehlern wie Null-Pointer-Exceptions
. Diese Fehler sind schwer zu finden und zu beheben, da sie oft an einer anderen Stelle im Code auftreten, als wo der Fehler tatsächlich liegt.
public User getUser(int id) {
User user = getUserFromDatabase(id);
if (user == null) {
return null;
}
return user;
}
J37 Lösung
Verwende abgeleitete Objekte, um spezielle Fälle zu repräsentieren. Es kann beispielsweise ein NotFoundUser
-Objekt für den Fall eines nicht-gefundenen Benutzers erstellt werden.
Dieses leere Benutzer-Objekt verhält sich anders im Vergleich zu einem korrekten Benutzer-Objekt. So können damit keine Operationen durchgeführt werden, die nur für korrekte Benutzer erlaubt sind.
Die Prüfung auf einen nicht-gefundenen Benutzer kann durch Methoden des Objekts selbst erfolgen. Sollte dieses Objekt doch einmal verwendet werden, so gibt nur dann eine Exception, wenn die Operation am Objekt nicht erlaubt ist. Die Gültigkeit wird in operativen Methoden geprüft (siehe Trennung von operationalem und integrativem Code), so können Integrationsmethoden diese Werte einfach weitergeben.
Das folgende Beispiel zeigt die Verwendung eines speziellen Objekts NotFoundEntity
für den Fall, dass eine Entität (ein generisches Beispiel-Daten-Objekt) nicht gefunden wurde. Es werden keine Exceptions geworfen, sondern spezielle Objekte zurückgegeben, die spezielles Verhalten haben (Polymorphismus
). Wenn diese Objekte verwendet werden, wird das spezielle Verhalten automatisch ausgeführt. In diesem Fall wird ein leeres Array zurückgegeben. Alternativ kann auch ein Fehler geworfen werden, wenn das spezielle Objekt verwendet wird.
Kurzgesagt
Wenn spezielle Objekte verwendet werden, um spezielle Fälle zu repräsentieren, kann damit trotzdem gearbeitet werden und das Ergebnis ist immer gültig.
Optional
Die Klasse Optional in Java kann auch verwendet werden, um diese spezielle Fälle zu repräsentieren. Es ist nützlich, wenn bereits Klassen und Objekte aus einer Legacy-Anwendung verwendet werden, die nicht geändert werden können.
class Entity {
private int id;
private String name;
public Entity(int id, String name) {
this.id = id;
this.name = name;
}
public boolean isValid() {
return true;
}
public List<String> doSomething() {
return List.of();
}
}
class NotFoundEntity extends Entity {
public NotFoundEntity() {
super(-1, 'Unknown');
}
@Override
public boolean isValid() {
return false;
}
@Override
public void doSomething() {
//do nothing
}
}
//...
public Entity getEntityById(int id) {
if (id <= 0) {
return new NotFoundEntity();
}
// liefert immer ein gültiges Entity-Objekt
}
public void foo(int id) {
var entity = getEntityById(id);
List<String> result = entity.doSomething(); // liefert leer Array, wenn NotFoundEntity
}
J37 Vorteile
- Keine Null-Pointer-Exceptions
- Spezielle Fälle werden explizit repräsentiert.
- Unterscheidung zwischen verschiedenen Fällen durch unterschiedliches Verhalten und Objekte (statt
null
) - Keine ständige Überprüfung auf spezielle Werte notwendig (wie
null
,-1
,0
,''
,false
, etc.) - Code kann nicht fehlschlagen, weil keine spezielle Werte verwendet werden.
- Kein Exceptionhandling
- Vermeidet verschachtelte try-catch-Blöcke
- Testen von speziellem Verhalten wird einfacher oder braucht gar nicht mehr getestet zu werden, da es nichts zu testen gibt
- API wird einfacher, da keine Exceptions geworfen werden müssen und Rückgabewerte immer gültig und prüfbar (
isValid()
) sind
- Code wird einfacher und lesbarer, da spezielle Fälle keine zusätzlichen
if
-Anweisungen benötigen.
J37 Nachteile
- Architektur der Klassen und Objekte wird komplexer oder vorhandene Architektur muss angepasst werden.
- Methoden müssen in ihrer Dokumentation nun statt Exceptions spezielle Objekte beschreiben.
- Spezielle Objekte müssen erstellt und gepflegt werden.
- Spezielle Objekte können zu einer größeren Anzahl von Klassen führen.
- Umstellung bestehender Code kann aufwändig sein.
- Exceptions sind in Fleisch und Blut der meisten Entwickler und werden oft als einfacher angesehen.
- Performance kann durch die Erstellung von speziellen Objekten beeinträchtigt werden, insbesondere da Pfade nicht mehr durch Prüfung von speziellen Werten abgekürzt werden könnten.
Anderes Vorgehen gleiche Wirkung
Der Einsatz von speziellen Werten wie null
unterscheidet sich im Endergebnis nicht von speziellen Objekten. Eine Prüfung muss früher oder später erfolgen, ob es sich um einen speziellen Fall handelt (null
oder isValid()
).
Jedoch ist in der Entwicklung oft die Situation gegeben, dass Entwickler einen Eingabewert nicht prüfen und es dadurch zu Fehlern kommt. Durch den Einsatz von speziellen Objekten wird die Prüfung auf spezielle Werte automatisiert und der Code wird sicherer. Erst sogenannte Kernfunktionen, die die speziellen Objekte verwenden, müssen die Prüfung dann durchführen. Diese sind in der Regel besser getestet im Gegensatz zu den weiter oben in der Hierarchie liegenden Methoden.
Durch den Einsatz von speziellen Objekten wird es unwahrscheinlicher, dass Fehler wie null
-Pointer-Exceptions oder undefined
-Exceptions auftreten.
J37 Ausnahmen
- Für eine bereits existierende API sollte das Verhalten nicht einfach so geändert werden, da dies gegen Lisko-Substitutionsprinzip und das Prinzip Prinzip der konzeptuellen Integrität verstößt.
J38 JetBrains Annotations
JetBrains Annotations sind eine Reihe von Annotationen, die in Java-Code verwendet werden können, um zusätzliche Informationen über den Code zu geben. Die Annotationen werden von JetBrains entwickelt und in ihren IDEs wie IntelliJ IDEA verwendet, um den Code zu analysieren und zu überprüfen.
JetBrains Annotationen sollen verwendet werden, um den Code zu dokumentieren und auf Null-Referenzen und andere Probleme hinzuweisen.
J38 Problem
Es kann schwierig sein, den Code auf Null-Referenzen und andere Probleme zu überprüfen, die während der Laufzeit auftreten können. Außerdem können schlecht dokumentierte Methoden und Klassen zu Verwirrung und Fehlern führen.
public void foo(String s) {
if (s == null) {
throw new NullPointerException();
}
// ...
}
J38 Refactoring
Mit den Annotations von JetBrains können Entwickler Methoden und Klassen genau dokumentieren und auf Null-Referenzen und andere Probleme hinweisen. Zum Beispiel kann die @NotNull
-Annotation verwendet werden, um anzuzeigen, dass eine Variable, ein Parameter oder ein Rückgabewert einer Methode nicht null sein darf. Die @Nullable
-Annotation kann verwendet werden, um anzuzeigen, dass ein Parameter oder Rückgabewert einer Methode null sein kann.
Andere mögliche Annotations-Typen von Jetbrains sind:
org.intellij.lang.annotations.Flow
: Verwendet zur Angabe von Flussbedingungen, die von einem Code-Analyse-Tool verwendet werden können, um mögliche Fehler im Code zu identifizieren.org.intellij.lang.annotations.JdkConstants
: Verwendet zur Verwendung von Konstanten aus der JDK-Codebasis, um Compilerwarnungen und -fehler zu vermeiden.org.intellij.lang.annotations.Language
: Verwendet zur Angabe der Sprache, die in einem String-Literal verwendet wird, um Tools wie Code-Analyse-Tools und IDEs zu unterstützen.org.jetbrains.annotations.ApiStatus
: Verwendet zur Kennzeichnung von API-Elementen (Methoden, Klassen usw.) mit ihrem aktuellen Stabilitätsstatus (experimentell, stabil, veraltet usw.).org.jetbrains.annotations.Contract
: Verwendet zur Angabe von Vertragsbedingungen, die eine Methode erfüllen muss, wie z.B. dass sie keinenull
-Rückgabewerte liefern darf oder dass sie einen bestimmten Wertebereich zurückgeben muss.org.jetbrains.annotations.Debug
: Verwendet zur Markierung von Code-Elementen (Klassen, Methoden usw.), die nur für Debugging-Zwecke verwendet werden sollten und nicht in einer Produktionsumgebung aufgerufen werden sollten.org.jetbrains.annotations.DependsOn
: Verwendet zur Angabe von Abhängigkeiten zwischen Code-Elementen (Klassen, Methoden usw.), um Tools wie IDEs und Build-Tools bei der Erstellung von Abhängigkeitsdiagrammen zu unterstützen.org.jetbrains.annotations.ExpectedFailure
: Verwendet zur Markierung von Tests, die auf fehlgeschlagene Tests warten oder darauf, dass bestimmte Ausnahmen ausgelöst werden.org.jetbrains.annotations.FileCharset
: Verwendet zur Angabe des Zeichensatzes, der für eine bestimmte Datei verwendet werden soll.org.jetbrains.annotations.MagicConstant
: Verwendet zur Verwendung von Enum-ähnlichen Konstanten mit einem bestimmten Wertebereich und einem vordefinierten Satz von Konstantenwerten.org.jetbrains.annotations.Nls
: Verwendet zur Angabe von lokalisierten Strings, um Tools wie IDEs und Code-Analyse-Tools bei der Lokalisierung von Code zu unterstützen.org.jetbrains.annotations.NotNull
: Verwendet zur Angabe von Argumenten, die nichtnull
sein dürfen.org.jetbrains.annotations.Nullable
: Verwendet zur Angabe von Argumenten, dienull
sein dürfen.org.jetbrains.annotations.Pattern
: Verwendet zur Überprüfung von Strings mit einem regulären Ausdruck.org.jetbrains.annotations.PropertyKey
: Verwendet zur Angabe von Schlüsseln für lokalisierte Strings.org.jetbrains.annotations.Range
: Verwendet zur Überprüfung von Argumenten auf einen bestimmten Wertebereich.org.jetbrains.annotations.RegExp
: Verwendet zur Überprüfung von Strings mit einem regulären Ausdruck.org.jetbrains.annotations.Subst
: Verwendet zur Ersetzung von Platzhaltern in Strings durch bestimmte Werte.org.jetbrains.annotations.TestOnly
: Verwendet zur Markierung von Code-Elementen (Klassen, Methoden usw.), die nur für Tests verwendet werden sollten und nicht in einer Produktionsumgebung aufgerufen werden sollten
public static String formatName(@NotNull String firstName, @NotNull String lastName) {
return lastName + ", " + firstName;
}
public static void divide(@Contract("_,0 -> fail") int a, int b) {
if (b == 0) {
throw new IllegalArgumentException("Divisor cannot be zero");
}
int result = a / b;
System.out.println(result);
}
public class PaymentMethod {
public static final String CREDIT_CARD = "credit_card";
public static final String PAYPAL = "paypal";
public static final String GOOGLE_PAY = "google_pay";
private String method;
public PaymentMethod(@MagicConstant(stringValues = {CREDIT_CARD, PAYPAL, GOOGLE_PAY}) String method) {
this.method = method;
}
// ...
}
public static void validateAge(@Range(from = 18, to = 99) int age) {
if (age < 18 || age > 99) {
throw new IllegalArgumentException("Age must be between 18 and 99");
}
System.out.println("Valid age: " + age);
}
public static void validateEmail(@Pattern(regexp = "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$") String email) {
if (!email.matches("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$")) {
throw new IllegalArgumentException("Invalid email format");
}
System.out.println("Valid email: " + email);
}
J38 Vorteile
- Reduziert die Anzahl von Null-Referenz-Exceptions
- Verbessert die Dokumentation von Code
- Unterstützt statische Analysewerkzeuge und IDEs bei der Fehlererkennung. IntelliJ IDEA zeigt z.B. eine Warnung an, wenn eine Methode mit
@NotNull
-Annotation einennull
-Wert zurückgibt. - Verbessert die Lesbarkeit von Code für andere Entwickler
J38 Nachteile
- Erfordert zusätzliche Zeit und Arbeit, um Annotations in den Code zu integrieren
- Kann dazu führen, dass der Code unübersichtlich wird, wenn zu viele Annotations verwendet werden
J38 Ausnahmen
- Für kleine und einfache Projekte können Annotations möglicherweise nicht erforderlich sein
- Es kann Fälle geben, in denen der Aufwand, Annotations zu verwenden, den Nutzen überwiegt.
J38 weiterführende Literatur/Links
- JetBrains Annotations Dokumentation: https://www.jetbrains.com/help/idea/nullable-and-notnull-annotations.html
- "Effective Java" von Joshua Bloch: Ein Buch, das die Verwendung von Annotations in Java detailliert beschreibt.
J39 Eingabeprüfungen in REST-API mit Annotation
Eingabeprüfungen in RESTful Web Services sollen verwendet werden, um unerwartete Fehler zu vermeiden und die Sicherheit zu erhöhen.
J39 Problem
RESTful Web Services erlauben den Austausch von Daten zwischen verschiedenen Systemen über HTTP-Anfrage. Diese Daten können jedoch in unerwarteter Weise falsch formatiert oder ungültig sei. Eine fehlgeschlagene Eingabeprüfung kann zu unerwarteten Ergebnissen oder sogar zu Sicherheitsproblemen führen.
@Path("/products/{category}/{productId}")
public class ProductResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
public Response getProduct(
@PathParam("category") String category,
@PathParam("productId") int productId) {
// ...
}
}
In diesem Beispiel gibt es zwei Pfadparameter: category
und productId
. Der category
-Parameter kann einen beliebigen String enthalten und productId
muss eine ganze Zahl sei. Es gibt keine Eingabeprüfung auf die Werte der Parameter.
J39 Lösung
Eine Möglichkeit, die Eingabeprüfung in RESTful Web Services zu verbessern, besteht darin, Annotationen zu verwenden, um die zulässigen Werte und Formate von Parametern zu definiere. JAX-RS bietet eine Vielzahl von Annotationen an, die dazu verwendet werden können, Eingabeprüfungen durchzuführen.
Einige der wichtigsten Annotationen, die in REST-Methoden verwendet werden können, um Eingabeprüfungen durchzuführen:
@PathParam
- Ermöglicht den Zugriff auf den Wert eines Pfadparameters in einer REST-Anfrage.@QueryParam
- Ermöglicht den Zugriff auf den Wert eines Abfrageparameters in einer REST-Anfrage.@HeaderParam
- Ermöglicht den Zugriff auf den Wert eines Header-Parameters in einer REST-Anfrage.@CookieParam
- Ermöglicht den Zugriff auf den Wert eines Cookie-Parameters in einer REST-Anfrage.@FormParam
- Ermöglicht den Zugriff auf den Wert eines Formular-Parameters in einer REST-Anfrage.@BeanParam
- Ermöglicht die Verwendung eines POJOs, das mit@PathParam
,@QueryParam
,@HeaderParam
,@CookieParam
und@FormParam
annotiert ist, um eine Gruppe von Parametern zu erfassen.@DefaultValue
- Legt einen Standardwert für einen Parameter fest, falls er in der REST-Anfrage nicht vorhanden ist.@Min
- Legt den minimalen Wert für eine numerische Eingabe fest.@Max
- Legt den maximalen Wert für eine numerische Eingabe fest.@NotNull
- Legt fest, dass ein Parameter in der REST-Anfrage nicht null sein darf.@Size
- Legt die Größe eines Parameters in der REST-Anfrage fest.@UUID
- Legt fest, dass ein Parameter in der REST-Anfrage eine gültige UUID sein muss.@Pattern
- Legt ein reguläres Ausdrucksmuster fest, das ein Parameter in der REST-Anfrage erfüllen muss.
Diese Annotationen können verwendet werden, um sicherzustellen, dass die Eingaben in einer REST-Anfrage korrekt sind und um unerwartete Fehler bei der Verarbeitung zu vermeiden.
@Path("/products/{category}/{productId : \\d+}")
public class ProductResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
public Response getProduct(
@PathParam("category") @Pattern(regexp = "^(books|electronics|clothing)$") String category,
@PathParam("productId") int productId) {
// ...
}
// liefert 400-Fehler, wenn category nicht "books", "electronics" oder "clothing" ist
}
@GET
@Path("/{id}")
public Response getResource(@PathParam("id") @UUID String id) {
// code here
}
// liefert 400-Fehler, wenn id keine gültige UUID ist
// UUID-Format: 123e4567-e89b-12d3-a456-426614174000
@GET
@Path("/{number}")
public Response getNumber(@PathParam("number") @Min(1) @Max(10) int number) {
// code here
}
// liefert 400-Fehler, wenn number < 1 oder number > 10
@POST
@Path("/example/{name}/{age}/{email}/{username}")
@Consumes(MediaType.APPLICATION_JSON)
public Response exampleMethod(
@PathParam("name") final String name,
@PathParam("age") @Min(value = 18, message = "Age must be at least 18") @Max(value = 120, message = "Age must not exceed 120") final int age,
@PathParam("email") @Email(message = "Invalid email address") final String email,
@PathParam("username") @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,}$", message = "Username must be at least 8 characters long and contain at least one letter and one number") final String username
) {
// Methodenlogik hier...
return Response.ok().build();
}
J39 Vorteile
- Bessere Eingabeprüfung: Annotationen ermöglichen eine präzisere Definition der zulässigen Werte und Formate von Parametern, was zu einer besseren Eingabeprüfung führt.
- Sicherheit: Eine effektive Eingabeprüfung kann dazu beitragen, Sicherheitsprobleme zu verhindern, die durch unerwartete oder ungültige Eingaben verursacht werden können.
- In der Regel wird der HTTP-Statuscode "400 Bad Request" zurückgegeben, wenn eine Eingabeprüfung in einer REST-API fehlschlägt.
- Bessere Lesbarkeit und Nachvollziehbarkeit: Annotationen können verwendet werden, um die Bedeutung von Parametern in REST-Methoden zu dokumentieren.
J39 Nachteile
- Nicht alle Eingabeprüfungen können mit Annotationen durchgeführt werden. Eine manuelle Prüfung im Code ist in einigen Fällen erforderlich.
J39 Weiterführende Literatur/Links
J40 Verwendung von com.machinezoo.noexception
in Callbacks wie z.B. forEach
in Java
Es ist eine bewährte Praxis in Java, die Bibliothek com.machinezoo.noexception
zu verwenden, um die Verwendung von try-catch
-Blöcken in Callback-Funktionen wie forEach
zu reduzieren. Durch die Verwendung dieser Bibliothek wird der Code sauberer und lesbarer, da die Ausnahmebehandlung von Callbacks elegant behandelt wird.
J40 Problem
Bei der Verwendung von Callback-Funktionen wie forEach
in Java besteht die Notwendigkeit, Ausnahmen innerhalb des Callbacks zu behandeln. Dies führt zu zusätzlichem Code und erhöht die Komplexität, insbesondere wenn mehrere Ausnahmen behandelt werden müssen.
List<String> list = Arrays.asList("apple", "banana", "cherry");
try {
list.forEach(item -> {
try {
// Operationen, die eine Ausnahme werfen können
// ...
} catch (Exception e) {
// Ausnahmebehandlung
// ...
}
});
} catch (Exception e) {
// Ausnahmebehandlung
// ...
}
J40 Refactoring
Durch die Verwendung von com.machinezoo.noexception
kann die Ausnahmebehandlung in Callback-Funktionen eleganter gehandhabt werden. Die Bibliothek bietet verschiedene Hilfsmethoden an, um Ausnahmen in Callbacks zu behandeln, ohne dass zusätzliche try-catch
-Blöcke erforderlich sind.
import com.machinezoo.noexception.Exceptions;
List<String> list = Arrays.asList("apple", "banana", "cherry");
list.forEach(Exceptions.sneak().consumer(item -> {
// Operationen, die eine Ausnahme werfen können
// ...
}));
J40 Vorteile
- Reduzierung des Boilerplate-Codes durch die Verwendung von
com.machinezoo.noexception
- Sauberer und lesbarer Code ohne zusätzliche
try-catch
-Blöcke in Callback-Funktionen - Bessere Trennung von Geschäftslogik und Ausnahmebehandlung
J40 Nachteile
- Einführung einer zusätzlichen Abhängigkeit durch die Verwendung von
com.machinezoo.noexception
- Erhöhte Komplexität des Codes durch die Verwendung von Hilfsmethoden
J40 Ausnahmen
Es kann Situationen geben, in denen die Verwendung von com.machinezoo.noexception
nicht angemessen ist, z. B. wenn das Projekt bereits eine andere Lösung für die Behandlung von Ausnahmen verwendet oder wenn die Einführung einer zusätzlichen Abhängigkeit vermieden werden soll.
J40 Weiterführende Literatur/Links
J41 Kapselung von API-Methoden zur Vereinfachung und besseren Testbarkeit
J41 Problem
API-Methoden können oft komplexe Logik benötigen, um beispielsweise Datenumwandlungen oder Filterungen für die Eingabeparameter und Resultate durchzuführen. Wenn diese Komplexität für die API-Methode notwendig ist und direkt in der eigenen Methode angwendet wird, kann dies zu unübersichtlichem Code und Schwierigkeiten bei der Testbarkeit führen. Darüber hinaus kann es erforderlich sein, die API-Methode in Tests zu mocken, was zu erhöhtem Aufwand führt.
// Beispiel-API-Methode
public String[] getActiveUsers(int[] userIds) {
// Komplexe Logik zur Umwandlung und Filterung
// ...
// Rückgabe der Benutzernamen
return usernames;
}
J41 Lösung
Um die Komplexität der API-Methode zu reduzieren und die Testbarkeit zu verbessern, sollte die Logik in eine eigene Methode ausgelagert werden, die die API-Methode aufruft und dabei die erforderlichen Umwandlungen und Filterungen durchführt.
// Eigentliche Arbeitsmethode
private void foo() {
List<String> = getActiveUsers(userIds);
// ...
}
// Kapselungsmethode für die Komplexität der API
public List<String> getActiveUsers(List<Integer> userIds) {
List<User> activeUsernames = api.getUsers(userIds.toArray(new String[0])));
// Rückgabe der Benutzernamen als Array
return activeUsernames.stream()
.filter(User::isActive)
.collect(Collectors.toList());
}
J41 Vorteile
- Bessere Lesbarkeit und Wartbarkeit des Codes durch Auslagerung der Komplexität des API-Aufrufs in eine eigene Methode.
- Verbesserte Testbarkeit, da die kapselnde Methode leichter zu testen ist und die API-Methode nur über die kapselnde Methode getestet werden muss.
- Erhöhte Flexibilität, da die kapselnde Methode bei Bedarf weitere Anpassungen oder Erweiterungen der Funktionalität ermöglicht, ohne die API-Methode direkt zu verändern.
J41 Ausnahmen
In bestimmten Fällen kann es aus Performance-Gründen oder aufgrund von spezifischen Anforderungen notwendig sein, die Komplexität direkt in der API-Methode zu belassen. In solchen Fällen sollte jedoch sorgfältig abgewogen werden, ob die Vorteile der Kapselung überwiegen.
J41 Weiterführende Literatur/Links
J42 String-Formatierung in Java
Beim Logging mit SLF4J ist es wichtig, die Platzhalter-Zeichen korrekt zu verwenden und nicht mit den Platzhaltern von String.Format zu verwechseln. Leider ist in Java ein Verwechseln von Platzhaltern möglich, wenn man nicht aufpasst.
J42 Problem
SLF4J bietet Platzhalter für das Einfügen von Werten in Log-Nachrichten. Die Platzhalter werden jedoch manchmal mit den Platzhaltern von String.Format verwechselt, was zu unerwartetem Verhalten oder sogar Fehlern führen kann.
SLF4J vs. String.format vs. MessageFormat
- SLF4J: verwendet geschweifte Klammern
{}
als Platzhalter und erwartet die Variablen in der Reihenfolge der Platzhalter. - String.format: verwendet Prozentzeichen
%
als Platzhalter und erwartet die Variablen in der Reihenfolge der Platzhalter und mit dem entsprechenden Typ (%s
für String,%d
für Integer, etc.). - MessageFormat: verwendet geschweifte Klammern
{}
als Platzhalter und erwartet die Variablen mit dem entsprechenden Index ({0}
für die erste Variable,{1}
für die zweite Variable, etc.).
String name = "John";
int age = 30;
// falsch
log.info("Name: %s, Age: %d", name, age);
String.format("Name: {}, Age: {}", name, age);
MessageFormat.format("Name: %s, Age: %d", name, age)
J42 Refactoring
Platzhalter für das Logging mit SLF4J werden mit geschweiften Klammern verwendet. Platzhalter für String.format werden mit Prozentzeichen verwendet. Zu beachten ist, dass Platzhalter die geschweiften Klammer sind und die Reihenfolge der Platzhalter mit der Reihenfolge der Variablen übereinstimmt. Das Logging für Exceptions erfordert keinen Platzhalter, wenn das Objekt der Exception als letzter Parameter für das Logging übergeben wird.
String name = "John";
int age = 30;
log.info("Name: {}, Age: {}", name, age);
String.format("Name: %s, Age: %d", name, age);
MessageFormat.format("Name: {0}, Age: {1}", name, age)
J42 Weiterführende Literatur/Links
J43 Rückgabe von Collections sollen immer unveränderlich sein
Wenn interne Datenstrukturen wie Collections (List, Set, Map) zurückgegeben werden müssen, sollen diese immer immutable sein, d.h. unveränderlich, sein, damit die internen Datenstrukturen nicht von außen verändert werden können.
J43 Problem
Wenn interne Datenstrukturen wie Collections (List, Set, Map) zurückgegeben werden, können diese von außen verändert werden, was dazu führen kann, dass die interne Datenstruktur inkonsistent wird oder unerwartete Ergebnisse auftreten.
public List<String> getNames() {
return names;
}
List<String> names = getNames();
names.add("Alice");
J43 Lösung
Um zu verhindern, dass interne Datenstrukturen von außen verändert werden, sollten immer Kopien der internen Datenstrukturen zurückgegeben werden, die unveränderlich sind.
Für einfache Listen wie Strings oder primitive Datentypen können List.copyOf()
oder Collections.unmodifiableList()
verwendet werden, um unveränderliche Listen zu erstellen.
public List<String> getNames() {
return List.copyOf(names);
}
// oder
public List<String> getNames() {
return Collections.unmodifiableList(names);
}
Tiefe Kopie vs. Flache Kopie
List.copyOf()
und Collections.unmodifiableList()
erstellen nur eine flache Kopie der Liste, d.h. die Elemente der Liste sind nicht kopiert, sondern nur die Referenzen zu den Elementen.
Wenn die Elemente der Liste ebenfalls verändert werden können, sollten tiefe Kopien der Elemente erstellt werden, um sicherzustellen, dass die Element nicht verändert werden können.
Statt tiefe Kopien zu erstellen soll das Prinzip Tell, Don't Ask angewendet werden, um die Verantwortung für die Veränderung der Elemente an die Klasse zu übergeben, die die Elemente besitzt.
Mutable vs. Immutable vs. Unmodifiable
- Mutable: Ein Objekt oder Datenstruktur kann verändert werden, d.h. es Werte veränder oder Elemente hinzugefügt, entfernt oder geändert werden.
- Immutable: Ein Objekt oder Datenstruktur kann nicht verändert werden, d.h. seine internen Felder ist konstanz oder für eine Menge können keine Elemente hinzugefügt, entfernt oder geändert werden. Beispiele für unveränderliche Objekte sind
String
,Integer
,LocalDate
, etc. Methoden wieMap.of()
,List.of()
,Set.of()
,Map.copyOf()
,List.copyOf()
,Set.copyOf()
erstellen unveränderliche Mengen, Listen und Maps, d.h. die Datenstruktur können weder verändert noch erweitert werden. Nur eine Kopie der immutablen Datenstruktur kann verändert werden. - Unmodifiable: Eine nicht-modifizierbare Datenstruktur, die eine Ansicht auf eine veränderliche Datenstruktur darstellt, d.h. alle Änderungen an der unveränderlichen Datenstruktur werden blockiert, aber die ursprüngliche Datenstruktur kann verändert werden. Beispiele für Methoden, die unveränderliche Datenstrukturen
Collections.unmodifiableList()
,Collections.unmodifiableSet()
,Collections.unmodifiableMap()
erzeugen. Dies wird durch eine Wrapper-Klasse erreicht, die die Methoden zum Hinzufügen und Entfernen von Elementen blockiert, bzw. eine AusnahameUnsupportedOperationException
wirft. Der Vorteil besteht darin, dass die ursprüngliche Datenstruktur nicht kopiert werden muss, sondern nur eine Ansicht auf die Datenstruktur erstellt wird. Wenn komplexe Objekte in einer unveränderlichen Datenstruktur gespeichert werden, können die Objekte jedoch selbst verändert werden.
J44 Statische Initialisierer vermeiden
Statische Initialisierungen in Java sind eine Möglichkeit, in Klassen statische Felder direkt beim Start der Anwendung zu initialisieren. Statische Initialisierungen sind jedoch, wie der Name schon sagt, statisch und können nicht verändert werden, was zu Problemen bei der Testbarkeit und Wartbarkeit führen kann, wenn diese Werte doch verändert werden müssen.
J44 Problem
Statische Felder, die direkt mit einem Wert initialisiert werden, können zur Lauzeit nicht verändert werden. In Tests kann es notwendig sein, diese Werte zu verändern, um bestimmte Szenarien zu testen und Mocks zu verwenden. Im ersten Beispiel wird ein statisches Feld mit einer Umgebungsvariable initialisiert, die auf dem Produktivsystem gesetzt wird, jedoch in einer Testumgebung überschrieben werden muss.
public class Config {
public static final String ENVIRONMENT = System.getProperty("environment", "dev");
}
Auch Logging mit dem SLF4J-Framework kann in Tests nicht getestet werden, wenn der Logger statisch initialisiert wird.
public class MyClass {
private static final Logger log = LoggerFactory.getLogger(MyClass.class);
}
Warum Logging getestet werden sollte
Logging sollte in Tests getestet werden, um sicherzustellen, dass die richtigen Log-Nachrichten mit den richtigen Parametern und Werten protokolliert werden. Werte, die über Platzhalter ({}
, %s%d
, {0}{1}
) in Log-Nachrichten eingefügt werden, sollten überprüft werden, um sicherzustellen, dass die richtigen Werte und im richtigen Format protokolliert werden. Logging in Fehlerfällen sollte ebenfalls getestet werden, um sicherzustellen, dass die richtigen Fehlermeldungen protokolliert werden. Sollte ein Logging im Fehlerfall selbst eine Exception werfen, wird die ursprüngliche Exception verschluckt und kann nicht erkannt werden.
J44 Lösung
Statt statische Initialisierungen zu verwenden, kann Dependency Injection verwendet werden, um die Werte zur Laufzeit zu setzen.
Die Klasse sollte sich nicht darum kümmern müssen, woher die Umgebungsvariable kommt, sondern nur, dass sie gesetzt ist. Es ist die Aufgabe des Aufrufers, die Umgebungsvariable zu setzen. So kann der Wert auch aus einer Datei, einer Datenbank oder als CLI-Argument kommen.
public class Config {
private String environment;
public Config(String environment) {
this.environment = environment;
}
public String getEnvironment() {
return environment;
}
}
Es gibt viele Lösungen für den Einsatz von Dependency Injection mit SL4J.
- Logger als Parameter übergeben
- Logger über Factory-Methode erstellen
- Einen Log-Adapter verwenden, um die Log-Implementierung zu wrappen
public class MyClass {
private final Logger log;
public MyClass(Logger log) {
this.log = log;
}
public class MyClass {
private static final Logger;
public MyClass(ILoggFactory loggerFactory) {
this.Logger = loggerFactory.getLogger(MyClass.class);
}
}
public interface CustomLoggerInterface {
void info(String message);
void error(String message);
}
public class LogAdapter implements CustomLoggerInterface {
private final org.slf4j.Logger logger;
public LogAdapter(org.slf4j.Logger logger) {
this.logger = logger;
}
public void info(String message) {
logger.info(message);
}
public void error(String message) {
logger.error(message);
}
}
public class MyClassToLog {
private final Logger log;
public MyClassToLog(Logger log) {
this.log = log;
}
}
void Main() {
org.slf4j.Logger logger = LoggerFactory.getLogger(MyClassToLog.class);
LogAdapter logAdapter = new LogAdapter(logger);
MyClassToLog myClassToLog = new MyClassToLog(logAdapter);
}
Ein Adapter kann auch verwendet werden, um die Log-Implementierung zu wrappen und so die Log-Implementierung zu entkoppeln. Damit ist die Klasse nicht mehr von einer spezifischen Log-Implementierung abhängig (hier das Logger-Interface), sondern nur noch von einer generischen Log-Implementierung, die im Projekt definiert ist.
Spezieller Code für Tests
Bei diesem Code handelt es sich nicht um speziellen Code für Tests, sondern um eine allgemeine Verbesserung der Codequalität. Die Klasse wir dadurch vom konkreten Logger entkoppelt und kann leichter getestet werden.
J45 Verwenden von Schnittstellen
Verwende Schnittstellen (Interfaces) anstelle von konkreten Klassen, um die Flexibilität und Testbarkeit des Codes zu erhöhen.
J45 Problem
Wenn Klassen von konkreten Klassen abhängig sind, sind sie an die konkrete Implementierung gebunden und können nicht einfach durch eine andere Implementierung ersetzt werden.
public class MyClass {
private final MySpecialService myService = new MySpecialService();
}
J45 Lösung
Klassen sollten so wenig wie möglich von konkreten Implementierungen abhängig sein, um die Flexibilität und Testbarkeit des Codes zu erhöhen. Dafür sollten Schnittstellen (Interfaces) verwendet werden, um die Abhängigkeit von konkreten Implementierungen zu reduzieren.
public interface MyService {
void doSomething();
}
public class MySpecialService implements MyService {
public void doSomething() {
// ...
}
}
public class MySpecialService2 implements MyService {
public void doSomething() {
// ...
}
}
public class MyClass {
private final MyService myService;
public MyClass(MyService myService) {
this.myService = myService;
}
}
Der Klasse MyClass
ist es nun egal, welche konkrete Implementierung von MyService
verwendet wird.
Aufwand
Das Schreiben von Schnittstellen erhöht nur scheinbar den Aufwand, denn durch die dadurch entstene Flexibilität und Testbarkeit wird der (viel größere) Aufwand für Tests und spätere Erweiterungen reduziert. Zusätzlich ermöglicht das Nachdenken über die richtige Schnittstelle eine bessere Strukturierung des Codes und eine bessere Trennung von Verantwortlichkeiten.
Es führ nichts an Schnittstellen vorbei.
J45 Siehe auch
J46 Verwenden von record
s statt Klassen
Verwende für Datenobjekte, die sonst aus einer Klasse mit gettern und settern bestehen, record
s, um komplexe Datentypen konsistent zu halten und einfach zu erstellen.
J46 Problem
Klassen enthalten oftmals eine Menge Daten, die sie manipulieren müssen. Diese Daten werden aber auch oft als Get- und Set-Methoden angeboten, um die Daten zu manipulieren. Dies wiederspricht jedoch dem Prinzip der Tell, Don't Ask, da die Klasse die Daten manipulieren sollte und nicht der Aufrufer.
Zusätzlich gibt es eine Menge von Klassen, die nur Daten speichern und keine Logik enthalten.
Wenn die Daten von einem externen Objekt manipuliert werden, ist es nie sichergestellt, dass die Daten noch korrekt und konsistent sind. Ungültige Werte führen dann zu unerwarteten Ergebnissen und Fehlern, die schwer zu finden und noch schwerer zu refaktorisieren sind.
public class ManyAttributes {
private String attribute1;
private int attribute2;
private boolean attribute3;
private double attribute4;
public String getAttribute1() {
return attribute1;
}
public void setAttribute1(String attribute1) {
this.attribute1 = attribute1;
}
// etc.
}
J46 Lösung
Klassen sollten Daten nur über ihre Methoden manipulieren und nicht über Get- und Set-Methoden. Wenn Klassen jedoch komplexe Datenstrukturen als Eingabe benötigen oder als Ausgabe produzieren, sollten record
s verwendet werden.
Records sind unveränderliche Datenstrukturen, die nur über ihren Konstruktor initialisiert werden können. Konstruktoren von Records können Prüfungen auf die Eingabedaten durchführen und sicherstellen, dass die Daten konsistent sind, bevor der Record erstellt wird.
public record ManyAttributes(String attribute1, int attribute2, boolean attribute3, double attribute4) {
public ManyAttributes {
if (attribute1 == null || attribute1.isEmpty()) {
throw new IllegalArgumentException("attribute1 must not be null or empty");
}
if (attribute2 < 0) {
throw new IllegalArgumentException("attribute2 must be greater than 0");
}
if (attribute4 < 0.0) {
throw new IllegalArgumentException("attribute4 must be greater than 0.0");
}
}
}
public class MyClass {
public void doSomething(ManyAttributes manyAttributes) {
// ...
}
}
Kohäsion
Records sollten für Daten eingesetzt, die zusammengehören und eine hohe Kohäsion haben, weil sie sich beispielweise auf ein gemeinsames Konzept beziehen und sich gemeinsam verändern.
J46 Vorteile
- Records sind unveränderlich und können nur über ihren Konstruktor initialisiert werden.
- Records können Prüfungen auf ihre Eingabedaten durchführen und sicherstellen, dass die Daten konsistent sind.
- Records sind einfach zu erstellen und zu verwenden und erhöhen die Lesbarkeit und Wartbarkeit des Codes.
- Der Zugriff auf Records in Multi-Thread-Umgebungen ist sicher, da Records unveränderlich sind.
J46 Nachteile
- Records können nicht verändert werden, was in einigen Fällen dazu führt, dass eine Kopie des Records erstellt werden muss, um die Daten zu verändern.
- Records können nicht von anderen Klassen erben.
- Nachträgliche Änderungen an der Struktur sind aufwändig (nicht jedoch das Hinzufüren von neuen Feldern).
J47 Nebeneffekte vermeiden
Methoden und Funktionen sollten keine Nebeneffekte haben, die nicht offensichtlich sind. Dies verbessert die Vorhersagbarkeit, Wartbarkeit und Testbarkeit des Codes.
J47 Problem
Nebeneffekte sind unerwünschte Veränderungen des Zustands eines Systems, die durch eine Funktion oder Methode verursacht werden. Sie können schwer zu erkennen und zu debuggen sein, da sie nicht offensichtlich sind und an einer anderen Stelle im Code auftreten können.
Einige Beispiele für unerwünschte Nebeneffekte:
J47 1. Veränderung eines Objekts innerhalb einer Methode
Im folgenden Beispiel wird die Methode addValue
definiert, die einen Wert zu einem Objekt hinzufügt. Diese Methode verändert jedoch direkt den übergebenen Parameter und hat damit einen Nebeneffekt:
class Data {
int value;
}
public class SideEffectExample {
static void addValue(Data obj) {
obj.value = 42; // Nebeneffekt: Das Original-Objekt wird verändert
}
}
J47 2. Veränderung des internen Zustands einer Klasse
Die folgende Klasse enthält eine Methode getValue
, die einen Wert berechnen soll. Allerdings verändert sie dabei eine interne Variable (this.result
), was zu unerwarteten Seiteneffekten führen kann:
class Calculator {
private int result = 0;
public int getValue(int value) {
result += value; // Nebeneffekt: Interner Zustand wird verändert
return result;
}
}
J47 3. Veränderung einer globalen Variable
Das folgende Beispiel zeigt eine Methode, die eine globale Variable verändert:
class GlobalState {
static int value = 0;
}
public class SideEffectExample {
static void increment() {
GlobalState.value++; // Nebeneffekt: Änderung einer globalen Variable
}
}
J47 Lösung
Um Nebeneffekte zu vermeiden, sollten Methoden so gestaltet werden, dass sie keine externen Zustände verändern. Stattdessen sollte mit Kopien gearbeitet oder Werte als Rückgabe geliefert werden.
J47 1. Arbeiten mit unveränderlichen Objekten
Statt das Originalobjekt zu modifizieren, sollte eine neue Instanz mit den geänderten Werten zurückgegeben werden:
class Data {
private final int value;
public Data(int value) {
this.value = value;
}
public Data addValue() {
return new Data(42); // Keine Änderung am Original-Objekt
}
}
J47 2. Vermeidung von Zustandsänderungen in Klassen
Methoden sollten keine internen Variablen verändern, sondern den neuen Wert zurückgeben:
class Calculator {
private final int result;
public Calculator(int result) {
this.result = result;
}
public int calculate(int value) {
return result + value; // Kein Nebeneffekt
}
}
J47 3. Funktionale Programmierung statt Mutation
Statt eine globale Variable zu ändern, sollte eine Methode den neuen Wert berechnen und zurückgeben:
public class PureFunctionExample {
static int increment(int value) {
return value + 1;
}
}
J47 4. Dependency Injection statt globaler Variablen
Globale Variablen sollten vermieden werden. Stattdessen kann Dependency Injection genutzt werden, um Abhängigkeiten explizit zu übergeben:
class Dependency {
private int value;
public void increment() {
value++;
}
}
class GlobalObject {
private final Dependency dependency;
public GlobalObject(Dependency dependency) {
this.dependency = dependency;
}
public void foo() {
dependency.increment();
}
}
public class Main {
public static void main(String[] args) {
Dependency dependency = new Dependency();
GlobalObject obj = new GlobalObject(dependency);
obj.foo();
}
}
J47 Fazit
- Vermeide direkte Modifikationen von Objekten oder globalen Variablen.
- Verwende unveränderliche (immutable) Datenstrukturen.
- Bevorzuge reine Methoden, die keine Nebeneffekte haben.
- Nutze Dependency Injection, um Abhängigkeiten explizit zu machen.