Infinito Nirone 7

白羽の矢を刺すスタイル

SparseArray から要素を取り出したときに ClassCastException が発生するパターン

SparseArrayAndroidフレームワークにあるコレクションの一種で、Integer を key にした HashMap よりもメモリ効率がよいとされるコレクションです。

SparseArray には 2 通りの値を取り出すメソッドがあります。一つはSparseArray#get(int)もう一つはSparseArray#valueAt(int)です。 どちらのメソッドも同じint型の引数をとりますが、getメソッドの引数はkeyで渡された値をもとにバイナリサーチをかけて内部の配列のindexを決めており、valuesAtメソッドの引数はindexで値がそのまま内部の配列のindexとして扱われます。

SparseArray はまた要素を追加した後に削除することもできます。こちらも 2 通りのメソッドがあり、それぞれSparseArray#delete(int) / SparseArray#remove(int)SparseArray#removeAt(int)で、delete(int) / remove(int)の引数はkeyでこれをもとにバイナリサーチをしてアクセスすべきindexを決め、removeAt(int)の引数はindexでそのまま内部の配列のインデックスとなります。

他にも同じパターンで引数のintkeyなのかindexなのかで挙動の異なるメソッドがあります。

さてここで、一度 SparseArray に保存した値を削除し、再度取り出すことを試してみます。

SparseArray#put(int, V)で指定したkeyに保存したのち、SparseArray#remove(int)で指定したkeyに対応する値を削除、SparseArray#valueAt(int)で先頭の要素を取り出します。

val array: SparseArray<String> = SparseArray()
array.put(0, "hoge")
Log.d("SparseArray", "Value at [0] == ${array.valueAt(0)}")
array.remove(0)
Log.d("SparseArray", "Value at [0] == ${array.valueAt(0)}")

同じようなことをSparseArray#get(int)で実行する場合は次の通りで、get メソッドに渡すkeykeyAt(int)で取り出します。

val array: SparseArray<String> = SparseArray()
array.put(0, "hoge")
Log.d("SparseArray", "Value at [0] == ${array.get(array.keyAt(0))}")
array.remove(0)
Log.d("SparseArray", "Value at [0] == ${array.get(array.keyAt(0))}")

それぞれどのような結果になるかというと、SparseArray#valueAt(int)の場合は最後の行でClassCastExceptionが発生してクラッシュし、SparseArray#get(int)の場合は最後の行でログに"Value at [0] == null"と出力されます。

SparseArray では要素の削除を実行すると、内部で保持している配列の該当箇所に削除したことを示す DELETED という Object 型の定数を代入します。SparseArray は型パラメータでどの型のオブジェクトが保存されるか指定できますが、実際には内部で要素を保持している配列は Object[] です。そして SparseArray#get(int) はその場所にある要素が DELETED なら null ないしは指定した値を返すようになっていますが、SparseArray#valueAt(int)は特にそのようなチェックなしに指定した場所にある要素を返しています。これがSparseArray#valueAt(int)を使ったときにClassCastExceptionが投げられる理由です。

要素が全部なくなったのに要素にアクセスしようとするというのはよくない状況です。get(int)keyAt(int)を組み合わせて null チェックをすることでClassCastExceptionは回避できますが、根本的に並行処理に問題がある(SparseArray はスレッドセーフではない)ということなので、クラッシュレポート等で身に覚えのない ClassCastException がある時にはこのパターンを疑ってみると良いと思います。