Skip to content

Instantly share code, notes, and snippets.

@mike-neck
Last active December 23, 2017 14:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mike-neck/61fba10bc29bacf18e1d274c2067b5a2 to your computer and use it in GitHub Desktop.
Save mike-neck/61fba10bc29bacf18e1d274c2067b5a2 to your computer and use it in GitHub Desktop.
JSR305 の nullness annotations と Kotlin

JSR305 の nullness annotations についてよく勘違いをしてしまうので、まとめてみました。

最初に

JSR305 はソフトウェアのバグを検出をサポートするためのアノテーション群の提供を求めるJSRです。 ここで提案されているアノテーションとfindbugsやIntelliJなどの静的解析ツールを用いることで、ソフトウェアの品質を高められます。

ここでは JSR305 のアノテーションとして JetBrains Annottions を事例として取り扱います。なお、同等の意味を持つ findbugs アノテーションでも同じことが 言えますので、そちらを利用している方は適宜読み替えてください。


アノテーションのダウンロード

JetBrains Annotations はGradleで次のように指定することでプロジェクトに組み込めます。

build.gradle.kts

dependencies {
  implementation("org.jetbrains:annotations:13.0")
  // または
  compileOnly("org.jetbrains:annotations:13.0")
}

nullness annotations

JetBrains Annotations の nullness annotations には次のようなものがあります。

@NotNull

  • このアノテーションが付与されたパラメーター/変数/フィールド/戻り値を null にすることは禁止されている

@Nullable

  • このアノテーションが付与されたパラメーター/変数/フィールド/戻り値が null となることは妥当である

また、この二つの関係ですが次のような包含関係になっています。

Nullable contains NotNull


サンプル

では、早速試してみましょう。次のような build.gradle.kts を書いてプロジェクトを作ります。

plugins {
  id("java-library")
}
repositories {
  mavenCentral()
}
dependencies {
  implementation("org.jetbrains:annotations:13.0")
}

次のようなインターフェースを作ります。

interface Packet {}
interface Network {}
interface Io {
  @Nullable
  Packet packetFrom(@NotNull final Network network); // [1]
  @NotNull
  Packet merging(@NotNull final Network leftChannel, @Nullable final Network rightChannel); // [2]
}
  1. メソッド packetFromnull ではない Network を受け取って、 null なこともある Packet を返す
  2. メソッド mergingnull ではない Network と、 null も渡せる Network を受け取り、 null ではない Packet を返す

この Io インターフェースを実装したクラスを作って適当な実装をします。

class DiskIo implements Io {
  private final int io;
  DiskIo(final int io) {
    this.io = io;
  }
  @Nullable
  @Override
  public Packet packetFrom(@NotNull final Network network) {
    return null;
  }
  @NotNull
  @Override
  public Packet merging(@NotNull final Network leftChannel, @Nullable final Network rightChannel) {
    return new Packet() {};
  }
}

上記のような実装であれば、IntelliJ のインスペクション Probable bugs には特に何も出力されません。

では敢えて @NotNull を 違反してみます。 merging メソッドを次のように変えてみましょう。

  @NotNull
  @Override
  public Packet merging(@NotNull final Network leftChannel, @Nullable final Network rightChannel) {
    return null;
  }

そして IntelliJ のインスペクションを走らせると次のように出力されます。

'null' is returned by the method decleared as @NotNull

大方の予想通り、 null が許されない戻り値に null を指定したため、インスペクションエラーとなっています。あまりにも当然すぎる結果なので、これは特に驚くにあたりません。


それでは別の @NotNull の違反を作ってみましょう。 packetFrom メソッドを次のように変更します。

  @Nullable
  @Override
  public Packet packetFrom(@NotNull final Network network) {
    if (network == null) throw new IoNotAvailableException("network should be not null."); // 独自の例外を投げる
    return null;
  }

これを見たとき、どのように考えるでしょうか?

  • null 値の入力は許されていないので、この null チェックは不要である

でしょうか?それとも

  • null 値の入力は許されていないため、実際に null チェックをおこなうことで不正な入力を防いでいる

でしょうか?

