跳轉到

Java進階:Optional類別的使用與最佳實踐

1. 引言

今天我們深入探討Optional,水一下天數XD,在Java程式設計中,NullPointerException一直是困擾開發者的問題。
為解決這個問題,Java 8引入Optional類別,這是一個可以包含或不包含非null值的容器對象,Optional類別的設計目的是為更好地表達可能缺失的值,並提供一種更優雅的方式來處理可能為null的對象。

我們演示NullPointerException產生的場景:

public class NullPointerExampleDemo {
    public static void main(String[] args) {
        // 1. 訪問空對象的成員
        String str = null;
        System.out.println(str.length()); // NullPointerException

        // 2. 訪問空數組
        int[] arr = null;
        System.out.println(arr[0]); // NullPointerException

        // 3. 調用空對象的方法
        NullPointerExampleDemo demo = null;
        demo.someMethod(); // NullPointerException

        // 4. 拆箱時遇到 null 值
        Integer num = null;
        int value = num; // NullPointerException
    }

    public void someMethod() {
        System.out.println("This is some method");
    }
}

2. Optional類別的基本概念和創建方法

2.1 Optional的基本概念

Optional是一個泛型類,其中T代表包含的對象類型。Optional可以有兩種狀態:

  1. 包含一個非null值
  2. 不包含任何值(空)

使用Optional可以強制開發者考慮值不存在的情況,從而減少潛在的NullPointerException。

2.2 創建Optional對象

Java提供幾種創建Optional對象的方法:

2.2.1 Optional.empty()

創建一個空的Optional對象:

Optional<String> empty = Optional.empty();

2.2.2 Optional.of(T value)

創建一個包含非null值的Optional對象。如果傳入null,會拋出NullPointerException:

String name = "John";
Optional<String> optionalName = Optional.of(name);

// 以下程式碼會拋出NullPointerException
// Optional<String> nullOptional = Optional.of(null);

2.2.3 Optional.ofNullable(T value)

創建一個可能包含null值的Optional對象。如果傳入null,則返回一個空的Optional:

String name = "John";
Optional<String> optionalName = Optional.ofNullable(name);

String nullName = null;
Optional<String> nullOptional = Optional.ofNullable(nullName); // 不會拋出異常

2.3 判斷Optional是否包含值

Optional提供幾種方法來檢查是否包含值:

2.3.1 isPresent()

如果Optional包含值,返回true;否則返回false:

Optional<String> optionalName = Optional.of("John");
boolean isPresent = optionalName.isPresent(); // true

Optional<String> emptyOptional = Optional.empty();
boolean isEmpty = emptyOptional.isPresent(); // false

2.3.2 isEmpty() (Java 11+)

如果Optional為空,返回true;否則返回false:

Optional<String> optionalName = Optional.of("John");
boolean isEmpty = optionalName.isEmpty(); // false

Optional<String> emptyOptional = Optional.empty();
boolean isEmpty = emptyOptional.isEmpty(); // true

2.4 獲取Optional中的值

2.4.1 get()

如果Optional包含值,返回該值;如果為空,拋出NoSuchElementException:

Optional<String> optionalName = Optional.of("John");
String name = optionalName.get(); // "John"

Optional<String> emptyOptional = Optional.empty();
// 以下程式碼會拋出NoSuchElementException
// String value = emptyOptional.get();

由於get()方法可能拋出異常,因此在使用時應該先檢查Optional是否包含值。

3. Optional的核心方法和使用場景

3.1 orElse() 和 orElseGet()

這兩個方法用於在Optional為空時提供默認值。

3.1.1 orElse(T other)

如果Optional包含值,則返回該值;否則返回指定的默認值:

String name = Optional.ofNullable(nullableName).orElse("Unknown");

3.1.2 orElseGet(Supplier<? extends T> other)

與orElse()類似,但允許延遲生成默認值:

String name = Optional.ofNullable(nullableName).orElseGet(() -> "Unknown");

使用場景:當需要為可能為null的值提供默認值時。orElseGet()適用於默認值計算成本較高的情況。

3.2 orElseThrow()

如果Optional為空,拋出指定的異常:

String name = Optional.ofNullable(nullableName)
    .orElseThrow(() -> new IllegalArgumentException("Name is required"));

使用場景:當值不存在時需要拋出特定異常。

3.3 ifPresent() 和 ifPresentOrElse()

3.3.1 ifPresent(Consumer<? super T> action)

如果Optional包含值,則執行指定的操作:

Optional.ofNullable(name).ifPresent(System.out::println);

3.3.2 ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction) (Java 9+)

如果Optional包含值,則執行指定的操作;否則執行另一個操作:

