2016/02/06

Android: NonNullアノテーション

Android Studio 0.5.5から@NonNullアノテーションがサポートされた.
今回はメソッドの引数に@NonNullアノテーションをつけるケースについて書いた.

@NonNull

@NonNullアノテーションはフィールドやメソッドの引数, メソッドのReturn値にNullを許容しないことを表明するアノテーションである.
このアノテーションが便利な点は, @NonNullアノテーションにNullを代入したり, 戻り値のNullチェックをしたりする場合に, IDEが@NonNullの制約をCode Inspectionで示してくれるところだ.

ただし, これはIDE付属のCode Inspection設定で報告レベルを調整可能でもあるし, @NonNullの値にNullを設定したからといってシンタックスエラーにはならない.
@NonNullはドキュメンテーションや支援機能であって, Nullを防ぐものではない点に注意が必要である.

下記のコードはhogeメソッドの引数sにNullを渡す恐れがある. しかし, これはCode Inspectionとして報告されない.

private String piyo;

public void foo() {
    hoge(piyo);
}

public void hoge(@NonNull String s) {
}

@NonNullはメソッドの呼び出し元でNullが渡らないことを保証する必要がある.
Code Inspectionは@NonNullフィールド, メソッド引数, 戻り値の非Null性を保証するものではないからだ.

@NonNullの使いどころ

メソッドが引数にNullを許容せず, NullPointerExceptionをスローするケースは少なくない.
そういう時こそ@NonNullアノテーションの出番だ. メソッドの利用者に配慮する意味でも@NonNullを是非つけておきたいところだ.

@NonNullをつければ, メソッド引数の事前条件確認としてのNullチェックはスキップしてもいいだろう.
ただ, “もしNullが渡ってきたら”を考えておいたほうが良さそうなケースがある.

例えば, 次のコードの場合を考えてみる.

public class Hoge {
    @NonNull private final Foo foo;

    public Hoge(@NonNull Foo foo) {
        this.foo = foo;
    }

    public String bar() {
        this.foo.call();
    }

    public void piyo(@NonNull Foo f) {
        f.call();
    }
// ...

クラスHogeはコンストラクタで@NonNullなfooを引数にとる.
このコンストラクタがNullPointerExceptionを返すことはないが, Hogeクラスとしてメンバ変数fooがnullの状態になることを許容したくないのだ.
それを明示するためにもコンストラクタの引数fooには@NonNullアノテーションを付けておいた.

もし, HogeのコンストラクタでfooにNullが渡った場合のことを考えると少し厄介な問題を引き起こすかもしれない.

まず, メソッドbarを確認してみる.
this.fooの値は同クラスのコンストラクタで@NonNullな引数fooで初期化されたものであり, 非Nullであることが事前条件として成立している(おまけにfinal付きだ).
もし, 仮にthis.fooがNullであるような事態に陥ったとしてもbarは実行時の予期せぬ例外としてNullPointerExceptionをスローする.
try-catchで例外を握りつぶすこともない. このメソッドのコードに問題はなさそうだ.

次に, コンストラクタを確認してみる.
このコンストラクタはシンプルで, 引数fooを受け取り, これでメンバー変数を初期化する.
もし, 仮にメソッドの引数fooにNullが代入されてしまっても, このコンストラクタは何の異常も報告しない(にも関わらず@NonNullアノテーションを使用している).
このケースは問題に発展するかもしれない.

メンバ変数fooが意図せずNullで初期化されていることを知るのはメソッドbarを実行した時だ.
クラスHogeが単純な責務しかもたず, 特定のクラスとしか関連しない場合や, コンストラクタで初期化した直後にbarを呼ぶ場合は, fooにNullを渡した犯人を突き止めることができるかもしれない.
しかし, クラスHogeが複数のクラスで共有されるshared objectのような立場であった場合.
Nullでthis.fooが初期化されるタイミングでは沈黙し, Nullを渡した犯人とは異なる運の悪い別インスタンスがbarを呼ぶことではじめてNullPointerExceptionが報告される.

こうなると話はややこしくなる.
当然, NullPointerExceptionのスタックトレースに犯人の痕跡は残っていない.
@NonNullであるはずの引数fooに一体誰がNullを代入したのか. NonNull Code Inspectionの網をくぐり抜けた犯人を追うことになる.

事件はすぐに解決できるかもしれないが, 事件を未然に防ぐことに努めるべきだろう.
クラスHogeのコンストラクタで何かできることはあるだろうか.

まず@NonNullのアノテーションを外してみよう. そしてJavadocの事前条件欄にでも“Nullは受け付けない”旨を強調した上で, コードでもNullチェックをしてみよう.

public class Hoge {
    /**
     * @param foo can noooooooooooooot be NULL!
     */
    public Hoge(Foo foo) {
        if (foo == null) {
            throw new NullPointerException("...");
        }
        this.foo = foo;
    }

こうすれば望まれないNullが設定された際には即座にNullPointerExceptionをスローすることができ, 犯人をスタックトレース上にあぶり出すことができるはずだ.
だが, これだとせっかくのNonNull Code Inspectionの恩恵を受けられない. 犯人をあぶり出すことができるようになっても, 誤ってNullを設定してしまう犯人(イージーなミス)が増えてしまいそうだ.

@NonNullアノテーションの恩恵は残しておきたい.
なので@NonNullアノテーションはそのまま残して, Nullチェックのコードを追加してみる.

