1. Java 11のnull
Javaではint
やboolean
などのプリミティブ型の値はnull
になることはない。
なので、参照型の値だけを考えれば良い。
ただし、プリミティブ型のラッパークラス(Integer
やBoolean
クラスなど)のボクシングやアンボクシングには気をつける必要がある。
nullオブジェクトパターン
何かキーが押されたら、そのキーに対応した処理を実行する、という例を考えてみよう:
...
public final class Main {
private final Map<Character, Runnable> map;
public Main() {
map = new HashMap<>();
map.put('h', () -> moveCursorLeft());
map.put('j', () -> moveCursorDown());
map.put('k', () -> moveCursorUp());
map.put('l', () -> moveCursorRight());
}
public void handleKeyInput() {
var c = getPressedKey();
var action = map.get(c);
// actionはnullの可能性があるので、次のメソッド呼び出しは
// NullPointerExceptionをスローするかもしれない
action.run();
}
/**
キーボードの押されたキーの文字を返す.
キーが押されてない場合は、押されるまでブロックする.
@return
押されたキーの文字
*/
private char getPressedKey() {
...
}
...
Map
インターフェースのget(Object)
メソッドはnull
を返す可能性があるので、
戻り値がnull
かどうかを確認するコード、
すなわちnullチェックを追加する必要がある:
private void handleKeyInput() {
var c = getPressedKey();
var action = map.get(c);
if (action == null) {
// 登録されてないキーは何もしない
return;
}
action.run();
}
このnullチェックは何を表しているのだろうか。
Map
の代わりにif
—else if
—else
やswitch
を用いて実装すれば、
そのときにelse
やdefault
で記述する処理が、
このnullチェックに相当することがわかる
(#20「分岐よりサブタイピングを選ぶ」を参照)。
なお、
if
やswitch
では循環的複雑度(cyclomatic complexity)が上昇するし、 テストも面倒になる。 さらにelse
やdefault
の書き忘れはコンパイル時も実行時も直ちにはエラーにはならないので、 間違いに気づきにくい。
さて、このnullチェックは次のように消すことができる:
private static final Runnable DO_NOTHING = () -> {};
...
private void handleKeyInput() {
var c = getPressedKey();
// もちろん、DO_NOTHINGの代わりに空のメソッドのメソッド参照を渡してもよい
var action = map.getOrDefault(c, DO_NOTHING);
action.run();
// 実際、actionは不要で次のように書いて良い:
//
// map.getOrDefault(c, DO_NOTHING)
// .run();
}
get(Object)
の代わりにgetOrDefault(Object, V)
を用いた。
対応する操作が存在しない文字に対しては、
null
ではなく、
DO_NOTHING
という何もしないRunnable
が返ってくる。
文字に操作が関連付けられているかどうかに関わらず、返された操作を実行するだけでよくなった。
そして、nullチェックのためのif
はライブラリ側におっかぶせることで消失した。
このように、
null
の代わりに特別なオブジェクトを使う技法をnullオブジェクトパターン [1] と呼ぶ。
書籍 Refactoring [2] ではIntroduce Null Objectで紹介されている。
いつものことだが、このパターンも銀の弾丸ではない。
何もしないRunnable
やConsumer
みたいなオブジェクトが使えるパターンではうまくいくことが多い。
しかし、Function
やSupplier
みたいなオブジェクトを使うパターンは向いていないこともある。
次のような例を考えてみよう:
interface Color {
/**
指定した名前にマッチするColorインスタンスを返す.
@param name
...
@return
{@code name}にマッチするものが無ければ{@code null},
そうでなければマッチしたインスタンス.
*/
static Color findByName(String name) {
...
}
/**
RGBを24ビットで表した整数値を返す.
@return
...
*/
int getRgb();
}
findByName(String)
は、
Map
のget(Object)
と同様、
検索系の操作の定番で、
欲しいものが無かったらnull
を返す。
呼び出す側のコードは次のようになる:
var yellow = Color.findByName("YELLOW");
// yellowはnullの可能性があるので、次のメソッド呼び出しは
// NullPointerExceptionをスローするかもしれない
var rgb = yellow.getRgb();
nullチェックを追加すれば終わりだが、
先ほどのMap
の例と同様に、
nullオブジェクトのようなものを導入してみるとどうなるのだろうか:
interface Color {
/** 不正なカラーを表す. */
static final Color INVALID = () -> -1;
/**
指定した名前にマッチするColorインスタンスを返す.
@param name
...
@return
{@code name}にマッチするものが無ければ{@link INVALID},
そうでなければマッチしたインスタンス.
*/
static Color findByName(String name) {
...
}
...
不正な色のRGB値といってもよくわからないので、 定番の−1でも返しておくことにした。 すると、これを使う側は次のようになる:
var yellow = Color.findByName("YELLOW");
var rgb = yellow.getRgb();
// 結局、rgbが-1かどうかを調べる必要がある
rgb
が−1かどうかをチェックしなくても、続くコードを実行できてしまう。
状況によっては、このようなコードはかえって危険である。
必要なnullチェックが抜けていたら例外をスローし、
以降のコードを実行しない方が幸せなこともあるからだ。
そう、問題はnull
かどうかではなく、必要なチェックをするかどうかだ。
だが、そのチェックこそ、
このようなnullオブジェクトパターンもどきがうまくいかない理由である。
findByName(String)
メソッドの中身を想像するに、
その中で次のような本質的なチェックが既に実装されているはずだ:
interface Color {
...
static Color findByName(String name) {
...
// 次のifが本質的なチェック
if (nameに関連するColorオブジェクトが見つからない) {
// パスA
return INVALID;
}
// パスB
return 見つかったColorオブジェクト;
}
...
そして、呼び出し側のコードのチェックは次のようになっているはずだ:
var yellow = Color.findByName("YELLOW");
var rgb = yellow.getRgb();
if (rgb == -1) {
// 処理Aを実行
} else {
// 処理Bを実行
}
つまり、本質的なチェックでパスAを通過したならば処理Aを、
パスBならば処理Bを、呼び出し側は決定的に実行する(deterministicである)。
ここまできたら、もう気が付いたと思うが、
findByName(String)
に追加で「処理A」と「処理B」も渡してしまえば、
呼び出し側は戻り値もそのチェックも不要になる。
要するに、次のような形に変えてやればよい:
interface Color {
...
static void findByName(String name, 処理A, 処理B) {
...
if (nameに関連するColorオブジェクトが見つからない) {
// 処理Aを実行
return;
}
// 処理Bを実行 with 見つかったColorオブジェクト
}
...
}
...
// 呼び出し側は戻り値もそのチェックも不要
Color.findByName("YELLOW", 処理A, 処理B);
Optional
クラス
この手の操作、つまり「値の有無の情報と、 さらに値がある場合はその値を取得する」操作は、 次のやり方で記述できる:
- 型
T
の値と値の有無を表すboolean
値を保持するインスタンスを戻り値とする - 戻り値をやめて、値を受け取るための
Consumer<T>
と値が無いことを知らせるRunnable
を引数に追加する
前者は言語によっては例えばタプルを使っても実現できる。
後者は単にコールバックで結果を得るだけだ。
仮にPair<K, V>
のような酷いクラスを利用可能だとすると、
先ほどのfindByName
メソッドは次のようなコードになる:
interface Color {
static Pair<boolean, Color> findByName(String name) {
...
}
static void findByName(String name,
Consumer<Color> found,
Runnable notFound) {
...
}
...
しかし、この調子でAPIを記述していけば、 コードはすぐにボイラープレートまみれになるだろう。
幸い、Javaにはこれらの操作をカプセル化したOptional
クラス†1がある。
先ほどの例をそのOptional
クラスを使って書き直してみよう:
†1
Optional
はJava 8から追加された。
interface Color {
/**
指定した名前にマッチするColorインスタンスを返す.
@param name
...
@return
{@code name}にマッチするColorインスタンスを
含む{@link Optional<T> Optional}, または
空のOptional (マッチするものがない場合).
*/
static Optional<Color> findByName(String name) {
...
}
}
使う側は次のようになる:
var yellow = Color.findByName("YELLOW");
yellow.ifPresent(c -> {
var rgb = c.getRgb();
System.out.println("yellow: rgb=" + rgb);
});
// あるいは、次のように書いても良いが...
var blue = Color.findByName("BLUE");
if (blue.isPresent()) {
var rgb = blue.get().getRgb();
System.out.println("blue: rgb=" + rgb);
}
ifPresent(Consumer)
は値が存在すれば、
その値をパラメータとして引数のConsumer
を実行する。
存在しなければ何もしない。
存在のチェックをライブラリ側におっかぶせることで、うまくいく。
逆に、isPresent()
とget()
の組み合わせは最悪で、
結局isPresent()
のチェックを忘れるとget()
で例外をスローするだけだ。
ただの言葉遊びでしかない。
Optional<T>
には、 そのほかにも、orElse(T)
、orElseGet(Supplier)
のようなデフォルト値、またはデフォルト値を返すラムダ式、 を指定して値を取り出すメソッドなど、がある。
このOptional
クラスのような型を選択型†2(option type)[3]
と呼ぶ。
†2 不確実型(maybe type)と呼ぶこともある。
Javaにはないが、
他の言語の中にはnull許容型(nullable type)があるものもある。
これは選択型と少しだけ異なる。
選択型ではOptional<Optional<T>>
とネストできるが、null許容型はそれができない。
null許容型については次のC#のパートで取り扱う。
少し話を逸らす。YELLOWが見つかったら...、 BLUEが見つからなかったら...、 のような簡単な課題はより簡単に書けるようになったかもしれない。 しかし、現実の問題はより難しい。 YELLOWとBLUEの両方が見つかったら...、となっただけで、 簡単にはいかなそうな匂いを嗅ぎとれただろうか。 次のように書けばいい、と考えるかもしれない:
var yellow = Color.findByName("YELLOW"); yellow.ifPresent(c1 -> { var blue = Color.findByName("BLUE"); blue.ifPresent(c2 -> { // c1, c2を使った処理 ... }); });
確かにそうかもしれないが、では色が2つではなく、 n 個になったらどうすればよいだろうか。 この後にヒントもあるので、ちょっと考えてみてほしい。
集合とnull
Optional
クラスは、所詮nullチェックをカプセル化しただけのクラス、なのだろうか。
その本質はいったい何か考えてみよう。
次のストリームAPIの使用例をみてみよう:
var firstFavorite = List.of("foo", "bar", "baz")
.stream()
.filter(matchesFavorite)
.findFirst();
firstFavorite.ifPresent(s -> { ... });
// 結果的に次と同じ
List.of("foo", "bar", "baz")
.stream()
.filter(matchesFavorite)
.findFirst()
.ifPresent(s -> { ... });
matchesFavorite
は適当なPredicate<String>
とする。
Stream
インターフェースのfindFirst()
は(良く考えられていて)この場合Optional<String>
のインスタンスを返すので、
値の有無のチェックが必要になる。
あえてfindFirst()
を使わないで同じことをやってみると、
次のようになる:
var favoriteList = List.of("foo", "bar", "baz")
.stream()
.filter(matchesFavorite)
.limit(1)
.collect(Collectors.toList());
以前との違いは、戻り値の型がOptional<T>
からList<T>
になったことだ。
リストといっても、
要素の数は0か1、カッコ良く言うと高々1個である。
したがって、実質的にこのList<T>
はOptional<T>
と同じだから、
次のように続けることができる:
favoriteList.forEach(s -> { ... });
// または
for (var s : favoriteList) {
// このループは高々1回しか実行されない
...
}
// あるいは、次のように書いても良いが...
if (favoriteList.size() != 0) {
var s = favoriteList.get(0);
...
}
したがって、
あえてfindFirst()
を使わないバージョンは次のように書けることがわかる:
List.of("foo", "bar", "baz")
.stream()
.filter(matchesFavorite)
.limit(1)
.forEach(s -> { ... });
もちろん、
Optional<T>
があるJavaでこのようなコードを書いていたら、
コードレビューで直されるだけだ。
Optional<T>
が長さが高々1である特殊なList<T>
とみなせることがわかれば、
このようなコードは忘れて良い。
Optional
の代わりにList
を使えるとしても、ArrayList
でラップしていたらオーバーヘッドが大きい。 要素が最大1つという制約を用いるList
の実装クラスがあれば、Optional
と同じくらいの軽さで実現できる。 だが、そのようなものを自前で実装しなくても、 実はOptional
がデビューするずっとから、その目的に合致するCollections.singletonList(T)
が追加されている。 これとCollections.emptyList()
を使ってOptional
のようなことが実現できる (Collections.singleton(T)
とCollections.emptySet()
でも良いが、Set
から要素を取り出すのは面倒なので、ここでの説明にはList
を用いた)。
さて、これらを書籍 Effective Java [4] の Item 43: Return empty arrays or collections, not nulls と合わせてまとめると、次のようになる:
インスタンスの個数 | 型 | null /その代わりに... |
---|---|---|
高々1個 | T |
null |
高々1個 | Optional<T> |
Optional<T>.empty() |
0個以上 | T[] |
public static final T[] EMPTY = {}; |
0個以上 | List<T> |
Collections.emptyList() |
0個以上 | Stream<T> |
Stream<T>.empty() |
参照がnull
かどうか、
というのは配列の要素数が0かどうかと同じレベルの問題である。
「int
だとメモリもったいないからshort
にする」のと同じノリで、
要素数が高々1個の場合、配列だと重たいから参照を使う†3にすぎない。
だから、null
を撲滅しようと
俺が悪いんじゃない、全部
null
が悪いんです。 null安全な言語を使わないからダメなんです。
と叫んでる人たちは、 目的が達成された暁には、 今度はサイズチェックが必要な固定長配列も撲滅するのだろう。
†3 細かいことを言うと、配列の方が表現力は高い。 配列の場合は値の有無の情報(配列の長さが0または1)と値そのものは独立しているので、 配列の要素そのものを
null
にして、「値有り、値はnull
」を表現できる。 一方、値の有無をnull参照か否かで表す場合は、値そのものをnull
とすることは不可能である。
実際のところ、
CやC++ではNULL
ポインタの間接参照
(indirectionまたはdereferencing)
は未定義の振る舞いだからあってはならないのである(#28「未定義の振る舞い」を参照)。
Javaはnull
を間接参照したときにNullPointerException
をスローすると定義しているので、
CやC++のNULL
と比べればJavaのnull
は安全である。
そういった違いがあるものを同列に扱っても意味はない。
Optional
の問題点
では、高々1個にはOptional
を使えば全てが解決するのか。
もちろん、そんなわけはない。
それはnull
の代わりに空の配列を返すのと同様で、
次のような問題がある:
Optional
はプリミティブ型ではなく、参照型であるisPresent()
で確認せずにget()
することができてしまう- 標準ライブラリには
null
を返したり受けとる昔のAPIがたくさん残っている - すべてを
Optional
にすると実行時の性能が犠牲になる
最初の問題はコンパイラがOptional
を特別扱いしないことに起因する。
Optional
型と思ったらnull
だった、ということが普通に起こりうる。
例えば、次のようなコードはシュールだが実用性はない:
Optional<String> maybeString = null;
あるいは:
public Optional<String> getMaybeString() {
return null;
}
当たり前だが、戻り値の型がOptional<T>
であるメソッドがnull
を返したり、
Optional<T>
型のパラメータを受け取るメソッドにnull
を渡すこともできる。
かといって、現状のJavaでnull
がOptional<T>.empty()
にボクシングされてもスッキリはしないが。
2番目の問題も、コンパイラの問題だ。
コンパイル時に静的解析で発見できるバグを、
実行時にNoSuchElementException
が発生するまで発見を遅らせる可能性がある。
3番目の問題は、過去の資産との向き合い方である。
昔のAPIとはnull
で、
最新のAPIとはOptional
で正しく統合しなくてはならない。
最後の問題は、 いつものことだが、 実行時の性能を犠牲にすることを許さない人が大勢いる、 ということだ。
コンパイラの静的解析
コンパイラの静的解析(データフロー解析)と、 ソースコードにメタデータをアノテーションすることを組み合わせることで、 参照型の値のnullチェックが適切であるかをコンパイル時に知ることができる。 JDKには今のところ含まれていないが、 静的解析ツールでは次のようなものがある:
また、次のIDEも同様な仕組みに対応している:
- IntelliJ IDEA
- Eclipse
- Android Studio
ただし、アノテーションで使用するクラスは今のところ標準化†4されていない。
そのため、各実装で独自の似たようなアノテーションクラス
(@NonNull
、@NotNull
、@Nonnull
など)
が定義されており、互換性がないことに注意する必要がある。
†4 JSR-305/308で標準化しようとしている。 なぜ乱立してるかは次を参照:
https://stackoverflow.com/questions/4963300/which-notnull-java-annotation-should-i-use
IntelliJ IDEAの実装を例にすると、
@NotNull
と@Nullable
で次のようにフィールド、
パラメータ、
戻り値などをアノテートする:
public final class ContactInfo {
private @NotNull String name;
private @NotNull List<@NotNull String> mailList;
private @Nullable Integer age;
public ContactInfo(@NotNull String name,
@NotNull List<@NotNull String> mailList,
@Nullable Integer age) {
this.name = name;
this.mailList = Collections.unmodifiableList(mailList);
this.age = age;
}
...
public @Nullable String getPrimaryMail() {
var list = mailList;
return list.size() == 0 ? null : list.get(0);
}
...
public @Nullable Integer getAge() {
return age;
}
}
そして、このContactInfo
クラスを使う次のコードを用意する:
public final class Main {
private static void sendMail(@NotNull String mailAddress,
@NotNull String name) {
System.out.println(mailAddress + " " + name);
}
public static void main(String[] args) {
var listContainingNull = new ArrayList<String>();
listContainingNull.add(null);
var infoList = List.of(
new ContactInfo("Jack", listContainingNull, null),
new ContactInfo("Jack", List.of("jack@example.com"), null),
new ContactInfo("Kim", Collections.emptyList(), 18));
infoList.stream()
.filter(i -> i.getAge() < 20)
.forEach(i -> sendMail(i.getPrimaryMail(), i.getName()));
}
}
IntelliJ IDEAのAnalyze ➜ Inspect Code...を実行すると、 次のような結果が得られる:
確かに、i.getAge()
はnull
になりえるのでnullチェックが必要なのに、
nullチェック無しでアンボクシングしているから指摘は正しい。
同様に、次の指摘:
sendMail
の最初のパラメータにi.getPrimaryMail()
の戻り値を指定しているが、
これもnull
になりうる値をnull
であってはならないパラメータに渡しているので、
指摘通りである。
しかしながら、
ContactInfo
のコンストラクタの第2パラメータにlistContainingNull
を渡すところはスルーしている。
どうやら、
List<@NotNull String>
のような型パラメータに対するアノテーションは機能していないようだ。
とはいえ、このようなアノテーションが標準化されて、かつ実用的†5になれば、
妙なボイラープレート(public
メソッドの最初に引数の値がnull
かどうか確認する儀式)と
そのためのAPIは不要になる。
†5 静的解析は完璧ではない。偽陽性(false positive)や偽陰性(false negative)はゼロにはならない。また、メタデータは人が付与するため、APIが間違ってアノテートされると、その後は大惨事が起きる。
水際対策
残念ながら、標準ライブラリの必要なAPI全てにメタデータが付加され、コンパイラが警告を出せるようになるまでは、null
にまつわるアノテーションは概念実証(Proof of Concept: PoC)でしかない。
Javaに限らずnull安全でない言語では、
現実的にはnull
に対して次のような水際対策でnull
の上陸を食い止めるしかない:
- できるだけ
null
を使わない null
を返す/受け取る可能性があるAPIを使用したら、 速やかにnull
かどうか確認する(null
の確認を先送りしない)null
を手にしてしまったら、 それを適切な別の表現に変換するなどして、 速やかに処分する(null
の処分を先送りしない)
null
の処分の先送りとは、例えば次のようなことである:
null
を何の罪も無い他のオブジェクトにおっかぶせる(Optional<T>
のofNullable(T)
メソッドなど、そのためのクラスに渡すのはよい)null
を別の型に伝搬する†6- 不適切なnullオブジェクトに変換する
NullPointerException
をキャッチする†7
†6 別の型に伝搬する例:
(s == null) ? null : s.getValue()
†7
NullPointerException
に限らず、RuntimeException
とその派生の例外はキャッチすべきではないが...
特にJavaでは、null
かもしれない参照はすぐにOptional.ofNullable(T)
でラップして、
そのまま確認、処分すればよい。ラップしたまま先送りしないこと。
null
の源泉
水際対策ができれば、あとは自分がnull
をなるべく生み出さないようにする。Javaの代表的なnull
の源泉は、未初期化のフィールド、配列の生成、未初期化のローカル変数、といったところだろう:
// 未初期化のフィールド
private String name;
public void foo() {
// 配列の生成(要素は全てnull)
var array = new Object[SIZE];
// 未初期化のローカル変数
String s;
...
}
すべてのクラスを不変にはできない†8ので、
インスタンスの状態に応じて値が変化するフィールドは当然存在する。
しかし、そうしたフィールドがnull
になるべき必然性はない。
例えば、null
が値を設定されていないことを表す、というのであれば、
null
を使う代わりに、
フィールドの型をT
からOptional<T>
に変えて、
それを表すためにOptional<T>.empty()
をさしておくこともできる。
†8 簡単な例は、相互参照する2つのインスタンス、循環参照のリストなど。
そのインスタンスのどの状態においても特定のフィールドが
null
でありうる、というのなら、そのフィールドへのアクセスすべてについて
nullチェックが必要であり、Optional<T>
でラップすることに意味がある。
そうではなく、
インスタンスの状態によってそのフィールドがnull
または非null
であることが決まっていることもある。
例えば、あるプライベートのインスタンスメソッドを呼び出している間、
特定のフィールドが非null
であることを保証している、
という状況にあるなら、クラスを分割する、ステートパターンを適用するなど、
設計を見直す方がいいかもしれない。
配列もできるだけストリームAPIの終端操作で生成し、 それでは実現できないもの†9だけを許容すべきである。
†9 直ぐに思い浮かぶのはハッシュテーブルの実装など。
最後のローカル変数だが、基本的にローカル変数は宣言時に初期値を必ず代入すること。 新入社員やそれに準じるスキルの方々が、 次のようなコードを書いてドヤ顔をしているのをよくみかける:
String s;
if (state == State.A) {
s = ...
} else {
s = ...
}
三項演算子(またはJava 12のswitch
式)で書ける場合もあるし、
それができない場合はvar s = method();
のように値を返すメソッドに分離する、
あるいはそれをメソッドに分けるとやたらパラメータが多くなるのであれば、
Supplier
かFunction
(ラムダ式)を定義して、その式の戻り値を代入する、
などを検討してみる(C#ならラムダ式の代わりにローカル関数を使ってもよい)。
未初期化のローカル変数には、似たようなケースが多々ある。 例えば、
try
—catch
でtry
の直前に変数を宣言だけしておいて、try
ブロックの中でその変数に値を代入するケースも同様である。