1. What Is Optional
Optional is a new core library data type created to replace null. The Optional class is a class that can hold either a null or a non-null value. It is a feature that already exists in other languages (e.g., Scala), and it was included in Java with JDK8. Let's look at the problems that arise when using null and how Optional improves the code.
When developing in Java, you frequently encounter NullPointerException (NPE). As in the code below, if an object is null and you try to use the null value, an NPE occurs.
@Test(expected = NullPointerException.class)
public void testOldJavStyle_throw_NPE() {
String str = null;
System.out.println(str.charAt(0)); //NPE occurs
}
To resolve the NPE, you have to add a conditional statement that checks for null. Null was developed with the intent of representing the absence of a value, but by introducing null, the code became much less readable and harder to maintain. Let's see how the code changes when using the Optional class. As the null-check conditional disappears, the code becomes much cleaner.
//null conditional
@Test
public void testOldJavStyle_checkNull() {
String str = "test";
if (str != null) {
System.out.println(str.charAt(0));
}
}
//Using Optional
@Test
public void testOptionalJavaStyle_checkNull() {
String str = "test";
Optional<String> optStr = Optional.ofNullable(str);
optStr.ifPresent(s -> System.out.println(s.charAt(0)));
}
2. Using Optional
2.1 Creating an Optional Object
The Optional class provides three static factory methods.
- Optional.empty() : creates an empty Optional object
- Optional.of(value) : used when the value is not null
- Optional.ofNullable(value) : used when it is uncertain whether the value is null or not
1. Optional.empty()
Optional<String> optStr = Optional.empty();
Optional.empty() creates an empty Optional object.
2. Optional.of(value)
String str = "test";
Optional<String> optStr1 = Optional.of(str);
Optional.of() creates an Optional object that holds a non-null object.
Since it creates an object that is not null, passing null to create it results in an NPE.
String nullStr = null;
Optional<String> optStr2 = Optional.of(nullStr); NPE occurs
3. Optional.ofNullable(value)
String str = "test";
Optional<String> optStr1 = Optional.ofNullable(str);
When you want to hold an object that you are not sure is null or not, you create an Optional object through Optional.ofNullable().
If null is passed in, it creates an empty Optional object.
Optional<String> optStr2 = Optional.ofNullable(null); //returns an empty Optional object
2.3 How to Access and Use the Object Held by Optional
Let's look at how to use the various methods for accessing the object held by an Optional.
- ifPresent(function)
- return a default value when null
- orElse(T other) : returns the argument when empty
- orElseGet(Supplier<? extends T> other) : returns the return value of the functional argument
- throw an exception when null
- orElseThrow(Supplier<? extends X> exceptionSupplier) : throws the exception created by the argument function
1. IfPresent(function) This is a method that runs the function passed as an argument when the Optional object is non-null. If the Optional object is null, the function passed as an argument is not executed.
@Test
public void test_1_otional_usage_ifPresent() {
String str = "test";
Optional<String> optStr1 = Optional.ofNullable(str);
optStr1.ifPresent(s -> System.out.println(s.charAt(0))); prints t
Optional<String> optStr2 = Optional.ofNullable(null);
optStr2.ifPresent(s -> System.out.println(s.charAt(0))); not printed
}
2. orElse This is a method that returns a substitute value when the object held in the Optional is null.
@Test
public void test_2_otional_usage_orElse() {
Optional<String> optStr = Optional.ofNullable(null);
String result = optStr.orElse("test"); //returns test if null
System.out.println(result); //test
}
3. orElseGet This is similar to orElse, but the difference is that it takes a method as an argument and returns the value returned by the function.
@Test
public void test_2_otional_usage_orElseGet() {
Optional<String> optStr = Optional.ofNullable(null);
String result = optStr.orElseGet(this::getDefaultValue);
System.out.println(result); //default
}
private String getDefaultValue() {
LOG.info("calling getDefaultValue");
return "default";
}
4. The Difference Between orElse and orElseGet In terms of the end result, orElse and orElseGet appear to have no difference, but looking at the code below, you can see that in the case of orElseGet() the function passed as an argument is executed only when the value is null. You only need to be careful when using the orElse method.
@Test
public void test_optional_usage_diff_orElse_orElseGet() {
String str = "test";
String result1 = Optional.ofNullable(str).orElse(getDefaultValue()); the getDefaultValue() function runs even when it is not null
LOG.info("orElse result: {}", result1);
String result2 = Optional.ofNullable(str).orElseGet(this::getDefaultValue); getDefaultValue() is not executed
LOG.info("orElseGet result: {}", result2);
}

5. orElseThrow Instead of returning a default value when null, you can throw an exception.
@Test(expected = IllegalArgumentException.class)
public void test_3_optional_usage_orElseThrow() {
String str = null;
String result = Optional.ofNullable(str).orElseThrow(IllegalArgumentException::new);
LOG.info("result {}", result);
}
3. How to Use Optional in Streams
3.1 filter(Predicate) : Filtering Elements Based on a Condition
filter() is a Stream API that returns, as a new stream, only the elements for which the result of the Predicate function passed as an argument is true. In other words, you can understand it as filtering only the elements that match the condition. Here is a version implemented without Optional and an example that uses Optional.
//Without Optional
private boolean isLastNameFrank(Person person) {
if (person != null && person.getLastName() != null) {
return person.getLastName().toLowerCase().equals("frank");
}
return false;
}
//Using Optional
private boolean isLastNameFrank(Person person) {
return Optional.ofNullable(person)
.map(Person::getLastName)
.map(String::toLowerCase)
.filter(s -> s.equals("frank")).isPresent();
}
@Test
public void test_1_stream_usage_filter_with_optional() {
Map<Person, Boolean> personVsExpectedMap = new HashMap<Person, Boolean>() {{
put(new Person("Frank", "Oh"), true); **stores the Person object and the expected result**
put(new Person(null, "Oh"), false);
put(new Person("David", "Oh"), false);
put(new Person("John", "Oh"), false);
}};
boolean expectedResult;
for (Person person : personVsExpectedValueMap.keySet()) {
expectedResult = personVsExpectedValueMap.get(person);
assertEquals(expectedResult, **isLastNameFrank** (person));
}
}
To better understand the various Stream APIs, it is easier to understand how the lambda function is called internally by looking at the actual implementation code. filter takes a Predicate function as an argument, runs the function passed as the Predicate on the stream, and if the result is true it passes the stream's this, and otherwise you can see that the actual returned result is of type Optional.
@Test
public void test_1_stream_usage_filter_with_optional() {
Map<Person, Boolean> personVsExpectedMap = new HashMap<Person, Boolean>() {{
put(new Person("Frank", "Oh"), true); **stores the Person object and the expected result**
put(new Person(null, "Oh"), false);
put(new Person("David", "Oh"), false);
put(new Person("John", "Oh"), false);
}};
boolean expectedResult;
for (Person person : personVsExpectedValueMap.keySet()) {
expectedResult = personVsExpectedValueMap.get(person);
assertEquals(expectedResult, **isLastNameFrank** (person));
}
}
For reference, the meaning of Predicate<? super T> is that the Predicate's type parameter must be T or a supertype of T. The lower bound that can go into T is determined. For example, in the case of List<? super Integer>, it includes List