2013年6月27日木曜日

[Android]SQLiteのカーソル処理ヘルパーを作ってみた

ちょっと前にGDD Blog: [Android]SQLiteによるカーソルのループ処理の書き方っていうのを書きました。なんかしっくりこないので、ヘルパーを書いてみました。
コードはこんな感じ。


/**
 * カーソルのヘルパー.
 */
public class CursorHelper {

    /**
     * 初回フラグ.
     */
    private boolean mIsFirst = true;

    /**
     * 操作対象のカーソル.
     */
    private final Cursor mCursor;

    /**
     * 次有無判定
     *
     * @return true:次あり false:次なし
     */
    public boolean hasNext() {
        if (!mIsFirst) {
            // 2回目以降
            return mCursor.moveToNext();
        }

        mIsFirst = false;
        // 初回
        return mCursor.moveToFirst();
    }

    /**
     * コンストラクタ.
     *
     * @param cursor 操作対象のカーソル
     */
    public CursorHelper(Cursor cursor) {
        mCursor = cursor;
    }
}


で、利用側はこんな感じ。


    public void onButton3(View view) {

        // DBをオープンして、簡単なクエリーを投げる
        SQLiteOpenHelper helper = new DBOpenHelper(this);
        SQLiteDatabase db = helper.getReadableDatabase();
        Cursor cursor = db
                .rawQuery("select * from TEST_TABLE", new String[] {});
        CursorHelper ch = new CursorHelper(cursor);

        // hasNext兼next
        while (ch.hasNext()) {
            // ・・・処理
            Log.d("sql", "code2 data=" + cursor.getString(1));

        }

        cursor.close();
        db.close();
        helper.close();
    }
ヘルパーのhasNextでnextの処理もしなければいけない点と、毎回if文を通る残念な感じはありますが、利用側からはわかりやすいような気もします。 んーイテレータで書いたほうがかっこいいかも。

2013年6月21日金曜日

[Android]try-with-resources風に処理したい

IO系の処理などで、Java1.7の言語仕様で追加されたtry-with-resources構文をつかうとすっきりかけるんですが、現時点で、AndroidのJavaランタイムDalvikの処理系は1.6系です。そのためJava1.7以降に追加された言語仕様を使うことができません。

そこで、Androidで開放漏れを起こしやすい代表的なリソースを開放するヘルパークラスを実装してみました。コードはこんな感じ。


public class Closer {

    private final Stack mTargets = new Stack();

    /**
     * Closeableの登録.
     *
     * @param closeable 対象
     * @return 入力と同じ
     */
    public  T put(T closeable) {
        if (closeable == null) {
            return closeable;
        }
        mTargets.push(closeable);
        return closeable;
    }

    /**
     * SQLiteDatabaseの登録.
     *
     * @param db 対象
     * @return 入力と同じ
     */
    public SQLiteDatabase putDb(SQLiteDatabase db) {
        if (db == null) {
            return db;
        }
        mTargets.push(new SQLiteDatabaseCloser(db));
        return db;
    }

    /**
     * SQLiteDatabaseの登録.
     *
     * @param helper 対象
     * @return 入力と同じ
     */
    public SQLiteOpenHelper putDbHelper(SQLiteOpenHelper helper) {
        if (helper == null) {
            return helper;
        }
        mTargets.push(new SQLiteOpenHelperCloser(helper));
        return helper;
    }

    /**
     * クローズ.
     * 

* 登録と逆順にクローズする. */ public void close() { while (!mTargets.empty()) { try { mTargets.pop().close(); } catch (IOException e) { Log.w("Closer", "Closer#close error!!", e); } } } @Override protected void finalize() throws Throwable { if (!mTargets.isEmpty()) { Log.w("Closer", "Closer#close not call!!"); close(); } super.finalize(); } /** * SQLiteDatabaseのラッパー. */ private class SQLiteDatabaseCloser implements Closeable { private final SQLiteDatabase mTarget; SQLiteDatabaseCloser(SQLiteDatabase target) { mTarget = target; } @Override public void close() throws IOException { mTarget.close(); } } /** * SQLiteOpenHelperのラッパー. */ private class SQLiteOpenHelperCloser implements Closeable { private final SQLiteOpenHelper mTarget; SQLiteOpenHelperCloser(SQLiteOpenHelper target) { mTarget = target; } @Override public void close() throws IOException { mTarget.close(); } } }


