Skip to content
This repository has been archived by the owner on Jun 20, 2023. It is now read-only.

2.07. 直列化とコレクション、永続化

KeithYokoma edited this page May 28, 2013 · 46 revisions

この章では、Android 向けの直列化とコレクション、永続化について解説します。

参考 : Bundle | Android Developers
参考 : SparseArray | Android Developers
参考 : Serializable | Android Developers
参考 : Parcelable | Android Developers
参考 : Parcel | Android Developers
参考 : JSONObject | Android Developers
参考 : JSONArray | Android Developers
参考:Storage Options | Android Developers

目次

コレクション

Java には Collection インタフェースを実装したコレクションクラスが、コレクションフレームワークとして提供されています。
この、コレクションフレームワークで提供されている各クラスは、データセットの管理をする上で非常に重要な役割を持っています。

この項目では、Android に最適化されたコレクションフレームワークのうち、Bundle、SparseArrayについて説明をします。

Bundle

Bundle は、String をキーとして、各種の Android 向けに最適化されたオブジェクト(各種プリミティブ型、Serializable 型、Parcelable 型、これらの配列ないし List コレクション)をマッピングするためのコレクションです。
Intent の Extras は、内部では Bundle オブジェクトとして管理しています。

Bundle オブジェクトにマッピングを追加するメソッドは put*() です。
マッピングに追加する型に合わせてメソッドが用意されています。

これに対応して、マッピングから値を取り出すメソッドが、get*() として定義されています。
追加した時と取り出す時の型は一致している必要があります。

呼び出し元のActivity

    public void send(View v, int value) {
        Intent intent = new Intent(this, ResultActivity.class);
        Bundle bundle = new Bundle();
        bundle.putString("data","value");
        intent.putExtra("bundlePrams", parcelableData);
        startActivity(intent);
    }

呼び出し先のActivity

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Bundle extras = getIntent().getExtras();
        Bundle bundlePrams = extras.getParcelable("bundlePrams");
        Toast.makeText(this, "data:" + bundlePrams.getString("data"), Toast.LENGTH_LONG).show();
    }

SparseArray

SparseArray は、int 型の値を key とした HashMap のようなものです。
key を int 型に固定することにより、HashMap よりも高速に動作します。

SparseArray では、int 型の key の値は、連続している必要がありません。この点が、Sparse という名前の所以です。

Android では、リソースへのアクセスのために R.id.hoge など int 型の ID を用いることが多いため、それに対するマッピングを行いたいケースは多くなります。HashMapを利用する際にはSparseArrayが利用できないかを検討してみてください(Android Lint が警告を出してくれます)。

また、SparseArray では、値の型は自由な型が指定出来ますが、value が boolean または int の場合には、SparseBooleanArraySparseIntArray が利用可能です。

int 型では key の空間が足りない場合は、key に long 型を利用した LongSparseArray も存在します。

SparseArray<String> array = new SparseArray<String>();
array.add(R.id.TextView1, "hoge");
array.add(R.id.TextView2, "fuga");
array.add(R.id.TextView3, "foo");
array.add(R.id.TextView4, "bar");

直列化

この項目では、Java で提供されている直列化フレームワークと、Android で提供されている直列化フレームワークの両方を解説します。

Serializable

Java で提供されている直列化フレームワークで、オブジェクトの保存と復旧の方法を決めるためのインタフェースです。
ObjectInputStreamObjectOutputStreamの I/O の仕組みを利用して、オブジェクトを何らかの形式で以ってデータ化(シリアライズ)可能であること、またそのデータ化したものからの復旧(デシリアライズ)が可能であることを保証します。
実際にどのような形式で直列化を行うかは、個々の実装に依存します。

public class MyObject implements Serializable {
    public static final long serialVersionUID = -4324129709521L;
    private String mName;
}

Serializable インタフェースには、メソッドの宣言がありません(このようなインタフェースの事をマーカインタフェースと呼ぶ)。

直列化のフレームワークで扱うものは、クラスのフィールドです。特に、フィールド名とそのフィールドが持つデータについてを扱います。
ですので、staticな変数や定数、transientな変数、メソッドは直列化されません。
また直列化のフレームワークでは、一定のフォーマットへの変換を想定する\ため、クラスのフィールドの互換性を管理する必要があります。
互換性のないクラスや、フィールドに変更が加えられたクラスでは、直列化のフレームワークが上手く機能しなくなることがあります。この場合、自前で直列化の実際の処理を記述する必要が有ることに注意してください。

特に理由がなければ、以降で述べる Parcelable や JSONObject などより軽量なフレームワークの利用を推奨します。

Parcel と Parcelable

Android で提供されている直列化フレームワークです。
ただし、Serializable と異なり、永続化の為のフレームワークではありません。
プロセス間通信でハイパフォーマンスを得るために設計されたインタフェースです。Intent や Bundle も Parcelable を実装したクラスになっています。

