The practice of Optional in Java 8

I am Xiao Hei, a programmer who "lingers" on the Internet

Pay attention to the official account of the same name [Xiao Hei Java], more dry cargo content is served first, and more occasional lottery offers.

Running water doesn't compete first. It's important to talk

preface

No matter a white programmer, CRUD BOY who has worked for three or five years, or a senior Java Engineer, we often encounter NullPointerException due to the existence of null in our daily development. In order to avoid the occurrence of this exception, we often need to make some non null judgments on the objects used, At the beginning, we will use if... else for processing, such as the following code:

Person person = getPersonById("123");
if (person != null) {
    Address add = person.getAddress();
    if (add != null) {
        String city = add.getCity();
        if (city != null) {
            System.out.println(city);
        }
    }
}

This if judgment leads to poor code readability and maintainability, and it is easy to forget, resulting in bugs.

Java 8 has introduced optional < T > to solve this problem. Let's take a look at the introduction of the official documents.

A container object which may or may not contain a non-null value. If a value is present, isPresent() will return true and get() will return the value.

Additional methods that depend on the presence or absence of a contained value are provided, such as orElse() (return a default value if value not present) and ifPresent() (execute a block of code if the value is present).

This is a value-based class; use of identity-sensitive operations (including reference equality (==), identity hash code, or synchronization) on instances of Optional may have unpredictable results and should be avoided.

From the official documents, we can see that optional < T > is a container object, which stores a value, which may or may not be empty, and also provides a series of methods depending on whether the value is empty; Optional < T > is a value based class, which can also be understood as a value based class. Therefore, operations such as = =, hash and synchronization should be avoided, because these operations are generally based on the spatial address of the object rather than the value.

method

Next, let's take a look at the methods provided by optional < T >.

Create Optional

Optional.empty() returns an empty optional instance.

public static<T> Optional<T> empty() {
    @SuppressWarnings("unchecked")
    Optional<T> t = (Optional<T>) EMPTY;
    return t;
}

Returns an empty Optional.

Optional.of(T value) returns the current non null value with optional

public static <T> Optional<T> of(T value) {
    return new Optional<>(value);
}
private Optional(T value) {
    this.value = Objects.requireNonNull(value);
}

The object passed in by this method must not be null. If it is null, a null pointer exception will be thrown.

Optional.ofNullable(T value) returns an option of the specified value. If it is not empty, it returns an empty option.

public static <T> Optional<T> ofNullable(T value) {
    return value == null ? empty() : of(value);
}

The only difference between ofNullable(T value) and of(T value) is that you can pass in an empty object. If it is empty, it is equal to empty().

Other relevant methods

isPresent

Returns true if a value exists, otherwise false.

public boolean isPresent() {
return value != null;
}

ifPresent

If a value exists, it is used to call the specified consumer, otherwise no action is performed.

public void ifPresent(Consumer<? super T> consumer) {
if (value != null)
consumer.accept(value);
}

Note the difference between and isPresent() method.

orElse

If the value exists, return the value; otherwise, return other.

public T orElse(T other) {
    return value != null ? value : other;
}

orElseGet

Return value (if any), otherwise call other and return the result of the call.

public T orElseGet(Supplier<? extends T> other) {
    return value != null ? value : other.get();
}

orElseThrow

Returns the contained value, if any, or throws an exception created by the provided exceptionSupplier.

public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
    if (value != null) {
        return value;
    } else {
        throw exceptionSupplier.get();
    }
}

map

Convert the original option < T > to option < U >. If the value is empty, the returned option < U > is also empty.

public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent())
        return empty();
    else {
        // Wrap the result of apply as Optional in the map method
        return Optional.ofNullable(mapper.apply(value));
    }
}

For example, the purpose of the following code is to get the address of person in pOptional. If person is empty, it will return optional Empty(), otherwise optional ofNullable(person.getAddress()):

Optional<Person> pOptional = Optional.ofNullable(getPersonById(123));
Optional<Address> address = pOptional.map(new Function<Person, Address>() {
    @Override
    public Address apply(Person person) {
        // The value returned is
        return person.getAddress();
    }
});

The above code can be simplified to:

Optional<Address> add = pOptional.map(Person::getAddress);