finalizeでクローズ処理を実行しているのは保険です。利用側の実装はこんな感じ。


    public void onButton1(View view) {

        byte[] b = "あいうえお".getBytes(Charset.defaultCharset());

        // ヘルパークラスのインスタンスを用意する
        Closer closer = new Closer();

        try {
            // Closeable系のリソースのインスタンスを生成するとき、Closerを経由する
            InputStream in = closer.put(new ByteArrayInputStream(b));
            Writer w = closer.put(new StringWriter());

            byte[] buf = new byte[128];
            in.read(buf);
            w.write(new String(buf, Charset.defaultCharset()));

            // DBOpenHelperはCloseableではないので専用メソッド経由
            SQLiteOpenHelper helper = closer
                    .putDbHelper(new DBOpenHelper(this));
            // SQLiteDatabaseはCloseableではないので専用メソッド経由
            SQLiteDatabase db = closer.putDb(helper.getReadableDatabase());
            Cursor cursor = closer.put(db.rawQuery("select * from TEST_TABLE",
                    new String[] {}));
            if (cursor.moveToFirst()) {
                // 処理
            }

        } catch (IOException e) {
            Log.e("closer", "error!!", e);
        } finally {
            // 内包するリソースを開放する
            closer.close();
        }
    }
try-with-resources程ではないですが、だいぶすっきりするような気がします。tryブロックの前に変数宣言しなくて済むのがいい感じ(個人的な感覚)。。。ですよね?

ちなみにこういったクラスを用意しなかったら、こんな感じ。


   public void onButton1(View view) {

        byte[] b = "あいうえお".getBytes(Charset.defaultCharset());

        InputStream in = null;
        Writer w = null;

        SQLiteOpenHelper helper = null;
        SQLiteDatabase db = null;
        Cursor cursor = null;
        try {
            in = new ByteArrayInputStream(b);
            w = new StringWriter();

            byte[] buf = new byte[128];
            in.read(buf);
            w.write(new String(buf, Charset.defaultCharset()));

            helper = new DBOpenHelper(this);
            db = helper.getReadableDatabase();
            cursor = db.rawQuery("select * from TEST_TABLE", new String[] {});
            if (cursor.moveToFirst()) {
                // 処理
            }

        } catch (IOException e) {
            Log.e("closer", "error!!", e);
        } finally {
            if (cursor != null) {
                cursor.close();
            }
            if (db != null) {
                db.close();
            }
            if (helper != null) {
                helper.close();
            }
            if (w != null) {
                try {
                    w.close();
                } catch (IOException e) {
                }
            }
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                }
            }
        }
    }

・・・うんざりですね。

[Android]SQLiteによるカーソルのループ処理の書き方

