出wwwうっかりlazyデリゲートとGson併用してクラッシュ奴w
— すいはん (@suihan742) August 5, 2020
たまに忘れてやってしまうやつ。
追記 (2020-08-06)
-> Androidアプリ開発で発生したさらなる問題
誤り
data class Hoge(
val str : String,
val num : Int
) {
val message : String by lazy {
str + num
}
}
fun incorrectExample() {
val hoge = Hoge("hoge", 1234)
val gson = Gson()
val json = gson.toJson(hoge)
val deserialized = gson.fromJson<Hoge>(json, Hoge::class.java)
System.out.println(desrialized.message)
// ==> ヌルポ
}
non-null型だろうがお構いなしにヌルポ。
正しい書き方
data class Hoge(
val str : String,
val num : Int
) {
constructor() : this("", 0)
@delegate:Transient
val message : String by lazy {
str + num
}
}
fun correctExample() {
val hoge = Hoge("hoge", 1234)
val gson = Gson()
val json = gson.toJson(hoge)
val deserialized = gson.fromJson<Hoge>(json, Hoge::class.java)
System.out.println(desrialized.message)
// ==> "hoge1234"
}
やったぜ。
原因
Gson(とかその類のもの)はデシリアライズの際にまず無引数のコンストラクタを探し、存在するならそれを使用する。存在しない場合はUnSafeなあの手この手の悪事を働いてとにかく指定されたクラスのインスタンスを無理矢理作る。
そして作ったインスタンスに対してフィールドにJsonから変換した値を突っ込んでいく。
そういうわけで、lazyなプロパティを使用していようがいまいがgson.fromJson()から返された段階で既に中身が初期化されている扱いになってしまうため、遅延初期化が実行されずnullになってしまう。
なので、要点は以下の二点。
無引数のコンストラクタを用意する
@delegate:Transientアノテーション付けてlazyプロパティのシリアライズを回避する
余談
無引数コンストラクタをデータクラスの外部に晒したくなかったら、privateでも大丈夫っぽい。
Androidアプリ開発で発生したさらなる問題
この記事の方法を適用したデータクラスをさらにSerializable
にして、Bundle#putSerializable()
,Bundle#getSerializable()
を使用してシリアライズ・デシリアライズしようとすると依然としてlazyプロパティがnull
になる模様。
なので、予めオブジェクトをJson化してBundle#putString(key,value)
で突っ込む・Bundle#putString(key)
で取り出す方法を取るのが余計な頭使わなくて楽そう。
ひとまずこんな感じで拡張関数書いた。
/** Bundle#putObject(), Bundle#getObject()で使用するGsonインスタンス */
val Bundle.gson : Gson by lazy {
GsonBuilder()
.serializeNulls()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.registerTypeAdapter(Boolean::class.java, BooleanDeserializer())
.create()
}
/** シリアライズしたオブジェクトをBundleに渡す */
fun Bundle.putObject(key: String, value: Any?) {
putString(key, this.gson.toJson(value))
}
/** keyに対応する文字列をT型にデシリアライズして返す */
inline fun <reified T> Bundle.getObject(key: String) : T? {
val json = getString(key) ?: return null
return try {
this.gson.fromJson<T>(json, object : TypeToken<T>() {}.type)
}
catch (e: Throwable) {
null
}
}
// ------ //
/** Intentのデータ授受用 */
fun Intent.putObjectExtra(key: String, value: Any?) {
this.putExtras((this.extras ?: Bundle()).apply {
putObject(key, value)
})
}
/** Intentのデータ授受用 */
inline fun <reified T> Intent.getObjectExtra(key: String) : T? {
return this.extras?.getObject(key)
}
Gson使っているけど、kotlinx.serializationでもなんでも。