Optional w Javie – koniec z NullPointerException

TL;DR: Optional w Java 8+ to wrapper dla values które mogą być null. Zamiast sprawdzać if (value != null), używasz Optional.of(value).ifPresent() lub .orElse(). To functional approach do handling null values który redukuje NullPointerException i czyni kod bardziej readable.

Dlaczego Optional rozwiązuje billion-dollar mistake?

Tony Hoare, twórca null reference, nazwał to „billion-dollar mistake” przez koszty bugów z NullPointerException. Optional to jak airbag w samochodzie – nie zapobiega wypadkom, ale znacznie redukuje szkody. Zamiast defensive null checks wszędzie, masz explicit contract że value może nie istnieć.

Optional został wprowadzony w Java 8 (2014) inspirowany Scala Option i Haskell Maybe. To part of functional programming features jak lambda expressions i Stream API.

Co się nauczysz:

  • Czym jest Optional i dlaczego zastępuje null checks
  • Tworzenie Optional: of(), ofNullable(), empty()
  • Sprawdzanie wartości: isPresent(), ifPresent()
  • Pobieranie wartości: get(), orElse(), orElseGet()
  • Functional operations: map(), filter(), flatMap()
Wymagania wstępne: Java 8+, podstawy lambda expressions będą pomocne, znajomość podstawowych patterns jak null checking.

Problem z null w Javie

Tradycyjny null checking – defensive programming

// Typowy kod z null checks - verbose i error-prone
public class UserService {
    
    public String getUserEmail(Long userId) {
        User user = userRepository.findById(userId);
        if (user != null) {
            Profile profile = user.getProfile();
            if (profile != null) {
                Contact contact = profile.getContact();
                if (contact != null) {
                    String email = contact.getEmail();
                    if (email != null && !email.isEmpty()) {
                        return email.toLowerCase();
                    }
                }
            }
        }
        return "no-email@example.com";  // Default value
    }
    
    // Alternative - może throw NPE!
    public String getUserEmailUnsafe(Long userId) {
        User user = userRepository.findById(userId);
        return user.getProfile().getContact().getEmail().toLowerCase();  // 💥 NPE waiting to happen
    }
}
Pułapka: Nested null checks tworzą pyramid of doom. Łatwo zapomnieć o null check na którymś poziomie, prowadząc do NullPointerException w production.

Ten sam kod z Optional

public class UserService {
    
    public String getUserEmail(Long userId) {
        return userRepository.findById(userId)           // Optional
                .map(User::getProfile)                   // Optional
                .map(Profile::getContact)                // Optional
                .map(Contact::getEmail)                  // Optional
                .filter(email -> !email.isEmpty())      // Optional
                .map(String::toLowerCase)                // Optional
                .orElse("no-email@example.com");        // String
    }
}
Pro tip: Optional chain operations są null-safe. Jeśli którykolwiek step zwraca empty Optional, reszta chain jest skipped i dostaniesz empty Optional.

Tworzenie Optional

Sposoby tworzenia Optional

// 1. Optional.of() - value NEVER null
Optional notNull = Optional.of("Hello");
// Optional willThrow = Optional.of(null);  // 💥 NullPointerException

// 2. Optional.ofNullable() - value może być null
String name = getName();  // może zwrócić null
Optional maybeName = Optional.ofNullable(name);

// 3. Optional.empty() - explicitly empty
Optional empty = Optional.empty();

// 4. Factory methods w business logic
public Optional findUserByEmail(String email) {
    User user = database.findByEmail(email);
    return Optional.ofNullable(user);  // Safe wrapper
}

// 5. Conditional Optional creation
public Optional getValidatedEmail(String input) {
    if (input != null && input.contains("@")) {
        return Optional.of(input);
    }
    return Optional.empty();
}

Sprawdzanie i pobieranie wartości

Checking presence

Optional optional = Optional.ofNullable(getString());

// Check if value is present
if (optional.isPresent()) {
    String value = optional.get();  // Safe tylko after isPresent()
    System.out.println("Value: " + value);
}

// Better: use ifPresent() with lambda
optional.ifPresent(value -> {
    System.out.println("Value: " + value);
    logValue(value);
});

// Java 9+: ifPresentOrElse()
// optional.ifPresentOrElse(
//     value -> System.out.println("Found: " + value),
//     () -> System.out.println("Not found")
// );

Getting values with defaults

Optional optional = getOptionalString();

// 1. orElse() - always evaluates default
String result1 = optional.orElse("Default Value");
String result2 = optional.orElse(computeDefault());  // computeDefault() always called!

// 2. orElseGet() - lazy evaluation  
String result3 = optional.orElseGet(() -> computeDefault());  // Called only if empty

// 3. orElseThrow() - custom exception
String result4 = optional.orElseThrow(() -> new IllegalStateException("Value required"));

// 4. get() - dangerous! Use only when you're 100% sure
// String result5 = optional.get();  // 💥 NoSuchElementException if empty

// Real-world examples
public String getUserDisplayName(Long userId) {
    return userService.findById(userId)
            .map(User::getDisplayName)
            .orElse("Anonymous User");
}

public User getCurrentUser() {
    return sessionService.getCurrentUserId()
            .flatMap(userService::findById)
            .orElseThrow(() -> new SecurityException("No authenticated user"));
}
Uwaga: Nigdy nie używaj optional.get() bez wcześniejszego isPresent() check. To defeats the purpose of Optional i może throw NoSuchElementException.

Functional operations na Optional

map() – transformacja wartości

// map() transformuje value jeśli present
Optional name = Optional.of("john doe");

Optional upperName = name.map(String::toUpperCase);  // Optional["JOHN DOE"]
Optional nameLength = name.map(String::length);     // Optional[8]

