48
49

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 5 years have passed since last update.

チームで取り組む!JavaでNullと真正面から向き合う

Last updated at Posted at 2018-06-24

2009年のカンファレンスでNull参照を発明したことについて謝罪している。

それは10億ドルにも相当する私の誤りだ。

Null安全でない言語を使っている開発者がいつも悩まされている憎き問題です。
計算機科学者である アントニー・ホーア | wikipedia が上記のような発言をしたとおり、言語に織り込まれた技術的負債といっても過言ではありません。

現代の新規開発においては null安全でない言語は、もはやレガシー言語だ | Qiita 記事のようにNull安全でない言語自体を回避するという考え方もありでしょう。
しかしながら、様々な理由でNull安全でない言語を選択しなければいけない場面は尚存在しています。

そこで今回はJavaにおけるNullと向き合う際の手法について考えていきます。

スクリーンショット 2018-06-20 19.31.07.png

読み進める上での注意点

  • 対策案には「オススメ度」をつけていますが経験に基づく個人的な感想です
  • チームで取り入れる際のおすすめ度は3段階で示しています( :star::star: :star: :star:

JSR-305

Javaの仕様としてソフトウェア欠陥検出のために定義されたアノテーション群です。

そのため、Runtimeで動作を期待されたアノテーションではなく、あくまで開発時の補助的な機能として定義された仕様です(たとえば、Find BugsやIntelliJなどのIDEでのサポート)。

戻り型が null かどうかを宣言する

オススメ度: :star: :star: :star:

import javax.annotation.Nullable;

@Nullable
public String createText() {
    // ...
import javax.annotation.Nonnull;

@Nonnull
public String createText() {
    // ...

チェックサポートの機能を利用できることもとても重要ですが、チームとして取り組む場合は以下点で効力が最大化されると感じました。

  • 設計・実装者のメソッドごとの意図が明確に残せる
    • 副作用としてレビュー時の質が向上する
  • メソッドを利用する実装者がnullについて考慮する手間が減る
    • 不要なnullチェックが削減される

引数に null を許容するかどうかを宣言する

オススメ度: :star:

import javax.annotation.Nullable;
import javax.annotation.Nonnull;

public String createText(@Nonnull String id,
                         @Nullable String option) {
    // ...

「戻り型が null かどうかを宣言」と同様のメリットを得られる一方で、チームとしては以下デメリットが目立ちました。

  • すべての引数について付加するのは手間である
    • 手間であるがゆえに形骸化しやすい
    • 引数がアノテーション祭り状態となり、視認性が落ちる

Java 8+ 標準 Optional

Optional で戻り型として null を示唆する

オススメ度: :star:

import java.util.Optional;

public Optional<String> getText() {
   // ...

Java 8 Optional については数多の記事で言及があるので詳細は省きます。
ここであまりオススメでない理由としては以下が挙げられます。

  • Optional 型はシリアライズ可能(Serializable)なクラスではないため、使い方が限定される
    • ライブラリ(特にO/R Mapper系)がサポートされていないケースではルールが破綻しやすい

■ 参考

表明プログラミング

達人プログラマーで紹介されている事前条件をチェックする「表明プログラミング」。
設計/実装者の「より高いレベルのコンセプト」をコードとして残すことができるため、非常に価値があります。

プログラムの前提を assert でテストする

オススメ度: :star: :star:

private String createText(String id) {
    assert (id != null) : "id must not be null.";
    // ...

Javaでの表明プログラミングの代表例といえば assert でしょう。
ただし、コードとしては以下点に注意する必要があります(アサーションを使用したプログラミング | oracle.com からの抜粋)

■ アサーションの有効化および無効化
デフォルトでは、実行時にアサーションは無効になっています。2つのコマンド行スイッチを使用して、アサーションの有効/無効を切り替えることができます。
様々な詳細レベルでアサーションを有効にするには、-enableassertionsまたは-eaスイッチを使用します。

assert はあくまで実装者の意思表明のための補助手段であり、プロダクトコードのRuntimeで例外を発生させる用途ではない、と読み取ることができます。
また公式ドキュメントを注意深く読むと、副作用のある処理についてなど重要な記載されていますので採用の際にはどういった点で使用するか熟考する必要があります。

ライブラリのアサーションを活用する

オススメ度: :star: :star: :star:

import org.springframework.util.Assert;

public String createText(String id) {
    Assert.notNull(id, "ID must not be null.");
    // ...

Springフレームワークの場合 org.springframework.util.Assert の利用がそれにあたります。

JavaのAssertionとは違い、Runtimeであっても例外を発生させます。
nullチェックの簡略化ではありますがRuntimeでも動作するため、実装者の非常に強い意思表明をコードに残すことができます。

間接的な null 防御

変数を final 宣言しImmutableにする

オススメ度: :star: :star:

public String createText(final String id) {
    // ...
public String createText(String id) {
    final String prefix = something(id);
    // ...

finalの使用はnullとは直接的な関わりはなさそうに見えます。
しかしAssertionでの非null保証との組み合わせで、自メソッド内で非 null をほぼ完全保証できます。
結果、中間処理での「何が来るかわからないのでとりあえずnullチェック入れる」ような不要な処理を自信を持って削除できます。

CollectionをImmutableにする

オススメ度: :star: :star:

Java標準
import java.util.Collections;

public String createText() {
    List immutables = Collections.singletonList("value1", "value2");
    // ...
import java.util.Collections;

public String createText() {
    Map<String, String> immutables = Collections.singletonMap("key1", "value1");
    // ...
import java.util.Collections;

public List<Store> fetchStores() {
   List<Store> stores  = findAll();
   
   return Collections.unmodifiableCollection(stores);
Guava
import com.google.common.collect.ImmutableMap;

public String createText() {
    Map<String, String> immutables = ImmutableMap.of("key1", "value1", "key2", "value2");
    // ...
import com.google.common.collect.ImmutableList;

public String createText() {
    List<String> immutables = ImmutableList.of("value1", "value2");
    // ...

コレクションのImmutable化に関してはかなり局所的な防御となります。

後続の中間処理でのnull例外をケアすることを目的として、一度作成したコレクションにnull要素を混ぜ込まれる危険を減らします。
地味ではありますが可能な限り取り入れるべきです。

用例

では実際に適用前/後でどのくらい情報量に差が出るかを見てみましょう。

before

public class StoreService {
    
    public Store fetchStore(String storeId){
        // ...
    }

    public List<Store> fetchSisterStores(String storeId){
        // ...
    }

    public List<Coupon> fetchCoupons(String storeId,
                                         List<Category> categories){
        // ...
    }
}

after

public class StoreService {

    @Nonnull
    public Store fetchStore(final String storeId) {
        Assert.notNull(storeId, "store-id must not be null.");
        // ...

    }

    @Nullable
    public List<Store> fetchSisterStores(final String storeId) {
        Assert.notNull(storeId, "store-id must not be null.");

        // ...
    }

    @Nonnull
    public List<Coupon> fetchCoupons(final String storeId,
                                     final List<Category> categories) {
        Assert.notNull(storeId,    "store-id must not be null.");
        Assert.notNull(categories, "categories must not be null.");

        // ...
    }
}

より表現に富んだコードになったのではないでしょうか?
シグニチャの前後の数行を読むだけで、大体の用例を即時に吸い上げることができ利用者にも優しいコードとなりました。

レビュアーの視点で見てみると fetchSisterStores( ... )fetchCoupons( ... ) で返す List 型の仕様の異なりがより鮮明に浮き上がり、レビューの質も上がりそうですよね。

まとめ

チームとしてのnull対策を選択し、nullによる技術的負債を極限まで削ぎ落としましょう!
そしてチーム全員で以下のメリットを実感していきましょう!

  • コードの可読性の向上
  • レビュー品質の向上
  • ヌルポで枕を濡らす夜の減少

それではよき null ライフを!

48
49
1

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
48
49

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?