java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java Optional处理空值

Java Optional优雅处理空值与链式转换的实战指南

作者:唐青枫

Optional 是 Java 8 引入的一个容器类,这篇文章主要介绍了Java8中Optional类的使用场景和方法,包括创建Optional、处理null、链式调用以及与Stream的配合等,希望对大家有所帮助

简介

OptionalJava 8 引入的一个容器类,包路径是:

java.util.Optional

它用来表达一种很常见的情况:这个结果可能有值,也可能没有值。

比如按 ID 查询用户:

User user = userRepository.findById(1L);

如果找到了,返回 User

如果没找到,传统写法通常返回 null

Optional 的写法是:

Optional<User> user = userRepository.findById(1L);

这段代码的含义更明确:findById 可能查到用户,也可能查不到用户。

一句话概括:Optional 用来把“可能为空”这件事写进方法返回值里,让空值处理变得更清楚。

为什么需要 Optional

先看一段普通判空代码:

public String getUserCity(User user) {
    if (user != null) {
        Address address = user.getAddress();
        if (address != null) {
            String city = address.getCity();
            if (city != null) {
                return city;
            }
        }
    }

    return "未知城市";
}

代码本身不复杂,但层级一多,判空会越来越长。

如果中间漏掉一次判空:

return user.getAddress().getCity();

只要 useraddressnull,就会出现:

NullPointerException

使用 Optional 后,可以把“取值、转换、兜底”连在一起:

public String getUserCity(User user) {
    return Optional.ofNullable(user)
            .map(User::getAddress)
            .map(Address::getCity)
            .orElse("未知城市");
}

这段代码表达的是:

Optional 的基本结构

Optional<T> 里的 T 表示实际包装的数据类型。

Optional<String> name;
Optional<User> user;
Optional<Order> order;

Optional 只有两种状态:

创建一个有值的 Optional

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

创建一个空的 Optional

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

创建 Optional

常用创建方式有 3 个:

Optional.of

Optional.of 用来包装一个确定不为 null 的值。

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

System.out.println(name);

输出:

Optional[Java]

如果传入 null

Optional<String> name = Optional.of(null);

会直接抛出:

NullPointerException

所以 of 适合这种场景:值一定不为空,只是需要包装成 Optional。

Optional.ofNullable

Optional.ofNullable 是最常见的创建方式。

它允许传入 null

String value = null;

Optional<String> name = Optional.ofNullable(value);

System.out.println(name);

输出:

Optional.empty

如果传入非空值:

String value = "Java";

Optional<String> name = Optional.ofNullable(value);

System.out.println(name);

输出:

Optional[Java]

日常项目里,包装一个来源不确定的对象时,通常使用 ofNullable

Optional.empty

Optional.empty 用来创建一个空的 Optional

Optional<User> user = Optional.empty();

它常用于方法返回空结果:

public Optional<User> findUserById(Long id) {
    if (id == null) {
        return Optional.empty();
    }

    User user = queryUser(id);
    return Optional.ofNullable(user);
}

判断是否有值

Optional 提供了两个常见判断方法:

isPresent() 表示是否有值:

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

System.out.println(name.isPresent());

输出:

true

isEmpty() 表示是否为空,它是 Java 11 新增的方法:

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

System.out.println(name.isEmpty());

输出:

true

判断后再 get 的写法可以用,但不算 Optional 最有价值的写法:

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

if (name.isPresent()) {
    System.out.println(name.get());
}

更常见的写法是直接使用 ifPresentmaporElse 等方法。

get 取值

get() 可以直接取出 Optional 里的值。

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

System.out.println(name.get());

输出:

Java

但空 Optional 调用 get() 会抛异常:

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

System.out.println(name.get());

异常:

NoSuchElementException: No value present

所以实际项目里更常见的是先明确空值处理方式,而不是直接写:

optional.get()

更合适的方式是:

optional.orElse(...)
optional.orElseGet(...)
optional.orElseThrow(...)
optional.ifPresent(...)

ifPresent:有值才执行

ifPresent 适合处理“有值就做一件事,没值就不处理”的场景。

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

name.ifPresent(value -> System.out.println("name = " + value));

输出:

name = Java

如果是空值:

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

name.ifPresent(value -> System.out.println("name = " + value));

不会输出任何内容。

