読者です 読者をやめる 読者になる 読者になる
スポンサーリンク

Android SDKの,ParcelableとSerializableの違いを比較 - Intentで独自オブジェクトを運搬する際,役立つのはどちら?

Android java

重要なお知らせ:

この記事で公開した情報は,AndroidのMVCフレームワーク「Android-MVC」の機能の一部として取り込まれました。

より正確な設計情報や,動作可能な全ソースコードを閲覧したい場合,「Android-MVC」の公式ページより技術情報を参照してください。


AndroidのMVCフレームワーク - 「Android-MVC」
http://code.google.com/p/android-mvc-...


Androidアプリの画面遷移時には,

Intentオブジェクト内にputExtra()でデータを詰め込んで,次の画面に渡す。


Intentに対してStringとかintとか,プリミティブな値を格納するのは容易だ。

しかし,格納する値のデータ構造が複雑になってくると,

独自オブジェクトのインスタンスを丸ごと格納したくなる。

どうすれば可能か。


この場合,オブジェクトの「直列化(シリアライズ)」の手法がキーになる。

  • (0)SerializableとParcelableについての背景情報
  • (1)Parcelableでint値を1つだけ運搬するサンプルコード
  • (2)非Parcelableオブジェクトを,Parcelableオブジェクトに格納するとどうなるか
  • (3)シンプルなSerializableが最も無難か
  • (4)インナークラスをSerializableにしたい時の試練
  • 比較と結論

(0)SerializableとParcelableについての背景情報

要約:

  • オブジェクトをSerializableにすれば,アプリ内を飛ぶIntent上に,オブジェクトを直接乗っける事ができる。
  • オブジェクトをParcelableにすれば,アプリ間をまたぐ事もできる。ただしその分,直列化の処理を自前でコーディングする必要がある。


Android上でのSerializableの利用に関する参考情報:

Androidの画面遷移 Intentを使ってobjectを渡す方法
http://d.hatena.ne.jp/hidecheck/20091...

  • 非常に簡単なサンプルコード


Android: Difference between Parcelable and Serializable?
http://stackoverflow.com/questions/33...

  • Serializable is a standard Java interface.
  • Parcelable is an Android specific interface where you implement the serialization yourself. (自前で直列化する分,めんどくなるが)
  • However, you can use Serializable objects in Intents.


Parcelableの利用を勧める技術資料やサンプルコード集:

Intent で Parcelable を実装した自作オブジェクトの受け渡しをしてみた
http://rtaki.blogspot.com/2011/01/int...

  • IntentにはputExtra(String name, Parcelable value) と getParcelableExtra(String name) がある
  • Parcelable を自作クラスに適用すれば, Activity 間でオブジェクトの受け渡しができる


[Android] android.os.Parcelable / Parcel
http://www.adakoda.com/adakoda/2009/0...

  • Parcelableインタフェースを実装しているクラスであれば、そのインスタンスも、putExtra(String name, Parcelable value)を呼び出すことで、インスタンスごと渡すことができる


Android Parcelable を使ってクラスのメンバを一時保存
http://y-anz-m.blogspot.com/2010/03/a...

  • 「独自にデータクラスを用意していて、このクラスのメンバごと保存したいんだけど…」という場合に登場するのが Parcelable
  • ParcelableはParcel にデータを書き/読みするためのインタフェース


Androidアプリ入門 No.65 Intent アクションでデータを受け取る Parcelable
http://d.hatena.ne.jp/kuwalab/2011020...

  • Parcelという単語は小包という意味。データをまとめて渡すようなものをイメージ
  • Serializableでは同一のアプリケーションでの明示的なIntentであれば問題ない。しかし暗黙的なIntentの場合,自分が作成したActivityがアクションを受け取るとは限らない。なのでSerializeしたオブジェクトが復元できる保証はない。
  • Parcelableを使用することで,アプリ間でオブジェクトのようなものの受け渡しをすることができる。アプリ間でParcelableクラスの実体が異なっても構わない。


Android Developers : Parcelable
http://developer.android.com/intl/ja/...

  • 利用時は,Parcelable.Creatorインタフェースを実装した「CREATOR」という名前のフィールドが必要
    • →これだけでは何のことか伝えづらいので,サンプルコードを参照のこと。

