15
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

JavaAdvent Calendar 2020

Day 20

Java8 Optionalの逆引きレシピ

Last updated at Posted at 2020-12-19

はじめに

Java Advent Calendar 2020の20日目の記事です。(Zennとダブルポストです。)

2014年にJava8がリリースされ、そこからOptionalの機能が導入されました。
Javaのリリースサイクルも変わり、2020年12月時点では、最新のLTS版はJava11(2018年リリース)、 Java15(2020年9月リリース)が最新です。

Java8がリリースされてから、11がでるまでの間に始まったプロジェクトでは、まずJava8が使われているでしょう。AWSではJava8のサポートをかなりのばしているので、まだまだ使う機会がありそうです。

Java8のOptionalは決して使いやすいとはいえないですが、それでもマッチしたケースではそれなり有効です。ケース別にどういった使い方をすればいいかをまとめてみました。

英語ではOptionalを使ってはいけないケースをまとめた記事も結構あります。そのうちのいくつかをピックアップしてみました。

原則、Optional#get()は使わない

大前提として、Optional#get() を原則使わないようにしましょう。
Optional#get()は値が存在しないときは例外スローされます。なので、これをいきなり使うのはnull checkしないで参照するのと同じことになります。(NullPointerExceptionのかわりに、NoSuchElementExceptionがスローされます。)

なんのチェックもしないでOptional#get()を使った場合、最近のIDEやSonarLintなどの静的解析ツールでは、警告がでてきます。Optionalからの値の取り出し方として良くないとして、すでに知られているといえます。Optionalを使い始めのときにやりがちなので、注意が必要です。

Optionalを使うためには、Optionalで包んだ値の取り出し方(使い方)を知っていることが重要です。関数型のパラダイムを最初からとりいれている言語であれば、取り出し方自体がJava8に比べて豊富で、IDEでだいたい然るべきメソッドにたどり着きやすいと思います。Java8では選択肢が少ないとの、高階関数のAPIがあまり使い勝手が良くないので、取り出し方を先に意識しないと、「Optional使ってみたけど、なんか読みづらいし、書きにくい」、となりがちです。

Optionalからの取り出し方

各メソッドの詳細はjavadocを参照してください。

以下のメソッドをつかって取り出します。

メソッド 使用例 サンプルコード
ifPresent 存在するときだけ処理する userOpt.ifPresent(x -> x.save())
orElse 存在しないときは初期値を返す userOpt.orElse(new User())
orElseGet 存在しないときは、関数を使って初期値を返す userOpt.orElseGet(User::new)
orElseThrow 存在しないときは例外スローする userOpt.orElseThrow(IllegalStateException::new)

なんらかの変換や絞り込みが必要な場合は、以下のメソッドを先に使ってから、上記にあげたメソッドで値を取り出します。

メソッド 使用例 サンプルコード
filter ある条件を満たすものに絞り込む user.filter(User::isActive)
map プロパティをとりだす、別の型にするなどの変換をする user.map(User::getName)

Java8ではあまり使うことはなさそうですが、単純にやると、Optional<Optional<T>> のように、Optionalが2重になるような変換がある場合は、flatMapを使います。今回はこれはとりあげません。

上記を組み合わせて、必要な値の取り出しや処理します。

包んだ値をそのまま使う

Spring Data JPA では、findByIdなど、検索結果が最大1件しかない場合はOptionalで受け取ることができます。こういった場合、entityのままで扱いたいケースが多いです。

ifPresent


// 更新処理
Optional<User> user = userRepository.findById(id);
user.ifPresent(x -> {
   user.setStatus(2);
   userRepository.save(user);
});

orElse


// ユーザ詳細画面の表示
@GetMapping(/{id})
public ModelAndView showUser(@PathVariable("id") Long id) {
  ModelAndView view = new ModelAndView("userDetail");
  User user = userRepostitory.findById(id).orElse(new User());
  view.set("user", user);
  return view;
}

orElseThrow


// ユーザ詳細情報のAPI
@GetMapping(/{id})
public ResponseEntity<User> findById(@PathVariable("id") Long id) {
  return userRepository.findById(id).orElseThrow(() -> new NotFoundException(id + " does not exist"));
}