ifPresentOrElse

ifPresentOrElseJava 9 新增的方法。

它可以同时处理有值和无值两种情况。

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

name.ifPresentOrElse(
        value -> System.out.println("name = " + value),
        () -> System.out.println("name 不存在")
);

输出:

name 不存在

orElse:为空时返回默认值

orElse 用来给空值设置默认值。

String name = Optional.ofNullable(null)
        .orElse("默认名称");

System.out.println(name);

输出:

默认名称

有值时返回原值:

String name = Optional.ofNullable("Java")
        .orElse("默认名称");

System.out.println(name);

输出:

Java

orElseGet:为空时再生成默认值

orElseGet 接收的是一个 Supplier

只有 Optional 为空时,才会执行里面的逻辑。

String name = Optional.ofNullable(null)
        .orElseGet(() -> "默认名称");

System.out.println(name);

输出:

默认名称

orElse 和 orElseGet 的区别

这两个方法很像,但执行时机不一样。

准备一个方法:

private static String createDefaultName() {
    System.out.println("生成默认名称");
    return "默认名称";
}

使用 orElse

String name = Optional.of("Java")
        .orElse(createDefaultName());

System.out.println(name);

输出:

生成默认名称
Java

虽然 Optional 里已经有 "Java",但 createDefaultName() 还是执行了。

再看 orElseGet

String name = Optional.of("Java")
        .orElseGet(() -> createDefaultName());

System.out.println(name);

输出:

Java

createDefaultName() 没有执行。

简单理解:

orElse:默认值先准备好,不管最后用不用
orElseGet:真正为空时,才去生成默认值

如果默认值只是一个普通字符串,用 orElse 很自然:

String name = optionalName.orElse("匿名用户");

如果默认值需要查询数据库、调用接口、创建复杂对象,用 orElseGet 更合适:

User user = optionalUser.orElseGet(() -> userRepository.createGuestUser());

orElseThrow:为空时抛异常

有些场景下,空值不是正常结果,而是业务异常。

比如订单必须存在:

Order order = orderRepository.findById(orderId)
        .orElseThrow(() -> new IllegalArgumentException("订单不存在"));

Java 10 开始,orElseThrow() 可以不传参数。

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

空值时会抛出:

NoSuchElementException

业务代码里通常更推荐带上明确的异常类型和提示信息:

User user = userRepository.findById(userId)
        .orElseThrow(() -> new IllegalStateException("用户不存在"));

map:转换里面的值

map 用来把 Optional 里的值转换成另一个值。

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

Optional<Integer> length = name.map(String::length);

System.out.println(length.orElse(0));

输出:

4

如果原来的 Optional 是空的,map 不会执行:

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

Optional<Integer> length = name.map(value -> {
    System.out.println("计算长度");
    return value.length();
});

System.out.println(length.orElse(0));

输出:

0

不会输出 计算长度

map 处理对象属性

map 很适合用来安全获取对象属性。

String city = Optional.ofNullable(user)
        .map(User::getAddress)
        .map(Address::getCity)
        .orElse("未知城市");

这段代码里:

不用写多层 if,也不用担心中间某一步出现空指针。

flatMap:处理返回 Optional 的方法

如果转换方法本身返回的就是 Optional,应该使用 flatMap

先看一个类:

class User {
    private final Address address;

    User(Address address) {
        this.address = address;
    }

    public Optional<Address> getAddress() {
        return Optional.ofNullable(address);
    }
}

此时 getAddress() 返回的是:

Optional<Address>

如果使用 map

Optional<Optional<Address>> address = Optional.of(user)
        .map(User::getAddress);

结果会变成嵌套结构:

Optional<Optional<Address>>

使用 flatMap 可以把嵌套压平:

Optional<Address> address = Optional.of(user)
        .flatMap(User::getAddress);

再配合城市字段:

String city = Optional.of(user)
        .flatMap(User::getAddress)
        .flatMap(Address::getCity)
        .orElse("未知城市");

简单区分:

filter:按条件保留值

filter 用来给 Optional 里的值加条件。

条件满足,值保留。

条件不满足,变成空 Optional

Optional<Integer> age = Optional.of(20);

Optional<Integer> adultAge = age.filter(value -> value >= 18);

System.out.println(adultAge.isPresent());

输出:

true

