Java Optional orElse vs orElseGet: Performance Differences

Learning Objectives

  • Understand the eager evaluation behavior of orElse() and its performance implications
  • Recognize when orElseGet() provides more efficient code execution through lazy evaluation
  • Identify scenarios where orElse() is appropriate for simple default values
  • Implement the correct method based on whether the default value requires computation

Introduction

You're using Optional, feeling good about your functional programming style, and then you notice something odd in your logs. That expensive database call inside orElse() is executing every single time, even when the Optional contains a value. You assumed Optional would short-circuit the default value computation. It doesn't.

The difference between orElse() and orElseGet() looks trivial at first glance. Both provide fallback values when an Optional is empty. Both have similar names and similar signatures. But orElse() evaluates its argument immediately, before it even checks if the Optional is empty. orElseGet() only evaluates when the Optional is actually empty. That single difference creates performance implications you can't ignore in production code.

How orElse() Works

The orElse() method takes a parameter—not a function, just a value. That parameter gets evaluated before orElse() even executes. Java evaluates method arguments before the method runs, so whatever expression you pass to orElse() runs unconditionally.

package blog.academy.javapro;

import java.util.Optional;

public class OrElseBehavior {
    public static void main(String[] args) {
        Optional<String> value = Optional.of("existing value");
        
        // The method call happens BEFORE orElse() executes
        String result = value.orElse(generateDefault());
        System.out.println("Result: " + result);
    }
    
    private static String generateDefault() {
        System.out.println("generateDefault() called - this always executes!");
        return "default value";
    }
}

Run this code. You'll see "generateDefault() called" printed even though the Optional contains "existing value". The generateDefault() method executes before orElse() gets the chance to check if the Optional is empty. The value gets computed, then thrown away because the Optional wasn't empty.

This happens because of how Java evaluates expressions. When you write value.orElse(generateDefault()), Java first evaluates generateDefault() to get its return value, then passes that return value to orElse(). The method doesn't take a lambda or function—it takes an already-computed value.

How orElseGet() Works

The orElseGet() method takes a Supplier<T>—a function that produces a value. The function only executes if the Optional is empty. If the Optional contains a value, orElseGet() returns it immediately without touching the Supplier.

package blog.academy.javapro;

import java.util.Optional;

public class OrElseGetBehavior {
    public static void main(String[] args) {
        Optional<String> value = Optional.of("existing value");
        
        // The lambda only executes if the Optional is empty
        String result = value.orElseGet(() -> generateDefault());
        System.out.println("Result: " + result);
        
        // Test with empty Optional
        Optional<String> empty = Optional.empty();
        String emptyResult = empty.orElseGet(() -> generateDefault());
        System.out.println("Empty result: " + emptyResult);
    }
    
    private static String generateDefault() {
        System.out.println("generateDefault() called - only when Optional is empty!");
        return "default value";
    }
}

Now generateDefault() only runs when the Optional is empty. The lambda () -> generateDefault() wraps the call, and orElseGet() only invokes that lambda when needed. This is lazy evaluation—deferring computation until you actually need the result.

The Performance Impact

The performance difference matters when computing the default value is expensive. Database queries, API calls, complex calculations—these operations kill performance if they run unnecessarily.

package blog.academy.javapro;

import java.util.Optional;

public class PerformanceComparison {
    public static void main(String[] args) {
        Optional<String> user = findUserInCache("john_doe");
        
        // Bad: database query ALWAYS executes
        long start = System.nanoTime();
        String result1 = user.orElse(fetchUserFromDatabase("john_doe"));
        long elapsed1 = System.nanoTime() - start;
        System.out.println("orElse() took: " + elapsed1 + " ns");
        
        // Good: database query only executes if cache miss
        Optional<String> user2 = findUserInCache("john_doe");
        start = System.nanoTime();
        String result2 = user2.orElseGet(() -> fetchUserFromDatabase("john_doe"));
        long elapsed2 = System.nanoTime() - start;
        System.out.println("orElseGet() took: " + elapsed2 + " ns");
        
        System.out.println("Performance difference: " + (elapsed1 - elapsed2) + " ns");
    }
    
    private static Optional<String> findUserInCache(String username) {
        System.out.println("Checking cache for: " + username);
        // Simulate cache hit
        return Optional.of("cached_user_data");
    }
    
    private static String fetchUserFromDatabase(String username) {
        System.out.println("EXPENSIVE: Fetching " + username + " from database");
        try {
            Thread.sleep(100); // Simulate database latency
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return "database_user_data";
    }
}

The orElse() version hits the database every time, wasting 100ms even when the cache contains the data. The orElseGet() version only queries the database on cache misses. In high-throughput systems, this difference compounds—thousands of unnecessary operations per second.

When to Use orElse()

Use orElse() for simple, pre-computed constants or literals. When the default value already exists and requires no computation, orElse() is cleaner and perfectly acceptable.

package blog.academy.javapro;

import java.util.Optional;

public class WhenToUseOrElse {
    private static final String DEFAULT_STATUS = "PENDING";
    
