Java 8 Optionals

本文深入探讨了 Java8 中 Optional 类的使用方法,解释了如何避免 NullPointerException 的出现,介绍了 Optional 的创建、检查值的存在、获取默认值、异常处理等操作,并通过实例展示了 Optional 在复杂业务逻辑中的应用。
摘要由CSDN通过智能技术生成
1 Overview

All of us must have encountered NullPointerException in our applications. This exception happen when you try to utilize a object reference which has not been initialized, initialized with null or simply does not point to any instance. NULL simply means ‘absence of a value’.

1.1 What is the Type of Null?

In Java, we use a reference type to gain access to an object, and when we don’t have a specific object to make our reference point to, then we set such reference to null to imply the absence of a value. Right?

The use of null is so common that we rarely put more thoughts on it. For example, field members of objects are automatically initialized to null and programmers typically initialize reference types to null when they don’t have an initial value to give them and, in general, null is used everytime where we don’t know or we don’t have a value to give to a reference.

FYI, in Java null is actually a type, a special one. It has no name so we cannot declare variables of its type or cast any variables to it; in fact there is only a single value that can be associated with it (i.e. the literal null). Remember that unlike any other types in Java, a null reference can be safely assigned to any other reference types without any error(See JLS 3.10.7 and 4.1).

1.2 What is wrong with just returning null?

Generally, the API designers put the descriptive java docs in APIs and mention there that API can return a null value, and in which case(s). Now, the problem is that the caller of the API might have missed reading the javadoc for any reason, and forget about handling the null case. This is going to be a bug in future for sure.

And believe me, this happens frequently and is one of the main causes of null pointer exceptions, although not th
e only one. So, take a lesson here that always read the java dcos of an API when you are using it first time (… at least ) [?].
Now we know that null is a problem in most of the cases, what’s best way to handle them?
A good solution is to always initialize your object references with some value, and never with null. In this way, you will never encounter NullPointerException. Fair enough. But in practical we always don’t have a default value for a reference. So, how those cases should be handled?
Above question is right in many senses. Well, java 8 Optionals are the answer here.

1.3 How Java 8 Optionals provide the solution?

Optional is a way of replacing a nullable T reference with a non-null value. An Optional may either contain a non-null T reference (in which case we say the reference is “present”), or it may contain nothing (in which case we say the reference is “absent”).

Remember that it is never said that optional “contain null”.

Optional<Integer> canBeEmpty1 = Optional.of(5);
canBeEmpty1.isPresent();                    // returns true
canBeEmpty1.get();                          // returns 5
 
Optional<Integer> canBeEmpty2 = Optional.empty();
canBeEmpty2.isPresent();                    // returns false

You can also view Optional as a single-value container that either contains a value or doesn’t.

It is important to note that the intention of the Optional class is not to replace every single null reference. Instead, its purpose is to help design more-comprehensible APIs so that by just reading the signature of a method, you can tell whether you can expect an optional value. This forces you to fetch the value from Optional and work on it, and at the same time handle the case where optional will be empty. Well, this is exactly the solution of null references/return values which ultimately result into NullPointerException.

1.4 Java Optional Class Methods
MethodsDescription
public static Optional empty()It returns an empty Optional object. No value is present for this Optional.
public static Optional of(T value)It returns an Optional with the specified present non-null value.
public static Optional ofNullable(T value)It returns an Optional describing the specified value, if non-null, otherwise returns an empty Optional.
public T get()If a value is present in this Optional, returns the value, otherwise throws NoSuchElementException.
public boolean isPresent()It returns true if there is a value present, otherwise false.
public void ifPresent(Consumer<? super T> consumer)If a value is present, invoke the specified consumer with the value, otherwise do nothing.
public Optional filter(Predicate<? super T> predicate)If a value is present, and the value matches the given predicate, return an Optional describing the value, otherwise return an empty Optional.
public Optional map(Function<? super T,? extends U> mapper)If a value is present, apply the provided mapping function to it, and if the result is non-null, return an Optional describing the result. Otherwise return an empty Optional.
public Optional flatMap(Function<? super T,Optional mapper)If a value is present, apply the provided Optional-bearing mapping function to it, return that result, otherwise return an empty Optional.
public T orElse(T other)It returns the value if present, otherwise returns other.
public T orElseGet(Supplier<? extends T> other)It returns the value if present, otherwise invoke other and return the result of that invocation.
public T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X extends ThrowableIt returns the contained value, if present, otherwise throw an exception to be created by the provided supplier.
public boolean equals(Object obj)Indicates whether some other object is “equal to” this Optional or not. The other object is considered equal if: 1. It is also an Optional and;2. Both instances have no value present or;3. the present values are “equal to” each other via equals().
public int hashCode()It returns the hash code value of the present value, if any, or returns 0 (zero) if no value is present.
public String toString()It returns a non-empty string representation of this Optional suitable for debugging. The exact presentation format is unspecified and may vary between implementations and versions.
2. Optional Basic
2.1 Creating Optional Objects