如果条件不满足:

Optional<Integer> age = Optional.of(16);

Optional<Integer> adultAge = age.filter(value -> value >= 18);

System.out.println(adultAge.isPresent());

输出:

false

常见业务写法:

Optional<User> activeUser = Optional.ofNullable(user)
        .filter(User::isActive);

表示:

user 不为空,并且是启用状态,才继续保留。

or:为空时切换到另一个 Optional

orJava 9 新增的方法。

它适合处理多个来源的查找逻辑。

Optional<User> user = findByEmail(email)
        .or(() -> findByPhone(phone))
        .or(() -> findByUsername(username));

含义是:

注意,or 返回的还是 Optional

User result = findByEmail(email)
        .or(() -> findByPhone(phone))
        .or(() -> findByUsername(username))
        .orElseThrow(() -> new IllegalArgumentException("用户不存在"));

stream:把 Optional 转成 Stream

streamJava 9 新增的方法。

它可以把一个 Optional 转成包含 0 个或 1 个元素的 Stream

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

long count = name.stream().count();

System.out.println(count);

输出:

1

Optional

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

long count = name.stream().count();

System.out.println(count);

输出:

0

它常用于集合处理。

比如有一组用户 ID,需要查询用户,但有些 ID 查不到:

List<User> users = userIds.stream()
        .map(userRepository::findById)
        .flatMap(Optional::stream)
        .collect(Collectors.toList());

这里的 findById 返回 Optional<User>

flatMap(Optional::stream) 会自动丢掉空结果,只留下查到的用户。

实战 Demo:订单查询与金额展示

下面这组代码可以直接复制运行,演示 Optional 在业务代码里的常见用法。

import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

public class OptionalOrderDemo {

    public static void main(String[] args) {
        OrderService orderService = new OrderService();

        System.out.println(orderService.getBuyerName("A001"));
        System.out.println(orderService.getBuyerName("A002"));
        System.out.println(orderService.getBuyerName("A404"));

        System.out.println(orderService.getPaidAmountText("A001"));
        System.out.println(orderService.getPaidAmountText("A002"));
        System.out.println(orderService.getPaidAmountText("A003"));

        orderService.findOrder("A001")
                .filter(Order::isPaid)
                .ifPresent(order -> System.out.println("已支付订单: " + order.getOrderNo()));

        try {
            Order order = orderService.findOrder("A404")
                    .orElseThrow(() -> new IllegalArgumentException("订单不存在"));
        } catch (IllegalArgumentException e) {
            System.out.println("异常信息: " + e.getMessage());
        }
    }

    static class OrderService {
        private final List<Order> orders = Arrays.asList(
                new Order("A001", new Buyer("张三", new Address("上海")), new BigDecimal("99.00"), "PAID"),
                new Order("A002", new Buyer("李四", null), new BigDecimal("35.50"), "PAID"),
                new Order("A003", null, new BigDecimal("88.00"), "CANCELLED")
        );

        public Optional<Order> findOrder(String orderNo) {
            return orders.stream()
                    .filter(order -> order.getOrderNo().equals(orderNo))
                    .findFirst();
        }

        public String getBuyerName(String orderNo) {
            return findOrder(orderNo)
                    .map(Order::getBuyer)
                    .map(Buyer::getName)
                    .orElse("未知买家");
        }

        public String getPaidAmountText(String orderNo) {
            return findOrder(orderNo)
                    .filter(Order::isPaid)
                    .map(Order::getAmount)
                    .map(amount -> "支付金额: " + amount)
                    .orElse("未支付或订单不存在");
        }

        public String getBuyerCity(String orderNo) {
            return findOrder(orderNo)
                    .map(Order::getBuyer)
                    .map(Buyer::getAddress)
                    .map(Address::getCity)
                    .orElse("未知城市");
        }
    }

    static class Order {
        private final String orderNo;
        private final Buyer buyer;
        private final BigDecimal amount;
        private final String status;

        Order(String orderNo, Buyer buyer, BigDecimal amount, String status) {
            this.orderNo = orderNo;
            this.buyer = buyer;
            this.amount = amount;
            this.status = status;
        }

        String getOrderNo() {
            return orderNo;
        }

        Buyer getBuyer() {
            return buyer;
        }

        BigDecimal getAmount() {
            return amount;
        }