flatMap

From the result, the original optional < T > is converted to optional < U > just like the map method, except for the mapper in the method parameter. The mapper of the map() method requires the apply() method to return a specific value, while the apply() method of the mapper parameter of flatMap() returns an optional < U > object.

public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent())
        return empty();
    else {
        return Objects.requireNonNull(mapper.apply(value));
    }
}

The cases in the above map() are implemented as follows using flatMap():

Optional<Address> address1 = pOptional.flatMap(new Function<Person, Optional<Address>>() {
    @Override
    public Optional<Address> apply(Person person) {
        // It needs to be packaged as Optional
        return Optional.of(person.getAddress());
    }
});

It can also be abbreviated as:

pOptional.flatMap(person1 -> Optional.of(person1.getAddress()));

filter

Filter the value. If the value exists and is returned as true in predict, it returns option itself. Otherwise, it returns empty().

public Optional<T> filter(Predicate<? super T> predicate) {
    Objects.requireNonNull(predicate);
    if (!isPresent())
        return this;
    else
        return predicate.test(value) ? this : empty();
}

For example, the following cases:

Optional<Person> pOptional = Optional.ofNullable(getPersonById(123));
Optional<String> nameOpt = pOptional.filter(new Predicate<Person>() {
    @Override
    public boolean test(Person person) {
        return person.getAge() > 10;
    }
}).map(Person::getName);
System.out.println(nameOpt.orElse("Unknown"));

Abbreviated as:

Optional<String> nameOpt = pOptional.filter(p -> p.getAge() > 10).map(Person::getName);
System.out.println(nameOpt.orElse("Unknown"));

So through the above method, do you want to immediately transform your if... else in the code? Review our initial code:

Person person = getPersonById("123");
if (person != null) {
    Address add = person.getAddress();
    if (add != null) {
        String city = add.getCity();
        if (city != null) {
            System.out.println(city);
        }
    }
}

Let's transform it with Optional:

Person person = getPersonById("123");
Optional<String> cityOption = Optional.ofNullable(person)
        .map(Person::getAddress)
        .map(Address::getCity);
System.out.println("city is :"+cityOption.orElse("Unknown city."));

Does it feel like the code looks "tall", but is this correct? In this process, many Optional objects are actually created, which is at least more wasteful in space.

What should be the correct way to use Optional?

Optional usage principles

Brian Goetz, the architect of the Java language, clearly defines:

Optional is intended to provide a limited mechanism for library method return types where there needed to be a clear way to represent "no result," and using null for such was overwhelmingly likely to cause errors.

Optional aims to provide a limited mechanism for the return type of library methods;

It needs a clear way to express "no result";

Using null in this case is likely to lead to an error.

The original design intention of Optional is to serve as the return value of the method to clearly indicate that the method does not return a result, rather than null, so as to avoid the error caused by the caller's return of null.

With this design intention, let's take a look at the principles to be followed when using Optional.

Do not overuse Optional

Sometimes when we learn something, it's like using it immediately. We want to put everything in it. For example, with a hammer, everything looks like a nail.

Always want to knock twice. For example, the following code:

Avoid:

public String fetchStatus() {
    String status = ... ;
    return Optional.ofNullable(status).orElse("PENDING");
}

Instead:

public String fetchStatus() {
    String status = ... ;
    return status == null ? "PENDING" : status;
}

Through the very simple function of binocular operation, it is necessary to use Optional, which not only makes the readability worse, but also constructs an additional Optional object.

Do not declare any domain objects as Optional

Do not set the parameters of any method (especially setter) and constructor to Optional.

Because Optional cannot be serialized, it is not intended to be used as a property of an object at design time.

If you want to use option to ensure that the property of the object is not set to null value, you can implement it as follows:

// PREFER
public class Customer {
    private final String name;     // Cannot be empty
    private final String postcode; // Can be empty
    public Cart(String name, String postcode) {
        this.name = Objects.requireNonNull(name, () -> "Name cannot be null");
        this.postcode = postcode;
    }

    public Optional<String> getPostcode() {
        return Optional.ofNullable(postcode);
    }
    ...
}

You can see that the getPostcode() method returns an Optional, but don't change all getter s in your code, especially when returning a set or array. Just return an empty set and array, because the set itself has an empty() method to judge whether it is empty.

