Overview
The Optional class is a container object which may or may not contain a non-null value. It helps avoid NullPointerException and makes the code more readable by explicitly indicating the possibility of absent values.
1. Creating Optional Objects
Basic Creation Methods
import java.util.*;
import java.util.Optional;
public class OptionalCreation {
public static void main(String[] args) {
// 1. Empty Optional
Optional<String> emptyOptional = Optional.empty();
System.out.println("Empty Optional: " + emptyOptional);
// 2. Optional with non-null value
Optional<String> nonEmptyOptional = Optional.of("Hello World");
System.out.println("Non-empty Optional: " + nonEmptyOptional);
// 3. Optional that may contain null
String nullableString = null;
Optional<String> nullableOptional = Optional.ofNullable(nullableString);
System.out.println("Nullable Optional: " + nullableOptional);
// This will throw NullPointerException
try {
Optional<String> nullOptional = Optional.of(null);
} catch (NullPointerException e) {
System.out.println("Cannot use Optional.of() with null: " + e.getMessage());
}
// Practical examples
String name = "Alice";
Optional<String> optName = Optional.ofNullable(name);
System.out.println("Name Optional: " + optName);
List<String> emptyList = null;
Optional<List<String>> optList = Optional.ofNullable(emptyList);
System.out.println("List Optional: " + optList);
}
}
Factory Methods in Practice
import java.util.*;
public class OptionalFactoryMethods {
public static Optional<String> findUserEmail(Long userId) {
// Simulate database lookup
if (userId == 1L) {
return Optional.of("[email protected]");
} else if (userId == 2L) {
return Optional.of("[email protected]");
} else {
return Optional.empty();
}
}
public static Optional<Integer> parseInteger(String input) {
try {
return Optional.of(Integer.parseInt(input));
} catch (NumberFormatException e) {
return Optional.empty();
}
}
public static void main(String[] args) {
// Using factory methods
System.out.println("User 1 email: " + findUserEmail(1L));
System.out.println("User 999 email: " + findUserEmail(999L));
System.out.println("Parse '123': " + parseInteger("123"));
System.out.println("Parse 'abc': " + parseInteger("abc"));
// Chaining creation with processing
String[] inputs = {"10", "20", "thirty", "40"};
Arrays.stream(inputs)
.map(OptionalFactoryMethods::parseInteger)
.forEach(opt ->
System.out.println("Parsed: " + opt.orElse(-1)));
}
}
2. Checking Presence and Accessing Values
Basic Checking Methods
import java.util.*;
public class OptionalChecking {
public static void main(String[] args) {
Optional<String> present = Optional.of("Hello");
Optional<String> absent = Optional.empty();
// 1. isPresent() - check if value exists
System.out.println("Present is present: " + present.isPresent());
System.out.println("Absent is present: " + absent.isPresent());
// 2. isEmpty() - Java 11+ (opposite of isPresent)
System.out.println("Present is empty: " + present.isEmpty());
System.out.println("Absent is empty: " + absent.isEmpty());
// 3. ifPresent() - execute action if value exists
present.ifPresent(value -> System.out.println("Value: " + value));
absent.ifPresent(value -> System.out.println("This won't print"));
// 4. ifPresentOrElse() - Java 9+
present.ifPresentOrElse(
value -> System.out.println("Value found: " + value),
() -> System.out.println("No value found")
);
absent.ifPresentOrElse(
value -> System.out.println("Value found: " + value),
() -> System.out.println("No value found")
);
// Practical example
Optional<String> username = getUserName(1L);
username.ifPresent(name ->
System.out.println("Welcome, " + name + "!"));
getUserName(999L).ifPresentOrElse(
name -> System.out.println("Welcome, " + name + "!"),
() -> System.out.println("User not found")
);
}
public static Optional<String> getUserName(Long userId) {
Map<Long, String> users = Map.of(
1L, "Alice",
2L, "Bob",
3L, "Charlie"
);
return Optional.ofNullable(users.get(userId));
}
}
Safe Value Access
import java.util.*;
public class SafeValueAccess {
public static void main(String[] args) {
Optional<String> present = Optional.of("Hello");
Optional<String> absent = Optional.empty();
// 1. get() - throws NoSuchElementException if empty
try {
String value = present.get();
System.out.println("Present value: " + value);
String absentValue = absent.get(); // Throws exception
} catch (NoSuchElementException e) {
System.out.println("Cannot get value from empty Optional");
}
// 2. orElse() - provide default value
String presentOrElse = present.orElse("Default");
String absentOrElse = absent.orElse("Default");
System.out.println("Present orElse: " + presentOrElse);
System.out.println("Absent orElse: " + absentOrElse);
// 3. orElseGet() - provide default via Supplier
String presentOrElseGet = present.orElseGet(() -> "Default from Supplier");
String absentOrElseGet = absent.orElseGet(() -> {
System.out.println("Computing default value...");
return "Default from Supplier";
});
System.out.println("Present orElseGet: " + presentOrElseGet);
System.out.println("Absent orElseGet: " + absentOrElseGet);
// 4. orElseThrow() - throw exception if empty
try {
String presentValue = present.orElseThrow();
System.out.println("Present value: " + presentValue);
String absentValue = absent.orElseThrow(); // Throws NoSuchElementException
} catch (NoSuchElementException e) {
System.out.println("orElseThrow on empty Optional");
}
// 5. orElseThrow with custom exception
try {
String value = absent.orElseThrow(() ->
new IllegalArgumentException("Value is required"));
} catch (IllegalArgumentException e) {
System.out.println("Custom exception: " + e.getMessage());
}
}
public static void demonstrateUsage() {
Optional<String> configValue = getConfig("timeout");
// Safe access patterns
int timeout = configValue
.map(Integer::parseInt)
.orElse(30); // Default timeout
String requiredValue = configValue
.orElseThrow(() -> new IllegalStateException("Configuration required"));
System.out.println("Timeout: " + timeout);
}
public static Optional<String> getConfig(String key) {
Map<String, String> config = Map.of("timeout", "60");
return Optional.ofNullable(config.get(key));
}
}
3. Transforming and Filtering Optionals
map() and flatMap()
import java.util.*;
public class OptionalTransformation {
static class User {
private String name;
private String email;
public User(String name, String email) {
this.name = name;
this.email = email;
}
public String getName() { return name; }
public Optional<String> getEmail() {
return Optional.ofNullable(email);
}
@Override
public String toString() {
return "User{name='" + name + "', email='" + email + "'}";
}
}
public static void main(String[] args) {
// 1. map() - transform value if present
Optional<String> name = Optional.of("alice");
Optional<String> upperName = name.map(String::toUpperCase);
System.out.println("Original: " + name);
System.out.println("Uppercase: " + upperName);
Optional<String> empty = Optional.empty();
Optional<String> emptyUpper = empty.map(String::toUpperCase);
System.out.println("Empty mapped: " + emptyUpper);
// 2. flatMap() - for nested Optionals
User userWithEmail = new User("Alice", "[email protected]");
User userWithoutEmail = new User("Bob", null);
Optional<String> email1 = Optional.of(userWithEmail)
.flatMap(User::getEmail);
Optional<String> email2 = Optional.of(userWithoutEmail)
.flatMap(User::getEmail);
System.out.println("User with email: " + email1);
System.out.println("User without email: " + email2);
// Difference between map and flatMap
Optional<Optional<String>> badEmail = Optional.of(userWithEmail)
.map(User::getEmail);
Optional<String> goodEmail = Optional.of(userWithEmail)
.flatMap(User::getEmail);
System.out.println("With map: " + badEmail);
System.out.println("With flatMap: " + goodEmail);
// Practical transformation chain
Optional<String> input = Optional.of(" hello world ");
String result = input.map(String::trim)
.map(String::toUpperCase)
.map(s -> s.replace(" ", "_"))
.orElse("DEFAULT");
System.out.println("Transformed: " + result);
}
}
filter()
import java.util.*;
public class OptionalFiltering {
static class Product {
private String name;
private double price;
private boolean inStock;
public Product(String name, double price, boolean inStock) {
this.name = name;
this.price = price;
this.inStock = inStock;
}
public String getName() { return name; }
public double getPrice() { return price; }
public boolean isInStock() { return inStock; }
}
public static void main(String[] args) {
// 1. Basic filtering
Optional<String> name = Optional.of("Alice");
Optional<String> longName = name.filter(n -> n.length() > 3);
Optional<String> shortName = name.filter(n -> n.length() > 10);
System.out.println("Original: " + name);
System.out.println("Long name: " + longName);
System.out.println("Short name: " + shortName);
// 2. Filtering objects
Product expensiveProduct = new Product("Laptop", 999.99, true);
Product cheapProduct = new Product("Pen", 1.99, false);
Optional<Product> availableExpensive = Optional.of(expensiveProduct)
.filter(Product::isInStock)
.filter(p -> p.getPrice() > 100);
Optional<Product> availableCheap = Optional.of(cheapProduct)
.filter(Product::isInStock)
.filter(p -> p.getPrice() > 100);
System.out.println("Available expensive: " +
availableExpensive.map(Product::getName).orElse("None"));
System.out.println("Available cheap: " +
availableCheap.map(Product::getName).orElse("None"));
// 3. Complex filtering with transformation
Optional<String> email = Optional.of(" [email protected] ");
Optional<String> validEmail = email.map(String::trim)
.map(String::toLowerCase)
.filter(e -> e.contains("@"))
.filter(e -> e.endsWith(".com"));
System.out.println("Original email: " + email);
System.out.println("Valid email: " + validEmail);
// 4. Practical validation example
Optional<Integer> age = Optional.of(25);
Optional<Integer> validAge = age.filter(a -> a >= 18)
.filter(a -> a <= 120);
Optional<Integer> invalidAge = Optional.of(150)
.filter(a -> a >= 18)
.filter(a -> a <= 120);
System.out.println("Valid age: " + validAge);
System.out.println("Invalid age: " + invalidAge);
}
}
4. Chaining Optional Operations
Complex Transformation Chains
import java.util.*;
public class OptionalChaining {
static class Address {
private String street;
private String city;
public Address(String street, String city) {
this.street = street;
this.city = city;
}
public Optional<String> getStreet() {
return Optional.ofNullable(street);
}
public Optional<String> getCity() {
return Optional.ofNullable(city);
}
}
static class User {
private String name;
private Address address;
public User(String name, Address address) {
this.name = name;
this.address = address;
}
public Optional<String> getName() {
return Optional.ofNullable(name);
}
public Optional<Address> getAddress() {
return Optional.ofNullable(address);
}
}
public static void main(String[] args) {
// Complex object graph
User completeUser = new User("Alice",
new Address("123 Main St", "New York"));
User partialUser = new User("Bob",
new Address(null, "Boston"));
User minimalUser = new User(null, null);
// Safe navigation through Optional chain
Optional<String> city1 = Optional.of(completeUser)
.flatMap(User::getAddress)
.flatMap(Address::getCity);
Optional<String> city2 = Optional.of(partialUser)
.flatMap(User::getAddress)
.flatMap(Address::getCity);
Optional<String> city3 = Optional.of(minimalUser)
.flatMap(User::getAddress)
.flatMap(Address::getCity);
System.out.println("Complete user city: " + city1.orElse("Unknown"));
System.out.println("Partial user city: " + city2.orElse("Unknown"));
System.out.println("Minimal user city: " + city3.orElse("Unknown"));
// Complex transformation pipeline
String result = Optional.of(completeUser)
.flatMap(User::getName)
.map(String::toUpperCase)
.map(name -> "User: " + name)
.filter(s -> s.length() > 5)
.orElse("No user name available");
System.out.println("Transformed: " + result);
// Practical example: configuration processing
Optional<String> rawConfig = Optional.of(" DB_TIMEOUT=30 ");
int timeout = rawConfig.map(String::trim)
.filter(s -> s.startsWith("DB_TIMEOUT="))
.map(s -> s.substring("DB_TIMEOUT=".length()))
.flatMap(OptionalChaining::parseIntSafe)
.filter(t -> t > 0 && t <= 300)
.orElse(60);
System.out.println("Database timeout: " + timeout + " seconds");
}
public static Optional<Integer> parseIntSafe(String input) {
try {
return Optional.of(Integer.parseInt(input));
} catch (NumberFormatException e) {
return Optional.empty();
}
}
}
5. Practical Real-World Examples
Repository Pattern with Optional
import java.util.*;
import java.util.concurrent.*;
public class RepositoryExample {
static class User {
private Long id;
private String username;
private String email;
public User(Long id, String username, String email) {
this.id = id;
this.username = username;
this.email = email;
}
// Getters
public Long getId() { return id; }
public String getUsername() { return username; }
public Optional<String> getEmail() {
return Optional.ofNullable(email);
}
@Override
public String toString() {
return String.format("User{id=%d, username='%s', email='%s'}",
id, username, email);
}
}
static class UserRepository {
private Map<Long, User> users = new ConcurrentHashMap<>();
public UserRepository() {
// Sample data
users.put(1L, new User(1L, "alice", "[email protected]"));
users.put(2L, new User(2L, "bob", null)); // No email
users.put(3L, new User(3L, "charlie", "[email protected]"));
}
public Optional<User> findById(Long id) {
return Optional.ofNullable(users.get(id));
}
public Optional<String> findUsernameById(Long id) {
return findById(id)
.map(User::getUsername);
}
public Optional<String> findEmailByUsername(String username) {
return users.values().stream()
.filter(user -> username.equals(user.getUsername()))
.findFirst()
.flatMap(User::getEmail);
}
public boolean userHasEmail(Long userId) {
return findById(userId)
.flatMap(User::getEmail)
.isPresent();
}
}
public static void main(String[] args) {
UserRepository repository = new UserRepository();
// Safe queries
System.out.println("=== User Lookups ===");
repository.findById(1L).ifPresent(user ->
System.out.println("Found: " + user));
repository.findById(999L).ifPresentOrElse(
user -> System.out.println("Found: " + user),
() -> System.out.println("User 999 not found")
);
System.out.println("\n=== Email Lookups ===");
Optional<String> email = repository.findEmailByUsername("bob");
System.out.println("Bob's email: " + email.orElse("No email"));
System.out.println("\n=== Validation ===");
System.out.println("User 1 has email: " + repository.userHasEmail(1L));
System.out.println("User 2 has email: " + repository.userHasEmail(2L));
// Processing multiple results
System.out.println("\n=== All Users with Email ===");
Arrays.asList(1L, 2L, 3L, 999L).forEach(id -> {
String result = repository.findById(id)
.flatMap(User::getEmail)
.map(email -> "User " + id + ": " + email)
.orElse("User " + id + ": No email or not found");
System.out.println(result);
});
}
}
Configuration Management
import java.util.*;
import java.util.function.*;
public class ConfigurationManager {
private Map<String, String> config = new HashMap<>();
public ConfigurationManager() {
// Sample configuration
config.put("app.name", "MyApplication");
config.put("app.version", "1.0.0");
config.put("database.url", "jdbc:mysql://localhost:3306/mydb");
config.put("database.timeout", "30");
config.put("cache.enabled", "true");
// Note: "database.pool.size" is missing
}
public Optional<String> getString(String key) {
return Optional.ofNullable(config.get(key));
}
public Optional<Integer> getInt(String key) {
return getString(key).flatMap(this::safeParseInt);
}
public Optional<Boolean> getBoolean(String key) {
return getString(key).map(Boolean::parseBoolean);
}
public String getStringOrDefault(String key, String defaultValue) {
return getString(key).orElse(defaultValue);
}
public int getIntOrDefault(String key, int defaultValue) {
return getInt(key).orElse(defaultValue);
}
public <T> Optional<T> getAndConvert(String key, Function<String, T> converter) {
return getString(key).map(converter);
}
public void ifPresent(String key, Consumer<String> action) {
getString(key).ifPresent(action);
}
private Optional<Integer> safeParseInt(String value) {
try {
return Optional.of(Integer.parseInt(value));
} catch (NumberFormatException e) {
return Optional.empty();
}
}
public static void main(String[] args) {
ConfigurationManager config = new ConfigurationManager();
System.out.println("=== Configuration Access ===");
// Safe access with defaults
String appName = config.getString("app.name").orElse("Unknown App");
int timeout = config.getInt("database.timeout").orElse(60);
int poolSize = config.getInt("database.pool.size").orElse(10);
System.out.println("App Name: " + appName);
System.out.println("Database Timeout: " + timeout);
System.out.println("Database Pool Size: " + poolSize);
// Conditional processing
config.ifPresent("cache.enabled", value ->
System.out.println("Cache is configured: " + value));
config.getBoolean("cache.enabled").ifPresent(cacheEnabled -> {
if (cacheEnabled) {
System.out.println("Initializing cache...");
}
});
// Complex configuration processing
String dbUrl = config.getString("database.url")
.map(url -> url.replace("localhost", "production-db"))
.orElseThrow(() ->
new IllegalStateException("Database URL is required"));
System.out.println("Resolved DB URL: " + dbUrl);
// Validation chain
boolean isValid = config.getString("app.name")
.filter(name -> !name.trim().isEmpty())
.filter(name -> name.length() <= 50)
.isPresent();
System.out.println("App name is valid: " + isValid);
}
}
6. Java 9+ Enhancements
Java 9+ Optional Features
import java.util.*;
import java.util.stream.*;
public class Java9OptionalFeatures {
public static void main(String[] args) {
// 1. ifPresentOrElse - Java 9
Optional<String> present = Optional.of("Hello");
Optional<String> absent = Optional.empty();
present.ifPresentOrElse(
value -> System.out.println("Value: " + value),
() -> System.out.println("No value")
);
absent.ifPresentOrElse(
value -> System.out.println("Value: " + value),
() -> System.out.println("No value")
);
// 2. stream() - Convert Optional to Stream
List<String> values = Stream.of(present, absent, Optional.of("World"))
.flatMap(Optional::stream)
.collect(Collectors.toList());
System.out.println("All values: " + values);
// 3. or() - Provide alternative Optional
Optional<String> primary = Optional.empty();
Optional<String> secondary = Optional.of("Backup");
Optional<String> tertiary = Optional.of("Last Resort");
Optional<String> result1 = primary.or(() -> secondary);
Optional<String> result2 = primary.or(() -> secondary)
.or(() -> tertiary);
System.out.println("Primary or secondary: " + result1);
System.out.println("Primary or secondary or tertiary: " + result2);
// Practical examples with new methods
System.out.println("\n=== Practical Examples ===");
// Processing with stream
List<Optional<String>> options = Arrays.asList(
Optional.of("A"),
Optional.empty(),
Optional.of("B"),
Optional.empty(),
Optional.of("C")
);
List<String> collected = options.stream()
.flatMap(Optional::stream)
.collect(Collectors.toList());
System.out.println("Collected values: " + collected);
// Fallback chain with or()
Optional<String> finalValue = getFromCache()
.or(() -> getFromDatabase())
.or(() -> getDefault());
System.out.println("Final value: " + finalValue.orElse("Nothing found"));
}
static Optional<String> getFromCache() {
System.out.println("Checking cache...");
return Optional.empty(); // Cache miss
}
static Optional<String> getFromDatabase() {
System.out.println("Querying database...");
return Optional.of("Database Value");
}
static Optional<String> getDefault() {
System.out.println("Using default...");
return Optional.of("Default Value");
}
}
7. Best Practices and Common Patterns
Optional Best Practices
import java.util.*;
public class OptionalBestPractices {
// 1. DON'T use Optional for fields
static class BadUser {
private Optional<String> name; // BAD!
public BadUser(Optional<String> name) {
this.name = name;
}
}
static class GoodUser {
private String name; // GOOD - use null for absent values
public GoodUser(String name) {
this.name = name;
}
public Optional<String> getName() { // GOOD - Optional in getters
return Optional.ofNullable(name);
}
}
// 2. DON'T use Optional in collections
static class BadService {
private List<Optional<String>> items; // BAD!
}
static class GoodService {
private List<String> items; // GOOD - use empty list
public List<String> getItems() {
return items != null ? items : Collections.emptyList();
}
}
// 3. DO use Optional as return type
public static Optional<String> findUser(Long id) {
// Implementation
return id == 1L ? Optional.of("Alice") : Optional.empty();
}
// 4. DON'T use Optional as method parameter
public static void badMethod(Optional<String> param) { // BAD!
// ...
}
public static void goodMethod(String param) { // GOOD
// Handle null explicitly if needed
if (param == null) {
// handle null case
}
}
// 5. DO use functional style with Optional
public static void processUser(Long userId) {
// GOOD - functional style
findUser(userId)
.map(String::toUpperCase)
.filter(name -> name.length() > 3)
.ifPresentOrElse(
name -> System.out.println("Hello, " + name),
() -> System.out.println("User not found")
);
}
// 6. DON'T overuse Optional.get()
public static void badAccess(Optional<String> optional) {
if (optional.isPresent()) {
String value = optional.get(); // BAD - use orElse/orElseGet instead
System.out.println(value);
}
}
public static void goodAccess(Optional<String> optional) {
// GOOD - use functional methods
optional.ifPresent(System.out::println);
// Or provide default
String value = optional.orElse("Default");
System.out.println(value);
}
// 7. DO consider performance for orElse vs orElseGet
public static void performanceExample() {
Optional<String> empty = Optional.empty();
// orElse - always evaluates the default
String value1 = empty.orElse(expensiveOperation());
// orElseGet - lazy evaluation
String value2 = empty.orElseGet(() -> expensiveOperation());
}
private static String expensiveOperation() {
System.out.println("Performing expensive operation...");
return "Expensive Result";
}
public static void main(String[] args) {
// Demonstrate best practices
processUser(1L);
processUser(999L);
performanceExample();
}
}
8. Common Anti-Patterns to Avoid
Optional Anti-Patterns
import java.util.*;
public class OptionalAntiPatterns {
// 1. Anti-pattern: Using Optional for fields
class BadEntity {
private Optional<String> name; // DON'T DO THIS
public Optional<String> getName() {
return name;
}
}
class GoodEntity {
private String name; // DO THIS
public Optional<String> getName() {
return Optional.ofNullable(name);
}
}
// 2. Anti-pattern: Using Optional in method parameters
public void badMethod(Optional<String> parameter) { // DON'T DO THIS
if (parameter.isPresent()) {
// ...
}
}
public void goodMethod(String parameter) { // DO THIS
if (parameter != null) {
// ...
}
}
// 3. Anti-pattern: Unnecessary Optional creation
public Optional<String> unnecessaryOptional(String input) {
if (input == null) {
return Optional.empty();
}
return Optional.of(input); // DON'T DO THIS - just return the string
}
public String cleanMethod(String input) {
return input; // DO THIS - let caller handle null
}
// 4. Anti-pattern: Using isPresent()-get() pattern
public void badAccessPattern(Optional<String> optional) {
if (optional.isPresent()) {
String value = optional.get(); // DON'T DO THIS
System.out.println(value);
}
}
public void goodAccessPattern(Optional<String> optional) {
optional.ifPresent(System.out::println); // DO THIS
}
// 5. Anti-pattern: Using Optional in collections
public void badCollectionUsage() {
List<Optional<String>> badList = new ArrayList<>(); // DON'T DO THIS
badList.add(Optional.of("A"));
badList.add(Optional.empty());
}
public void goodCollectionUsage() {
List<String> goodList = new ArrayList<>(); // DO THIS
goodList.add("A");
// Don't add nulls, or handle them explicitly
}
// 6. Anti-pattern: Chaining orElse incorrectly
public void badChaining() {
Optional<String> optional = Optional.empty();
String result = optional.map(String::toUpperCase)
.orElse(getDefaultValue()); // DEFAULT ALWAYS COMPUTED
System.out.println(result);
}
public void goodChaining() {
Optional<String> optional = Optional.empty();
String result = optional.map(String::toUpperCase)
.orElseGet(() -> getDefaultValue()); // LAZY COMPUTATION
System.out.println(result);
}
private String getDefaultValue() {
System.out.println("Computing default value...");
return "Default";
}
public static void main(String[] args) {
OptionalAntiPatterns examples = new OptionalAntiPatterns();
examples.badChaining();
System.out.println("---");
examples.goodChaining();
}
}
Key Summary
- Use Optional as return type to explicitly indicate possible absence
- Avoid Optional for fields, parameters, and in collections
- Prefer functional style (
map,flatMap,filter) over imperative checks - Use
orElseGetfor expensive default computations - Consider Java 9+ features (
stream(),or(),ifPresentOrElse) - Never return null from an Optional-returning method
- Use
Optional.empty()instead ofnullfor empty results
Optional is a powerful tool for making your code more readable and null-safe when used appropriately!