// Chain multiple transformations
Optional formatted = name
        .map(String::trim)
        .map(String::toUpperCase)
        .map(s -> s.replace(" ", "_"));  // Optional["JOHN_DOE"]

// Real-world example: processing user input
public Optional processEmail(String input) {
    return Optional.ofNullable(input)
            .map(String::trim)
            .map(String::toLowerCase)
            .filter(email -> email.contains("@"))
            .filter(email -> email.length() > 5);
}

filter() – conditional filtering

Optional number = Optional.of(42);

// Filter based on condition
Optional evenNumber = number.filter(n -> n % 2 == 0);  // Optional[42]
Optional oddNumber = number.filter(n -> n % 2 == 1);   // Optional.empty

// Complex filtering
public Optional getActiveAdminUser(Long userId) {
    return userService.findById(userId)
            .filter(User::isActive)
            .filter(user -> user.hasRole("ADMIN"))
            .filter(user -> user.getLastLoginDate().isAfter(LocalDate.now().minusDays(30)));
}

// Validation with Optional
public Optional validatePassword(String password) {
    return Optional.ofNullable(password)
            .filter(p -> p.length() >= 8)
            .filter(p -> p.matches(".*[A-Z].*"))      // Contains uppercase
            .filter(p -> p.matches(".*[0-9].*"));     // Contains digit
}

flatMap() – handling nested Optionals

// Problem: map() może create Optional>
public Optional getUserCity(Long userId) {
    Optional user = userService.findById(userId);
    
    // ❌ Wrong: creates Optional>
    // Optional> nested = user.map(u -> u.getAddress());
    
    // ✅ Correct: flatMap flattens nested Optional
    return user.flatMap(User::getAddress)       // Optional
.map(Address::getCity); // Optional } // Complex example: safe navigation public Optional getUserCompanyName(Long userId) { return userService.findById(userId) // Optional .flatMap(User::getProfile) // Optional .flatMap(Profile::getEmployment) // Optional .flatMap(Employment::getCompany) // Optional .map(Company::getName); // Optional } // Method signatures for the example above: // class User { // public Optional getProfile() { ... } // } // class Profile { // public Optional getEmployment() { ... } // } // class Employment { // public Optional getCompany() { ... } // }

Optional best practices

Kiedy używać Optional

Używaj OptionalNIE używaj Optional
Return values z metodFields w klasach
Method parameters (occasionally)Collections (użyj empty collection)
Chain operationsPrimary keys/IDs
Null-safe computationsPerformance-critical code

Common anti-patterns

// ❌ DON'T: Optional as field
public class User {
    private Optional name;  // BAD!
    // Use nullable field instead
}

// ❌ DON'T: get() without isPresent()
Optional optional = getOptional();
String value = optional.get();  // Może throw exception!

// ❌ DON'T: isPresent() + get() combo
if (optional.isPresent()) {
    String value = optional.get();
    doSomething(value);
}
// Use ifPresent() instead

// ❌ DON'T: Optional for collections
public Optional> getUsers() {  // BAD!
    // Return empty list instead
}

// ✅ DO: Proper Optional usage
public Optional findUserByEmail(String email) {
    return Optional.ofNullable(userRepository.findByEmail(email));
}

public String getDisplayName(Optional user) {
    return user.map(User::getName)
               .orElse("Guest");
}

Integration z Stream API

Optional w streams

List userIds = Arrays.asList(1L, 2L, 3L, 999L);

// Filter out empty Optionals i get values
List existingUsers = userIds.stream()
        .map(userService::findById)          // Stream>
        .filter(Optional::isPresent)         // Keep only present
        .map(Optional::get)                  // Extract values
        .collect(Collectors.toList());

// Java 9+: flatMap z Optional.stream()
// List users = userIds.stream()
//         .map(userService::findById)      // Stream>
//         .flatMap(Optional::stream)       // Stream
//         .collect(Collectors.toList());

// Collect first present Optional
Optional firstActiveUser = userIds.stream()
        .map(userService::findById)
        .filter(Optional::isPresent)
        .map(Optional::get)
        .filter(User::isActive)
        .findFirst();
Czy Optional ma performance overhead?

Tak, minimal overhead przez object creation i method calls. W performance-critical code rozważ traditional null checks. Ale w większości cases readability i safety benefits outweigh minimal performance cost.

Kiedy używać orElse() vs orElseGet()?

orElse() gdy default value jest simple (constant, literal). orElseGet() gdy default value requires computation – lambda jest executed tylko when Optional is empty.

Czy mogę serialize Optional?

Optional nie implement Serializable intentionally. Nie używaj Optional jako fields w Serializable classes. Use nullable fields i wrap w Optional w getter methods.

Co z Optional w JPA/Hibernate?

JPA 2.1+ supports Optional w query methods: findById(Long id) zwraca Optional. Hibernate również supports Optional return types w custom repository methods.

Jak testować kod z Optional?

Test both present i empty cases. Use assertThat(optional).isPresent(), assertThat(optional).isEmpty(), assertThat(optional).contains(expectedValue). Mockito supports Optional return values.

🚀 Zadanie dla Ciebie

Refactor legacy code do użycia Optional:

  1. UserService: Zmień findById() żeby zwracał Optional
  2. Null chains: Replace nested null checks z Optional chains
  3. Validation: Create validateUser() method using Optional.filter()
  4. Stream integration: Process list of user IDs do existing users
  5. Error handling: Use orElseThrow() z custom exceptions

Porównaj before/after code readability i count eliminated null checks.

Przydatne zasoby:

Używasz już Optional w swoim kodzie? Jakie największe benefits widzisz w porównaniu do traditional null checking?

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Przewijanie do góry