Parcelable のインタフェースに書き込み処理と復元(読み込み)処理を定義することによりメッセージングで型を限定されずオブジェクトを渡すことが出来るようになっています。
このことにより、Intent で呼び出す Activity などに対してパラメータとしてプリミティブ型や String 型ではなく、独自に定義した型を渡したい場合に利用することができます。

Parcel、Parcelable は次のような特徴を持っています。

  1. Parcelable の実装クラスの中に Parcelable ではないクラスを含めることが可能
  2. 異なるアプリケーションへのメッセージングでも同一オブジェクトが復元できることが保証される

Parcelable を実装するには、下記の手順が必要です。

  1. Parcelable#writeToParcel() メソッド内でParcelに対してデータを書き込む
  2. CREATORという定数を定義し、Parcelable.Creator<T>を実装
  3. Parcelable.Creator#createFromParcel()Parcelからオブジェクトを生成する処理を実装

Parcel から読み込む順序はParcelable#writeToParcel()で書き込んだ順序と同じになるようにする必要があります。

Parcelable を Intent で受け渡しするサンプルは下記のようになります。

受け渡し対象のParcelableを実装したクラス

public class MyParcelable implements Parcelable {
    public static final Parcelable.Creator<MyParcelable> CREATOR = new Parcelable.Creator<MyParcelable>() {
        public MyParcelable createFromParcel(Parcel in) {
            return new MyParcelable(in);
        }

        public MyParcelable[] newArray(int size) {
            return new MyParcelable[size];
        }
    };
    private int mData;

    public MyParcelable() {}

    private MyParcelable(Parcel in) {
        mData = in.readInt();
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel out, int flags) {
        out.writeInt(mData);
    }

    public void setData(int data) {
        mData = data;
    }
    public int getData(){
        return mData;
    }
}

呼び出し元のActivity

    public void send(View v, int value) {
        Intent intent = new Intent(this, ResultActivity.class);
        MyParcelable parcelableData = new MyParcelable();
        parcelableData.setData(value);
        intent.putExtra("parcelableData", parcelableData);
        startActivity(intent);
    }

呼び出し先のActivity

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Bundle extras = getIntent().getExtras();
        MyParcelable parcelData = extras.getParcelable("parcelableData");
        Toast.makeText(this, "data:" + parcelData.getData(), Toast.LENGTH_LONG).show();
    }

JSONObject と JSONArray

オブジェクトのシリアライズ・デシリアライズ形式の 1 つである JSON 形式を取り扱うためのフレームワークです。 JSONRPC など主にネットワーク経由でのデータの転送に利用します。

JSON におけるオブジェクトに対応するものがJSONObject、JSON におけるオブジェクトの配列に対応するものがJSONArrayとなります。
このため、JSONObject と JSONArray を正しく使い分ける必要があります。
例えば、下記のような場合はJSONExceptionとなります。

JSONObject obj = new JSONObject("[\\"value1\\",\\"value2\\",\\"value3\\"]");

JSONObject から値を取得する方法として、二種類の方法が提供されています。

1 つは、JSONObject#get()のような、getを接頭辞とするメソッドを用いて取得する方法です。
それぞれ、取得する型に合わせて、JSONObject#getString()JSONObject#getInt()など、型を指定したものを使います。

JSON 文字列上の型と異なる型を取得するメソッドを用いた場合、適宜型が変換されることと、存在しない key を指定した場合にJSONExceptionがスローされることに注意してください。

2 つには、JSONObject#opt()のような、optを接頭辞とするメソッドを用いて取得する方法です。
こちらも、それぞれ取得する型に合わせてJSONObject#optString()JSONObject#optInt()などのメソッドを利用します。

こちらは、存在しない key を指定した場合には、フォールバックの値が使用されます。

いずれの場合においても、下記のような JSON をJSONObject#getString()JSONObject#optString()でパースする場合、null値ではなく、文字列として"null"が帰ってくることに注意が必要です。

{
    "hoge": null
}

値がnull値かどうかを判定するメソッドとして、JSONObject#isNull()があります。

JSON を取り扱うその他のライブラリ

JSONObject や JSONArray 以外にも、JSON を取り扱う為のライブラリがあります。

その 1 つとして、Gson という、JSON 文字列から Java のオブジェクトにマッピングするフレームワークがあります。
このライブラリでは、自分で JSON 文字列から値を取り出す処理を書かなくても、JSON 文字列でのデータ構造と、Java のクラスのデータ構造を一致させておくことで、自動で JSON 文字列から Java のオブジェクトを生成してくれます。

