V
Vel·ToolKit
简洁 · 高效 · 即开即用
ZH
第 15 章 / 共 20 章

Optional 与防空

用 Optional 表达"可能没有"、链式取值、避免反模式

null 的问题与 Optional 出现的原因

Java 长期被 NullPointerException 折磨:返回值是 null 还是真实对象?调用方往往要靠文档(甚至运行时崩溃)才能知道。Java 8 引入 Optional<T>——一种"可能有值、也可能没有"的容器类型,在 API 签名上明确表达"这个返回值需要你处理 null 情况"。

  • ✅ 最佳应用场景:作为方法返回值——"按 id 查不到用户"、"集合是空时取第一个"、"Map 查不到 key"
  • ✅ 适合:用作 Stream.findFirst / Map.get 等"可能没有"的语义
  • ❌ 反模式:用作字段(占内存又难序列化)、用作方法参数(应该用方法重载)、用作集合元素
  • 原则:Optional 表达的是"接口契约"——告诉调用者"你必须明确处理空情况"

创建 Optional

// CreateOptional.java
import java.util.Optional;

public class CreateOptional {
    public static void main(String[] args) {
        Optional<String> a = Optional.of("alice");      // 值不能是 null,否则 NPE
        Optional<String> b = Optional.ofNullable(null);  // 允许 null,得到 empty
        Optional<String> c = Optional.empty();           // 显式空

        System.out.println(a.isPresent()); // true
        System.out.println(b.isPresent()); // false
        System.out.println(c.isEmpty());   // true (Java 11+)
    }
}

取值:orElse / orElseGet / orElseThrow

三种取值方式的关键区别在"何时计算 fallback":orElse 总会先算好 fallback;orElseGet 只在 empty 时调 supplier(延迟计算,开销大的 fallback 推荐用它);orElseThrow 没值则抛异常。

// GetValue.java
import java.util.Optional;

public class GetValue {
    static String expensiveDefault() {
        System.out.println("computing default...");
        return "default";
    }

    public static void main(String[] args) {
        Optional<String> has = Optional.of("hello");
        Optional<String> none = Optional.empty();

        // orElse:永远先算 expensiveDefault(),哪怕 Optional 有值
        System.out.println(has.orElse(expensiveDefault()));    // 先打印 "computing...",再 hello

        // orElseGet:只在 empty 时调用 supplier
        System.out.println(has.orElseGet(GetValue::expensiveDefault)); // 不会打印 "computing..."
        System.out.println(none.orElseGet(GetValue::expensiveDefault)); // 会

        // orElseThrow:用于"应该有值但没有"的异常路径
        try {
            none.orElseThrow(() -> new IllegalStateException("user not found"));
        } catch (IllegalStateException e) {
            System.out.println("caught: " + e.getMessage());
        }
    }
}

map / flatMap 链式取值

应用场景:用户对象 -> 地址对象 -> 城市名,任意一层可能 null。传统 null 检查写成嵌套 if 又长又脆弱;Optional 链 + map/flatMap 一行搞定。

// ChainNav.java
import java.util.Optional;

public class ChainNav {
    record Address(String city) {}
    record User(String name, Address address) {}
    record Order(User buyer) {}

    static Optional<Order> lookup(long id) {
        return id == 1
            ? Optional.of(new Order(new User("Alice", new Address("Shanghai"))))
            : Optional.empty();
    }

    public static void main(String[] args) {
        // 传统嵌套 null 检查(反例)
        Order order = lookup(1).orElse(null);
        String city = null;
        if (order != null && order.buyer() != null && order.buyer().address() != null) {
            city = order.buyer().address().city();
        }
        System.out.println("old: " + city);

        // 链式:清晰、无 NPE 风险
        String city2 = lookup(1)
            .map(Order::buyer)
            .map(User::address)
            .map(Address::city)
            .orElse("<unknown>");
        System.out.println("new: " + city2);

        // 不存在的 order
        String city3 = lookup(999)
            .map(Order::buyer)
            .map(User::address)
            .map(Address::city)
            .orElse("<unknown>");
        System.out.println("none: " + city3);
    }
}

