🐻‍❄️

[Android] Kotlin の object 式で生成したインスタンスをシリアライズするとエラーになることがある

2022/02/16に公開

背景

Android では Activity や Fragment 間でオブジェクトを受け渡すときには Bundle を利用してオブジェクトの受け渡しをします。Android の Bundle にオブジェクトを詰める際には Parcelable か Serializable を継承してあるインスタンスである必要があるので Bundle に詰めるオブジェクトの定義に対してParcelable や Serializable を実装するケースがあると思います。

問題

通常のケースでは class に Serializable を継承させるため問題は起きないと思うのですが、以下のように interface に対して Serializable を実装して、それを object 式を利用してインスタンスを作成した場合にシリアライズに失敗します。

data class PersonNameDataHolder(val name: String)

data class PersonAgeDataHolder(val age: Int)

interface Person : Serializable {
    val name: String
    val age: Int
}

private fun writePersonToFile(fileName: String, person: Person) {
    val file = FileOutputStream(fileName)
    ObjectOutputStream(file).use { outputStream ->
        outputStream.writeObject(person)
    }
}

fun main() {
    val personAgeDataHolder = PersonAgeDataHolder(100)
    val personNameDataHolder = PersonNameDataHolder("PERSON_NAME")

    val person = object : Person {
        override val age: Int = personAgeDataHolder.age
        override val name: String = personNameDataHolder.name
    }

    try {
        writePersonToFile(fileName = "person.txt", person = person)
        println("Person Serialization Success")
    } catch (e: Exception) {
        println("Person Serialization Error: $e")
    }
}
-- 出力結果 --
Person Serialization Error: java.io.NotSerializableException: PersonAgeDataHolder
-- 出力ファイル --
エラーのためファイルは出力されない

何が起きているか?

このような object 式で Person を生成すると、生成された Person が PersonAgeDataHolder と PersonNameDataHolder の参照を持ってしまうようです。

val person = object : Person {
    override val age: Int = personAgeDataHolder.age
    override val name: String = personNameDataHolder.name
}

以下はデバッガでインスタンスの内容を確認したものになりますが生成された Person が PersonAgeDataHolder と PersonNameDataHolder を所持していることがわかります。そのため Person のシリアライズ時に Serializable を実装していない PersonAgeDataHolder や PersonNameDataHolder をシリアライズしようとしてエラーが発生していたみたいです。

Image.png

対処方法

プリミティブ変数に格納してから object 式のコードブロックに渡す

PersonAgeDataHolder や PersonNameDataHolder などをプリミティブな変数に格納してから渡すと生成された Person にはシリアライズ不可能な値が含まれなくなるのでシリアライズに成功するようになります。ですが結局 object 式で生成したインスタンスはこのプリミティブな変数の参照を持つのであまり良い改善策ではないと思います。

val age = personAgeDataHolder.age
val name = personNameDataHolder.name
val person = object : Person {
    override val age: Int = age
    override val name: String = name
}

object 式のコードブロックで利用するインスタンスも Serializable にする

PersonAgeDataHolder や PersonNameDataHolder を Serializable にすると生成された Person にシリアライズ不可能な値が含まれなくなるのでシリアライズに成功するようになります。ですが利用するクラスは処理ごとに変わるので全てに Serializable をつけるのはあまり難しいかなと思います。

data class PersonNameDataHolder(val name: String) : Serializable
data class PersonAgeDataHolder(val age: Int) : Serializable

val personAgeDataHolder = PersonAgeDataHolder(100)
val personNameDataHolder = PersonNameDataHolder("PERSON_NAME")

val person = object : Person {
    override val age: Int = personAgeDataHolder.age
    override val name: String = personNameDataHolder.name
}

それじゃ一番いい対処方法はなにか

設計段階で object 式の特徴を理解して、以下の方針で進めるのが不具合の一番の対処になるかなと思います。このような問題の抜け道はいくつかありますが、設計の不備であるので少しずづ直していくしかないのかなと思います。

  • 基本は interface を継承した data class を定義してインスタンス化する
  • object 式を利用したインスタンス生成は極力さける

参考文献

今回の記事を動作確認をしたサンプルコード
https://github.com/kaleidot725-kotlin/object-formula

Discussion