最近までは僕自身は後者の考え方をすることが多かったのですが、一般的には契約プログラミングあるいはDesign by Contractに基いて前者の考え方を採用する人が多いのではないでしょうか。 packetFrom というメソッドが期待通り動くという利益を享受するためには、メソッドの使用者はパラメーター network に対して null ではない値を 渡す責務を負うという考え方です。 null を渡した時に packetFrom メソッドが NullPointerException ないしは何らかの例外または期待していない出力値が返ってきた場合には Io インターフェースの実装のバグではなく、 使用者のバグとします。それによって、 Io インターフェースの実装をシンプルに保てるということになります。契約プログラミングについては日本でも多くの方がブログなどを書いていますので、詳しくはそちらを参照するとよいでしょう。

話を元に戻すと、 @NotNull なパラメーターに対して null チェックを行ったコードに対して IntelliJ でインスペクションすると probable bugs に次のような結果が返ってきます。

Condition 'network == null' is always 'false'

この条件の結果は常に false になるということです。そして IntelliJ IDEA による修正提案は Remove 'if' statement となります。


世の中には決まりを破ってしまう人もいるもので、 packetFrom メソッドにnull を渡そうとする人がいます。例えば、テストコードを書く我々のような人びとです。

class IoTest {
  @Test
  void passNull() {
    final Io io = new DiskIo();
    assertThrows(IoNotAvailableException.class, () -> io.packetFrom(null));
  }
}

このテストをIntelliJ IDEA上で実行してみると次のような結果になります。

org.opentest4j.AssertionFailedError: Unexpected exception type thrown ==> expected: <com.example.quality.Ios.IoNotAvailableException> but was: <java.lang.IllegalArgumentException>
	at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:59)
	at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:38)
	at org.junit.jupiter.api.Assertions.assertThrows(Assertions.java:1108)
	at com.example.IosTest.passNull(IosTest.java:32)
...(省略)...
Caused by: java.lang.IllegalArgumentException: Argument for @NotNull parameter 'network' of com/example/quality/Ios$DiskIo.packetFrom must not be null
	at com.example.quality.Ios$DiskIo.$$$reportNull$$$0(Ios.java)
	at com.example.quality.Ios$DiskIo.packetFrom(Ios.java)

IoNotAvailableException を期待していましたが、 IllegalArgumentException が返されてテストが失敗しました。しかし IllegalArgumentException を投げるようなコードは記述していません。この例外は一体どこから湧いてきたのでしょうか?

DiskIo.classjavap で覗いてみると次のようになっています。

  public com.example.quality.data.Packet packetFrom(com.example.quality.data.Network);
    Code:
      stack=3, locals=2, args_size=2
         0: aload_1
         1: ifnonnull     8
         4: iconst_0
         5: invokestatic  #50                 // Method $$$reportNull$$$0:(I)V
         8: aload_1
         9: ifnonnull     22
        12: new           #2                  // class com/example/quality/Ios$IoNotAvailableException
        15: dup
        16: ldc           #3                  // String network should be not null.
        18: invokespecial #4                  // Method com/example/quality/Ios$IoNotAvailableException."<init>":(Ljava/lang/String;)V
        21: athrow
        22: aconst_null
        23: areturn

メソッドの先頭でパラメーターを読み込んでおいて null チェックを行った後に null だった場合には $$$reportNull$$$0 という書いた記憶のないメソッドを呼び出すようになっています。IntelliJ IDEAは @NotNull が付与されているフィールド/ローカル変数/パラメーター/戻り値には上記の null チェックを挟み込んでいるようです。したがって、 null チェックによる例外の発生を確かめるテストは、IDEでは失敗するのに、ビルドツールでは成功するため、不安定になります。 @NotNull はプログラム内の null チェックを減らしてプログラムをシンプルにすることを目標にしていることを考えると、 @NotNull が付与されたパラメーターに対する null チェックが働くことのテストは書かないほうがよいかもしれません。


Kotlin

タイトルにKotlinを含めたので、Kotlinに言及しないわけにはいきません。

先ほどの Io インターフェースをKotlinで実装すると次のような実装になります。

class KIo: Io {
  override fun packetFrom(network: Network): Packet? = null
  override fun merging(leftChannel: Network, rightChannel: Network): Packet = object: Packet {}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment