Quantcast
Channel: Baeldung
Viewing all articles
Browse latest Browse all 4536

Guide To Java 8 Optional

$
0
0

The Master Class of "Learn Spring Security" is live:

>> CHECK OUT THE COURSE

1. Overview

In this article, we’re going to show the new Optional class that was introduced in Java 8.

The purpose of the class is to provide a type-level solution for representing optional values instead of using null references.

To get a deeper understanding of why you should care about the Optional class, take a look at the official Oracle’s article.

Optional is part of the java.util package, so we need to make this import:

import java.util.Optional;

2. Creating Optional Objects

There are several ways of creating Optional objects. To create an empty Optional object:

@Test
public void whenCreatesEmptyOptional_thenCorrect() {
    Optional<String> empty = Optional.empty();
    assertFalse(empty.isPresent());
}

The isPresent API is used to check if there is a value inside the Optional object. A value is present if and only if we have created Optional with a non-null value. We will look at the isPresent API in the next section.

We can also create an Optional object with the static of API:

@Test
public void givenNonNull_whenCreatesNonNullable_thenCorrect() {
    String name = "baeldung";
    Optional.of(name);
}

The value is available to process:

@Test
public void givenNonNull_whenCreatesOptional_thenCorrect() {
    String name = "baeldung";
    Optional<String> opt = Optional.of(name);
    assertEquals("Optional[baeldung]", opt.toString());
}

However, the argument passed to the of method cannot be null, otherwise, we will get a NullPointerException:

@Test(expected = NullPointerException.class)
public void givenNull_whenThrowsErrorOnCreate_thenCorrect() {
    String name = null;
    Optional<String> opt = Optional.of(name);
}

But, in case we expect some null values for the passed in argument, we can use the ofNullable API:

@Test
public void givenNonNull_whenCreatesNullable_thenCorrect() {
    String name = "baeldung";
    Optional<String> opt = Optional.ofNullable(name);
    assertEquals("Optional[baeldung]", opt.toString());
}

In this way, if we pass in a null reference, it does not throw an exception but rather returns an empty Optional object as though we created it with the Optional.empty API:

@Test
public void givenNull_whenCreatesNullable_thenCorrect() {
    String name = null;
    Optional<String> opt = Optional.ofNullable(name);
    assertEquals("Optional.empty", opt.toString());
}

3. Checking Value With isPresent()

When we have an Optional object returned from an accessor method or created by us, we can check if there is a value in it or not with the isPresent API:

@Test
public void givenOptional_whenIsPresentWorks_thenCorrect() {
    Optional<String> opt = Optional.of("Baeldung");
    assertTrue(opt.isPresent());

    opt = Optional.ofNullable(null);
    assertFalse(opt.isPresent());
}

This API returns true if and only if the value wrapped is not null.

4. Conditional Action With ifPresent()

The ifPresent API enables us to run some code on the wrapped value if it is found to be non-null. Before Optional, we would do something like this:

if(name != null){
    System.out.println(name.length);
}

This code checks if the name variable is null or not before going ahead to execute some code on it. This approach is lengthy and that is not the only problem. Inherent in this approach is a lot of potential for bugs.

After getting used to such an approach, one could easily forget to perform the null checks in some part of the code.

This can result in a NullPointerException at runtime if a null value finds its way into that code. When a program fails due to input issues, it is often a result of poor programming practices.

Optional makes one deal with nullable values explicitly as a way of enforcing good programming practice. Let us now look at how the above code could be refactored in Java 8.

In typical functional programming style, we can execute perform an action on an object that is actually present:

@Test
public void givenOptional_whenIfPresentWorks_thenCorrect() {
    Optional<String> opt = Optional.of("baeldung");

    opt.ifPresent(name -> System.out.println(name.length()));
}

In the above example, we use only two lines of code to replace the five that worked in the first example. One line to wrap the object into an Optional object and the next to perform implicit validation as well as execute the code.

5. Default Value With orElse

The orElse API is used to retrieve the value wrapped inside an Optional instance. It takes one parameter which acts as a default value. With orElse, the wrapped value is returned if it is present and the argument given to orElse is returned if the wrapped value is absent:

