2009年のカンファレンスでNull参照を発明したことについて謝罪している。
それは10億ドルにも相当する私の誤りだ。
Null安全でない言語を使っている開発者がいつも悩まされている憎き問題です。
計算機科学者である アントニー・ホーア | wikipedia が上記のような発言をしたとおり、言語に織り込まれた技術的負債といっても過言ではありません。
現代の新規開発においては null安全でない言語は、もはやレガシー言語だ | Qiita 記事のようにNull安全でない言語自体を回避するという考え方もありでしょう。
しかしながら、様々な理由でNull安全でない言語を選択しなければいけない場面は尚存在しています。
そこで今回はJavaにおけるNullと向き合う際の手法について考えていきます。
読み進める上での注意点
- 対策案には「オススメ度」をつけていますが経験に基づく個人的な感想です
- チームで取り入れる際のおすすめ度は3段階で示しています( 〜 )
策
JSR-305
Javaの仕様としてソフトウェア欠陥検出のために定義されたアノテーション群です。
そのため、Runtimeで動作を期待されたアノテーションではなく、あくまで開発時の補助的な機能として定義された仕様です(たとえば、Find BugsやIntelliJなどのIDEでのサポート)。
戻り型が null
かどうかを宣言する
オススメ度:
import javax.annotation.Nullable;
@Nullable
public String createText() {
// ...
import javax.annotation.Nonnull;
@Nonnull
public String createText() {
// ...
チェックサポートの機能を利用できることもとても重要ですが、チームとして取り組む場合は以下点で効力が最大化されると感じました。
- 設計・実装者のメソッドごとの意図が明確に残せる
- 副作用としてレビュー時の質が向上する
- メソッドを利用する実装者がnullについて考慮する手間が減る
- 不要なnullチェックが削減される
引数に null
を許容するかどうかを宣言する
オススメ度:
import javax.annotation.Nullable;
import javax.annotation.Nonnull;
public String createText(@Nonnull String id,
@Nullable String option) {
// ...
「戻り型が null
かどうかを宣言」と同様のメリットを得られる一方で、チームとしては以下デメリットが目立ちました。
- すべての引数について付加するのは手間である
- 手間であるがゆえに形骸化しやすい
- 引数がアノテーション祭り状態となり、視認性が落ちる
Java 8+ 標準 Optional
Optional
で戻り型として null
を示唆する
オススメ度:
import java.util.Optional;
public Optional<String> getText() {
// ...
Java 8 Optional
については数多の記事で言及があるので詳細は省きます。
ここであまりオススメでない理由としては以下が挙げられます。
-
Optional
型はシリアライズ可能(Serializable)なクラスではないため、使い方が限定される- ライブラリ(特にO/R Mapper系)がサポートされていないケースではルールが破綻しやすい
■ 参考
表明プログラミング
達人プログラマーで紹介されている事前条件をチェックする「表明プログラミング」。
設計/実装者の「より高いレベルのコンセプト」をコードとして残すことができるため、非常に価値があります。
プログラムの前提を assert
でテストする
オススメ度:
private String createText(String id) {
assert (id != null) : "id must not be null.";
// ...
Javaでの表明プログラミングの代表例といえば assert
でしょう。
ただし、コードとしては以下点に注意する必要があります(アサーションを使用したプログラミング | oracle.com からの抜粋)
■ アサーションの有効化および無効化
デフォルトでは、実行時にアサーションは無効になっています。2つのコマンド行スイッチを使用して、アサーションの有効/無効を切り替えることができます。
様々な詳細レベルでアサーションを有効にするには、-enableassertionsまたは-eaスイッチを使用します。
assert
はあくまで実装者の意思表明のための補助手段であり、プロダクトコードのRuntimeで例外を発生させる用途ではない、と読み取ることができます。
また公式ドキュメントを注意深く読むと、副作用のある処理についてなど重要な記載されていますので採用の際にはどういった点で使用するか熟考する必要があります。
ライブラリのアサーションを活用する
オススメ度:
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にする
オススメ度:
public String createText(final String id) {
// ...
public String createText(String id) {
final String prefix = something(id);
// ...
final
の使用はnull
とは直接的な関わりはなさそうに見えます。
しかしAssertionでの非null
保証との組み合わせで、自メソッド内で非 null
をほぼ完全保証できます。
結果、中間処理での「何が来るかわからないのでとりあえずnull
チェック入れる」ような不要な処理を自信を持って削除できます。
CollectionをImmutableにする
オススメ度:
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
ライフを!