map vs flatMap:当映射函数本身返回 Optional 时用 flatMap,否则用 map。否则会出现 Optional<Optional<...>>。

// FlatMap.java
import java.util.Optional;

public class FlatMap {
    static Optional<String> nickname(String name) {
        return name.equals("Alice") ? Optional.of("Al") : Optional.empty();
    }

    public static void main(String[] args) {
        Optional<String> user = Optional.of("Alice");

        // map 把 Optional<String> 包装成 Optional<Optional<String>>,不对
        // Optional<Optional<String>> wrong = user.map(FlatMap::nickname);

        // flatMap 自动拆掉一层
        Optional<String> nick = user.flatMap(FlatMap::nickname);
        System.out.println(nick.orElse("-"));
    }
}

ifPresent / ifPresentOrElse

副作用风格的处理:有值时做一件事、可能还要在空时做另一件事。比 if (opt.isPresent()) 更声明式。

// IfPresent.java
import java.util.Optional;

public class IfPresent {
    public static void main(String[] args) {
        Optional<String> name = Optional.of("Alice");

        name.ifPresent(n -> System.out.println("hello, " + n));

        Optional<String> none = Optional.empty();
        none.ifPresentOrElse(
            n  -> System.out.println("hello, " + n),
            () -> System.out.println("no user")
        );
    }
}

filter:条件成立才保留

// FilterOpt.java
import java.util.Optional;

public class FilterOpt {
    public static void main(String[] args) {
        Optional<String> name = Optional.of("");

        String result = name
            .filter(s -> !s.isBlank())   // 空字符串被过滤掉
            .orElse("<empty>");
        System.out.println(result);   // <empty>
    }
}

搭配 Stream

应用场景:把 Stream 里的 Optional 结果拍平成实际值的流;或者 findFirst 之后再做下一步。

// StreamOpt.java
import java.util.List;
import java.util.Optional;

public class StreamOpt {
    static Optional<Integer> parsePositive(String s) {
        try {
            int v = Integer.parseInt(s);
            return v > 0 ? Optional.of(v) : Optional.empty();
        } catch (NumberFormatException e) {
            return Optional.empty();
        }
    }

    public static void main(String[] args) {
        List<String> inputs = List.of("3", "-1", "abc", "7", "0");

        // 解析每个、去掉无效的、求和。Optional::stream 是 Java 9+
        int sum = inputs.stream()
            .map(StreamOpt::parsePositive)
            .flatMap(Optional::stream)
            .mapToInt(Integer::intValue)
            .sum();
        System.out.println(sum); // 10
    }
}

实战:Repository 接口的典型用法

Spring Data 的 JpaRepository.findById 就返回 Optional<T>。模仿这个签名能让 API 显著更清晰——调用方必然要处理"找不到"的情况。

// RepoExample.java
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

public class RepoExample {
    record User(long id, String name, String email) {}

    static class UserRepository {
        private final Map<Long, User> db = new HashMap<>();
        UserRepository() {
            db.put(1L, new User(1, "Alice", "alice@ex.com"));
            db.put(2L, new User(2, "Bob",   "bob@ex.com"));
        }
        Optional<User> findById(long id) {
            return Optional.ofNullable(db.get(id));
        }
    }

    public static void main(String[] args) {
        UserRepository repo = new UserRepository();

        // 1) 取邮箱,不存在用默认
        String email = repo.findById(1)
            .map(User::email)
            .orElse("unknown");
        System.out.println(email);

        // 2) 不存在则抛业务异常
        User u = repo.findById(99)
            .orElseThrow(() -> new IllegalStateException("user 99 not found"));
        System.out.println(u);    // 不会执行,前一行抛了
    }
}

反模式

  • 不要把 Optional 作为类的字段:占内存、难序列化,没人这么用
  • 不要把 Optional 作为方法参数:用方法重载更清晰
  • 不要返回 Optional<List<T>>:返回空列表 List.of() 即可
  • 不要返回 Optional<T[]>:用空数组
  • 不要 opt.get() 不判空:和 throw NullPointerException 没区别
  • 不要 opt.isPresent() + opt.get():用 ifPresent / map / orElse