@Test
public void whenOrElseWorks_thenCorrect() {
    String nullName = null;
    String name = Optional.ofNullable(nullName).orElse("john");
    assertEquals("john", name);
}

6. Default Value With orElseGet

The orElseGet API is similar to orElse. However, instead of taking a value to return if the Optional value is not present, it takes a supplier functional interface which is invoked and returns the value of the invocation:

@Test
public void whenOrElseGetWorks_thenCorrect() {
    String nullName = null;
    String name = Optional.ofNullable(nullName).orElseGet(() -> "john");
    assertEquals("john", name);
}

7. Difference Between orElse and orElseGet

To a lot of programmers who are new to Optional or Java 8, the difference between orElse and orElseGet is not clear. As a matter if fact, these two APIs give the impression that they overlap each other in functionality.

However, there is a subtle but very important difference between the two which can affect the performance of your code drastically if not well understood.

Let’s create a method called getMyDefault in the test class which takes no arguments and returns a default value:

public String getMyDefault() {
    System.out.println("Getting Default Value");
    return "Default Value";
}

We will create two tests and observe their side effects to establish both where these two APIs overlap and where they differ:

@Test
public void whenOrElseGetAndOrElseOverlap_thenCorrect() {
    String text;

    System.out.println("Using orElseGet:");
    String defaultText = 
      Optional.ofNullable(text).orElseGet(this::getMyDefault);
    assertEquals("Default Value", defaultText);

    System.out.println("Using orElse:");
    defaultText = Optional.ofNullable(text).orElse(getMyDefault());
    assertEquals("Default Value", defaultText);
}

In the above example, we wrap a null text inside an Optional object and we attempt to get the wrapped value using each of the two approaches. The side effect is as below:

Using orElseGet:
Getting default value...
Using orElse:
Getting default value...

The getMyDefault API is called in each case. It so happens that when the wrapped value is not present, then both orElse and orElseGet APIs work exactly the same way.

Now let’s run another test where the value is present and ideally, the default value should not even be created:

@Test
public void whenOrElseGetAndOrElseDiffer_thenCorrect() {
    String text = "Text present";

    System.out.println("Using orElseGet:");
    String defaultText 
      = Optional.ofNullable(text).orElseGet(this::getMyDefault);
    assertEquals("Text present", defaultText);

    System.out.println("Using orElse:");
    defaultText = Optional.ofNullable(text).orElse(getMyDefault());
    assertEquals("Text present", defaultText);
}

In the above example, we are no longer wrapping a null value and the rest of the code remains the same. Now let’s take a look at the side effect of running this code:

Using orElseGet:
Using orElse:
Getting default value...

Notice that when using orElseGet to retrieve the wrapped value, the getMyDefault API is not even invoked since the contained value is present.

However, when using orElse, whether the wrapped value is present or not, the default object is created. So in this case, we have just created one redundant object that is never used.

In this simple example, there is no significant cost to creating a default object, as the JVM knows how to deal with such. However, when a method such as getMyDefault has to make a web service call or even query a database, then the cost becomes very obvious

8. Exceptions with orElseThrow

The orElseThrow API follows from orElse and orElseGet in and adds a new approach for handling an absent value. Instead of returning a default value when the wrapped value is not present, it throws an exception:

@Test(expected = IllegalArgumentException.class)
public void whenOrElseThrowWorks_thenCorrect() {
    String nullName = null;
    String name = Optional.ofNullable(nullName).orElseThrow(
      IllegalArgumentException::new);
}

Method references in Java 8 come in handy here, to pass in the exception constructor.

9. Returning Value with get()

The final approach for retrieving the wrapped value is the get API:

@Test
public void givenOptional_whenGetsValue_thenCorrect() {
    Optional<String> opt = Optional.of("baeldung");
    String name = opt.get();

    assertEquals("baeldung", name);
}

However, unlike the above three approaches, the get API can only return a value if the wrapped object is not null, otherwise, it throws a no such element exception:

@Test(expected = NoSuchElementException.class)
public void givenOptionalWithNull_whenGetThrowsException_thenCorrect() {
    Optional<String> opt = Optional.ofNullable(null);
    String name = opt.get();
}

