Optional in Java: Richtige Verwendung und Anti-Patterns
1. Übersicht
Optional wurde mit Java 8 eingeführt, um Werte, die „möglicherweise nicht vorhanden” sind, deutlicher darzustellen als null. Aber viele Entwickler verwenden es falsch: Sie missbrauchen Optional als Feld, Parameter oder rufen .get() auf, als würde es nie leer sein. Dieser Artikel hilft dir zu verstehen, wann du Optional verwenden solltest, welche Pattern richtig sind, und zeigt häufige Anti-Patterns, die du vermeiden solltest.
Nach dem Lesen wirst du wissen, wie du APIs designst, die Optional zurückgeben, wie du fehlende Werte elegant mit map/filter/orElseGet verarbeitest, und wie du sofort Code erkennst, der „falsch Optional” verwendet und refaktoriert werden muss.
2. Was ist Optional und wann sollte man es verwenden
Optional<T> ist ein Container, der einen Wert vom Typ T enthalten kann oder nicht. Hauptzweck: in der Methoden-Signatur deutlich zu machen, dass der Rückgabewert möglicherweise nicht vorhanden ist, und den Aufrufer zu zwingen, diesen Fall zu behandeln, anstatt einen Null-Check zu vergessen.
public class CustomerRepository {
private final Map<Long, Customer> store = new HashMap<>();
public Optional<Customer> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
}
findById gibt Optional<Customer> statt Customer (das null sein kann) zurück. Der Aufrufer sieht sofort in der Signatur, dass er den Fall „nicht gefunden” behandeln muss, ohne die Javadoc lesen zu müssen.
Hinweis: Optional wurde für Rückgabewerte entworfen, nicht für alle Stellen im System, die null sein können.
3. So erstellt man Optional-Instanzen
Optional<String> present = Optional.of("Hello"); // Wert ist garantiert nicht null
Optional<String> empty = Optional.empty(); // leer
Optional<String> nullable = Optional.ofNullable(null); // kann null sein, wird automatisch zu empty
Optional.of(value)wirft sofortNullPointerException, wennvaluenull ist — verwende es, wenn du sicher bist, dass der Wert nicht null ist.Optional.ofNullable(value)ist sicherer und passt, wenn der Wert aus einer unkontrollierten Quelle kommt (Query-Ergebnis, map.get, …).Optional.empty()erstellt eine leere Instanz, verwende es, wenn du im Voraus weißt, dass kein Wert vorhanden ist.
4. Richtige Behandlung von Werten im Inneren
4.1. map, filter, flatMap
public record Order(Long id, BigDecimal amount, String couponCode) {}
public class DiscountService {
public Optional<BigDecimal> calculateDiscount(Order order) {
return Optional.ofNullable(order.couponCode())
.filter(code -> code.startsWith("SALE"))
.map(code -> order.amount().multiply(BigDecimal.valueOf(0.1)));
}
}
filter behält den Wert nur, wenn die Bedingung wahr ist, sonst gibt es Optional.empty() zurück. map transformiert den Wert, wenn er vorhanden ist, macht nichts, wenn es leer ist. Diese Kette vermeidet komplett verschachtelte if-else-Blöcke für Null-Checks.
4.2. orElse, orElseGet, orElseThrow
public class PaymentService {
private final CustomerRepository repository;
public PaymentService(CustomerRepository repository) {
this.repository = repository;
}
public Customer resolveCustomer(Long id) {
return repository.findById(id)
.orElseThrow(() -> new CustomerNotFoundException(id));
}
public String resolveCustomerName(Long id) {
return repository.findById(id)
.map(Customer::name)
.orElse("Guest");
}
}
orElseThrow(supplier)wirft eine aussagekräftige Exception, wenn nicht gefunden — verwende es dort, wo der Wert erforderlich ist.orElse("Guest")verwende, wenn der Standardwert eine einfache Konstante ist, die immer berechnet wird, unabhängig davon, ob Optional einen Wert hat.orElseGet(supplier)verwende, wenn der Standardwert rechenintensiv ist — rufe den Supplier nur auf, wenn Optional leer ist.
// Falsch: ruft immer buildDefaultCustomer() auf, ob Optional einen Wert hat oder nicht
repository.findById(id).orElse(buildDefaultCustomer());
// Richtig: ruft es nur auf, wenn wirklich nötig
repository.findById(id).orElseGet(this::buildDefaultCustomer);
4.3. ifPresent und ifPresentOrElse
public class NotificationService {
public void notifyCustomer(Optional<Customer> customer) {
customer.ifPresentOrElse(
c -> System.out.println("E-Mail senden an " + c.email()),
() -> System.out.println("Kein Kunde zum Versenden vorhanden")
);
}
}
ifPresentOrElse (seit Java 9) kombiniert beide Branches mit/ohne Wert in einem Aufruf, statt das längliche if (optional.isPresent()) {...} else {...}.
5. Verhalten mit JUnit 5 + AssertJ testen
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.26.3</version>
<scope>test</scope>
</dependency>
class DiscountServiceTest {
private final DiscountService service = new DiscountService();
@Test
void shouldReturnDiscountWhenCouponValid() {
Order order = new Order(1L, BigDecimal.valueOf(100), "SALE10");
Optional<BigDecimal> discount = service.calculateDiscount(order);
assertThat(discount)
.isPresent()
.get()
.isEqualTo(BigDecimal.valueOf(10.0));
}
@Test
void shouldReturnEmptyWhenNoCoupon() {
Order order = new Order(2L, BigDecimal.valueOf(100), null);
Optional<BigDecimal> discount = service.calculateDiscount(order);
assertThat(discount).isEmpty();
}
}
AssertJ hat spezielle Assertions für Optional: isPresent(), isEmpty(), hasValue(...). Verwende sie statt .get() aufzurufen und dann zu vergleichen — der Test-Code wird natürlicher zu lesen.
6. Häufige Anti-Patterns
6.1. .get() aufrufen ohne Überprüfung
// Falsch: Wenn Optional leer ist, wird NoSuchElementException geworfen,
// dadurch wird der Grund für die Verwendung von Optional komplett negiert
Customer customer = repository.findById(id).get();
Das direkte Aufrufen von .get() macht Optional zu nichts anderem als null in einer zusätzlichen Schicht — du kannst immer noch zur Laufzeit abstürzen und hast dabei noch einen Zettel Code verschwendet. Verwende stattdessen orElseThrow() (mit aussagekräftiger Exception) oder verarbeite es über map/ifPresent.
6.2. Optional als Feld in Entity/DTO verwenden
// Falsch: Optional ist nicht Serializable, sollte nicht in JPA-Entitys erscheinen
@Entity
public class Customer {
private Optional<String> middleName; // vermeiden
}
Optional implementiert nicht Serializable, wurde nicht dafür entworfen, als Feld oder Konstruktor-Parameter zu dienen. Wenn middleName null sein kann, lass es ein String sein und null — umhülle Optional nur beim Zurückgeben aus dem Getter, wenn nötig:
// Richtig: Feld bleibt String, umhülle Optional nur im Getter, wenn gewünscht
public Optional<String> getMiddleName() {
return Optional.ofNullable(middleName);
}
6.3. Optional als Methoden-Parameter verwenden
// Falsch: zwingt den Aufrufer, Optional.of/ofNullable vor dem Aufruf selbst zu umhüllen
public void updateAddress(Optional<String> newAddress) { ... }
Dies schiebt die Komplexität zum Aufrufer und bringt keinen Vorteil gegenüber Overloading oder @Nullable. Verwende stattdessen Overloading oder übergebe String mit normalem Null-Check:
// Richtig
public void updateAddress(String newAddress) {
this.address = newAddress; // der Aufrufer entscheidet selbst, ob null oder nicht übergeben wird
}
6.4. isPresent() überprüfen und dann get() aufrufen wie beim alten if-null
// Falsch: schreibt alten Null-Check-Stil neu, nutzt keine Vorteile von Optional
if (repository.findById(id).isPresent()) {
Customer c = repository.findById(id).get(); // ruft die Query 2x auf!
System.out.println(c.name());
}
Dieser Code ist nicht nur langweilig, sondern ruft findById zwei Mal auf, wenn es wirklich eine Query ist. Verwende stattdessen:
// Richtig
repository.findById(id).ifPresent(c -> System.out.println(c.name()));
6.5. Optional für Collections verwenden
// Falsch: Collection haben bereits das Konzept von „leer" (leere Liste), brauchen keine Optional
public Optional<List<Order>> findOrdersByCustomer(Long customerId) { ... }
Die Rückgabe von Optional<List<Order>> zwingt den Aufrufer, zwei Ebenen von Leerheit zu handhaben: leeres Optional und leere Liste — redundant. Gib immer eine leere Liste zurück, wenn es keine Daten gibt:
// Richtig
public List<Order> findOrdersByCustomer(Long customerId) {
return orders.getOrDefault(customerId, List.of());
}
6.6. Optional in kritischen Pfaden mit hohem Durchsatz verwenden
Optional erzeugt einen zusätzlichen Object-Wrapper, das ist in den meisten Anwendungen kein großer Aufwand, aber in Code, der Millionen Mal pro Sekunde läuft (z. B. Schleifen zur Verarbeitung großer Datenströme), kann das ständige Boxing den GC beeinflussen. Überprüfe in diesen Fällen direkt auf Null oder verwende einen Sentinel-Wert.
Hinweis: Keine vorschnelle Optimierung — überdenke dies nur, wenn du profiled hast und siehst, dass Optional wirklich ein Engpass ist.
7. Übersichtstabelle: Wann ja, wann nein
| Situation | Optional verwenden? |
|---|---|
| Rückgabewert einer Methode, kann möglicherweise nicht vorhanden sein | ✅ Ja |
| Feld in Entity/DTO | ❌ Nein |
| Parameter eines Konstruktors/einer Methode | ❌ Nein |
| Rückgabe einer Collection (List, Set, Map) | ❌ Nein — leere Collection zurückgeben |
| Lokale Variable in einer Methode, um wiederholte Null-Checks zu vermeiden | ⚠️ Je nachdem — überdenken, manchmal ist if/else klarer |
8. Fazit
Optional ist ein gutes Werkzeug, um den API-Kontrakt deutlich zu machen und NullPointerException auf der Aufrufer-Ebene zu vermeiden, aber es entfaltet seinen Wert nur, wenn es richtig eingesetzt wird: als Rückgabewert einer Methode. Verwende es nicht als Feld, Parameter oder Wrapper für Collections — an diesen Stellen macht Optional Code nur verworrener, ohne etwas zu lösen. Frage dich beim Schreiben von neuem Code: „Hilft Optional dem Aufrufer hier, klarer zu handeln, oder ist es nur null in einer neuen Verpackung?” — die Antwort sagt dir, ob du es behalten oder fallen lassen sollst.