(1)Parcelableでint値を1つだけ運搬するサンプルコード

上述の情報を踏まえて,まずは適用範囲が広くて便利そうなParcelableのサンプルコード。


画面遷移の仕様としては,InputActivityからResultActivityに対してIntentを投げる,という2画面構成を想定。


InputActivity側:

Intent intent = new Intent(
  InputActivity.this,
  ResultActivity.class
);

// Parcelableにint値をセット
IntentData intentData = new IntentData();
intentData.setMyIntValue(99);

// IntentにParcelableをセット
intent.putExtra("testParcelable", intentData);

startActivity(intent);


ResultActivity側:

// IntentからParcelableを取り出し
Bundle extras = getIntent().getExtras();
IntentData intentData = (IntentData) extras.getParcelable("testParcelable");

Toast.makeText(
    this,
    // Parcelableからint値を取り出し
    "mData = " + intentData.getMyIntValue(),
    Toast.LENGTH_LONG
).show();


ParcelableなIntentDataクラス:

package com.example;


import android.os.Parcel;
import android.os.Parcelable;

/**
 * Intentに任意のオブジェクトを運搬させるための入れ物。
 *
 */
public class IntentData implements Parcelable {

    // 運搬したいデータ
    private int mData;


    /**
     * 空っぽの状態でインスタンス作成
     */
    public IntentData() {
        // TODO 自動生成されたコンストラクター・スタブ
    }


    /**
     * Parcel化予定の値をset
     */
    public void setMyIntValue(int i) {
        mData = i;
    }


    /**
     * Parcel化された値をget
     */
    public int getMyIntValue() {
        return mData;
    }


    // ----- Parcelableインタフェースが利用 ここから -----


    /**
     * データのParcel化を実行する
     */
    @Override
    public void writeToParcel(Parcel out, int flags) {
        out.writeInt( mData );
    }


    /**
     * Parcelからデータを読み出すprivateコンストラクタ
     */
    private IntentData(Parcel in) {
        mData = in.readInt();
    }


    /**
     * ParcelからこのParcelableのインスタンスを作るためのCreator
     */
    public static final Parcelable.Creator<IntentData> CREATOR
    = new Parcelable.Creator<IntentData>() {
        public IntentData createFromParcel(Parcel in) {
            return new IntentData(in);
        }

        public IntentData[] newArray(int size) {
            return new IntentData[size];
        }
    };


    /**
     * シリアライズされたオブジェクトの種類を判別するためのビットマスクを返す
     */
    @Override
    public int describeContents() {
        return 0;
    }


    // ----- Parcelableインタフェースが利用 ここまで -----

}

上記のコードを動作させると,ResultActivity側でトーストが出て「99」と表示される。

Parcelable経由で,正常にint値を運搬できている。



最初はAPIの意味が把握しづらい。

まとめると,下記のような事情。

  • Serialize(直列化)されたデータ形式のAndroid特化版とも言うべき,Parcelという形式(クラス)が存在する。
  • SerializableのAndroid特化版とも言うべき,Parcelableなクラス(インタフェース)が存在する。
  • Parcelableなクラスは,一種のStream操作により,保持しているデータをParcel化する。

(2)非Parcelableオブジェクトを,Parcelableオブジェクトに格納するとどうなるか

intのようなプリミティブ値だけでなく,独自のオブジェクトもParcelable内に格納できるのか。

下記で検証する。


まず,運搬したい独自オブジェクトを作る。

package com.example;

/**
 * Parcelable経由で運搬実験するための適当なオブジェクト
 */
public class Hoge {
    public int i;
    public String str;
}


InputActivity側:

Intent intent = new Intent(
  InputActivity.this,
  ResultActivity.class
);

// Hogeオブジェクトを生成して色々セット
Hoge hoge = new Hoge();
hoge.i = 77;
hoge.str = "abc";

// ParcelableにHogeオブジェクトをセット
IntentData intentData = new IntentData();
intentData.setHogeObject( hoge );

// IntentにParcelableをセット
intent.putExtra("testParcelable", intentData);

startActivity(intent);


ResultActivity側:

// IntentからParcelableを取り出し
Bundle extras = getIntent().getExtras();
IntentData intentData = (IntentData) extras.getParcelable("testParcelable");

// ParcelableからHogeを取り出し
Hoge hoge = intentData.getHogeObject();