This is the major flaw of the get API. Ideally, Optional should help us to avoid such unforeseen exceptions. Therefore, this approach works against the objectives of Optional and will probably be deprecated in a future release.

It is, therefore, advisable to use the other variants which enable us to prepare for and explicitly handle the null case.

10. Conditional Return with filter()

The filter API is used to run an inline test on the wrapped value. It takes a predicate as an argument and returns an Optional object. If the wrapped value passes testing by the predicate, then the Optional is returned as is.

However, if the predicate returns false, then an empty Optional is returned:

@Test
public void whenOptionalFilterWorks_thenCorrect() {
    Integer year = 2016;
    Optional<Integer> yearOptional = Optional.of(year);
    boolean is2016 = yearOptional.filter(y -> y == 2016).isPresent();
    assertTrue(is2016);
    boolean is2017 = yearOptional.filter(y -> y == 2017).isPresent();
    assertFalse(is2017);
}

The filter API is normally used this way to reject wrapped values based on a predefined rule. You could use it to reject a wrong email format or a password that is not strong enough.

Let’s look at another meaningful example. Let’s say we want to buy a modem and we only care about its price. We receive push notifications on modem prices from a certain site and store these in objects:

public class Modem {
    private Double price;

    public Modem(Double price) {
        this.price = price;
    }
    //standard getters and setters
}

We then feed these objects to some code whose sole purpose is to check if the modem price is within our budget range for it. We want to spend between ten to fifteen dollars on it. Let us now take a look at the code without Optional:

public boolean priceIsInRange1(Modem modem) {
    boolean isInRange = false;

    if (modem != null && modem.getPrice() != null 
      && (modem.getPrice() >= 10 
        && modem.getPrice() <= 15)) {

        isInRange = true;
    }
    return isInRange;
}

Pay attention to how much code we have to write to achieve this, especially in the if condition. The only part of the if condition that is critical to the application is the last price-range check; the rest of the checks are defensive:

@Test
public void whenFiltersWithoutOptional_thenCorrect() {
    assertTrue(priceIsInRange1(new Modem(10.0)));
    assertFalse(priceIsInRange1(new Modem(9.9)));
    assertFalse(priceIsInRange1(new Modem(null)));
    assertFalse(priceIsInRange1(new Modem(15.5)));
    assertFalse(priceIsInRange1(null));
}

Apart from that, it’s possible to forget about the null checks on a long day without getting any compile time errors.

Now let us look at a variant with Optional filter API:

public boolean priceIsInRange2(Modem modem2) {
     return Optional.ofNullable(modem2)
       .map(Modem::getPrice)
       .filter(p -> p >= 10)
       .filter(p -> p <= 15)
       .isPresent();
 }

The map call is simply used to transform a value to some other value. Keep in mind that this operation does not modify the original value.

In our case, we are obtaining a price object from the Model class. We will look at the map API in detail in the next section.

First of all, if a null object is passed to this API, we don’t expect any problem.

Secondly, the only logic we write inside its body is exactly what the API name describes, price range check. Optional takes care of the rest:

@Test
public void whenFiltersWithOptional_thenCorrect() {
    assertTrue(priceIsInRange2(new Modem(10.0)));
    assertFalse(priceIsInRange2(new Modem(9.9)));
    assertFalse(priceIsInRange2(new Modem(null)));
    assertFalse(priceIsInRange2(new Modem(15.5)));
    assertFalse(priceIsInRange2(null));
}

The previous API promises to check price range but has to do more than that to defend against its inherent fragility. Therefore, the filter API can be used to replace unnecessary if statements to reject unwanted values.

11. Transforming Value with map()

In the previous section, we looked at how to reject or accept a value based on a filter.  A similar syntax can be used to transform the Optional value with the map API:

@Test
public void givenOptional_whenMapWorks_thenCorrect() {
    List<String> companyNames = Arrays.asList(
      "paypal", "oracle", "", "microsoft", "", "apple");
    Optional<List<String>> listOptional = Optional.of(companyNames);

    int size = listOptional
      .map(List::size)
      .orElse(0);
    assertEquals(6, size);
}

In this example, we wrap a list of strings inside an Optional object and use its map API to perform an action on the contained list. The action we perform is to retrieve the size of the list.