    public static void main(String[] args) {
        Optional<String> status = loadStatus();
        
        // Fine - literal constant
        String s1 = status.orElse("UNKNOWN");
        
        // Fine - pre-computed constant
        String s2 = status.orElse(DEFAULT_STATUS);
        
        // Fine - simple expression with no side effects
        String s3 = status.orElse("PREFIX_" + "SUFFIX");
        
        System.out.println("Status 1: " + s1);
        System.out.println("Status 2: " + s2);
        System.out.println("Status 3: " + s3);
    }
    
    private static Optional<String> loadStatus() {
        return Optional.empty();
    }
}

String literals, numeric constants, enum values, pre-computed static fields—these don't incur performance penalties. The value already exists in memory. Evaluating it is essentially free. Using orElseGet() for these cases adds lambda overhead without any benefit.

When to Use orElseGet()

Reach for orElseGet() when computing the default value involves any of these:

Method calls that might have side effects. Database queries, file I/O, network requests, logging—anything that does work.

package blog.academy.javapro;

import java.util.Optional;

public class WhenToUseOrElseGet {
    public static void main(String[] args) {
        Optional<String> config = loadConfig("feature.enabled");
        
        // Use orElseGet - method call with potential side effects
        String value = config.orElseGet(() -> loadDefaultConfig());
        System.out.println("Config value: " + value);
        
        // Use orElseGet - object creation
        Optional<UserSettings> settings = loadSettings();
        UserSettings userSettings = settings.orElseGet(() -> new UserSettings());
        System.out.println("Settings loaded: " + userSettings);
        
        // Use orElseGet - computation
        Optional<Integer> cached = getCachedTotal();
        Integer total = cached.orElseGet(() -> calculateExpensiveTotal());
        System.out.println("Total: " + total);
    }
    
    private static Optional<String> loadConfig(String key) {
        return Optional.empty();
    }
    
    private static String loadDefaultConfig() {
        System.out.println("Loading default configuration...");
        return "default_config";
    }
    
    private static Optional<UserSettings> loadSettings() {
        return Optional.empty();
    }
    
    private static Optional<Integer> getCachedTotal() {
        return Optional.of(1000);
    }
    
    private static Integer calculateExpensiveTotal() {
        System.out.println("Performing expensive calculation (this shouldn't print)");
        return 999;
    }
    
    static class UserSettings {
        UserSettings() {
            System.out.println("Creating new UserSettings instance");
        }
    }
}

Object construction can be expensive depending on the class. Even simple objects involve allocation and initialization. For complex objects with their own initialization logic, orElseGet() avoids unnecessary allocations.

A Subtle Trap

Don't mistake orElse() for conditional execution. Even though Optional might contain a value, orElse() still evaluates its argument. This trips up developers who assume Optional provides conditional evaluation throughout its API.

package blog.academy.javapro;

import java.util.Optional;

public class CommonMistake {
    public static void main(String[] args) {
        Optional<String> result = performOperation();
        
        // This ALWAYS logs, even on success
        String value = result.orElse(logAndReturnDefault());
        System.out.println("Final value: " + value);
    }
    
    private static Optional<String> performOperation() {
        return Optional.of("success");
    }
    
    private static String logAndReturnDefault() {
        System.out.println("ERROR: Operation failed! (except it didn't...)");
        return "default";
    }
}

Your logs show errors that didn't happen because logAndReturnDefault() runs every time. The fix is trivial—use orElseGet()—but the bug is subtle if you're not watching for it.

Summary

orElse() evaluates its argument immediately, before checking if the Optional is empty. The value you pass gets computed regardless of whether Optional needs it. This works fine for simple constants and literals where no actual computation occurs, but it wastes resources when the default value is expensive to produce.

orElseGet() takes a Supplier that only executes when the Optional is empty. If the Optional contains a value, the Supplier never runs. Use this whenever computing the default involves method calls, object creation, I/O operations, or any work you want to avoid doing unnecessarily.

The choice is straightforward: if your default value is already computed or trivially cheap, use orElse(). If it requires any real work, use orElseGet(). The performance difference might seem minor in single operations, but it compounds across thousands or millions of calls in production systems. Write orElseGet() by default for non-constant values, and you'll never accidentally introduce a performance bug.

Subscribe and Master Java

If this helped you, you'll love the full Java Program Library4 structured programs with real projects.

$49 /year
Posted in

Get the Java Weekly Digest

Stay sharp with curated Java insights delivered straight to your inbox. Join 5,000+ developers who read our digest to level up their skills.

No spam. Unsubscribe anytime.

Name