読者です 読者をやめる 読者になる 読者になる

かとじゅんの技術日誌

技術の話をするところ

nullという値は本当に必要か考えよう

今回は、nullの扱いは気をつけようねというお話。特にこれからの人に読んでもらいたい。
nullは変数を初期化する時などに、当たり前のように登場します。Javaではnullがないなんて考えられないわけです。(C#でも同様)
しかし、nullの考案者のTony Hoareは「10億ドル単位の過ち」と発言しています。
null参照の考案は10億ドル単位の過ち?(スラッシュドット・ジャパン) - エキサイトニュース
nullには、それだけの魔性があって使い方を考える必要があります。nullは”値が存在しない”という状態を表現する時に使われることが多いのですが、そのnullに対する注意が十分に払えずにトラブってしまうことが多いのです。Scalaではその"値が存在しない"という状態をOption型のNoneで表現することでnullを回避する手段が提供されています。JavaではScalaのようにはできませんが、回避する方法はあります。特にどういう場合に気をつける必要があるか、独断と偏見でポイントを絞って書いてみたいと思います。

nullの代わりに空の配列や空のコレクションを使う

ひとつ目は、メソッドの戻り値が配列やコレクションの場合に、空の配列や空のコレクションとしてnullを返した場合に、どういうデメリットがあるかというお話。これはEffective Java 第二版「項目43 nullではなく、空配列か空コレクションを返す」で詳しく語られています。
例えば、以下のようなユーザ情報を扱うUserを扱うリポジトリ UserRepositoryがあるとして、findByActiveメソッドは現在有効なUserだけを検索して返す責務を持っています。そのUserが1件も見つからなかった場合は"空のコレクション"という意味でnullを返しています。

public class UserRepository {
  public Collection<User> findByActive(){
    // ...
    if ( /* Userが1件もない */ ){
      return null;
    }
    Set<User> result = new HashSet<User>();
    // Userがある場合 result.add(...); などで追加
    return result;
  }
}

次に、クライアント側*1のコードとして、このようなコードを書いてみました。言わずもがな、findByActiveメソッドがnullを返す状況では、2行目でNullPointerException(以下、NPE)が発生します。nullチェックを忘れると痛い目にあいます。
そもそも、nullはオブジェクトではなく値です。なので、null.iterator()などとメソッドを呼ぶことができないわけです。nullを返してしまう考え方は、C言語から由来していると思うのですが、こういう面倒な側面があります。

Collection<User> users = userRepository.findByActive();
Iterator<User> iterator = users.iterator(); // NullPointerException
while(iterator.hasNext()){
  System.out.println(user.next());
}

nullを戻り値として返される状況では、次のようなコードを書くことになるでしょう。しかし、これはクライアント側にnullを意識させています。nullのせいで複雑になっていないでしょうか。

Collection<User> users = userRepository.findByActive();
if ( users != null ){ // null以外かどうかを、チェックする
  Iterator<User> iterator = users.iterator();
  while(iterator.hasNext()){
    System.out.println(user.next());
  }
}

この程度ならそれほど複雑化してないじゃん、という見方もありますが、nullチェックのif文のスコープがネストしていたらどうですか?おそらく、心のなかで「死ねばいいのに」と、つぶやいたでしょう。

Collection<User> users = userRepository.findByActive();
if ( users != null ){ // null以外かどうかを、チェックする
    Collection<Item> items = itemRepository.findByUsers(users);
    if ( items != null ){
      // (ry
    }
  }
}

これは配列の場合でも同じことがいえます。

User[] users = userRepository.findByActive();
if ( users != null ){ // null以外かどうかを、チェックする
 // (ry
}

もっとよい方法があります。nullの代わりに空の配列や空のコレクションを用いればよいのです。

public class UserRepository {
  public Collection<User> findByActive(){
    // ...
    if ( /* Userが1件もない */ ){
      return new HashSet<User>(); // nullの代わりに空のコレクションを返す
    }
    Set<User> result = new HashSet<User>();
    // Userがある場合 result.add(...); などで追加
    return result;
  }
}

配列であれば、

