こんにちは。中垣です。
コード書けたあとに実行してみたらNullPointerExceptionが起きるのいやですよね。なので今回はNull Safeなコードにする方法を書きます。
Null safeじゃなかったらどう問題なのか?
Null safeじゃない状況は引数や戻り値がNullになるかもしれない状態です。この状況での問題点は以下のことが考えられます。
- 常にNullに気配りをしないといけないため、開発体験が良くない
- オブジェクトはほぼすべてNullHandlingが必要かの判断をしないといけない
- 同じようなNullHandlingのコードを何回も書かないと行けない
- 開発者がNullHandling漏れをする可能性が大きい
- コードレビュワーがNullHandling漏れに気づけない
同じような経験したことないですか?コード例で実装どういう問題が起きるかを見てみます。
Null safeじゃないコード例
今回は以下のようなログインのメソッドを例にします。
public UserSession login(String username, String password) { // usernameでUserを取得 // passwordが一致していたらUserSessionを作成して返す }
実装してみます。
public UserSession login(String username, String password) { User user = findUser(username); if (!user.getPassword().equals(password)) { throw new AuthException(); } return new UserSession(user.getId(), user.getLastName()); }
シンプルでわかりやすいですね。じゃ実行してみましょう。
java.lang.NullPointerExceptionが起きました!
- Nullのuserに対してgetPassword()してる
- UserSessionのConstructorの引数に対してAssert.notNullがあるのにNullをわたしてる
コーディングしてる人は実際にコードを実行してみるまで大抵気づかなく、NullHandling漏れは簡単に起こり得ます。コードレビューしてる人は実際にコードを実行しないのでNullHandling漏れに気づけません。
もうちょっとNullに気をつけて実装していきます。ここでNullに気をつけているプログラマーの心の声をコメントに残しておきました。
public UserSession login(String username, String password) { // usernameとpasswordはNullかもしれないからNullHandlingしよう if (username == null || password == null) { throw new AuthException(); } User user = findUser(username); // findUserはNullを返すかもしれない。JavaDocかソース見てチェックしよう。 // Null返すっぽいからNullHandlingしておこう if (user == null) { throw new AuthException(); } // 「user.getPassword()」はNullかもしれない。 // Nullの場合「user.getPassword().equals(password)」はNullPointerException起きるから // 「password.equals(user.getPassword())」を使おう。 if (!password.equals(user.getPassword())) { throw new AuthException(); } // UserSessionを作るのにLastName必要だけど「user.getLastName()」はNullかもしれない。 // JavaDocかソースを見てチェックしよう。 // Doc書いてなかった。 // でもDatabaseのテーブルみるとlast_nameはNullableだったからNullHandlingしよう if (user.getLastName() == null) { return new UserSession(user.getId(), "Anonymous"); } return new UserSession(user.getId(), user.getLastName()); }
戻り値がすべてNullableだったらこんな感じに「かもしれないコーディング」をする必要があります。神経を尖らせておかないとすぐNullPointerExceptionが起きちゃいます。開発者も大変です。
問題が明確になったところで、今度はNull safeなプロジェクトを見てみます。
Null safeなプロジェクト
Null safeじゃない状況と比べて、Null safeの場合こんな利点があります。
- Nullをあまり気にしなくて良くて、開発体験が良い
- 開発者がNullHandling漏れをする可能性がすくない
- コードレビュワーがNullHandling漏れに気づける
本当かと疑ってますか?Null safeでまた同じコード例をNull safeで実装してみます。
Null safeなコード例
Null safeなコードではNullHandlingの代わりにOptionalクラスを使います。OptionalクラスはObjectはNullになりえる状態を表現するために作られたクラスです。
public UserSession login(String username, String password) { Optional<User> oUser = findUser(username); return oUser // パスワードが一致しなかったら除外 .filter(user -> password.equals(user.getPassword())) // User型をUserSession型にマッピングする .map(user -> { Optional<String> lastName = user.getLastName(); return new UserSession(user.getId(), lastName.orElse("Anonymous")); }) // Userが見つからない、またはパスワードが一致しなかったらエラー .orElseThrow(() -> new AuthException()); }
Stream APIを使ったことがある人はfilter
やmap
は見覚えがありますよね。Stream APIと同じでfilter
は除外するときに使い、map
は別の型にマッピングするときに使います。これによりコードが宣言的になりましたね。
僕はStream APIの書き方が大好きなのでOptionalを使ったコードのほうがElegantでBeautifulに見えますが、Streamに慣れてない人は上のOptionalを使ってない方が読みやすいって思うかもしれません。そういう人は今からStream APIを使い始めてください。これもまた開発が楽になってHappyになるものなので。
それではOptionalを使ったプログラマーの心の声も見てみましょう。
public UserSession login(String username, String password) { // 引数はNullにならないルールなのでNullHandling不要 Optional<User> oUser = findUser(username); // Optionalが返ってきた。値がない場合があるからハンドリングしよう。 return oUser .filter(user -> password.equals(user.getPassword())) .map(user -> { Optional<String> lastName = user.getLastName(); // Optionalが返ってきた。値がない場合があるからハンドリングしよう。 // 値がなかったら"Anonymous"っと。 return new UserSession(user.getId(), lastName.orElse("Anonymous")); }) .orElseThrow(() -> new AuthException()); }
「かもしれない」がなくなりましたね。なんて自身に満ちたプログラマーだ。Null safeなプロジェクトだとNullをそこまで気にしないで良いため、開発者は考えることが少なくなって開発が楽になります。MonsterやRedBullがない状態の集中力でも簡単にコーディングできます。コーヒーさえも不要かもしれない。今のプロジェクトをNull safeにしたくなりましたか?
次にNull safeなプロジェクトにする方法を教えます。
Null safeにする方法
labolでは上記の例のようにNull safeにコードが書けていますが、Nullを許容したプロジェクトを一気にNull safeにするのは難しいです。なので今回はNull許容したプロジェクトからNull safeに徐々に移行する方法を書きます。
ステップ1:@NotNull
を使う
一番簡単にNullHandlingの負荷を減らす方法としてはjavax.validation.constraints.NotNull
を使うことです。
このAnnotationを使うことで以下のことを表せます。
Field
やMethodの戻り値
がNullになりえないことMethodの引数
にNullを受け付けないこと
アノテーションがついたクラスやメソッドの実装者はNullにならないように気をつけて、使う側はそれをアノテーションでNullHandlingをするかの判断をするようにします。そうするとNullHandlingするかの判断する時間が削減されます。しかもこのやり方はMethod Signatureを変えることも無いのでRefactoringも不要で簡単に始められます。
こんな感じで@NotNull
を使えます。
public class User { @NotNull private final Integer userId; // 省略 @NotNull public Integer getAge(@NotNull LocalDate today) { // 省略 } }
このクラスの実装者がやること:
userId
がどのタイミングでもNullにならないようにするgetAge
で戻り値がNullにならないようにする
このクラスを使う側がやること:
LocalDate today
の引数にNullが入らないようにするgetAge
はNullHandlingしないでそのまま使う
これで@NotNull
のアノテーションがついているものに対してはNull関連の迷いがなくなりました。でもちゃんとチーム全員がこのルールを守らないとうまく機能しないのでそこはがんばってください。このアノテーションが増えれば開発も楽になります。
ステップ2:Optional
クラスを使う
ステップ1は簡単に始められて良いスタート地点ですが、毎回使うメソッドのMethod signatureを覗きに行かないといけないです。面倒くさがりの僕らにさらに楽に開発できるようになるのがOptional型です。Optional型は値がある場合と無い場合両方あることを表せます。この方法はMethod Signatureが変わるし、ルールがちょっと複雑なので@NotNull
と比べて導入がちょっと難しいです。
こんな感じに使います。
public class User { @NotNull private final Integer userId; private String lastName; // 省略 @NotNull public Integer getAge(@NotNull LocalDate today) { // 省略 } public Optional<String> getLastName() { return Optional.ofNullable(this.lastName); } }
Optionalは@NotNull
とは逆でNullableのときに使います。なので@NotNull
になっているところを変更することは無いです。
このクラスの実装者がやること:
- 戻り値がOptionalの場合、Nullにならないようにすること
// やっちゃだめな例 public Optional<String> getLastName() { if (this.lastName != null) { return Optional.of(this.lastName); } return null; // ここがダメ }
このクラスを使う側がやること:
- Optionalが返ってきたらHandlingすること
public void foo() { user.getLastName().orElse("Anonymous"); }
これでOptional型が返ってくるとMethod signatureさえも見ないでもHandlingを自然にできるようになりました。ここではOptionalなハンドル方法を一つしか見せてないですが、ifPresent
やmap
など他にも色々あるので使ってみてください。Optionalでおすすめされている使い方は戻り値のみです。なので最初はそれに沿って作ったほうが安全です。
なぜ戻り値のみかと言うのはこのStackOverflowの投稿でOracleのJava Language ArchitectのBrian Goetzさんが説明しています。気になる人は読んでみてください。
終わり
みんなもNull safeなコードベースを作って「かもしれないコーディング」を抜け出して「迷いなきコーディング」をEnjoyしてください。
最後に
ラボルでは、エンジニアを積極採用中です。1、2年目のエンジニアから経験豊富なテックリードやエンジニリングマネージャーまで、興味がある方はぜひご応募ください!!