SparseArray は Android のフレームワークにあるコレクションの一種で、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
でそのまま内部の配列のインデックスとなります。
他にも同じパターンで引数のint
がkey
なのか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 メソッドに渡すkey
をkeyAt(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 がある時にはこのパターンを疑ってみると良いと思います。