Toast.makeText(
    this,
    // Hogeから値を取り出し
    "i = " + hoge.i + ", str = " + hoge.str,
    Toast.LENGTH_LONG
).show();


肝心なParcelable:

package com.example;


import android.os.Parcel;
import android.os.Parcelable;

/**
 * Intentに任意のオブジェクトを運搬させるための入れ物。
 *
 */
public class IntentData implements Parcelable {

    // 運搬したいデータ
    private Object[] mData = new Object[1];


    /**
     * 空っぽの状態でインスタンス作成
     */
    public IntentData() {
        // TODO 自動生成されたコンストラクター・スタブ
    }


    /**
     * Parcel化予定の値をset
     */
	public void setHogeObject(Hoge hoge) {
		mData[0] = hoge;
	}


    /**
     * Parcel化された値をget
     */
    public Hoge getHogeObject() {
        return (Hoge) mData[0];
    }


    // ----- Parcelableインタフェースが利用 ここから -----


    /**
     * データのParcel化を実行する
     */
    @Override
    public void writeToParcel(Parcel out, int flags) {
        out.writeArray( mData );
    }


    /**
     * Parcelからデータを読み出すprivateコンストラクタ
     */
    private IntentData(Parcel in) {
        mData = in.readArray( Hoge.class.getClassLoader() );
    }


    /**
     * ParcelからこのParcelableのインスタンスを作るためのCreator
     */
    public static final Parcelable.Creator<IntentData> CREATOR
    = new Parcelable.Creator<IntentData>() {
        public IntentData createFromParcel(Parcel in) {
            return new IntentData(in);
        }

        public IntentData[] newArray(int size) {
            return new IntentData[size];
        }
    };


    /**
     * シリアライズされたオブジェクトの種類を判別するためのビットマスクを返す
     */
    @Override
    public int describeContents() {
        return 0;
    }


    // ----- Parcelableインタフェースが利用 ここまで -----

}

ParcelにはreadObject()とかwriteObject()のようなメソッドはない。

なので,Object[]を扱えるreadArray()とwriteArray()を使っている。


これを実行すると,下記のような例外になる。

ERROR/AndroidRuntime(6278): FATAL EXCEPTION: main
ERROR/AndroidRuntime(6278): java.lang.RuntimeException: Parcel: unable to marshal value com.example.Hoge@40541288
ERROR/AndroidRuntime(6278):     at android.os.Parcel.writeValue(Parcel.java:1132)
ERROR/AndroidRuntime(6278):     at android.os.Parcel.writeArray(Parcel.java:538)
ERROR/AndroidRuntime(6278):     at com.example.IntentData.writeToParcel(IntentData.java:49)
ERROR/AndroidRuntime(6278):     at android.os.Parcel.writeParcelable(Parcel.java:1151)
ERROR/AndroidRuntime(6278):     at android.os.Parcel.writeValue(Parcel.java:1070)
ERROR/AndroidRuntime(6278):     at android.os.Parcel.writeMapInternal(Parcel.java:488)
ERROR/AndroidRuntime(6278):     at android.os.Bundle.writeToParcel(Bundle.java:1552)
ERROR/AndroidRuntime(6278):     at android.os.Parcel.writeBundle(Parcel.java:502)
ERROR/AndroidRuntime(6278):     at android.content.Intent.writeToParcel(Intent.java:5476)
ERROR/AndroidRuntime(6278):     at android.app.ActivityManagerProxy.startActivity(ActivityManagerNative.java:1561)
ERROR/AndroidRuntime(6278):     at android.app.Instrumentation.execStartActivity(Instrumentation.java:1374)
ERROR/AndroidRuntime(6278):     at android.app.Activity.startActivityForResult(Activity.java:2827)
ERROR/AndroidRuntime(6278):     at android.app.Activity.startActivity(Activity.java:2933)
ERROR/AndroidRuntime(6278):     at com.example.InputActivity$1.onClick(InputActivity.java:42)
ERROR/AndroidRuntime(6278):     at android.view.View.performClick(View.java:2498)
ERROR/AndroidRuntime(6278):     at android.view.View$PerformClick.run(View.java:9129)
ERROR/AndroidRuntime(6278):     at android.os.Handler.handleCallback(Handler.java:587)
ERROR/AndroidRuntime(6278):     at android.os.Handler.dispatchMessage(Handler.java:92)
ERROR/AndroidRuntime(6278):     at android.os.Looper.loop(Looper.java:123)
ERROR/AndroidRuntime(6278):     at android.app.ActivityThread.main(ActivityThread.java:3728)
ERROR/AndroidRuntime(6278):     at java.lang.reflect.Method.invokeNative(Native Method)
ERROR/AndroidRuntime(6278):     at java.lang.reflect.Method.invoke(Method.java:507)
ERROR/AndroidRuntime(6278):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:864)
ERROR/AndroidRuntime(6278):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:622)
ERROR/AndroidRuntime(6278):     at dalvik.system.NativeStart.main(Native Method)