Using Optionalin in method parameters is another common error. This will lead to higher complexity of the code. Parameters should be checked inside the method, rather than forcing the caller to use Optional.

Do not use Optional instead of the collection return value

By returning an empty collection, the method does not return a result Emptylist(), emptyMap(), and emptySet() construct empty returns.

The comparison of Optional values uses equals

Because the Optional class is a value based class, and its equals method itself is to compare values, you can directly use the equals method if you need to compare the values in two options.

public boolean equals(Object obj) {
    if (this == obj) {
        return true;
    }
    if (!(obj instanceof Optional)) {
        return false;
    }
    Optional<?> other = (Optional<?>) obj;
    return Objects.equals(value, other.value);
}

Do not assign Null values to Optional variables

Optional.empty() initializes an option instead of null value. Option is a container. If it is defined as null, it has no meaning.

Avoid the following codes:

public Optional<Cart> fetchCart() {
    Optional<Cart> emptyCart = null;
    ...
}

Instead:

public Optional<Cart> fetchCart() {
    Optional<Cart> emptyCart = Optional.empty();
    ...
}

Make sure that Optional has a value before calling get()

If you need to call the get () method to get the Optional value, be sure to check the value in the Optional before calling; isPresent()-get() is usually used, but this method is not elegant; But if you must choose to use get(), don't forget isPresent().

Avoid the following codes:

Optional<Cart> cart = ... ; // An empty Optional may be returned
...
// If the cart is empty, a Java util. NoSuchElementException
Cart myCart = cart.get();

Instead:

if (cart.isPresent()) {
    Cart myCart = cart.get();
    ... 
} else {
    ... 
}

When the value is null, the default object is returned through the orElse() method

This method is more elegant than isPresent()-get(). However, there is a small problem here, that is, even if the option has a value, the orElse() method will be executed, and an object needs to be built, so there is a small loss in performance.

Avoid the following codes:

public static final String USER_STATUS = "UNKNOWN";
...
public String findUserStatus(long id) {

    Optional<String> status = ... ; // An empty Optional may be returned

    if (status.isPresent()) {
        return status.get();
    } else {
        return USER_STATUS;
    }
}

Instead:

public static final String USER_STATUS = "UNKNOWN";

public String findUserStatus(long id) {

    Optional<String> status = ... ; // An empty Optional may be returned

    return status.orElse(USER_STATUS);
}

When the value is null, the default object is returned through the orElseGet() method

orElseGet() is another elegant alternative to isPresent()-get(), and it is more efficient than orElse(), because the parameter of orElseGet() method is the Supplier in Java 8. The Supplier's get() method will be executed only when the value in Optional is empty, which has no performance loss compared with ` orElse().

Avoid the following codes:

public String computeStatus() {
    ... 
}

public String findUserStatus(long id) {

    Optional<String> status = ... ;

    if (status.isPresent()) {
        return status.get();
    } else {
        return computeStatus();
    }
}

Also avoid:

public String computeStatus() {
    ... 
}

public String findUserStatus(long id) {
    Optional<String> status = ... ; 
    // computeStatus() is also called when status is not empty
    return status.orElse(computeStatus()); 
}

Instead:

public String computeStatus() {
    ... // some code used to compute status
}

public String findUserStatus(long id) {
    Optional<String> status = ... ; 
    // computeStatus() is called only when status is empty
    return status.orElseGet(this::computeStatus);
}

summary

At first glance, Optional seems quite simple, but it is not easy to use it correctly. The Optional API is mainly used for return types to clearly express the possibility that there is no result in the return value.

  • Do not overuse Optional
  • Optional try to use it as the return value of the method
  • Determine whether there is a value before obtaining
  • Use the Optional API appropriately

Some improper use may lead to bugs in some cases. It is recommended to collect this article and refer to it when using Optional.

I am Xiao Hei, a programmer who "lingers" on the Internet

Pay attention to the official account of the same name [Xiao Hei Java], more dry cargo content is served first, and more occasional lottery offers.

Running water doesn't compete first. It's important to talk

Keywords: Java Back-end

Added by Idri on Sun, 02 Jan 2022 18:10:31 +0200