Opt

Introduction

When obtaining the properties of nested objects, because the child object cannot know whether it is null, it is necessary to check whether the attribute is null every time the attribute is accessed, which makes the code particularly cumbersome. Therefore, Opt is used to obtain the attribute object value in an elegant chain-like manner.

Declaration: The author of this class is [Achao], and the PR is from https://gitee.com/dromara/hutool/pulls/426.

Usage

First, we define a nested Bean:

// Lombok annotation
@Data
public static class User {
 private String name;
 private String gender;
 private School school;
 
 @Data
 public static class School {
 private String name;
 private String address;
 }
}

Suppose we want to obtain the address attribute:

User user = new User();
user.setName("hello");

// null
String addressValue = Opt.ofNullable(user)
                .map(User::getSchool)
                .map(User.School::getAddress)
                .get();

Because the value of the school object is null, directly obtaining it will cause a null pointer exception. Using Opt can avoid this judgment.

  • The ofBlankAble function is based on the logic of ofNullable, with additional checks for empty strings.
// ofBlankAble considers the situation where the string is an empty string compared to ofNullable
String hutool = Opt.ofBlankAble("").orElse("hutool");
Assert.equals("hutool", hutool);
  • Compared to the original Optional, the get method does not throw NoSuchElementException
  • If you want to use the get method from the original Optional to retrieve a value that is definitely not null, you should use orElseThrow
Object opt = Opt.ofNullable(null).get();
Assert.isNull(isEmpty);
  • This is based on the new isEmpty function in jdk11 Optional, which is used to determine the situation where there is no value
boolean isEmpty = Opt.empty().isEmpty();
Assert.isTrue(isEmpty);
  • The inspiration comes from the new ifPresentOrElse function in jdk9 Optional, which is used to perform some operations when a value exists and perform another operation when there is no value, supporting chaining programming
Opt.ofNullable("Hello Hutool!").ifPresentOrElse(System.out::println, () -> System.err.println("Ops!Something is wrong!"));
Opt.empty().ifPresentOrElse(System.out::println, () -> System.err.println("Ops!Something is wrong!"));
  • A new peek function has been added, which is equivalent to chaining ifPresent invocations (a personal commonly used function)
User user = new User();
Opt.ofNullable("hutool").peek(user::setUsername).peek(user::setNickname);
Assert.equals("hutool", user.getNickname());
Assert.equals("hutool", user.getUsername());
String name = Opt.ofNullable("hutool").peek(username -> username = "123").peek(username -> username = "456").get();
Assert.equals("hutool", name);
  • The inspiration comes from the new or function in jdk11 Optional, which is used to replace with another Opt when a value does not exist
String str = Opt.<String>ofNullable(null).or(() -> Opt.ofNullable("Hello hutool!")).map(String::toUpperCase).orElseThrow();
Assert.equals("HELLO HUTOOL!", str);
User user = User.builder().username("hutool").build();
Opt<User> userOpt = Opt.of(user);
String name = userOpt.map(User::getNickname).or(() -> userOpt.map(User::getUsername)).get();
Assert.equals("hutool", name);
  • Overloaded orElseThrow method is added to support writing hints with double colon operator and custom messages. The original form
orElseThrow(() -> new IllegalStateException("Ops!Something is wrong!"))

has been updated to

orElseThrow(IllegalStateException::new, "Ops!Something is wrong!")

Learning:

I often get asked by friends, “What are these lambda parameters in your Opt function, and how do I know how to write the corresponding lambda?”

Functional programming is such a beautiful thing!

For this situation, we can rely on our powerful IntelliJ IDEA.

For example, when I wrote to this point and got stuck:

User user = new User();
// IDEA prompts the parameter type below, if it's not displayed, move the cursor into the brackets and press Ctrl+p to actively bring it up          |Function<? super User,?> mapper|
Opt.ofNullable(user).map()

IDEA prompts us with the parameter type, but we don’t know what this Function is.

In fact, we just need to new one.

Here IDEA prompts the rest of the code, and we can choose Function:

Opt.ofNullable(user).map(new Function<User, Object>() {
})

Here starts the compilation error. Don’t worry, we will choose the return value based on the specific operation.

For example, I want to check if the user is null here, and if it’s not null, I will call the getSchool method to obtain the return value of type String.

We just need to write it like this, changing the second generic parameter, which represents the return value, to String:

Opt.ofNullable(user).map(new Function<User, String>() {
})

Then we use the “Fix All” feature in IntelliJ IDEA, which is default hotkey Alt+Enter.

Opt.ofNullable(user).map(new Function<User, String>() {
})                                                | 💡 Implement methods                  |  <- Select me
                                                  |   Introduce local variable          |
                                                  |   Rollback changes in current line   |

Select the first option “Implement methods”, and a dialog box will pop up asking you to select the method you want to implement.

Let’s choose our apply method here by pressing Enter or selecting it and clicking the “OK” button.

At this moment, the code becomes:

Opt.ofNullable(user).map(new Function<User, String>() {
    @Override
    public String apply(User user) {
        // Write your own logic here (don't forget to complete the statement with a semicolon at the end)
    }
})

Here, you can write your own logic inside the rewritten method (don’t forget to complete the statement with a semicolon at the end).

Opt.ofNullable(user).map(new Function<User, String>() {
    @Override
    public String apply(User user) {
        return user.getSchool();
    }
});

We can see that new Function<User, String>() has turned gray.

Press alt+enter (Enter) on it.

Opt.ofNullable(user).map(new Function<User, String>() {
    @Override                              | 💡 Replace with lambda             > |  <- Select me
    public String apply(User user) {       | 💡 Replace with method reference   > |
        return user.getSchool();           | 💎 balabala...                     > |
    }                                     |                                   > |
});

Choose the first option Replace with lambda, and it will automatically be shortened to a lambda.

Opt.ofNullable(user).map(user1 -> user1.getSchool());

If you choose the second option, it will be shortened to our double colon format.

Opt.ofNullable(user).map(User::getSchool);

See, it’s simple, isn’t it?