Java - 使用Optional优雅处理null

2019/11/11

Optional是什么?

先来看下文档中的定义:

A container object which may or may not contain a non-null value.

Optional就像是夏威夷果的外壳,而白色果仁就是对象,当然这个对象也有可能根本就没有,也就是null

那么为什么要使用Optional呢?

我的理解是,第一,由于实际项目中很多问题的根源都是NullPointException,因此我们需要有意识地判断某个对象或字段的值是否必须存在。第二,让代码显得更加优雅。

如何完成Optional对象与普通对象之间的转换?

@Test
public void optionalBasicTest(){
    String def  = "default";
    String name = "frankie";
    Optional<String> optName = Optional.of(name);

    String pet = null;
    Optional<String> optPet = Optional.ofNullable(pet);

    String name2 = optName.orElse(def);
    String pet2  = optPet.orElse(def);
}

综合实例

首先介绍下基本情况,假设有个Order类,其中包含orderId, total(订单金额), userInfo(用户信息),而用户信息包含userId, nickName(昵称), avatar(头像)

而我们需要做两件事情,第一,获得头像。第二,获得订单金额。由于头像并非订单中核心属性,因此如果获得不到,则默认为空字符串。但是订单金额是关键,因此,一旦无法正常获得金额,则直接抛异常。

@Setter
@Getter
public class Order {

    private String        orderId;
    private BigDecimal    total;
    private UserInfo      userInfo;
}

public class UserInfo {

    private String userId;
    private String nickName;
    private String avatar;
}

@Test
public void orElseAndOrElseThrowTest(){
    Order order       = new Order();
    UserInfo userInfo = new UserInfo();
    userInfo.setUserId("20004001234");
    order.setOrderId(UUID.randomUUID().toString());
    order.setUserInfo(userInfo);

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

    // Get avatar
    String avatar = optOrder
            .map(Order::getUserInfo)
            .map(UserInfo::getAvatar)
            .orElse("");

    // The avatar is  .
    System.out.println("The avatar is " + avatar + " .");
    
    // Get total
    BigDecimal total = optOrder
            .map(Order::getTotal)
            .orElseThrow(() -> new RuntimeException("Failed to get total!"));

    // java.lang.RuntimeException: Failed to get total!
    System.out.println("The total is " + total + " .");
}

实际例子

我们先来看下之前的写法:

@Service
public class UserUtils {

    public String getUserId(String userId){
        if (userId == null || userId.length() == 0){
            throw new RuntimeException("Failed to get userId.");
        }
        return userId;
    }
}


@Test
public void getUserIdTest(){
    String userId1 = null;
    String userId2 = "";
    String userId3 = "20004001000";

// ---------------------------- Using traditional way ----------------------------
//        java.lang.RuntimeException: Failed to get userId.
    System.out.println("Calling getUserId() using traditional way: " + userUtils.getUserId(userId1));

//        java.lang.RuntimeException: Failed to get userId.
    System.out.println("Calling getUserId() using traditional way: " + userUtils.getUserId(userId2));

//        Calling getUserId using traditional way: 20004001000
    System.out.println("Calling getUserId() using traditional way: " + userUtils.getUserId(userId3));
}


再来看下Optional的实现方式:

@Service
public class UserUtils {

    public String getUserIdUsingOptional(String userId){
        return Optional.ofNullable(userId)
                .filter(x -> !x.isEmpty())
                .orElseThrow(() -> new RuntimeException("Failed to get userId."));
    }
}

@Test
public void getUserIdTest(){
    String userId1 = null;
    String userId2 = "";
    String userId3 = "20004001000";

// ---------------------------- Using Optional way ----------------------------
//        java.lang.RuntimeException: Failed to get userId.
    System.out.println("Calling getUserId() using optional way: " + userUtils.getUserIdUsingOptional(userId1));

//        java.lang.RuntimeException: Failed to get userId.
    System.out.println("Calling getUserId() using optional way: " + userUtils.getUserIdUsingOptional(userId2));

//        Calling getUserId using optional way: 20004001000
    System.out.println("Calling getUserId() using optional way: " + userUtils.getUserIdUsingOptional(userId3));
}


orElse()orElseGet()的区别?

这两个方法的差异取决于optional对象是否包含值,
optional对象为空,相应的方法均会执行。
optional对象有值,orElse()会执行相应方法,但orElseGet()不会。

@Test
public void orElseAndOrElseGetTest(){

//      Situation1: If the optional object is empty, the corresponding methods will be executed.
    System.out.println(Optional.empty().orElse(B()));
//      B()...
//      B

    System.out.println(Optional.empty().orElseGet(() -> B()));
//      B()...
//      B

//      Situation2: If the optional object has a value,
//      orElse() will still execute the corresponding method, but orElse() will not.
    System.out.println(Optional.of("A").orElse(B()));
//      B()...
//      A

    System.out.println(Optional.of("A").orElseGet(() -> B()));
//      A
}


map()flatMap()的区别?

flatMap(): This method is similar to map(),
but the provided mapper is one whose result is already an {@code Optional},
and if invoked, {@code flatMap} does not wrap it with an additional{@code Optional}.

也就是说,当执行flatMap()中的函数时,一旦返回值已经是Optional类型,就不会再包装一层。

@Setter
@Getter
public class Computer {
    public Optional<SoundCard> soundCard;
}

public class SoundCard {
    public Optional<Usb> usb;
}

public class Usb {
    public String version;
}

@Test
public void testDifferenceBetweenMapAndFlatMap(){
    // Illustrate the data structure of class Computer, SoundCard and Usb.
    Computer computer   = new Computer();
    SoundCard soundCard = new SoundCard();
    Usb usb             = new Usb();
    
    usb.setVersion("10.18");
    soundCard.setUsb(Optional.ofNullable(usb));
    computer.setSoundCard(Optional.ofNullable(soundCard));

    Optional<Computer> optionalComputer = Optional.ofNullable(computer);

    // If your function already returns an optional,
    // flatMap() is bit smarter,
    // and does not double wrapper it, returning Optional<U>.
    Optional<SoundCard> optSoundCard1 = optionalComputer
            .flatMap(Computer::getSoundCard);

    Optional<Usb> optUsb1 = optSoundCard1
            .flatMap(SoundCard::getUsb);

    Optional<String> optVersion1 = optUsb1
            .map(Usb::getVersion);

    Optional<Optional<SoundCard>> optSoundCard2 = optionalComputer
            .map(Computer::getSoundCard);

    String version = optVersion1
            .orElse("UNKNOWN");

    System.out.println(version); // 10.18
}

参考


一位喜欢提问、尝试的程序员

(转载本站文章请注明作者和出处 姚屹晨-yaoyichen

Post Directory