「Parcel: unable to marshal value com.example.Hoge」とあるので,

明示的にParcelableないしSerializable指定していないHogeクラスは,直列化できなかったという事だ。

(※marshallとはダンプ・直列化みたいな意味)


たとえParcelableなクラス(IntentData)の中に格納しても,そこまで面倒は見てくれない。


参考:

androidエラー「Uncaught handler: thread main exiting due to uncaught exception,java.lang.RuntimeException: Parcel: unable to marshal value~」
http://www.techmaru.net/wordpress/201...

  • List使用時は,ListのT型もシリアライズ化しなければならない
  • 自作ListViewのデータクラスHogeListDataはSerializableインタフェースを実装していなかったので、Serializableを実装してみたら、例外がでなくなった


Exception: Parcel: Unable to marshal value?
https://groups.google.com/group/andro...

  • ImageButtonはSerializableですか?という質問に対し,「マニュアルを読めば2秒でわかる事を・・・」とあきれた回答がついている様子


Parcelableに関する結論として,

  • Parcelableにしたい物の中身は,全部Parcelableでなければならない。

割と当然の結論だが…。

※Serializableも「自分の中で状態が閉じていること」を要求するので,後出APIであるParcelableには,もうちょっと親切な振る舞いを期待していた。



大体予期していた事だけども,これは作業としてはめんどい。

シリアライズ処理の内容を,毎回,自前でコーディングする必要があるからだ。


Intent経由でたくさんのオブジェクトをやり取りしたい場合,

あちこちにParcelable対応のコードを埋め込まないと,

独自オブジェクトのIntent受け渡しは不可能。(Javaなのでmix-inできないから)



もし仮にCustomParcelableみたいな基底クラスを作って継承させても,共通化できるコードの量には限界がある。

ではどうするか。


(3)シンプルなSerializableが最も無難か

Parcelableを使おうとした元々の理由は,

  • Serializableの場合と比較すると応用が効き,アプリ外への暗黙的Intentにも利用できるから。

という事だった。

しかし,これは「アプリ間連携」を意識するという事。


まずは,アプリの外部がどうのこうのという応用的なゴールは脇に置いとこう。

そして「Intentに独自オブジェクトを運搬させたい」という,本来のゴールを思い出そう。

そうであれば,Serializableで十分。


InputActivity側:

Intent intent = new Intent(
  InputActivity.this,
  ResultActivity.class
);

// Hogeオブジェクトを生成
Hoge hoge = new Hoge();
hoge.i = 77;
hoge.str = "abc";

// IntentにHogeをセット
intent.putExtra("testSerializable", hoge);

startActivity(intent);


ResultActivity側:

// IntentからHogeを取り出し
Bundle extras = getIntent().getExtras();
Hoge hoge = (Hoge) extras.getSerializable("testSerializable");

Toast.makeText(
    this,
    // Hogeから値を取り出し
    "i = " + hoge.i + ", str = " + hoge.str,
    Toast.LENGTH_LONG
).show();


SerializableなHoge:

package com.example;

import java.io.Serializable;

/**
 * Intent経由で運搬させるための適当なオブジェクト
 */
public class Hoge implements Serializable
{
    private static final long serialVersionUID = 1L;

    // メインの属性
    public int i;
    public String str;
}

前回まであったIntentDataというクラスは,もう使わない。



これでプロジェクトを実行すると,画面遷移時には「i = 77, str = abc」というToastが無事,現れてくれる。

Hogeオブジェクトを,Intent経由で無事,運搬できている。


Parcelableを利用する場合に比べて,はるかにシンプルだ。


運搬対象のHogeクラスに対して付与すべきコード片は,たったの2か所。

通常の直列化可能クラスの作成時と同じように, implements Serializable して,シリアルIDをくっつけるだけ。

運搬対象を形式変換するための特別な外部クラスも作らなくて済む。



とはいえ,もちろんこの場合は,JavaプログラミングにおけるSerializableの用法の一般論が当てはまることになる。

直列化可能にしたいオブジェクトは,自分自身の中で状態が「閉じて」いなければならない。

Effective Java 第2版の11章「シリアライズ」の内容を読み返すとよい。項目17も参考になる。


「オブジェクトが一旦シリアライズされると,その符号化されたものは,実行中の仮想マシンから他の仮想マシンへ送信できます…

シリアライズにはプログラマの側で何も努力が要らないという広く知られた誤解があります。…

オブジェクトの転送や永続化に関してシリアライズに依存した何らかのフレームワークでクラスが使用されるのであれば,(Serializableインタフェースを)実装するのは基本的なことです。…

活動的な実体を表すクラスは,Serializableをめったに実装すべきではありません。…

継承のために設計されたクラスはSerializableをめったに実装すべきではありません…この規則を破るのが適切な時もあります。…

(デシリアライズ時の矛盾が問題にならない限り)serialVersionUIDにどのような値を選んでも問題はありません。

"Effective Java", 11章,2010.

Androidアプリの実行モデルは,アプリ毎にプロセスもDalvik VMも分かれている事を思い出そう。

また,ここでは「アプリ間連携」の可能性を除外しているので,デシリアライズ時の矛盾も発生しない事に注意しよう。


参考URL:

Effective Java 読書会 14 日目 「シリアライズ!シリアライズ!」(by amachang)
http://d.hatena.ne.jp/amachang/201003...


シリアライズする場合、なぜSerializableインターフェースを載せなければならない仕様なのか? serialVersionUIDの役割は何か?
http://okwave.jp/qa/q3997360.html#besta

  • Sun JavaVMは内部的にif(hoge instanceOf Serializable)のチェックを行っている
  • ファイルオブジェクトのインスタンスを保持(has-A)するようなクラスはシリアライズできません。・・・「シリアライズ不可能」とは,状態が自分の中で閉じていないという事

(4)インナークラスをSerializableにしたい時の試練

そんなわけで,無難なSerializableを使えば,大体望むものは何でもIntentに格納できるな〜と安心しかける。

ところがそこに罠がある。


インナークラスや匿名クラスは,簡単にはシリアライズできないのである。


例えば,下記のソースを見てほしい。

Android-MVCフレームワーク ver0.1 の,ビジネスロジックのサンプル
http://code.google.com/p/android-mvc-...

メソッドの戻り値部分は,下記のようになっている。

    ・・・

    // 実行結果を返す
    return new ActionResult(){
        @Override
        public void onNextActivityStarted(final Activity activity)
        {
            activity.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    Toast.makeText(
                        activity,
                        get("new_friend_name") + "さんを登録しました。",
                        Toast.LENGTH_LONG
                    ).show();
                }
            });
        }
    }
        .setRouteId("success")
        .add("new_friend_name", f.getName())
    ;

このコーディングスタイルは,自然な思考の流れに沿ったものだ。


「何かを返却する」ためにreturnと書いてから,戻り値オブジェクトを匿名クラスの形で定義し始める。

なので,

  • returnから後の部分は全部,メソッドの返却値として利用されることが一目瞭然に分かる。
  • このオブジェクトを変数に保持するような冗長さも不要になっている。
  • ソースコードの処理全体の流れがメソッドチェインで順番に記述されるので,把握しやすい。

流れるようなコーディングスタイルを実現するために,匿名クラスが一役買っているのだ。



だがしかし,ここで匿名クラスとして利用しているActionResultを,

Intentで運搬するためにSerializableにすると,問題が起こる。


まず,別ファイルにてActionResultの定義に「implements Serializable」し,serialVersionUIDもセットする。

すると,上記の匿名クラス利用側のソースにおいても,

「return new ActionResult(){」の次の行にserialVersionUIDをセットするように警告が出る。

それで大丈夫かと思い,IntentにputExtra()してシリアライズを試みると・・・


ERROR/AndroidRuntime(31477): FATAL EXCEPTION: AsyncTask #5

・・・

ERROR/AndroidRuntime(31477): Caused by: java.lang.RuntimeException: Parcelable encountered IOException writing serializable object (name = com.android_mvc.sample_project.domain.DBEditAction$1)
ERROR/AndroidRuntime(31477):     at android.os.Parcel.writeSerializable(Parcel.java:1176)
ERROR/AndroidRuntime(31477):     at android.os.Parcel.writeValue(Parcel.java:1130)
ERROR/AndroidRuntime(31477):     at android.os.Parcel.writeMapInternal(Parcel.java:488)
ERROR/AndroidRuntime(31477):     at android.os.Bundle.writeToParcel(Bundle.java:1552)
ERROR/AndroidRuntime(31477):     at android.os.Parcel.writeBundle(Parcel.java:502)
ERROR/AndroidRuntime(31477):     at android.content.Intent.writeToParcel(Intent.java:5476)
ERROR/AndroidRuntime(31477):     at android.app.ActivityManagerProxy.startActivity(ActivityManagerNative.java:1561)
ERROR/AndroidRuntime(31477):     at android.app.Instrumentation.execStartActivity(Instrumentation.java:1374)
ERROR/AndroidRuntime(31477):     at android.app.Activity.startActivityForResult(Activity.java:2827)
ERROR/AndroidRuntime(31477):     at android.app.Activity.startActivity(Activity.java:2933)
ERROR/AndroidRuntime(31477):     at com.android_mvc.framework.controller.routing.Router.switchByActionResult(Router.java:109)
ERROR/AndroidRuntime(31477):     at com.android_mvc.framework.controller.ControlFlowDetail$3.main(ControlFlowDetail.java:165)
ERROR/AndroidRuntime(31477):     at com.android_mvc.framework.task.SequentialAsyncTask.doInBackground(SequentialAsyncTask.java:102)
ERROR/AndroidRuntime(31477):     at com.android_mvc.framework.task.SequentialAsyncTask.doInBackground(SequentialAsyncTask.java:1)
ERROR/AndroidRuntime(31477):     at android.os.AsyncTask$2.call(AsyncTask.java:185)
ERROR/AndroidRuntime(31477):     at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:306)
ERROR/AndroidRuntime(31477):     ... 4 more
ERROR/AndroidRuntime(31477): Caused by: java.io.NotSerializableException: com.android_mvc.sample_project.domain.DBEditAction
ERROR/AndroidRuntime(31477):     at java.io.ObjectOutputStream.writeNewObject(ObjectOutputStream.java:1535)
ERROR/AndroidRuntime(31477):     at java.io.ObjectOutputStream.writeObjectInternal(ObjectOutputStream.java:1847)
ERROR/AndroidRuntime(31477):     at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:1689)
ERROR/AndroidRuntime(31477):     at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:1653)
ERROR/AndroidRuntime(31477):     at java.io.ObjectOutputStream.writeFieldValues(ObjectOutputStream.java:1143)
ERROR/AndroidRuntime(31477):     at java.io.ObjectOutputStream.defaultWriteObject(ObjectOutputStream.java:413)
ERROR/AndroidRuntime(31477):     at java.io.ObjectOutputStream.writeHierarchy(ObjectOutputStream.java:1241)
ERROR/AndroidRuntime(31477):     at java.io.ObjectOutputStream.writeNewObject(ObjectOutputStream.java:1575)
ERROR/AndroidRuntime(31477):     at java.io.ObjectOutputStream.writeObjectInternal(ObjectOutputStream.java:1847)
ERROR/AndroidRuntime(31477):     at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:1689)
ERROR/AndroidRuntime(31477):     at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:1653)
ERROR/AndroidRuntime(31477):     at android.os.Parcel.writeSerializable(Parcel.java:1171)
ERROR/AndroidRuntime(31477):     ... 19 more

という風に,インナークラスを囲っている外側のクラス(=エンクロージング・インスタンス)に対して「java.io.NotSerializableException」となる。


で,LogCatの指示に従って,アウタークラスであるDBEditActionもSerializableにしてみると,今度は同じエラーメッセージが

java.io.NotSerializableException: com.android_mvc.sample_project.activities.func_db.DBEditActivity