Optional.ofNullable(name).ifPresentOrElse(
    System.out::println,
    () -> System.out.println("Name is empty")
);

使用場景:根據Optional是否包含值執行不同的操作。

3.4 filter()

根據指定的條件過濾Optional中的值:

Optional<String> filteredName = Optional.of("John")
    .filter(name -> name.length() > 3);

使用場景:當需要在處理Optional值之前先進行條件檢查。

3.5 map() 和 flatMap()

3.5.1 map(Function<? super T, ? extends U> mapper)

如果Optional包含值,則應用映射函數:

Optional<Integer> nameLength = Optional.of("John")
    .map(String::length);

3.5.2 flatMap(Function<? super T, Optional<U>> mapper)

類似map(),但用於處理返回Optional的映射函數:

Optional<String> upperName = Optional.of("john")
    .flatMap(name -> Optional.of(name.toUpperCase()));

使用場景:當需要轉換Optional中的值或處理嵌套的Optional時。

3.6 or() (Java 9+)

提供一個替代的Optional:

Optional<String> result = Optional.empty()
    .or(() -> Optional.of("Default"));

使用場景:當需要提供一個備選的Optional時。

3.7 stream() (Java 9+)

將Optional轉換為Stream:

Stream<String> stream = Optional.of("value").stream();

使用場景:當需要將Optional與Stream API結合使用時。

4. Optional與Stream API的結合使用

昨天有提過,我們再複習一次。

4.1 將Optional轉換為Stream

從Java 9開始,Optional提供stream()方法,可以將Optional轉換為包含0或1個元素的Stream:

Optional<String> optional = Optional.of("value");
Stream<String> stream = optional.stream();

4.2 在Stream操作中使用Optional

4.2.1 過濾非空值

我們可以使用Optional.stream()方法來過濾掉Stream中的空值:

List<Optional<String>> listOfOptionals = Arrays.asList(
    Optional.empty(), Optional.of("A"), Optional.empty(), Optional.of("B"));

List<String> filteredList = listOfOptionals.stream()
    .flatMap(Optional::stream)
    .collect(Collectors.toList());

// 結果: [A, B]

4.2.2 映射可能為null的值

當我們需要對可能為null的值進行映射時,可以結合使用Optional和map操作:

List<String> names = Arrays.asList("John", null, "Jane");
List<String> uppercaseNames = names.stream()
    .map(name -> Optional.ofNullable(name)
        .map(String::toUpperCase)
        .orElse(""))
    .collect(Collectors.toList());

// 結果: [JOHN, , JANE]

4.3 使用flatMap處理嵌套的Optional

當我們有一個返回Optional的方法,並且想在Stream中使用時,flatMap是一個很好的選擇:

class User {
    Optional<Address> getAddress() { ... }
}

class Address {
    Optional<String> getStreet() { ... }
}

List<User> users = ...;
List<String> streets = users.stream()
    .map(User::getAddress)
    .flatMap(Optional::stream)
    .map(Address::getStreet)
    .flatMap(Optional::stream)
    .collect(Collectors.toList());

4.4 在Stream的終端操作中使用Optional

某些Stream的終端操作返回Optional,我們可以直接在這些結果上使用Optional的方法:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

Optional<Integer> max = numbers.stream()
    .max(Integer::compare);

int maxValue = max.orElse(0);

4.5 使用Optional處理Stream的結果

當我們不確定Stream操作是否會產生結果時,可以使用返回Optional的方法:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

Optional<String> firstLongName = names.stream()
    .filter(name -> name.length() > 5)
    .findFirst();

String longName = firstLongName.orElse("No long name found");

4.6 使用時機

  1. 優先使用Stream API的方法: 如filter、map等,而不是在Stream操作中頻繁使用Optional的方法。

  2. 使用flatMap和Optional.stream()來扁平化嵌套的Optional。

  3. 在Stream的終端操作後使用Optional的方法來處理結果,而不是在中間操作中過度使用Optional。

  4. 避免創建包含Optional的集合,而是在需要時將Optional轉換為Stream。

5. Optional的實踐和常見陷阱

5.1 實踐

5.1.1 明智地選擇何時使用Optional

  • 使用Optional作為方法的返回類型,表示結果可能不存在。
  • 不要使用Optional作為方法參數或類的字段。
// 好的做法
public Optional<User> findUserById(String id) { ... }

// 避免這樣做
public void processUser(Optional<User> user) { ... }

5.1.2 優先使用Optional的方法而不是顯式的null檢查

// 不推薦
if (user != null) {
    String name = user.getName();
    if (name != null) {
        System.out.println(name.toUpperCase());
    }
}