        boolean isPaid() {
            return "PAID".equals(status);
        }
    }

    static class Buyer {
        private final String name;
        private final Address address;

        Buyer(String name, Address address) {
            this.name = name;
            this.address = address;
        }

        String getName() {
            return name;
        }

        Address getAddress() {
            return address;
        }
    }

    static class Address {
        private final String city;

        Address(String city) {
            this.city = city;
        }

        String getCity() {
            return city;
        }
    }
}

运行结果:

张三
李四
未知买家
支付金额: 99.00
支付金额: 35.50
未支付或订单不存在
已支付订单: A001
异常信息: 订单不存在

这个 Demo 里包含了几个典型场景:

实战 Demo:查询不到时再走备用查询

实际项目里,经常会出现多个查询入口。

比如先按邮箱查用户,查不到再按手机号查。

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

public class OptionalFallbackDemo {

    public static void main(String[] args) {
        UserService userService = new UserService();

        User user = userService.findByEmail("none@example.com")
                .or(() -> userService.findByPhone("13800000000"))
                .orElseThrow(() -> new IllegalArgumentException("用户不存在"));

        System.out.println(user.getName());
    }

    static class UserService {
        private final List<User> users = Arrays.asList(
                new User("张三", "zhangsan@example.com", "13800000000"),
                new User("李四", "lisi@example.com", "13900000000")
        );

        public Optional<User> findByEmail(String email) {
            return users.stream()
                    .filter(user -> user.getEmail().equals(email))
                    .findFirst();
        }

        public Optional<User> findByPhone(String phone) {
            return users.stream()
                    .filter(user -> user.getPhone().equals(phone))
                    .findFirst();
        }
    }

    static class User {
        private final String name;
        private final String email;
        private final String phone;

        User(String name, String email, String phone) {
            this.name = name;
            this.email = email;
            this.phone = phone;
        }

        String getName() {
            return name;
        }

        String getEmail() {
            return email;
        }

        String getPhone() {
            return phone;
        }
    }
}

输出:

张三

这类写法比手动判断更紧凑:

Optional<User> user = findByEmail(email);

if (user.isEmpty()) {
    user = findByPhone(phone);
}

Optional 和 Stream 的配合

Stream 里的很多方法本身就会返回 Optional

比如 findFirst

Optional<String> first = Arrays.asList("Java", "Kotlin", "Go")
        .stream()
        .filter(name -> name.startsWith("K"))
        .findFirst();

System.out.println(first.orElse("没有匹配结果"));

输出:

Kotlin

再比如求最大值:

Optional<Integer> max = Arrays.asList(10, 20, 30)
        .stream()
        .max(Integer::compareTo);

System.out.println(max.orElse(0));

输出:

30

为什么这些方法返回 Optional

因为结果可能不存在。

Optional<String> first = List.<String>of()
        .stream()
        .findFirst();

空集合里没有第一个元素,所以返回空 Optional

OptionalInt、OptionalLong、OptionalDouble

除了 Optional<T>,Java 还提供了 3 个基本类型版本:

它们用来避免基本类型装箱。

import java.util.OptionalInt;

public class OptionalIntDemo {
    public static void main(String[] args) {
        OptionalInt maxAge = OptionalInt.of(30);

        System.out.println(maxAge.orElse(0));
    }
}

输出:

30

IntStream 的一些操作也会返回 OptionalInt

OptionalInt max = Arrays.asList(10, 20, 30)
        .stream()
        .mapToInt(Integer::intValue)
        .max();

System.out.println(max.orElse(0));

输出:

30

常见使用建议

适合作为方法返回值

Optional 最适合放在方法返回值上。

public Optional<User> findById(Long id) {
    User user = queryUser(id);
    return Optional.ofNullable(user);
}

调用方看到返回值类型,就知道这个方法可能查不到数据。

User user = userRepository.findById(id)
        .orElseThrow(() -> new IllegalArgumentException("用户不存在"));

不适合作为实体字段

实体字段通常不建议写成 Optional

public class User {
    private Optional<String> nickname;
}

更常见的写法是保持字段为普通类型:

public class User {
    private String nickname;

    public Optional<String> getNickname() {
        return Optional.ofNullable(nickname);
    }
}

原因很简单:字段表示数据本身,Optional 更适合表达方法返回结果是否存在。