包んだ値が持っているプロパティを使う

entityがもっている特定のプロパティだけが必要なケースがあります。
そういう場合は、Optional#mapを使います。


Optional<User> user = ...;
String userName = user.map(User::getName).orElse("");
Long userId = user.map(User::getId).orElseGet(() -> 1L);
UserStatus userStatus = user.map(User::getStatus).orElseThrow(IllegalStateException::new);
Integer userStatusValue = user.map(User::getStatus).map(UserStatus::getValue).orElse(0);

包んだ値をもとになんらかの変換して使う

entitをそのままでなく、画面表示用にいろんな加工が必要な場合などは、別のオブジェクトに変換することがあります。変換先のオブジェクトにstatic factory methodを用意しておくと、Optional#mapをうまく使えます。


public class UserViewDTO

   public static UserViewDTO of(User user) {...}

Optional<User> user = userRepository.findById(id);
UserViewDTO viewDTO = user.map(UserViewDTO::of).orElse(UserViewDTO::new);

Optionalの作り方

Optionalクラスにstatic factory methodが用意されています。

ofNullable

nullのときは空のOptionalを、そうでないときはその値のOptionalを返します。
実際のコーディングではこれを使うことが多いです。

Optionalで取り扱いたいが、ライブラリではそうなっていないときなどに、使います。
例えば、Spring Data JPAと違って、ebeanというORMでは、主キーでの検索結果はOptional型になっていません。同じ様にOptionalで扱いたければ、下記のようにします。

Optional<User> user = Optional.ofNullable(User.find.byId(1L));

empty

空のOptionalを返すメソッドです。よくあるのは、例外がおきたときなど、明示的に空のOptionalを返せるときに使います。

try {
  // 処理に成功したらOptionalを返す
  return Optional.ofNullable(...);
} catch (Exception e) {
  // 例外が発生したら空のOptionalを返す 
  return Optional.empty(); 
}

of

non-nullな値をOptionalにするときに使います。nullでないことがわかっていて、かつOptionalを使った方がいいという、実用的なケースがあまりなさそうです。返り値がOptionalのメソッドのテストコードなどでは、使いみちがあるかもしれません。

return Optional.of(new User());

ケース別使用例

存在しないときは初期値を使える

orElseかorElseGetを使います。初期値の生成のコストが大きいときは、orElseGetを使います。


Optional<User> user = ...;
Long userId =  user.map(User::getId).orElse(1L);

値がなにも入っていないインスタンスを初期値として渡す場合はorElseGetが使えます。
nullを返すよりも空のインスタンスの方が、なにかと取り扱いがいいです。
upsert処理用のインスタンスを作るときなどに使えます。


User modifiedUser = user.orElseGet(User::new);

存在しないときは例外スローできる

orElseThrowを使います。存在しないときの処理を、例外をキャッチする別の箇所でやれる場合などに使えます。Rest APIで、id指定のエンドポイントの実装などに使えます。


@GetMapping("/users/{id}")
public ResponseEntity<User> showById(@PathVariable("id") long id) {
   Optional<User> userOpt = userRepository.findById(id);
   User user = user.orElseThrow(() -> new NotFoundException("not found"));
   return ResponseEntity.ok(user);
}

存在しないときはなにも処理しない

ifPresentを使います。存在するときにだけになにか処理するときに使えます。
更新系の処理で、正しいidが指定されたときだけ実行するというときなどに使えます。


Optional<User> user = userRepository.findById(id);
user.ifPresent(x -> {
  x.setName(form.getName());
  userRepository.save(x);
});

存在しないときは別の処理をする

Java8では、isPresentとif文を使うことになります。
この場合は、isPresentのチェック後に、Optional#get()で値を取り出します。
存在しない場合はログ出力するときなどです。


Optional<User> userOpt = ...;
if (userOpt.isPresent()) {
  User user =  userOpt.get();
  // 存在したときの処理
} else {
  // 存在しないときの処理
}