のように,オブジェクトの参照をつたって芋づる式に広がってゆく。

こんなエラーメッセージの言う事を字面通り受け取って,盲目的に従っていたら,しまいには全部のオブジェクトをSerializableにする必要が生じてしまう。



なので,一度基本に立ち返る必要がある。

Java言語の文法仕様では,インナークラスとはどういう位置づけにあるのか。

インナークラスのSerializeという行為自体に,問題があるのか。

インナークラスもシリアライズ出来る Sep. 14, 1998
http://www2s.biglobe.ne.jp/~dat/java/...

  • インナークラスのシリアライズに失敗しているのは、アウタークラス が Serializable を implements していないからでは?


classとstatic classとでは、親の参照の持ち方が違う
http://blog.xole.net/article.php?id=585

  • staticを付けない「class」で宣言すると,親の参照を持つことになる
  • サンプルでは、Serializableを実装しているオブジェクトのバイト数を取得しようとしていますが、(staticを付けない)通常のclassで内部クラスを実装し、それを行おうとすると、親クラス側について例外が発生してしまう。内部クラスのHogeは親クラス(InnerClassSample)の参照を持っているため。試しに、親クラスでSerializableを実装することで、メソッドtestの実行は可能となる
  • static classで内部クラスを実装した場合は例外が発生しません


Unable to serialize anonymous classes.(Oracleの公式ページ,匿名クラスをシリアライズできないのはバグかという質問)
http://bugs.sun.com/bugdatabase/view_...
Because inner classes declared in non-static contexts
contain implicit non-transient references to enclosing class
instances, serializing such an inner class instance will result in
serialization of its associated outer class instance as well.

訳:
staticでないコンテキスト上で宣言されたインナークラスは,エンクロージング・インスタンスに対する暗黙の非transientの参照を持ちます。
(※transientはシリアライズの対象外にするための演算子。非transientとは,シリアライズの対象から逃げられないということ)
そのため,そのようなインナークラスのインスタンスをシリアライズしようとすると,それと関連を持つアウタークラスも同様に直列化処理の対象になります。


※全く同じ事が,Java言語仕様の
1.10 The Serializable InterfaceのNOTEに書かれている。
http://docs.oracle.com/javase/7/docs/...


SER05-J. Do not serialize instances of inner classes
https://www.securecoding.cert.org/con...

  • Risk Assessment : Serialization of inner classes can introduce platform dependencies and can cause serialization of instances of the outer class.
  • コンパイラの実装依存でシリアライズ対象範囲の挙動が変わってしまう可能性があるため,インナークラスの直列化そのものがリスクである

Effective Javaのp284にも,下記のような注意書きがある。



内部クラスは,Serializableを実装すべきではありません。

内部クラスは,エンクロージングインスタンスに対する参照を保持するため…コンパイラ生成による人工的フィールド(synthetic field)を使用しています。…

したがって,内部クラスのデフォルトのシリアライズ形式は,不明確です。

しかし,staticのメンバークラスは,Serializableを実装できます。


まとめると,インナークラスをシリアライズするためには

  • インナークラスをstaticコンテキストで定義すること
  • アウタークラスや,関連する全てのオブジェクトをSerializableにすること


のいずれかが要求される。

※その理由は,ただ単にJava言語のコンパイラの仕様である。プログラマの便益を図った仕様ではない。このあたりにJavaのうんざりするダークサイドが見え隠れする。*1



ではもう一歩初心に戻って,Java言語において,インナークラスがstaticになる場合の効用とは何だっただろうか。

[Java]なんでstaticがついてないとダメなの?
http://d.hatena.ne.jp/nowokay/20070125

  • staticがついてないものを使ってるときにはthisが省略されてる
  • this.new B();
  • new A().new B();


staticな内部クラスは内部クラスじゃあない
http://d.hatena.ne.jp/jyukutyo/200610...

  • new OuterClass().new InnerClass(); (非staticな)内部クラスの生成には外部クラスのインスタンスが必要
  • InnerClassにstaticをつけちゃうと、staticなだけに外部クラスのインスタンス変数にアクセスできなくなる。だからstaticな内部クラスは内部クラスと呼べなくなっちゃう


