Talk about OPTIONAL in Java

 An interesting feature introduced from Java 8 is the Optional class. The main problem the Optional class solves is the notorious NullPointerException-an exception that every Java programmer knows very well.
 Essentially, this is a wrapper class that contains optional values, which means that the Optional class can contain objects or be empty.

 Optional is a powerful step towards functional programming in Java and helps to achieve it in a paradigm. But the meaning of Optional is obviously more than that.


We start with a simple use case. Before Java 8, any call to access object methods or properties may cause NullPointerException:
String isocode = user.getAddress().getCountry().getIsocode().toUpperCase();

In this small example, if we need to ensure that no exception is triggered, we have to explicitly check each value before accessing it:

if (user != null) {
    
    
    Address address = user.getAddress();
    if (address != null) {
    
    
        Country country = address.getCountry();
        if (country != null) {
    
    
            String isocode = country.getIsocode();
            if (isocode != null) {
    
    
                isocode = isocode.toUpperCase();
            }
        }
    }
}

As you can see, this can easily become verbose and difficult to maintain.

To simplify this process, let's take a look at how to do it with the Optional class. From creating and validating an instance, to using its different methods, and combining with other methods that return the same type, the following is the moment to witness the miracle of Optional.


Create Optional instance

To reiterate, objects of this type may contain values ​​or they may be empty. You can create an empty Optional using the method of the same name.

@Test(expected = NoSuchElementException.class)
public void whenCreateEmptyOptional_thenNull() {
    
    
    Optional<User> emptyOpt = Optional.empty();
    emptyOpt.get();
}

Not surprisingly, trying to access the value of the emptyOpt variable will result in a NoSuchElementException.

You can use the of() and ofNullable() methods to create an Optional that contains a value. The difference between the two methods is that if you pass a null value as a parameter, the of() method will throw a NullPointerException:

@Test(expected = NullPointerException.class)
public void whenCreateOfEmptyOptional_thenNullPointerException() {
    
    
    Optional<User> opt = Optional.of(user);
}

You see, we are not completely free from NullPointerException. Therefore, you should use of() when the object is not null.

If the object is either null or non-null, you should use the ofNullable() method:

Optional<User> opt = Optional.ofNullable(user);

Access the value of the Optional object

One of the ways to retrieve the actual value object from the Optional instance is to use the get() method:

@Test
public void whenCreateOfNullableOptional_thenOk() {
    
    
    String name = "John";
    Optional<String> opt = Optional.ofNullable(name);

    assertEquals("John", opt.get());
}

However, as you can see, this method will throw an exception when the value is null. To avoid exceptions, you can choose to verify that there are values ​​first:

@Test
public void whenCheckIfPresent_thenOk() {
    
    
    User user = new User("[email protected]", "1234");
    Optional<User> opt = Optional.ofNullable(user);
    assertTrue(opt.isPresent());

    assertEquals(user.getEmail(), opt.get().getEmail());
}

Another option to check if there is a value is the ifPresent() method. In addition to performing checks, this method also accepts a Consumer (consumer) parameter. If the object is not empty, it executes the passed Lambda expression:

opt.ifPresent( u -> assertEquals(user.getEmail(), u.getEmail()));

In this example, the assertion will be executed only when the user user is not null.

Next, let's take a look at the method of providing null values.


Return to default

The Optional class provides an API to return the value of the object, or return the default value when the object is empty.

The first method you can use here is orElse(). It works very straightforwardly. If there is a value, it returns that value, otherwise it returns the value of the parameter passed to it:

@Test
public void whenEmptyValue_thenReturnDefault() {
    
    
    User user = null;
    User user2 = new User("[email protected]", "1234");
    User result = Optional.ofNullable(user).orElse(user2);

    assertEquals(user2.getEmail(), result.getEmail());
}

Here the user object is empty, so user2 as the default value is returned.

If the initial value of the object is not null, then the default value will be ignored:

@Test
public void whenValueNotNull_thenIgnoreDefault() {
    
    
    User user = new User("[email protected]","1234");
    User user2 = new User("[email protected]", "1234");
    User result = Optional.ofNullable(user).orElse(user2);

    assertEquals("[email protected]", result.getEmail());
}

The second API of the same type is orElseGet()-its behavior is slightly different. This method will return a value when there is a value. If there is no value, it will execute the Supplier functional interface passed in as a parameter and return its execution result:

User result = Optional.ofNullable(user).orElseGet( () -> user2);

The difference between orElse() and orElseGet()

At first glance, these two methods seem to have the same effect. However, it is not. We create some examples to highlight the similarities and differences in their behavior.

Let's take a look at their behavior when the object is empty:

@Test
public void givenEmptyValue_whenCompare_thenOk() {
    
    
    User user = null
    logger.debug("Using orElse");
    User result = Optional.ofNullable(user).orElse(createNewUser());
    logger.debug("Using orElseGet");
    User result2 = Optional.ofNullable(user).orElseGet(() -> createNewUser());
}

private User createNewUser() {
    
    
    logger.debug("Creating New User");
    return new User("[email protected]", "1234");
}

In the above code, both methods call the createNewUser() method, which will record a message and return the User object.

The code output is as follows:

Using orElse
Creating New User
Using orElseGet
Creating New User

This shows that when the object is empty and the default object is returned, there is no difference in behavior.


Let's look at a similar example next, but here Optional is not empty:
@Test
public void givenPresentValue_whenCompare_thenOk() {
    
    
    User user = new User("[email protected]", "1234");
    logger.info("Using orElse");
    User result = Optional.ofNullable(user).orElse(createNewUser());
    logger.info("Using orElseGet");
    User result2 = Optional.ofNullable(user).orElseGet(() -> createNewUser());
}

The output this time:

Using orElse
Creating New User
Using orElseGet

In this example, both Optional objects contain non-null values, and both methods will return corresponding non-null values. However, the orElse() method still creates the User object. In contrast, the orElseGet() method does not create a User object.

When performing more intensive calls, such as calling Web services or data queries, this difference can have a significant impact on performance.


Return exception

In addition to the orElse() and orElseGet() methods, Optional also defines the orElseThrow() API-it will throw an exception when the object is empty, instead of returning an alternative value:

@Test(expected = IllegalArgumentException.class)
public void whenThrowException_thenOk() {
    
    
    User result = Optional.ofNullable(user)
      .orElseThrow( () -> new IllegalArgumentException());
}

Here, if the user value is null, an IllegalArgumentException will be thrown.

This method allows us to have richer semantics and can decide what kind of exception to throw instead of always throwing NullPointerException.

Now that we have a good understanding of how to use Optional, let's take a look at other methods that can transform and filter Optional values.


Conversion value

There are many ways to convert the value of Optional. We start with the map() and flatMap() methods.

Let's first look at an example of using the map() API:

@Test
public void whenMap_thenOk() {
    
    
    User user = new User("[email protected]", "1234");
    String email = Optional.ofNullable(user)
      .map(u -> u.getEmail()).orElse("[email protected]");

    assertEquals(email, user.getEmail());
}

map() applies (calls) a function as a parameter to the value, and then wraps the returned value in an Optional. This makes it possible to perform chain test calls on the return value-the next loop here is orElse().

In contrast, flatMap() also requires a function as a parameter, calls this function on the value, and returns the result directly.

In the following operation, we added a method to the User class to return Optional:

public class User {
    
        
    private String position;

    public Optional<String> getPosition() {
    
    
        return Optional.ofNullable(position);
    }

    //...
}

Since the getter method returns the Optional of the String value, you can use it as a parameter when calling flatMap() on the User's Optional object. The returned value is the unpacked String value:

@Test
public void whenFlatMap_thenOk() {
    
    
    User user = new User("[email protected]", "1234");
    user.setPosition("Developer");
    String position = Optional.ofNullable(user)
      .flatMap(u -> u.getPosition()).orElse("default");

    assertEquals(position, user.getPosition().get());
}

Filter value

In addition to converting values, the Optional class also provides methods to "filter" values ​​based on conditions.

filter() accepts a Predicate parameter and returns the value of the test result as true. If the test result is false, an empty Optional will be returned.

Let's look at an example of deciding whether to accept or reject User based on basic email verification:

@Test
public void whenFilter_thenOk() {
    
    
    User user = new User("[email protected]", "1234");
    Optional<User> result = Optional.ofNullable(user)
      .filter(u -> u.getEmail() != null && u.getEmail().contains("@"));

    assertTrue(result.isPresent());
}

If the filter test is passed, the result object will contain a non-null value.


Chain method of Optional class

In order to use Optional more fully, you can chain and combine most of its methods, because they all return the same and similar objects.

We use Optional to rewrite the first example introduced.

First, refactor the class so that its getter method returns an Optional reference:

public class User {
    
    
    private Address address;

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

    // ...
}
public class Address {
    
    
    private Country country;

    public Optional<Country> getCountry() {
    
    
        return Optional.ofNullable(country);
    }

    // ...
}

The above nested structure can be represented by the following figure:
Insert picture description here
Now you can delete the null check and replace it with the Optional method:

@Test
public void whenChaining_thenOk() {
    
    
    User user = new User("[email protected]", "1234");

    String result = Optional.ofNullable(user)
      .flatMap(u -> u.getAddress())
      .flatMap(a -> a.getCountry())
      .map(c -> c.getIsocode())
      .orElse("default");

    assertEquals(result, "default");
}

The above code can be further reduced by method references:

String result = Optional.ofNullable(user)
  .flatMap(User::getAddress)
  .flatMap(Address::getCountry)
  .map(Country::getIsocode)
  .orElse("default");

As a result, the code now looks much cleaner than the lengthy code that used conditional branching before.


How should Optional be used?

There are some things to consider when using Optional to decide when and how to use it.

The important point is that Optional is not Serializable. Therefore, it should not be used as a field of the class.

If the object you need to serialize contains Optional values, the Jackson library supports treating Optional as normal objects. In other words, Jackson will treat empty objects as null, and valued objects will treat their value as the value of the corresponding domain. This feature is in the jackson-modules-java8 project.


It is also not very useful in another case, when its type is used as a method or construction method parameter. Doing so will complicate the code and is completely unnecessary:
User user = new User("[email protected]", "1234", Optional.empty());

It is much easier to use overloaded methods to handle unnecessary parameters.

Optional is mainly used as a return type. After getting an instance of this type, if it has a value, you can get this value, otherwise you can perform some alternative actions.

The Optional class has a very useful use case, which is to combine it with streams or other methods that return Optional to build a fluent API.

Let's look at an example, using the findFirst() method of Stream to return an Optional object:

@Test
public void whenEmptyStream_thenReturnDefaultOptional() {
    
    
    List<User> users = new ArrayList<>();
    User user = users.stream().findFirst().orElse(new User("default", "1234"));

    assertEquals(user.getEmail(), "default");
}

to sum up

Optional is a useful addition to the Java language-it aims to reduce NullPointerExceptions in the code, although it cannot completely eliminate these exceptions.

It is also well-designed and naturally incorporates the functions supported by Java 8 functionality.

In general, this simple and powerful class helps create programs that are simpler, more readable, and have fewer errors than their counterparts.

Source: https://www.oschina.net/translate/understanding-accepting-and-leveraging-optional-in?lang=chs&page=2#

Guess you like

Origin blog.csdn.net/woaichihanbao/article/details/108071437