らぼるてっく。

てっくてっく歩いてっく。

JavaのOptionalとかを使ったらNull Safeなコードにできるよ

こんにちは。中垣です。

コード書けたあとに実行してみたら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を使ったことがある人はfiltermapは見覚えがありますよね。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を使うことで以下のことを表せます。

  • FieldMethodの戻り値が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なハンドル方法を一つしか見せてないですが、ifPresentmapなど他にも色々あるので使ってみてください。Optionalでおすすめされている使い方は戻り値のみです。なので最初はそれに沿って作ったほうが安全です。

なぜ戻り値のみかと言うのはこのStackOverflowの投稿でOracleのJava Language ArchitectのBrian Goetzさんが説明しています。気になる人は読んでみてください。

終わり

みんなもNull safeなコードベースを作って「かもしれないコーディング」を抜け出して「迷いなきコーディング」をEnjoyしてください。

最後に

ラボルでは、エンジニアを積極採用中です。1、2年目のエンジニアから経験豊富なテックリードやエンジニリングマネージャーまで、興味がある方はぜひご応募ください!!

labol.co.jp