    public Hoge(@NonNull Foo foo) {
        if (foo == null) {
            throw new NullPointerException("...");
        }
        this.foo = foo;
    }

些か奇妙なコードにも見える. そのせいかAndroid Studioも “Condition ‘foo == null’ is always ‘false’” と別のCode Inspectionをコメントしてきた.
確かにその通り(そうであって欲しいの)だが, 望まない副作用を避けるためにもこの場所にはNullチェックを書いておきたい事情があるのだ.
このまま放っておいても良いが, 常に警告されているのも気持ち良いものではないのでAndroid Studioには”そうではない”ことを伝えておく.

    public Hoge(@NonNull Foo foo) {
        // noinspection ConstantConditions
        if (foo == null) {
            throw new NullPointerException("...");
        }
        this.foo = foo;
    }

これで毎回 “些か奇妙なコード” をLint checkの度, 目にすることもなくなった.

毎回noinspectionを書くのが嫌なら, nonNullをチェックするutilメソッドを定義しておけばより簡素なコードにできる.

public static <T> T nonNull(T o) {
    if (o == null) {
        throw new NullPointerException("Require Non null object");
    }
    return o;
}

// this.foo = nonNull(foo);

ところで, 残ったメソッドpiyoの方は問題ないだろうか.

    public void piyo(@NonNull Foo f) {
        f.call();
    }

これは問題ないだろう. 引数fにNullを渡された場合でも潜伏期間なしにNullPointerExceptionがスローされ, スタックトレースには犯人が載っているはずだ.
fにNullを設定するとNullPointerExceptionがスローされる正しいコードだ.

まとめ

@NonNullはあくまでも補助的な機能であって非Nullを保証するものではない.
メソッドの引数にNullが渡るとNullPointerExceptionが発生することを明示する場合, @NonNullアノテーションはとても役に立つ.

本来Nullを許容しない(NullPointerExceptionをスローする)ことを明示する@NonNullアノテーションだが, Nullを受け取っても異常を報告せず終了してしまう@NonNullな引数を作ることもできてしまう.
そのようなケースでも, @NonNullが有益なケースはあるが, それによる副作用も考慮した上で, 必要なNullチェックは書いておこう.

以上.

2016/01/23

Android: SQLiteDatabaseとコンフリクトアルゴリズム

SQLiteDatabaseINSERT/UPDATE文でコンフリクト対応する話 .

はじめに

SQLiteはデータのコンフリクトを解消するコンフリクトアルゴリズムをサポートしており, CREATE TABLE構文でON CONFLICT句として指定できる.
INSERT, UPDATE構文では文体をより自然にするためにON CONFLICTではなくOR句として指定される.

Androidでもこれを使うことができる.
SQLiteDatabaseクラスを使ってINSERT or UPDATEをする際にコンフリクトアルゴリズムを指定できるメソッドがAPI Lv.8から用意されている.

SQLiteにおけるコンフリクト要因としてはUNIQUE, NOT NULL, CHECK, PRIMARY KEYといったカラム制約がある. ただし, FOREIGN KEY制約についてはこれが適用されない.

CONFLICT_*

AndroidではSQLiteDatabase.CONFLICT_*にあたる各定数で`コンフリクトアルゴリズムを指定でき, 下記がサポートされている.

SQLiteDatabaseは内部で下記のようにSQLを構築・発行している.
つまり, SQLiteのON CONFLICT または ORの仕様を理解すればよい.

private static final String[] CONFLICT_VALUES = new String[]
            {"",              // 0: CONFLICT_NONE
             " OR ROLLBACK ", // 1: CONFLICT_ROLLBACK
             " OR FAIL ",     // 2: CONFLICT_FAIL
             " OR IGNORE ",   // 3: CONFLICT_IGNORE
             " OR REPLACE "}; // 4: CONFLICT_REPLACE

public long insertWithOnConflict(String table, 
                                 String nullColumnHack,
                                 ContentValues initialValues, 
                                 int conflictAlgorithm) {
        acquireReference();
        try {
            StringBuilder sql = new StringBuilder();
            sql.append("INSERT");
            sql.append(CONFLICT_VALUES[conflictAlgorithm]);
            sql.append(" INTO ");
            sql.append(table);
            sql.append('(');
            ...

検証

今回使用するテーブルスキーマは下記

sqlite> .schema test
CREATE TABLE test ( _id INTEGER PRIMARY KEY, data STRING);

次のテストデータを用意.
_idは, あえて2をskipしておく.

sqlite> INSERT INTO test VALUES (1, "A");
sqlite> INSERT INTO test VALUES (3, "B");
sqlite> INSERT INTO test VALUES (4, "C");
sqlite> SELECT * FROM test;
_id         data      
----------  ----------
1           A         
3           B         
4           C         

このテーブルにINSERTUPDATE文を発行してコンフリクトを発生させた.

CONFLICT_ROLLBACK

OR ROLLBACKの指定.
コンフリクトが発生した場合, トランザクションはロールバックされる.

トランザクションをロールバックした後, SQLiteExceptionを発生させる.
一般的にtry-catch-finally節でデータベーストランザクションの操作を行うが, OR ROLLBACKを指定した場合, 発行先のSQLがロールバックまでを実行する.
そのため, OR ROLLBACK句によりSQLiteExceptionが発生したタイミングでは既にトランザクションが閉じているので注意が必要.
すでにトランザクションが閉じられたdbインスタンスへのdb.endTransaction()コールは新たなSQLiteExceptionを生む.

もしOR ROLLBACKがトランザクション内での実行ではなく, 単発のSQLである場合はCONFLICT_ABORTと同じ挙動になる.

try {
  db.beginTransaction();
  // ...
  // cv1はロールバックされ, cv2は失敗, cv3はSQLiteExceptionにより実行されない.
  db.insertWithOnConflict("test", null, cv1, CONFLICT_ROLLBACK);
  db.insertWithOnConflict("test", null, cv2, CONFLICT_ROLLBACK); // CONFLICT!
  db.insertWithOnConflict("test", null, cv3, CONFLICT_ROLLBACK);
  // ...
  db.setTransactionSuccessful();
  db.endTransaction();
} catch (SQLiteException e) {
  // Conflict occur

CONFLICT_ABORT

OR ABORTの指定.
コンフリクトが発生した場合, そのSQLコマンド自体が行った変更が取り消される.

SQLコマンドによる変更を取り消した後, SQLiteExceptionを発生させるがロールバックは実行されない.
これはSQLコマンドが複数行に作用する場合に効果がある.

OR ABORTはコマンドに閉じて作用するため, トランザクションで全体をロールバックするようなケースでは役に立たない.

try {
  // _idを1つずつインクリメントするが, 2レコード目でコンフリクトが発生する. 
  // クエリの結果, どのレコードも変更されないで終わる. 
  db.execSQL("UPDATE OR ABORT test SET _id=_id+1");
} catch (SQLiteException e) {
  // Conflict occur

CONFLICT_FAIL

OR FAILの指定.
コンフリクトが発生した場合でも, そのSQLコマンド自体がそこまでに行った変更は取り消さない.

コンフリクトが発生した後, SQLiteExceptionを発生させるがロールバックは実行されない.
これはSQLコマンドが複数行に作用する場合に効果がある.
OR ABORTがそのコマンドの失敗を取り消すのに対して, OR ABORTは取り消さない.

OR FAILはコマンドに閉じて作用するため, トランザクションで全体をロールバックするようなケースでは役に立たない.

try {
  // _idを1つずつインクリメントするが, 2レコード目でコンフリクトが発生する. 
  // クエリの結果, コンフリクトが発生しなかった最初のレコードだけ変更される. 
  db.execSQL("UPDATE OR ABORT test SET _id=_id+1");
} catch (SQLiteException e) {
  // Conflict occur

CONFLICT_IGNORE

OR IGNOREの指定.
コンフリクトが発生した場合でも, それを無視して処理を継続する.
複数行の更新を行うクエリの場合, コンフリクトが発生した1行に対しては無視され, その前後の処理は取り消されない.

コンフリクトが発生してもSQLiteExceptionは発生せず, ロールバックも実行されない.

try {
  // cv1は成功, cv2はコンフリクトで失敗するがエラーは無視される. cv3は成功.
  db.insertWithOnConflict("test", null, cv1, CONFLICT_IGNORE);
  db.insertWithOnConflict("test", null, cv2, CONFLICT_IGNORE);  // CONFLICT!
  db.insertWithOnConflict("test", null, cv3, CONFLICT_IGNORE);
} catch (SQLiteException e) {

CONFLICT_REPLACE

OR REPLACEの指定.
コンフリクトが発生した場合, 次のルールでコンフリクトの解消を試す.

  1. コンフリクトの原因がUNIQUE or PRIMARY KEY制約によるものの場合, 既存の行を削除して新たにINSERT or UPDATEを試みる.
  2. コンフリクトの原因がNOT NULL制約によるものの場合, NULL値をdefault valueに置き換えて試みる
  3. コンフリクトの原因がNOT NULL制約によるもの, かつ, default vlaueが定義されていない場合, CONFLICT_ABORTと同じ振る舞いとする.
  4. コンフリクトの原因がCHECK制約によるものの場合はCONFLICT_ABORTと同じ振る舞いとする.

コンフリクトの解消が成功した場合はSQLiteExceptionは発生しない.
コンフリクトの解消が失敗した場合はSQLiteExceptionが発生する.

  // cv1は成功, cv2はREPLACEされ成功, cv3は成功
  db.insertWithOnConflict("test", null, cv1, CONFLICT_REPLACE);
  db.insertWithOnConflict("test", null, cv2, CONFLICT_REPLACE);  // CONFLICT!
  db.insertWithOnConflict("test", null, cv3, CONFLICT_REPLACE);

CONFLICT_NONE

OR句の指定なし.
CONFLICT_ABORTと同じ挙動となる.

ちなみにON CONFLICT, OR句はSQL標準ではない.

以上.