The map API returns the result of the computation wrapped inside Optional. We then have to call an appropriate API on the returned Optional to retrieve its value.

Notice that the filter API simply performs a check on the value and returns a boolean. On the other hand, the map API takes the existing value, performs a computation using this value and returns the result of the computation wrapped in an Optional object:

@Test
public void givenOptional_whenMapWorks_thenCorrect2() {
    String name = "baeldung";
    Optional<String> nameOptional = Optional.of(name);

    int len = nameOptional
     .map(String::length())
     .orElse(0);
    assertEquals(8, len);
}

We can chain map and filter together to do something more powerful.

Let’s assume we want to check the correctness of a password input by a user; we can clean the password using a map transformation and check it’s correctness using a filter:

@Test
public void givenOptional_whenMapWorksWithFilter_thenCorrect() {
    String password = " password ";
    Optional<String> passOpt = Optional.of(password);
    boolean correctPassword = passOpt.filter(
      pass -> pass.equals("password")).isPresent();
    assertFalse(correctPassword);

    correctPassword = passOpt
      .map(String::trim)
      .filter(pass -> pass.equals("password"))
      .isPresent();
    assertTrue(correctPassword);
}

As you can see, without first cleaning the input, it will be filtered out – yet users may take for granted that leading and trailing spaces all constitute input. So we transform dirty password into a clean one with a map before filtering out incorrect ones.

12. Transforming Value with flatMap()

Just like the map API, we also have the flatMap API as an alternative for transforming values. The difference is that map transforms values only when they are unwrapped whereas flatMap takes a wrapped value and unwraps it before transforming it.

Previously, we’ve created simple String and Integer objects for wrapping in an Optional instance. However, frequently, we will receive these objects from an accessor of a complex object.

To get a clearer picture of the difference, let’s have a look at a Person object that takes a person’s details such as name, an age and a password:

public class Person {
    private String name;
    private int age;
    private String password;

    public Optional<String> getName() {
        return Optional.ofNullable(name);
    }

    public Optional<Integer> getAge() {
        return Optional.ofNullable(age);
    }

    public Optional<String> getPassword() {
        return Optional.ofNullable(password);
    }

    // normal constructors and setters
}

We would normally create such an object and wrap it in an Optional object just like we did with String. Alternatively, it can be returned to us by another API call:

Person person = new Person("john", 26);
Optional<Person> personOptional = Optional.of(person);

Notice now that when we wrap a Person object, it will contain nested Optional instances:

@Test
public void givenOptional_whenFlatMapWorks_thenCorrect2() {
    Person person = new Person("john", 26);
    Optional<Person> personOptional = Optional.of(person);

    Optional<Optional<String>> nameOptionalWrapper  
      = personOptional.map(Person::getName);
    Optional<String> nameOptional  
      = nameOptionalWrapper.orElseThrow(IllegalArgumentException::new);
    String name1 = nameOptional.orElse("");
    assertEquals("john", name1);

    String name = personOptional
      .flatMap(Person::getName)
      .orElse("");
    assertEquals("john", name);
}

Here, we’re trying to retrieve the name attribute of the Person object to perform an assertion.

Note how we achieve this with map API in the third statement and then notice how we do the same with flatMap API afterwards.

The Person::getName method reference is similar to the String::trim call we had in the previous section for cleaning up a password.

The only difference is that getName() returns an Optional rather than a String as did the trim() operation. This, coupled with the fact that a map transformation result is wrapped in an Optional object leads to a nested Optional.

While using map API, therefore, we need to add an extra call to retrieve the value before using the transformed value. This way, the Optional rapper will be removed. This operation is performed implicitly when using flatMap.

13. Conclusion

In this article, we covered most of the important features of Java 8 Optional class.

We have also briefly explored some reasons why we would choose to use Optional instead of explicit null checking and input validation.

Finally, we touched on the subtle but significant difference between orElse and orElseGet; here’s some good further reading on the topic.

The source code for all examples in the article are available on the Github project.

I just released the Master Class of "Learn Spring Security" Course:

>> CHECK OUT THE COURSE


Viewing all articles
Browse latest Browse all 4536

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>