// 推薦
Optional.ofNullable(user)
    .map(User::getName)
    .map(String::toUpperCase)
    .ifPresent(System.out::println);

5.1.3 使用orElse()、orElseGet()和orElseThrow()來處理空值

String name = Optional.ofNullable(user)
    .map(User::getName)
    .orElse("Unknown");

User user = Optional.ofNullable(findUserById(id))
    .orElseThrow(() -> new UserNotFoundException(id));

5.1.4 避免在Optional中包裝原始類型

對於原始類型,使用專門的Optional類如OptionalInt、OptionalLong和OptionalDouble。

// 不推薦
Optional<Integer> count = Optional.of(5);

// 推薦
OptionalInt count = OptionalInt.of(5);

5.1.5 使用Stream API來處理Optional集合

List<Optional<String>> optionals = ...;
List<String> result = optionals.stream()
    .flatMap(Optional::stream)
    .collect(Collectors.toList());

5.2 常見陷阱

5.2.1 使用Optional.get()而不檢查值是否存在

// 危險的做法
String name = optional.get(); // 可能拋出NoSuchElementException

// 安全的做法
String name = optional.orElse("Default");

5.2.2 過度使用Optional

不要為避免null就到處使用Optional。只在真正需要表示可能不存在的值時使用。

5.2.3 在性能關鍵的程式碼中過度使用Optional

Optional會帶來一些性能開銷。在性能關鍵的程式碼中,可能需要權衡使用傳統的null檢查。

5.2.4 誤用orElse()和orElseGet()

// orElse()總是會執行createExpensiveObject()
optional.orElse(createExpensiveObject());

// orElseGet()只在optional為空時執行lambda
optional.orElseGet(() -> createExpensiveObject());

5.2.5 在Stream操作中過度使用Optional

// 不推薦
stream.map(Optional::of)
    .filter(Optional::isPresent)
    .map(Optional::get);

// 推薦
stream.filter(Objects::nonNull);

5.2.6 將Optional序列化

Optional不是為序列化而設計的,如果需要序列化,考慮使用自定義的可序列化包裝類。

5.2.7 使用Optional作為類的字段

Optional不是為作為字段使用而設計的。考慮使用@Nullable注解或設計模式來表示可選字段。

6. Optional在實際項目中的應用案例

6.1 用戶資料處理

假設我們有一個用戶管理系統,需要處理可能不完整的用戶資料。

public class User {
    private String name;
    private Optional<String> email;
    private Optional<Address> address;

    // 構造函數、getter和setter
}

public class Address {
    private String street;
    private String city;
    private Optional<String> zipCode;

    // 構造函數、getter和setter
}

public class UserService {
    public String getUpperCaseUserEmail(String userId) {
        return findUserById(userId)
            .flatMap(User::getEmail)
            .map(String::toUpperCase)
            .orElse("Email not provided");
    }

    public String getUserCityOrDefault(String userId, String defaultCity) {
        return findUserById(userId)
            .flatMap(User::getAddress)
            .map(Address::getCity)
            .orElse(defaultCity);
    }

    private Optional<User> findUserById(String userId) {
        // 數據庫查詢邏輯
    }
}

6.2 配置管理

在處理應用程序配置時,Optional可以幫助我們處理可選的配置項。

public class ConfigManager {
    private Map<String, String> config;

    public Optional<String> getConfigValue(String key) {
        return Optional.ofNullable(config.get(key));
    }

    public int getIntConfig(String key, int defaultValue) {
        return getConfigValue(key)
            .map(Integer::parseInt)
            .orElse(defaultValue);
    }

    public List<String> getListConfig(String key) {
        return getConfigValue(key)
            .map(v -> Arrays.asList(v.split(",")))
            .orElse(Collections.emptyList());
    }
}

6.3 外部服務集成

當與外部服務集成時,我們經常需要處理可能失敗的操作。

public class ExternalServiceClient {
    public Optional<ExternalData> fetchData(String id) {
        try {
            // 調用外部服務的邏輯
            ExternalData data = // ...
            return Optional.ofNullable(data);
        } catch (Exception e) {
            logger.error("Error fetching data for id: " + id, e);
            return Optional.empty();
        }
    }
}

public class DataProcessor {
    private ExternalServiceClient client;

    public ProcessResult processData(String id) {
        return client.fetchData(id)
            .map(this::processExternalData)
            .orElseGet(this::handleMissingData);
    }

    private ProcessResult processExternalData(ExternalData data) {
        // 處理數據的邏輯
    }

    private ProcessResult handleMissingData() {
        // 處理數據缺失的邏輯
    }
}

6.4 數據轉換和驗證

在數據轉換和驗證的場景中,Optional可以幫助我們處理可能無效的輸入。