Java8でこのケースになった場合は、Optionalを使い続けるメリットがあまりないです。
orElseなどで、値を取り出し、そこで値があるかないかを別の形でチェックするのを検討してみましょう。


Optional<User> userOpt = ...;
User user = userOpt.orElseGet(User::new);
if (user.getId() == null) {
  // 存在しないケース
} else {
  // 存在するケース
}

Java9からifPresentOrElseが追加されていて、存在しないケースも関数でかけるようになりました。


Optional<User> user = ...;
user.ifPresentOrElse(
   x -> {
     // 存在したケース1
     x.setName(name);
     userRepository.save(x);
   }, 
  () -> {
   // 存在しないケース 
   logger.info("not found")
  }):

存在したとき、なんらかの条件で絞り込みたい

filterを使います。DBからの取得する段階では絞り込みができないときなどに使います。


Optional<User> user = userRepository.findById(id);
User activeUser = user.filter(x -> x.getStatus().isActive())
                      .orElseThrow(IllegalStateException::new);

Optionalは使わない方がいいケース

Stringや、intなどのプリミティブ型をOptionalで包みたい

Stringの場合は、nullを極力使わず、値がないことは空文字で表現すればだいたいのことはできます。


String name = ...;
if (StringUtils.isEmpty(name)) {
  ...
} else {
  ...
}

int,longなどは、OptionalInt、OptionalLongなどの専用のOptional型は用意されています。
これは、Listなど、コレクション型からStream APIを使った際に使うことが想定されているようです。

non-nullが担保できる場合は、プリミティブ型(int,longなど)を使い、nullableな場合は、ラッパー型(Integer,Longなど)を使い、必要に応じてnull checkすればいいです。

リストなどのコレクション型をOptionalで包みたい

基本的には不要です。nullを使わず、空のコレクションかどうかで、値の有無を判定しましょう。
例えば、返り値がコレクション型のメソッドを作る場合、nullではなく空のコレクションを返却すればいいです。そうすれば、Optionalでラップする機会はないです。大概のライブラリはそのような使用になっています。


List<Integer> list = ...;
if (list.isEmpty()) {
  ...
} else {
  ...
}

メソッドの引数にOptionalを使いたい

nullを引数に渡すことは一般的にはよくないことと知られています。これもそれの延長ということです。
Optionalを渡すと、なかで存在するかどうかのチェックをすることになります。
メソッド内でやるよりも、呼び出し側でOptionalをはずした方が、メソッドも使いやすく、読みやすくなるようです。


public void execute(String name, Optional<User> userOpt) {
   // 初期値にできるものがケースによって違うと、orElseでOptionalをはずしづらくなる
   User user = userOpt.orElse(...) 
}


public class Something {

  public void execute(String name, User user) {...}

}


Something something = new Something();
Optional<User> user = ...;

user.ifPresent(x -> something.execute("test", x));
something.execute("test", user.orElseGet(User::new));

成功(正常)か失敗(異常)かどうかを返り値で判定したい

成功のときは値があるOptional、そうでないときは空のOptionalを返す、として、呼び出し側で、成功か失敗に応じて処理を分けたい、というような使い方です。


try {
  ... 
  return Optional.of(...);
} catch (Exception e) {
  return Optional.empty();
}

Optional<Result> result = ...;
if (result.isPresent()) {
 ...
} else {
 ...
}

これはisPresentを使うケースにつながるので、単純なケースでない限りはやらない方が良さそうです。
Either型がある言語では、それを使えばいいのですが、ない場合はそれに近いものを用意せざるを得ないです。
結果用のオブジェクトをつくって、その中に成功かどうかのフラグや、返却したいものをつめるといいです。


public class SomethingResult {

  private final boolean isSuccess;

  private final Something result;

  private final Exception error;

  public static SomethingResult success(Something something) {...};

  public static SomethingResult error(Exception exception) {...};

}

try {
  ...
  return SomethingResult.success(...);
} catch (Exception e) {
  return SomethingResult.error(e);
}


SomethingResult result = ...;
if (result.isSuccess()) {
  ...
} else {
  ...
}

参考リンク

15
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?