对于数据库实体、JSON 序列化、ORM 映射,这种写法也更自然。

不适合作为方法参数

方法参数也通常不建议写成 Optional

public void updateNickname(Long userId, Optional<String> nickname) {
}

调用这个方法时,反而多了一层包装:

updateNickname(1L, Optional.of("小张"));
updateNickname(1L, Optional.empty());

更常见的写法是传普通参数:

public void updateNickname(Long userId, String nickname) {
}

如果参数有明确的业务含义,可以拆成更清楚的方法:

public void updateNickname(Long userId, String nickname) {
}

public void clearNickname(Long userId) {
}

不适合放进集合

集合里通常不建议放 Optional

List<Optional<User>> users;

更常见的是:

List<User> users;

如果没有数据,用空集合表示:

List<User> users = Collections.emptyList();

如果集合中某些元素可能不存在,一般在收集前处理掉空值:

List<User> users = userIds.stream()
        .map(userRepository::findById)
        .flatMap(Optional::stream)
        .collect(Collectors.toList());

get 的使用边界

get() 只有在已经确定有值时才安全。

if (optional.isPresent()) {
    User user = optional.get();
}

但大多数场景可以改成更直接的写法。

有默认值:

User user = optional.orElse(defaultUser);

空值时报错:

User user = optional.orElseThrow(() -> new IllegalArgumentException("用户不存在"));

有值时执行逻辑:

optional.ifPresent(user -> sendMessage(user));

转换字段:

String name = optional.map(User::getName).orElse("匿名用户");

链式调用不宜过长

Optional 支持链式调用,但业务逻辑很复杂时,拆成普通变量会更清楚。

比如这类代码:

String result = Optional.ofNullable(order)
        .map(Order::buyer)
        .filter(Buyer::isVip)
        .map(Buyer::address)
        .map(Address::city)
        .filter(city -> city.startsWith("上"))
        .map(city -> city + " VIP")
        .orElse("普通订单");

如果中间夹杂很多业务规则,拆开后通常更容易维护:

Optional<Order> orderOptional = Optional.ofNullable(order);

Optional<Buyer> vipBuyer = orderOptional
        .map(Order::buyer)
        .filter(Buyer::isVip);

String result = vipBuyer
        .map(Buyer::address)
        .map(Address::city)
        .filter(city -> city.startsWith("上"))
        .map(city -> city + " VIP")
        .orElse("普通订单");

链式调用适合表达清晰的数据流。

逻辑复杂时,适当拆开,变量名本身就是说明。

常用方法汇总

方法作用常见场景
Optional.of(value)创建有值的 Optional,值不能为 null明确值不为空
Optional.ofNullable(value)创建可能为空的 Optional包装外部返回值
Optional.empty()创建空 Optional返回空结果
isPresent()判断是否有值简单分支判断
isEmpty()判断是否为空Java 11+
ifPresent(...)有值时执行逻辑打印、通知、补充操作
ifPresentOrElse(...)有值和无值分别处理Java 9+
orElse(...)为空时返回默认值默认字符串、默认数字
orElseGet(...)为空时执行 Supplier 获取默认值默认值创建成本较高
orElseThrow(...)为空时抛异常必须存在的数据
map(...)转换内部值取字段、格式化
flatMap(...)转换返回 Optional 的值避免 Optional<Optional<T>>
filter(...)条件过滤状态判断、权限判断
or(...)为空时切换到另一个 OptionalJava 9+,备用查询
stream()转成 0 或 1 个元素的 StreamJava 9+,集合流水线

总结

Optional 的重点不是把所有 null 都消灭掉,而是把“结果可能不存在”这件事表达清楚。

最常见的使用流程是:

Optional.ofNullable(value)
        .map(...)
        .filter(...)
        .orElse(...);

或者:

repository.findById(id)
        .orElseThrow(() -> new IllegalArgumentException("数据不存在"));

实际项目里,Optional 最适合放在方法返回值上,用来表示查询结果、匹配结果、计算结果可能为空。

只要控制好边界,不把它塞进实体字段、方法参数和集合元素里,代码会更容易看出空值处理逻辑。

以上就是Java Optional优雅处理空值与链式转换的实战指南的详细内容,更多关于Java Optional处理空值的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文