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ć.
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()
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 } }
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 } }
Tworzenie Optional
Sposoby tworzenia Optional
// 1. Optional.of() - value NEVER null OptionalnotNull = 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
Optionaloptional = 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
Optionaloptional = 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")); }
Functional operations na Optional
map() – transformacja wartości
// map() transformuje value jeśli present Optionalname = 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
Optionalnumber = 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 Optional | NIE używaj Optional |
---|---|
Return values z metod | Fields w klasach |
Method parameters (occasionally) | Collections (użyj empty collection) |
Chain operations | Primary keys/IDs |
Null-safe computations | Performance-critical code |
Common anti-patterns
// ❌ DON'T: Optional as field public class User { private Optionalname; // 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
ListuserIds = 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();
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.
orElse() gdy default value jest simple (constant, literal). orElseGet() gdy default value requires computation – lambda jest executed tylko when Optional is empty.
Optional nie implement Serializable intentionally. Nie używaj Optional jako fields w Serializable classes. Use nullable fields i wrap w Optional w getter methods.
JPA 2.1+ supports Optional w query methods: findById(Long id) zwraca 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:
- UserService: Zmień findById() żeby zwracał Optional
- Null chains: Replace nested null checks z Optional chains
- Validation: Create validateUser() method using Optional.filter()
- Stream integration: Process list of user IDs do existing users
- 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?