public class DataValidator {
    public Optional<Integer> parseAndValidateAge(String ageString) {
        return Optional.ofNullable(ageString)
            .filter(s -> !s.isEmpty())
            .map(Integer::parseInt)
            .filter(age -> age > 0 && age < 120);
    }

    public Optional<String> validateEmail(String email) {
        return Optional.ofNullable(email)
            .filter(e -> e.matches("^[A-Za-z0-9+_.-]+@(.+)$"));
    }
}

public class UserRegistrationService {
    private DataValidator validator;

    public RegistrationResult registerUser(String name, String ageString, String email) {
        Optional<Integer> age = validator.parseAndValidateAge(ageString);
        Optional<String> validEmail = validator.validateEmail(email);

        if (age.isPresent() && validEmail.isPresent()) {
            // 進行用戶註冊
            return RegistrationResult.success();
        } else {
            List<String> errors = new ArrayList<>();
            age.ifPresentOrElse(
                a -> {}, 
                () -> errors.add("Invalid age")
            );
            validEmail.ifPresentOrElse(
                e -> {}, 
                () -> errors.add("Invalid email")
            );
            return RegistrationResult.failure(errors);
        }
    }
}

7. Optional的性能考量

雖然Optional提供許多便利,但在使用時也需要考慮其對性能的影響。

7.1 Optional的開銷

Optional是一個包裝對象(Wrapper Object),因此使用會帶來一些額外的開銷:

  1. 內存開銷: Optional對象本身需要額外的內存空間。
  2. 創建開銷: 每次創建Optional對象都需要一定的時間。
  3. 方法調用開銷: 使用Optional的方法(如map、flatMap等)會引入額外的方法調用。

7.2 何時使用Optional

考慮到性能因素,以下是一些使用Optional的建議:

  1. 方法返回值: 當方法的返回值可能為null時,使用Optional是合適的。
  2. 避免在性能關鍵的循環中過度使用: 在高頻率執行的程式碼中,傳統的null檢查可能更高效。
  3. 不要使用Optional作為類的字段: 這會增加不必要的內存開銷。

7.3 性能測試

讓我們通過一個簡單的性能測試來比較使用Optional和傳統null檢查的差異:

public class PerformanceTest {
    private static final int ITERATIONS = 10_000_000;

    public static void main(String[] args) {
        testTraditionalNullCheck();
        testOptional();
    }

    private static void testTraditionalNullCheck() {
        long start = System.nanoTime();
        for (int i = 0; i < ITERATIONS; i++) {
            String result = getValueTraditional(i % 2 == 0 ? "Hello" : null);
            if (result != null) {
                result.toLowerCase();
            }
        }
        long end = System.nanoTime();
        System.out.println("Traditional null check: " + (end - start) / 1_000_000 + " ms");
    }

    private static void testOptional() {
        long start = System.nanoTime();
        for (int i = 0; i < ITERATIONS; i++) {
            Optional<String> result = getValueOptional(i % 2 == 0 ? "Hello" : null);
            result.map(String::toLowerCase);
        }
        long end = System.nanoTime();
        System.out.println("Optional: " + (end - start) / 1_000_000 + " ms");
    }

    private static String getValueTraditional(String input) {
        return input;
    }

    private static Optional<String> getValueOptional(String input) {
        return Optional.ofNullable(input);
    }
}

這個測試比較傳統null檢查和使用Optional處理可能為null的值的性能差異。在大多數情況下,你可能會發現使用Optional的版本稍慢一些。

7.4 性能優化策略

如果你在使用Optional時遇到性能問題,可以考慮以下優化策略:

  1. 延遲創建Optional: 只在確實需要的時候才創建Optional對象。
public Optional<String> getValueOptimized(boolean condition) {
    if (condition) {
        return Optional.of("Value");
    }
    return Optional.empty();
}
  1. 使用專門的Optional類: 對於原始類型,使用OptionalInt、OptionalLong和OptionalDouble可以減少裝箱拆箱的開銷。

  2. 在熱點程式碼中使用傳統方法: 對於頻繁執行的程式碼,考慮使用傳統的null檢查以提高性能。

  3. 使用Stream API時要謹慎: 在處理大量數據時,過度使用Optional可能會導致性能下降。考慮使用其他Stream操作來過濾null值。

// 避免
list.stream()
    .map(Optional::ofNullable)
    .filter(Optional::isPresent)
    .map(Optional::get)
    .collect(Collectors.toList());

// 推薦
list.stream()
    .filter(Objects::nonNull)
    .collect(Collectors.toList());

本篇文章同步刊載iThome: iThome
筆者個人的網站: JUNYI