Inner Classのstatic有りと無しの違い
http://d.hatena.ne.jp/quabbin/2008092...

  • 「親クラス$子クラス.class」というようなファイル(バイトコード)をjavapで逆アセンブルして違いを観察
  • static修飾をされていないとthis$0の定義が走り、字面上は存在しない形でデフォルトコンストラクタの引数が一つ増えている
  • static修飾のないインナークラスでは,enclosingアクセスができるように初期化するコストがかかる

上記を要約するに,インナークラスの定義にstatic修飾が施されていれば,アウタークラスへの参照は途絶えてくれる。と言う事になる。



そういうわけで,この項の冒頭で取り上げたサンプルコードを問題なくSerializableにするためには,下記のようなコードを書けばよい事になる。

    ・・・

        // 実行結果を返す
        return new DBEDitActionResult()
            .setRouteId("success")
            .add("new_friend_name", f.getName())
        ;
    }


    // 実行結果オブジェクト(※static定義されている点に注意)
    static class DBEDitActionResult extends ActionResult
    {
        private static final long serialVersionUID = 1L;

        @Override
        public void onNextActivityStarted(final Activity activity)
        {
            ・・・
        }
    }

このように,定義方法を変更して,ActionResultを継承したstaticな内部クラスとして実装すれば,かつて匿名クラスだったモノは無事にSerializeできる。

上記のDBEDitActionResultは,問題なくIntentで運搬可能である。実行時例外は発生しない。



実際,上で取り上げた「Android-MVC framework」のver0.2では,この方法が採用された。

その結果,任意のオブジェクトに対して「implements IntentPortable」するだけで,該当オブジェクトをIntent経由で運搬可能となったのである。

Androidアプリ開発用のMVCフレームワーク 「Android-MVC」 ver0.2をリリース
http://language-and-engineering.hatenablog.jp/entry/20120323/p1

  • 「■エンティティを含む任意の自作オブジェクトが,Intent経由で気軽に運搬可能になった。 」という項目を参照。

とはいえ,このようなJava言語の無用な仕様に強制的に従わせられるおかげで,コードが分断されるため,可読性が損なわれるというデメリットもある。

匿名クラスの形態を保持することは不可能になるのだ。

ActionResultにデータをセットする部分と,セットされたデータを利用する部分が,望む・望まないを問わずに,強制的に分断されてしまう。


その都合さえ受け入れれば,このコードは一応動く。

ただ,理想的な設計や,理想的なDSLの構築は妨げられる。

それがJavaプラットフォーム。


比較と結論

独自オブジェクトをIntentで運搬したい場合,以下の2つの方法がある。

Parcelable

  • メリット:Android上でのパフォーマンスに最適化されている。アプリ間の通信にも使える。
  • デメリット:めんどい。コード量が多い。データの出し入れが冗長。


Serializable

  • メリット:ごくわずかなコードの付加だけで,手軽に利用できる。一般のJavaワールドで普及しているAPIでもある。
  • デメリット:アプリ内の明示的インテントに限定される。匿名クラスやインナークラスはstatic宣言で切り分ける必要がある。

利用目的を踏まえた上で,いずれかの方法を選んで使う事になる。


キーになるのは,他アプリとの連携の有無。

小規模アプリであっても,独自オブジェクトを含むような暗黙的インテントを投げるのであれば,Parcelableの利用が必須になる。

コンテンツリゾルバやコンテンツプロバイダを使うにあたり,AIDLやParcelableによるIPCが必要だ。

Android インターフェイス定義言語 ( AIDL ) / Parcelable を使用したパラメータ値の受け渡し
http://www.techdoctranslator.com/andr...


一方,複雑なアプリであっても,外部にインテントを投げないのであれば,Serializableで十分。


そして現状を言うと,他アプリとの連携時には,いちいち独自オブジェクトを同梱する必要など生じないのが本当のところだ。

Twitterでシェアとか,画像をビューワで閲覧とか,有名どころのアプリ連携の方法論はすでに確立しており,それらの方法は極めてシンプルだ。

なので,焦ってParcelableを使おうとする必要は,あまりないのかもしれない。



関連エントリ:

逆コンパイル + 逆アセンブル のための5つの無料ツール(javapを含む)
http://language-and-engineering.hatenablog.jp/entry/20081008/1223384382


 

*1:書籍「プロダクティブ・プログラマ」なども参照。