以下の内容はhttps://blog1.mammb.com/entry/2025/01/31/000000より取得しました。


Checker Framework で Java を Null safety にする


Checker Framework とは

Checker Framework は、Java の型システムを拡張して Java プログラム内のエラーを検出・防止します。

特定のエラーについて警告したり、エラーが発生しないことを保証するコンパイル時のツールで、Checker Framework は、様々な種類のチェッカーを提供しています。


ここでは Checker Framework の Nullness Checker の導入方法について説明します。

Nullness Checker は NullPointerExceptionが発生しないことを保証します(@NonNullで注釈が付けられた型を持つ式がnull に評価されることがないこと、および他の式の間接参照を禁止することを保証します)。


Gradle の設定

build.gradle.kts で Checker Framework プラグインを導入します。

plugins {
    java
    id("org.checkerframework") version "0.6.48"
}
// ...

dependencies {
    implementation("org.jspecify:jspecify:1.0.0")
    // ...
}

configure<org.checkerframework.gradle.plugin.CheckerFrameworkExtension> {
    checkers = listOf(
        "org.checkerframework.checker.nullness.NullnessChecker",
    )
}

依存に jspecify を追加します。jspecify では @NonNull, @Nullable, @NullMarked, @NullUnmarked というアノテーションが提供されます(Checker Framework は現時点で @NullMarked, @NullUnmarked をサポートしません)(jspecify は、例えば jakarta.annotation.Nonnullorg.springframework.lang.NonNullorg.jetbrains.annotations.NotNull といった乱立するアノテーションを共通化する目的のものです)。

checkers として org.checkerframework.checker.nullness.NullnessChecker チェッカーを指定します。

Checker Framework プラグインは JavaCompile タスクで実行されるため、java プラグインの後に導入する必要があります。


マルチモジュールプロジェクト構成で、Convention Plugins を使っている場合は、buildSrc/build.gradle.kts に以下の依存を追加し、

plugins {
    `kotlin-dsl`
}
repositories {
    gradlePluginPortal()
}
dependencies {
    implementation("org.checkerframework:org.checkerframework.gradle.plugin:0.6.48")
}

buildSrc/src/main/kotlin/buildlogic.xxx.gradle.kts 側で、以下のようにプラグインを設定します。

plugins {
    java
    id("org.checkerframework")
}

configure<org.checkerframework.gradle.plugin.CheckerFrameworkExtension> {
    checkers = listOf(
        "org.checkerframework.checker.nullness.NullnessChecker",
    )
}


Gradle 構成キャッシュは無効に

最近の Gradle の init タスクでプロジェクトを作成した場合、gradle.properties が生成され、以下のように構成キャッシュが有効になります。

org.gradle.configuration-cache=true

Checker Framework の Gradle プラグイン 0.6.48 では構成キャッシュがサポートされていないため、以下のように無効化しておく必要があります。

org.gradle.configuration-cache=false


Checker Framework の実行

プラグインが導入できれば、ビルド時に問題が報告されます。というか、compileJava タスク時にChecker Framework が動き、ビルドに失敗します。

例えば、以下のコードを考えます。

import org.jspecify.annotations.NonNull;  
  
public class App {  
  
    public String foo(@NonNull Object ref) {  
        return ref.toString();  
    }  
  
    public static void main(String[] args) {  
        new App().foo(null);  
    }  
  
}

このコードは、実行すると NullPointerException が発生します。

ビルドすると、以下のようなエラーとなりビルドに失敗します。

$ ./gradlew build
...
> Task :app:compileJava FAILED
App.java:13: error: [argument] incompatible argument for parameter ref of App.foo.
        new App().foo(null);
                      ^
  found   : null (NullType)
  required: @Initialized @NonNull Object
1 error

以下のように @Nullable とした場合は、

import org.jspecify.annotations.Nullable;

public class App {  
  
    public String foo(@Nullable Object ref) {  
        return ref.toString();  
    }
    // ...
}

以下のようなエラーとなりビルドに失敗します。

$ ./gradlew build
...
> Task :app:compileJava FAILED
App.java:9: error: [dereference.of.nullable] dereference of possibly-null reference ref
        return ref.toString();
               ^
1 error

さらに、アノテーション無しで null 呼び出しした場合も、

public class App {  
  
    public String foo(Object ref) {  
        return ref.toString();  
    }  
  
    public static void main(String[] args) {  
        new App().foo(null);  
    }  
  
}

以下のようなエラーとなりビルドに失敗します(Nullness Checker では、@NonNull がデフォルトであるため、ほとんど記述されません)。

$ ./gradlew build
...
> Task :app:compileJava FAILED
App.java:13: error: [argument] incompatible argument for parameter ref of App.foo.
        new App().foo(null);
                      ^
  found   : null (NullType)
  required: @Initialized @NonNull Object
1 error

以下のように、コード上で null を考慮した実装としていてもビルドエラーになります(チェッカーは、メソッドの型とシグネチャにのみ依存するため)。

public String foo(Object ref) {  
    return (ref == null) ? "" : ref.toString();  
}

以下のように @Nullable としてマークする必要があります。

public String foo(@Nullable Object ref) {  
    return (ref == null) ? "" : ref.toString();  
}

なお、注釈はフィールドやメソッドの戻り値など、あらゆる型に対して付与できます。

@Nullable Object obj;
@Nullable Object getObject(...) { ... }


まとめると、Nullness Checker は以下の場合に警告します。

@NonNull が付与されていない型の式を間接参照すると、NullPointerException が発生する可能性があるため、Nullness Checker が警告します。間接参照は、フィールドにアクセスしたときだけでなく、配列にインデックスが付けられたとき、例外が発生したとき、同期ブロックでロックが取られたときなどにも発生します。

@NonNull が付与された型が null になる場合、Nullness Checker が警告します。

@NonNull 型のフィールドがコンストラクタで初期化されない場合にも警告を発します。


Nullness Checker のその他のアノテーション

Nullness Checker は @Nullable@NonNull 以外にも多くのアノテーションを扱います。

分類 アノテーション
Nullness qualifiers @Nullable @NonNull, @PolyNull, @MonotonicNonNull
Nullness method annotations @RequiresNonNull @EnsuresNonNull @EnsuresNonNullIf
Initialization qualifiers @Initialized @UnknownInitialization @UnderInitialization
Map key qualifiers @KeyFor

これらは jspecify の提供するアノテーションではなく、org.checkerframework.checker 配下の Checker Framework 独自のアノテーションです(詳細は割愛します)。

Nullness Checker の警告を抑止したい場合は @SuppressWarnings("nullness") アノテーションを使うことができます。






以上の内容はhttps://blog1.mammb.com/entry/2025/01/31/000000より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14