2024-09-08

About Optional

Programming
Notes
This article was translated by GPT-5.2-Codex. The original is here.

Introduction

Understand the design philosophy! 1

TL;DR

Optional

Optional was introduced to the Java standard library in Java 8. Optional represents a value that either exists or does not exist. In Java, "no value" means null. Before Java 8, when a variable had no value to assign, you represented that by assigning null. With Optional, the official way is to represent "no value" with an object that represents the absence of a value.

The idea of representing a value that might not exist with a data structure is not new. Many languages, starting with Haskell, provide it in their standard libraries. I don't know whether it existed as early as Miranda, a predecessor of Haskell, but by the 1990s, there were already references to the Maybe type constructor. In other words, a concept from the 1990s finally made it into Java. Haskell itself was released in 1990, before Java (1995).

Optional instances

To create a value wrapped in Optional, call one of these static methods:

Each has a different purpose, so let's check them one by one.

Optional#ofNullable(T)

1
jshell> Optional.ofNullable(null)
2
$1 ==> Optional.empty
1
jshell> Optional.ofNullable("123")
2
$2 ==> Optional[123]

Optional#of(T)

1
jshell> Optional.of("123")
2
$3 ==> Optional[123]
1
jshell> Optional.of(null)
2
| 例外java.lang.NullPointerException
3
| at Objects.requireNonNull (Objects.java:209)
4
| at Optional.of (Optional.java:113)
5
| at (#5:1)

Optional#empty()

1
jshell> Optional.empty()
2
$5 ==> Optional.empty

How to use Optional

Example using Optional

To understand Optional, let's consider converting a string to an integer. This is a simple example of a computation that can fail. If the string is a valid integer format, we expect an Integer value.

In Java, it is common to use Integer#parseInt(String).

1
jshell> Integer.parseInt("123")
2
$6 ==> 123

If the string is not an integer, what happens? Integer#parseInt(String) throws an unchecked exception NumberFormatException.

1
jshell> Integer.parseInt("abc")
2
| 例外java.lang.NumberFormatException: For input string: "abc"
3
| at NumberFormatException.forInputString (NumberFormatException.java:67)
4
| at Integer.parseInt (Integer.java:668)
5
| at Integer.parseInt (Integer.java:786)
6
| at (#8:1)

So if you want to convert a string that might not be an integer, you end up writing:

1
Integer number;
2
try {
3
number = Integer.parseInt(str);
4
} catch (NumberFormatException e) {
5
// Handle failure
6
number = ...;
7
}

Writing this every time is tedious, so you'd want a utility. Here we consider two implementations: one without Optional, and one with Optional.

Represent failure with null

In Java, if a computation fails, it's common to throw an exception or return null. Integer#parseInt(String) throws, but then you must always catch it.

So let's write a utility method that returns null instead of throwing.

1
jshell> public class NumberUtils {
2
...>
3
...> public static Integer parseInt(String str) {
4
...> try {
5
...> return Integer.parseInt(str);
6
...> } catch (NumberFormatException e) {
7
...> return null;
8
...> }
9
...> }
10
...> }
11
| 次を作成しました: クラス NumberUtils

Try it with integer and non-integer strings.

1
jshell> NumberUtils.parseInt("123")
2
$9 ==> 123

For non-integer input, it returns null.

1
jshell> NumberUtils.parseInt("abc")
2
$10 ==> null

Now you can write:

1
Integer number = NumberUtils.parseInt(str);

This looks nicer than try/catch. But is it really safe?

Let's consider a slightly more complex example: given a tax-excluded price string, compute a tax-included price (10%).

More precisely:

  • Price is given in the format XXX円
  • If price is unknown, the string 未定 is given
  • If price is known, return the price with 10% tax added
  • If price is unknown, return 未定
  • If the format is invalid, return something else

Let's define a static method for this.2

1
public class Price {
2
3
public static String taxIncluded(String price) {
4
// Return price with 10% tax
5
}
6
}

Now implement it. To extract the numeric part you could use regex, but here we keep it simple.

1
jshell> public class Price {
2
...> public static String taxIncluded(String jpPrice) {
3
...> if (jpPrice == null) {
4
...> return null;
5
...> }
6
...>
7
...> if ("未定".equals(jpPrice)) {
8
...> return "未定";
9
...> }
10
...>
11
...> if (jpPrice.length() > 1 && jpPirce.charAt(jpPrice.length() - 1) == '円') {
12
...> String price = jpPrice.substring(0, jpPrice.length() - 1);
13
...> Integer priceInt = NumberUtils.parseInt(price);
14
...>
15
...> return ((int) Math.floor(priceInt * 1.1)) + "円";
16
...> }
17
...>
18
...> return null;
19
...> }
20
...> }
21
| 次を作成しました: クラス Price

We'll round down the tax because we're kind.3 If the format is neither 未定 nor XXX円, return null.

Does it work?

1
jshell> Price.taxIncluded("1000円")
2
$12 ==> "1100円"
3
4
jshell> Price.taxIncluded("未定")
5
$13 ==> "未定"
6
7
jshell> Price.taxIncluded(null)
8
$14 ==> null
9
10
jshell> Price.taxIncluded("1,000円")
11
| 例外java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because "<local2>" is null
12
| at Price.taxIncluded (#18:15)
13
| at (#20:1)

Oops 😵

NumberUtils#parseInt(String) returns null when conversion fails. We forgot to handle that, so fix it.

1
jshell> public class Price {
2
...> public static String taxIncluded(String jpPrice) {
3
...> if (jpPrice == null) {
4
...> return null;
5
...> }
6
...>
7
...> if ("未定".equals(jpPrice)) {
8
...> return "未定";
9
...> }
10
...>
11
...> if (jpPrice.length() > 1 && jpPrice.charAt(jpPrice.length() - 1) == '円') {
12
...> String price = jpPrice.substring(0, jpPrice.length() - 1);
13
...> Integer priceInt = NumberUtils.parseInt(price);
14
...>
15
...> if (priceInt == null) {
16
...> return null;
17
...> }
18
...>
19
...> return ((int) Math.floor(priceInt * 1.1)) + "円";
20
...> }
21
...>
22
...> return null;
23
...> }
24
...> }
25
| 次を変更しました: クラス Price

This is fine now. But the code contains things we don't want to care about in the main logic. Specifically:

  • When jpPrice is null
  • When priceInt is null

Here we intentionally wrote it this way, but we forgot NumberUtils#parseInt(String) returns null and caused a NullPointerException.

Now let's rewrite this using Optional.

Using Optional

Finally, the main topic: using Optional.

Let's see the code. As with the non-Optional version, start with NumberUtils.

1
jshell> public class NumberUtils {
2
...>
3
...> public static Optional<Integer> parseIntOptional(String str) {
4
...> try {
5
...> return Optional.of(Integer.parseInt(str));
6
...> } catch (NumberFormatException e) {
7
...> return Optional.empty();
8
...> }
9
...> }
10
...> }
11
| 次を置換しました: クラス NumberUtils

With Optional, return Optional.empty() instead of null, and Optional.of(T) on success.

Using NumberUtils#parseIntOptional(String):

1
jshell> NumberUtils.parseIntOptional("abc")
2
$18 ==> Optional.empty
3
4
jshell> NumberUtils.parseIntOptional("123")
5
$19 ==> Optional[123]

Now let's write Price#taxIncluded(String) using this and Optional. Different people may write it differently, but here's one approach.

1
jshell> public class Price {
2
...> public static String taxIncluded(String jpPrice) {
3
...> return Optional.ofNullable(jpPrice)
4
...> .flatMap(str -> {
5
...> if (str.equals("未定")) {
6
...> return Optional.of("未定");
7
...> }
8
...>
9
...> return Optional.of(str)
10
...> .filter(it -> it.length() > 1 && it.charAt(it.length() - 1) == '円')
11
...> .map(it -> it.substring(0, it.length() - 1))
12
...> .flatMap(NumberUtils::parseIntOptional)
13
...> .map(it -> (int) (it * 1.1))
14
...> .map(it -> it + "円");
15
...> })
16
...> .orElse(null);
17
...> }
18
...> }
19
| 次を作成しました: クラス Price

Alternatively, you can pull the 未定 logic outside flatMap:

1
public class Price {
2
public static String taxIncluded(String jpPrice) {
3
if ("未定".equals(jpPrice)) {
4
return "未定";
5
}
6
7
return Optional.ofNullable(jpPrice)
8
.flatMap(str ->
9
Optional.of(str)
10
.filter(it -> it.length() > 1 && it.charAt(it.length() - 1) == '円')
11
.map(it -> it.substring(0, it.length() - 1))
12
.flatMap(NumberUtils::parseIntOptional)
13
.map(it -> (int) (it * 1.1))
14
.map(it -> it + "円"))
15
.orElse(null);
16
}
17
}

Test it:

1
jshell> Price.taxIncluded("1000円")
2
$21 ==> "1100円"
3
4
jshell> Price.taxIncluded("未定")
5
$22 ==> "未定"
6
7
jshell> Price.taxIncluded(null)
8
$23 ==> null
9
10
jshell> Price.taxIncluded("1,000円")
11
$24 ==> null

Now compare the code with and without Optional.

1
public class Price {
2
public static String taxIncluded(String jpPrice) {
3
if (jpPrice == null) {
4
return null;
5
}
6
7
if ("未定".equals(jpPrice)) {
8
return "未定";
9
}
10
11
if (jpPrice.length() > 1 && jpPrice.charAt(jpPrice.length() - 1) == '円') {
12
String price = jpPrice.substring(0, jpPrice.length() - 1);
13
Integer priceInt = NumberUtils.parseInt(price);
14
15
if (priceInt == null) {
16
return null;
17
}
18
19
return ((int) Math.floor(priceInt * 1.1)) + "円";
20
}
21
22
return null;
23
}
24
}
1
public class Price {
2
public static String taxIncluded(String jpPrice) {
3
if ("未定".equals(jpPrice)) {
4
return "未定";
5
}
6
7
return Optional.ofNullable(jpPrice)
8
.flatMap(str ->
9
Optional.of(str)
10
.filter(it -> it.length() > 1 && it.charAt(it.length() - 1) == '円')
11
.map(it -> it.substring(0, it.length() - 1))
12
.flatMap(NumberUtils::parseIntOptional)
13
.map(it -> (int) (it * 1.1))
14
.map(it -> it + "円"))
15
.orElse(null);
16
}
17
}

The Optional version is shorter in lines, but that's not the point. The key is that the Optional version focuses on what we want to do (convert string to int and compute tax) without worrying about null along the way.

In the non-Optional version, we forgot NumberUtils#parseInt(String) can return null and got a NullPointerException. With Optional, if conversion fails, Optional.empty() is returned, so no NullPointerException.

If a computation returns Optional, you can flatten it with flatMap. Then you can write downstream processing in map, which only runs on success.

This means you can consider failure only at the end (Optional#orElse(T)), not at every step. In this requirement, if computation fails, return "something else", so I returned null, but I was following the non-Optional example. If failure is possible, why not return Optional itself?

1
jshell> public class Price {
2
...> public static Optional<String> taxIncluded(String jpPrice) {
3
...> if ("未定".equals(jpPrice)) {
4
...> return Optional.of("未定");
5
...> }
6
...>
7
...> return Optional.ofNullable(jpPrice)
8
...> .flatMap(str ->
9
...> Optional.of(str)
10
...> .filter(it -> it.length() > 1 && it.charAt(it.length() - 1) == '円')
11
...> .map(it -> it.substring(0, it.length() - 1))
12
...> .flatMap(NumberUtils::parseIntOptional)
13
...> .map(it -> (int) (it * 1.1))
14
...> .map(it -> it + "円"));
15
...> }
16
...> }
17
| 次を置換しました: クラス Price

If you think, "Returning null is fine," you might have forgotten the earlier mistake with NumberUtils#parseInt(String). When using that static method, are you confident you will always remember to null-check the return value?

Optional represents computations that may fail

We've compared code with and without Optional. Now let's look at the Javadoc again.

A container object which may or may not contain a non-null value.

...

API Note: Optional is intended primarily for use as a method return type where there is a clear need to represent "no result", and where using null is likely to cause errors. A variable whose type is Optional should never itself be null; it should always point to an Optional instance.

As an object, Optional is "a container that may or may not contain a non-null value." But if we return Optional.empty() on failure and Optional.of(T) on success, it becomes not just "some value or not", but a way to express success/failure.4

With Optional, you can write code centered on success, and consider recovery only at the end.

Let's re-examine the tax calculation with that in mind.

{7-14}java
1
public class Price {
2
public static Optional<String> taxIncluded(String jpPrice) {
3
if ("未定".equals(jpPrice)) {
4
return Optional.of("未定");
5
}
6
7
return Optional.ofNullable(jpPrice)
8
.flatMap(str ->
9
Optional.of(str)
10
.filter(it -> it.length() > 1 && it.charAt(it.length() - 1) == '円')
11
.map(it -> it.substring(0, it.length() - 1))
12
.flatMap(NumberUtils::parseIntOptional)
13
.map(it -> (int) (it * 1.1))
14
.map(it -> it + "円"));
15
}
16
}

In the Optional version, only the success path is highlighted, and failure branches are not explicit. You can focus on success without explicit null checks.

In contrast, the non-Optional version has failure handling in the main flow, fragmenting the tax logic.

{15-17,22}java
1
public class Price {
2
public static String taxIncluded(String jpPrice) {
3
if (jpPrice == null) {
4
return null;
5
}
6
7
if ("未定".equals(jpPrice)) {
8
return "未定";
9
}
10
11
if (jpPrice.length() > 1 && jpPrice.charAt(jpPrice.length() - 1) == '円') {
12
String price = jpPrice.substring(0, jpPrice.length() - 1);
13
Integer priceInt = NumberUtils.parseInt(price);
14
15
if (priceInt == null) {
16
return null;
17
}
18
19
return ((int) Math.floor(priceInt * 1.1)) + "円";
20
}
21
22
return null;
23
}
24
}

This shows that Optional lets you keep the intended logic together. In this example, failure was only possible for null input or invalid formats, but even with more possible failures, Optional can keep the code focused on the desired processing.

Benefits of using Optional

There are three main benefits:

  • Returning Optional makes it clear the method may fail
  • Returning Optional forces callers to handle the absence of values, improving null safety5
  • Wrapping with Optional allows you to ignore failure paths until the end

Problems with Optional

So far I've described the good parts, but Optional also has issues.

  • It can express success/failure but not the reason for failure
  • Using Optional-style code is still not mainstream
  • Optional is neither a monad nor a functor

It expresses success/failure but not the reason

Optional can return a value on success, but all failures collapse into Optional.empty(). So you cannot handle different failure reasons differently.

To express failure reasons, you usually need types like Either (Left, Right) or Result (Ok, Err). In Either, Right holds the success value (right is "correct"), and Left holds the failure value. Then you can return error values and still use methods like map and filter that operate on Right only.

I hope Java will provide something like this in the standard library one day, but as of Java 21 it does not. The most practical option is to use a functional library like vavr, but introducing such a library in production is hard.

It's partly a skill issue, but minor libraries also carry a maintenance risk.

Optional-style code is not mainstream

Optional is neither a monad nor a functor

Rules when using Optional

Conclusion

I explained the basic usage and properties of Optional.

I hope the world sees at least one fewer NullPointerException. I’ll stop here.

Footnotes

  1. "メディアの違いを理解せよ!" - Nicovideo Encyclopedia

  2. I can hear the voices saying "You implement tax logic in a static method?".

  3. (Q9) How should we handle rounding when setting tax-included prices?

  4. In languages without null, types like Optional (e.g., Haskell's Maybe) are often explained as representing success/failure of computations.

  5. In practice, people unfamiliar with Optional often call Optional#get() and trigger runtime exceptions 😡

Amazon アソシエイトについて

この記事には Amazon アソシエイトのリンクが含まれています。Amazonのアソシエイトとして、SuzumiyaAoba は適格販売により収入を得ています。