ただし、特定の端末でGsonライブラリを利用するとVerifyErrorでクラッシュしてしまうので、対象の端末でも利用可能にするためには、名前空間の変更が必要となります。<br /. 利用する場合は作成するアプリケーションのサポート端末に注意してください。

永続化

Androidでは永続化の手法がいくつか用意されています。

  • Shared Preferences
  • Internal Storage
  • External Storage
  • SQLite Databases
  • Network Connection

この章ではShared Preferences、Internal Storage、External Storageについて扱います。 SQLite Databasesについてはデータベースの章で扱います。 Network Connectionについてはネットワークを経由してサーバー上にデータを保存することを指しています。ネットワーク通信についてはネットワーク通信の章で扱います。サーバー上でのデータの保存についてはサーバーサイドの実装の話になるため省略します。

SharedPreferences

参考:SharedPreferences | Android Developers

SharedPreferencesはAndroid標準で用意されている永続化の方法の一つです。 データをkeyとvalueの組み合わせでプリミティブ型、String型のデータを保存します。 自分でファイルを扱うよりも非常に簡単にデータの永続化が可能になります。 また、値の変化を監視し、変化があった場合にコールバックで処理を行うことが可能です。

SharedPreferencesはアプリケーションのアンインストール時に削除されるため、アンインストール→再インストールを行った場合にはデータが消えてしまします。また、ユーザーがアプリケーションの設定からデータを削除することも可能です。 その場合でもデータを保持しておきたい場合は、サーバーにデータを保存するようにするのが良いでしょう。

通常は意識することはありませんが、SharedPreferencesを利用して保存したデータは通常 /data/data/$app_package_name/shared_prefs に xmlファイルとして保存されます。 ただし、特定の機種では保存場所が違うことに起因する問題が発生します。 対象の端末をサポートする場合、Android-Device-Compatibilityを利用するなどして問題を回避することを推奨します。

SharedPreferencesのインスタンスは Context#getSharedPreferences (String name, int mode)で取得します。
第一引数には扱いたいSharedPreferences名を指定します。
第二引数にはSharedPreferencesのモードを指定します。 モードは対象のSharedPreferencesに対するアクセス権の設定になります。デフォルトではMODE_PRIVATEになります。

モードについて

SharedPreferencesはファイル作成時にモードが指定出来ますが、通常はデフォルトのMODE_PRIVATEを利用するのが良いでしょう。 MODE_PRIVATEはgetSharedPreferences を呼んだ(ファイルを作成した) アプリケーションからのみアクセスできます。
他のアプリケーションから読み書きを可能にするMODE_WORLD_READABLEやMODE_WORLD_WRITEABLEも存在しますが、セキュリティ上の観点から利用しないことを強く推奨します。 Android アプリのセキュア設計・セキュアコーディングガイド の4.6ファイルを扱う に記述されているように、他のアプリケーションへデータを共有する場合はアプリ間連携の仕組みを利用するようにしてください。

また、複数のプロセスをもつアプリケーションで同じファイルを参照する場合に利用するMODE_MULTI_PROCESSというモードも存在しますが、サポートされるのはAPI Level11(Android3.0)以降なので、2.x系の端末をサポートする場合には利用できません。

データの保存

データを保存するには

  1. SharedPreferencesのedit()メソッドでSharedPreferences.Editorを取得
  2. SharedPreferences.Editor#putString(String key,String value)などで値を設定する
  3. SharedPreferences.Editor#commit()またはapply()で保存する

の手順で保存されます。
apply()は保存の完了を待たずに処理が戻ってきます。そのため、保存に成功したかを確認することができません。 また、apply()はAPI Level9(Android2.3)以降のサポートになるため、Android2.2以前の端末をサポートする場合利用できません。
また、メインスレッドからSharedPreferencesを扱っていて、保存の成功、失敗を意識しなくていい場合以外は、commit()を使用することが望ましいでしょう。

値を削除する場合はSharedPreferences.Editor#remove(String key)で個別に削除できます。 全ての値を削除したい場合はSharedPreferences.Editor#clear()メソッドで全ての値の削除出来ます。
値の削除に関してもcommit()を呼び出すまでは保存されません。

    // データの保存
    public boolean savePerson(String name, int age) {
        SharedPreferences sp = getSharedPreferences("person", MODE_PRIVATE);
        Editor editor = sp.edit();
        editor.putString("name", name);
        editor.putInt("age", age);
        return editor.commit();
    }

データの取得

データの取得は取得したSharedPreferencesからgetString,getBoolean,getFloat,getLong,getStringSetで取得出来ます。 保存時に指定したnameと同じnameを同じ型で取得することで取得出来ます。

    // データの取得
    private String mName;
    private int mAge;
    public void readPerson() {
        SharedPreferences sp = getContext().getSharedPreferences("person", MODE_PRIVATE);
        mName = sp.getString("name", "no name");
        mAge = sp.getInt("age", 0);
    }