There are 3 major ways to create an Optional.

  1. Use Optional.empty() to create empty optional.
Optional<Integer> possible = Optional.empty();
  1. Use Optional.of() to create optional with default non-null value. If you pass null in of(), then a NullPointerException is thrown immediately.
Optional<Integer> possible = Optional.of(5);
  1. Use Optional.ofNullable() to create an Optional object that may hold a null value. If parameter is null, the resulting Optional object would be empty (remember that value is absent; don’t read it null).
Optional<Integer> possible = Optional.ofNullable(null);
//or
Optional<Integer> possible = Optional.ofNullable(5);
2.2 Do something If Optional value is present

You got your Optional object is first step. Now let’s use it after checking whether it holds any value inside it.

Optional<Integer> possible = Optional.of(5);
possible.ifPresent(System.out::println);
2.3 Default Value With orElse()

The orElse() method is used to retrieve the value wrapped inside an Optional instance. It takes one parameter which acts as a default value. The orElse() method returns the wrapped value if it’s present and its argument otherwise:

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

The orElseGet() method 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);
}

2.5 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 of fact, these two methods give the impression that they overlap each other in functionality.

However, there’s a subtle but very important difference between the two which can affect the performance of our 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";
}

Let’s see two tests and observe their side effects to establish both where orElse() and orElseGet() overlap and where they differ:

@Test
public void whenOrElseGetAndOrElseOverlap_thenCorrect() {
    String text = null;
 
    String defaultText = Optional.ofNullable(text).orElseGet(this::getMyDefault);
    assertEquals("Default Value", defaultText);
 
    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:

Getting default value...
Getting default value...

e getMyDefault() method is called in each case. It so happens that when the wrapped value is not present, then both orElse() and orElseGet() 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() method 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.

2.6 Exceptions with orElseThrow()

The orElseThrow() method follows from orElse() and orElseGet() 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.

2.7 Returning Value with get()

The final approach for retrieving the wrapped value is the get() method:

@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, get() 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() method. 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.

2.8 Rejecting certain values using the filter method

We can run an inline test on our wrapped value with the filter method. 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 it will return an empty Optional:

@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 method is normally used this way to reject wrapped values based on a predefined rule. We 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.

Let’s 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 the 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:

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() method in detail in the next section.

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

Secondly, the only logic we write inside its body is exactly what the method 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 approach promises to check price range but has to do more than that to defend against its inherent fragility. Therefore, we can use the filter method to replace unnecessary if statements and reject unwanted values.

2.9 Transforming Value with map()

In the previous section, we looked at how to reject or accept a value based on a filter. We can use a similar syntax to transform the Optional value with the map() method:

@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 method to perform an action on the contained list. The action we perform is to retrieve the size of the list.

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

Notice that the filter method simply performs a check on the value and returns a boolean. On the other hand, the map method 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 we 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.

2.10 Transforming Value with flatMap()

Just like the map() method, we also have the flatMap() method 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 method 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() method in the third statement and then notice how we do the same with flatMap() method 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 wraps the result in an Optional object leads to a nested Optional.

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

参考:
《Java 8 Optionals : Complete Reference》

《Guide To Java 8 Optional》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值