AndroidでSQLiteからデータを参照する場合、以下のような手順で処理を実装します。
  • SQLiteOpenHelperを生成する
  • SQLiteOpenHelperから、SQLiteDatabaseを取得する(SQLiteOpenHelper#getReadableDatabase()など)
  • SQLiteDatabaseを使ってクSQLを実行する(SQLiteOpenHelper#rawQuery()など)
  • Cursorを使ってデータを取り出す
という手順が一般的だと思います。
上記以外にSQLiteOpenHelperの継承クラスを用意し、DB及びテーブルを準備する実装が別途必要になります。まずは、そのコード例です。

class DBOpenHelper extends SQLiteOpenHelper {

    public DBOpenHelper(Context context) {
        super(context, "test.db"null1);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        // テーブルの生成
        db.execSQL("CREATE TABLE TEST_TABLE(id INTEGER PRIMARY KEY, memo TEXT NOT NULL, latitude REAL, longitude REAL);");

        // 初期データの投入
        db.execSQL("INSERT INTO TEST_TABLE(memo) values('hoge1')");
        db.execSQL("INSERT INTO TEST_TABLE(memo) values('hoge2')");
        db.execSQL("INSERT INTO TEST_TABLE(memo) values('hoge3')");
        db.execSQL("INSERT INTO TEST_TABLE(memo) values('hoge4')");
        db.execSQL("INSERT INTO TEST_TABLE(memo) values('hoge5')");
        db.execSQL("INSERT INTO TEST_TABLE(memo) values('hoge6')");
        db.execSQL("INSERT INTO TEST_TABLE(memo) values('hoge7')");
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // nop
    }
}


で肝心のループ処理。Cursor#getCount()を使えば、件数を取得できます。カーソルの処理において、件数を使ってループさせるのはあまりかっこよくない(個人的に)ので、その方式は除外します。
public void onButton2(View view) {

    // DBをオープンして、簡単なクエリーを投げる
    SQLiteOpenHelper helper = new DBOpenHelper(this);
    SQLiteDatabase db = helper.getReadableDatabase();
    Cursor cursor = db
            .rawQuery("select * from TEST_TABLE"new String[] {});

    // 方式1:forを使ったループ
    for (boolean next = cursor.moveToFirst(); next; next = cursor
            .moveToNext()) {
        // ・・・処理
        Log.d("sql""code1 data=" + cursor.getString(1));
    }

    // 方式2:whileを使ったループ
    boolean next = cursor.moveToFirst();
    while (next) {
        // ・・・処理
        Log.d("sql""code2 data=" + cursor.getString(1));

        // 次のレコード
        next = cursor.moveToNext();
    }

    cursor.close();
    db.close();
    helper.close();

}

「方式1:forを使ったループ」は、Effective Javaの項目45の、ローカル変数のスコープを最小化するの方式を使った例です。
この方式では、nextというローカル変数がfor文内部だけのスコープになるので、いくつかのクエリーを実行する場合、ミスは少なくなると思いますが、for文はカウントアップするという感覚があるので(個人的に)、なんか可読性が低い気がします。

「方式2:whileを使ったループ」では、ローカル変数のスコープが広くなるという欠点と、cursor#moveToNext()をループの最後に残し続けなければならないという制約が残ります。そのため修正を行う際、事故が起こりそうな感じはありますが、(個人的に)可読性が良いと考えています。

どちらもビミョーですが、可読性の観点から「方式2:whileを使ったループ」のほうが良いと考えています。

そもそもCursor#hasNext()的なメソッドがあればこんなことにはならなかったのではないかと思います。そうすれば、while (cursor.hasNext())一択になるのに。 なんでないんだろう。。。

2013年6月18日火曜日

[Java][Android]Enum型を使う(Performance Tipsを読み直す)

Android DevelopersのPerformance Tipsには、パフォーマンス/サイズ上の理由からenumの利用を避けるべき。との方針がありました。んで、最新版を読み直してみるとその記述が消えていました。

とあるサイトによると、enumを1つで1KBくらいのサイズが余分に使われる。とのことでした。でも、最近のハードウェアではそんなの微々たるものだよな。と思っていました。
あと、インタフェース型での参照を使わない。っていうのも消えている様子。この辺はAndroidを初めたころ、すごく違和感があったことを記憶しています。

ということで、intからenumに変えるとどんな感じかを書いてみました。
各々の方式でオーディオプレイヤーのステータスを表現するとこんな感じ。

■intで表現した場合
static class Status {
    /** 停止. */
    static final int stop = 0;
    /** 一時停止. */
    static final int pause = 1;
    /** 再生. */
    static final int play = 2;
    /** 早送. */
    static final int next = 10;
    /** 巻戻. */
    static final int prev = 11;
    /** 録音. */
    static final int rec = 20;

    /** 再生中のSet. */
    static final private Set<Integer> STAT_PLAY = new HashSet<Integer>();
    static {
        STAT_PLAY.add(pause);
        STAT_PLAY.add(play);
    }

    /** 再生中. */
    static boolean isPlaying(int stat) {
        return STAT_PLAY.contains(stat);
    }

    /** 停止中. */
    static boolean isStoped(int stat) {
        return stat == stop;
    }

    /** 動作中. */
    static boolean isOperating(int stat) {
        return stat == stop;
    }
}

■enumで表現した場合
public enum Status {
    /** 停止. */
    stop(0),
    /** 一時停止. */
    pause(1),
    /** 再生. */
    play(2),
    /** 早送. */
    next(10),
    /** 巻戻. */
    prev(11),
    /** 録音. */
    rec(20);

    /** 値. */
    private int value = 0;

    /** 再生中のSet. */
    static final private Set<Status> STAT_PLAY = EnumSet.of(pause, play);

    /**
     * コンストラクタ.
     *
     * @param value
     */

    private Status(int value) {
        this.value = value;
    }

    /** 再生中. */
    public boolean isPlaying() {
        return STAT_PLAY.contains(this);
    }

    /** 停止中. */
    public boolean isStoped() {
        return stop == this;
    }

    /** 動作中. */
    public boolean isOperating() {
        return stop != this;
    }

}

利用はこんな感じ。メリット的には

  • タイプセーフ。他の数値を誤って指定できない
  • メソッドが実装できます。 isXXX系を実装すると便利

まぁ、一般論ですね。利用例はこんな感じ。

EnumEnum e = new EnumEnum();
// タイプセーフ。Statusしか渡せない    
e.enumTest(Status.play);
  

public boolean enumTest(Status stat){
    // enumなら、メソッドを実装できる
    return stat.isPlaying();
}


一時期は、利用を控えてみましたが、Performance Tipsから消えたことだし、enumをバンバン使っていく方向で行きましょう。

2013年6月9日日曜日

[Other]EclipseのCheckStyle Pluginで、Unable to instantiate DoubleCheckedLockingが出る

最近Eclipse4系も安定したこともあり、3.7系の環境を捨てて、入れ替えをしました。
で、ちょっと古めのプロジェクトを久々に開いてみると、以下のようなメッセージが。。。

Errors occurred during the build.
Errors running builder 'Checkstyle Builder' on project 'MyProject'.
cannot initialize module TreeWalker - Unable to instantiate DoubleCheckedLocking
cannot initialize module TreeWalker - Unable to instantiate DoubleCheckedLocking
cannot initialize module TreeWalker - Unable to instantiate DoubleCheckedLocking
cannot initialize module TreeWalker - Unable to instantiate DoubleCheckedLocking

どうもCheckstyleでエラーが出ている様子。で調べてみると、↓にたどり着きました。 

http://sourceforge.net/p/checkstyle/bugs/682/

 以下、抜粋。
 DoubleCheckedLocking check has been removed from Checkstyle 5.6, existing configuration files must be adapted (comment/remove check).
 (意訳) Checkstyle5.6でDoubleCheckedLockingは削除されました。なので、設定ファイルからDoubleCheckedLockingを消してね。

 だそうです。で、設定ファイルを見てみると、

    <module name="DoubleCheckedLocking">

 というのがあったので、削除してみると動作しました。

2013年6月1日土曜日

[Other]Eclipse4.2にJD-Pluginを入れる(設定変更が必要)

ソースが提供されていないクローズドなライブラリ(jar)で例外が発生したり、挙動がわからないくて途方に暮れうことってありませんか?

そんな時頼りになるのがデコンパイラ。

昔はJadClipseのを使っていましたが、今はJD-Pluginを使っています。
アップデートサイト
 http://feeling.sourceforge.net/update

しかし、このプラグインEclipse4.2の環境では、そのまま動作しません。設定の変更が必要です。

 Window→Preferences
  General→Editors→File Assosiations
   *.class

の設定をClass File Editorにする必要が有ります。

これをやってしまうと、ソース添付のあるjarがJD-Pluginになっちゃうんじゃない?
と思いますが、ソースが添付されているとそちらを優先していくれるみたいです。なんかよくわかりませんが、貴重な情報が手に入ることには変わりないですね。

っていう情報が、この辺に情報が載っています。
http://stackoverflow.com/questions/11371451/jd-decompiler-for-eclipse-4-2-juno

調査の作業が発生したとき、JD-GUIを別で起動してごにょごにょしてたのが、だいぶ楽になりました。