return new User[0];

とすればよいでしょう。
空の配列や、空のコレクションを返せば、クライアント側のコードは複雑にならなくて済みます。if文というのは不具合の住み着きやすい場所です。そういう意味でもこの方法は有益ではないかと思います。

Collection<User> users = userRepository.findByActive();
Iterator<User> iterator = users.iterator(); // NullPointerExceptionは発生しない
while(iterator.hasNext()){
  System.out.println(user.next());
}

上記の例では、HashSetをnewしていますが

Set<User> userSet = Collections.emptySet();
List<User> userList = Collections.emptyList();
Map<User> userMap = Collections.emptyMap();

などのAPIを利用してもよいでしょう。これは常套手段なんで覚えておくとよいと思います。

nullの代わりに何もしないオブジェクト Null Objectパターン

二つ目はNull Objectパターンです。これはデザインパターンの一種で、nullの代わりに何もしないオブジェクトを扱うパターンです。
例えば、円や矩形などの図形を描画するオブジェクトの例で考えてみます。

// 図形インターフェイス
public interface Shape {
  public Long getId();
  public void draw(Target target);
}
// 円
public class Circle implements Shape {
  // 中略
  @Override
  public void draw(Target target){
    // 円を描く
  }
}
// 矩形
public class Rectangle implements Shape {
  // 中略
  @Override
  public void draw(Target target){
    // 矩形を描く
  }
}
// Null Object
public enum NullShape implements Shape {
  INSTANCE;
  @Override
  public void draw(Target target){
    // 何もしない
  }
}

ShapeRepositoryのfindByIdメソッドで検索条件に該当するShapeが見つからなかった場合に、nullを返すのではなく何もしない図形クラスであるNullShapeのインスタンスを返すと同様にNPEは発生しません。当然、このインスタンスのメソッドを呼び出しても何もしないので、nullチェックも不要です。NullShapeが列挙型を採用しているのは、Null Objectはひとつのインスタンスで十分であるため、シングルトンを実装する手段として採用しています。*2

public class ShapeRepository {
  public Shape findById(Long id){
    // ...
    if ( /* idに該当するShapeがない場合 */ ){
      return NullShape.INSTANCE;
    }
    // shape = new Circle(); とか new Rectangle();
    return shape;
  }
}

// クライアント側のコード
Shape shape = shapeRespository.findById(1);
shape.draw(target); // NPEは発生しない

この例では、値がないという状態をNull Objectで表現しています。そもそも、Null Objectではなく、値が存在しないことを例外で扱う場合もあります。それはAPIへの要求に合わせて考えるべきです。

当たり前のように使っているnullは、使いどころを間違えるといろいろと面倒というが分かれば、それがより良い設計のスタートだと思います。

補足:
改めて考えを整理していたら、ふと思ったので「すべてのnullが有害ではないということ」を補足しておきます。逆のことを言っているように思えるかもしれませんが、こういうことです。
メソッドの利用者が、オブジェクトへの参照を求めているのにnullを返すのはよくないのですが、そもそも、オブジェクトへの参照ではなくnullが値としてちょうどよい場合があります。シングルトンの初期化済みフラグをnull値で代用する場合などです。以下のエントリを参照してください。
遅延初期化には気をつけろ - じゅんいち☆かとうの技術日誌
この場合の、nullはまだ未初期化であるというフラグの意味があり、オブジェクトである必要はありません。つまり、このif文のnullチェックは本質的な処理であり有害ではありません。また、このためだけにNull Objectを導入するのも振る舞いとして不適切でしょう。ただ、フラグでnullを扱う場合でも、クラス内とかメソッド内などの狭いスコープに限定した方がいいと思います。
まぁ、目的をきちんと考えて使い分けるとよいってことですね。それが始めての人には辛いところなんですが、頑張ってください!

あわせて読みたい
Javaでnullを回避するために似非Option型を作ってみる - じゅんいち☆かとうの技術日誌

*1:メソッドの利用者側

*2:Log4Jには何もしないAppenderとしてNullAppenderがあるようです。ストラテジパターンとは相性がよいかもしれません。