Null Safety
Definition

The Null Problem
// Null creates unclear contracts and runtime errors
public class UserService {
public User findById(String id) {
// What does this return if not found?
// null? Exception? Empty user?
return userRepository.findById(id);
}
public void processUser(User user) {
// Is null allowed? Documentation doesn't say!
String email = user.getEmail(); // NPE if user is null
String domain = email.split("@")[1]; // NPE if email is null
}
}
// Caller has no idea what to expect
User user = service.findById("123");
// Should I check for null? Is it even possible?
user.getName(); // Might explode
// Common pattern becomes:
if (user != null) {
if (user.getEmail() != null) {
if (user.getEmail().contains("@")) {
// Finally safe...
}
}
}
// Deeply nested, hard to read, easy to miss a check
Using Optional
// Optional makes "might not exist" explicit
public class UserService {
// Clear contract: might not find user
public Optional<User> findById(String id) {
return Optional.ofNullable(userRepository.findById(id));
}
// Clear contract: user is required
public void processUser(User user) {
Objects.requireNonNull(user, "user cannot be null");
// Now we know user is not null
}
}
// Caller MUST handle both cases
Optional<User> maybeUser = service.findById("123");
// Option 1: Check presence
if (maybeUser.isPresent()) {
User user = maybeUser.get();
// use user
}
// Option 2: Provide default
User user = maybeUser.orElse(defaultUser);
// Option 3: Throw if missing
User user = maybeUser.orElseThrow(
() -> new UserNotFoundException("123"));
// Option 4: Transform if present
String email = maybeUser
.map(User::getEmail)
.orElse("[email protected]");
// Option 5: Chain operations
String domain = maybeUser
.map(User::getEmail)
.filter(e -> e.contains("@"))
.map(e -> e.split("@")[1])
.orElse("unknown");
When to Use Optional

Null Object Pattern
// Replace null with a "do nothing" implementation
// Interface
public interface Logger {
void log(String message);
void error(String message, Exception e);
}
// Real implementation
public class ConsoleLogger implements Logger {
public void log(String message) {
System.out.println(message);
}
public void error(String message, Exception e) {
System.err.println(message);
e.printStackTrace();
}
}
// Null Object - does nothing, but safely
public class NullLogger implements Logger {
public void log(String message) { /* do nothing */ }
public void error(String message, Exception e) { /* do nothing */ }
}
// Now service never deals with null
public class Service {
private final Logger logger;
public Service(Logger logger) {
this.logger = logger != null ? logger : new NullLogger();
}
public void doSomething() {
logger.log("Doing something"); // Always safe!
// No null check needed
}
}
// Another example: Empty collection as null object
public List<Order> getOrders(String userId) {
List<Order> orders = orderRepo.findByUserId(userId);
return orders != null ? orders : Collections.emptyList();
}
// Caller can always iterate, no null check needed
Defensive Programming
// Strategies to prevent null issues
// 1. Validate at boundaries
public class UserController {
@PostMapping("/users")
public User createUser(@Valid @RequestBody UserRequest request) {
// @Valid ensures non-null fields
return userService.create(request);
}
}
// 2. Constructor validation
public class User {
private final String name;
private final EmailAddress email;
public User(String name, EmailAddress email) {
this.name = Objects.requireNonNull(name, "name cannot be null");
this.email = Objects.requireNonNull(email, "email cannot be null");
}
}
// 3. Use annotations
public class OrderService {
// Makes contract explicit
public Order processOrder(@NonNull Order order) {
// Annotation documents requirement
// Some tools check at compile time
}
@Nullable // Explicitly marks possible null
public User findUser(String id) {
// ...
}
}
// 4. Builder pattern with required fields
User user = User.builder()
.name("John") // Required
.email(email) // Required
.nickname("Johnny") // Optional
.build(); // Validates required fields
Collections and Null
// Never return null collections!
// BAD
public List<Order> getOrders(String userId) {
if (user.hasOrders()) {
return orderRepository.findByUserId(userId);
}
return null; // Caller must check!
}
// GOOD
public List<Order> getOrders(String userId) {
List<Order> orders = orderRepository.findByUserId(userId);
return orders != null ? orders : Collections.emptyList();
}
// Or use Spring Data
public interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> findByUserId(String userId); // Never returns null
}
// For Maps
public Map<String, Config> getConfigs() {
return configs != null ? configs : Collections.emptyMap();
}
// Now callers can always safely iterate
for (Order order : service.getOrders(userId)) {
// No null check needed!
}
service.getOrders(userId).stream()
.filter(Order::isPending)
.forEach(this::process);
// Safe even if no orders!
Tips & Tricks
