Skip to content

Null Safety


Definition

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

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

Null Safety Tips and Tricks