データ変更の監視

registerOnSharedPreferenceChangeListenerメソッドでlistenerを登録することで変更が発生した際にコールバックが実行されるようになります。 保存されている値に変更があった場合に、その変更を画面上に反映するといった場合に利用されます。 登録したlistenerはlistener側のライフサイクルが終わる際に、必ずunregisterOnSharedPreferenceChangeListenerを呼び出して登録解除をしてください。 登録解除をしなかった場合は、対象のオブジェクトに参照が残ってしまうためリークが発生するので注意が必要です。 registerOnSharedPreferenceChangeListenerの引数にはOnSharedPreferenceChangeListenerを実装したオブジェクトを指定します。 値の変更が発生した場合に、そのオブジェクトのonSharedPreferenceChangedメソッドが呼ばれるので、必要な処理を記述します。

public class MainActivity extends Activity implements OnSharedPreferenceChangeListener { 
    private SharedPreferences mSharedPreferences;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mSharedPreferences = getSharedPreferences("sample", MODE_PRIVATE);
        mSharedPreferences.registerOnSharedPreferenceChangeListener(this);
    }
    protected void onDestroy(){
        mSharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
        super.onDestroy();
    }

    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
        String value = sharedPreferences.getString(key, null);
        if (value != null) {
            TextView tv = (TextView) findViewById(R.id.PreferencesValue);
            tv.setText(value);
        }
    }
}

Internal Storage

Internal Storageはその名の通りデバイスの内部ストレージのことです。

内部ストレージに保存されたファイルはSharedPreferencesと同様、モードによってアクセス制御をすることができます。 これにより安全にファイルを保存することができます。ただし、内部ストレージは外部ストレージに比べて容量に制限があることが多いため、画像ファイルなどを大量に保存することには向きません。

ファイルを保存するためには

  1. ファイル操作モードを指定してopenFileOutput()を呼び出します。
  2. write()で保存したいバイト列を保存します。
  3. close()でストリームを閉じます。 closeはエラーが発生した場合でも必ず呼び出されるようにする必要があります。
String FILENAME = "hello_file";
String string = "hello world!";

FileOutputStream fos = openFileOutput(FILENAME, MODE_PRIVATE);
fos.write(string.getBytes());
fos.close();

ファイルの内容を取得する場合は

  1. openFileInput()でファイル名を指定しFileInputStreamを取得します。
  2. read()でファイル内容を読み出します。
  3. close()でストリームを閉じます。 保存時同様、close()は必ず呼び出されるようにしてください。

InputStream,OutputStreamの扱いについては、一般的なJavaのファイルの扱いに準じるので説明を省略します。

External Storage

参考:Context.getExternalFilesDir

External Storageは外部ストレージのことです。SDカードがこれに当たることが多いです。 一部端末では、異なることもあります。機種変更時などのバックアップ用のデータをSDカードに保存したつもりが、SDカードに保存できていないということもあります。

外部ストレージはアクセス制御をすることができません。 そのため、全てのアプリケーションからアクセスすることができるので、保存する内容には注意が必要です。

外部ストレージを取得するにはgetExternalFilesDir()で外部ストレージのルートディレクトリを取得出来ます また、引数のtypeでDIRECTORY_MUSIC, DIRECTORY_PODCASTS, DIRECTORY_RINGTONES, DIRECTORY_ALARMS, DIRECTORY_NOTIFICATIONS, DIRECTORY_PICTURES, DIRECTORY_MOVIESを指定することにより、用途別のディレクトリを取得ができます。

ファイルの扱いについては、一般的なJavaの扱いに準じますので説明は省略します。

実習・課題

コレクション・直列化

  1. (実習)JSONの文字列をパースし、画面に表示してください。
  2. (課題)JSONのレスポンスをパースし、一覧表示してください。
    また、ListView部分をタップすると、詳細画面を表示するようにしてください。
    その際、表示するデータはすべてMainActivityから渡すようにしてください。

永続化

  1. (実習)SharedPreferencesに値を保存、取得をしてください。
    アプリケーションを終了しても保存した値を取得できることの確認をしてください。 また、保存した値を削除してください。
  2. (実習)内部ストレージ、外部ストレージにそれぞれファイルを保存してください。
    内部ストレージにはテキストファイル、外部ストレージには画像ファイルを保存してください。 外部ストレージに保存されたファイルをファイラーなどで参照してみてください。
  3. (課題)SharedPreferencesを利用してアプリケーションを終了しても保存されるカウンターを作成してください。
    また、そのカウントを画面上に表示してください。その際、onSharedPreferenceChanged内から変更を行なってください。

GitHub Pagesへ移行しましたmixi-inc.github.ioへお願いします。

Clone this wiki locally