1. ホーム
  2. アンドロイド

List.subListメソッドで発生するStackOverflowError

2022-02-24 20:08:38
<パス

インラインプッシュ

<ブロッククオート

[長期有効】Byte Jump Meチームへの参加を歓迎します。 インサイダーリンク

0x1.例外の概要

java.lang.StackOverflowError
java.util.AbstractList$SubAbstractList.listIterator(AbstractList.java:308)
java.util.AbstractList$SubAbstractList.listIterator(AbstractList.java:308)
......
java.util.AbstractList$SubAbstractList.listIterator(AbstractList.java:308)
java.util.AbstractList$SubAbstractList.listIterator(AbstractList.java:308)
java.util.AbstractList$SubAbstractList.iterator(AbstractList.java:301)
java.util.AbstractCollection.toArrayList(AbstractCollection.java:349)
java.util.AbstractCollection.toArray(AbstractCollection.java:339)
java.util.Collections.sort(Collections.java:1863)


このクラッシュは、Android 4.4 以下 (<=JDK 6) のみで発生し、5.0 (>=JDK 7) 以上では発生しない。

0x2. 解析と解決

1) システムバージョン固有のバグなので、androidxrefで対応するシステムバージョンのソースコードを確認する。(アンドロイド4.4)
2) 再度クラッシュスタックを見ると、Collections.sort(Collections.java:1863)メソッドはList.toArrayメソッドを呼び出して再度ソートします。collections.sortは次のように表示されています。

質問です。直感的にコードを見ると、mNewCommingUsersTempはArrayListであり、ArrayListは自身をtoArrayに実装し、AbstractCollection.toArray(AbstractCollection.java:339)に行くべきではなく、StackOverflowは、AbstractList$SubAbstractList.listIterator(AbstractList.java:308)の再帰的呼び出しが原因になって起こっているのだと思います。
回答 だから、mNewCommingUsersTempの参照はArrayListオブジェクトからListインターフェースを実装する別のオブジェクトに変更する必要があります。 mNewCommingUsersTempはこの文で再割り当てされます: mNewCommingUsersTemp = mNewCommingUsersTemp.subList(0, NEW_COMMING_LIST_MAX_SIZE) だからそれは基本的にサブリストの鍋にロックされています。
3) subListメソッドについて。
mNewCommingUsersTempはArrayListオブジェクトとしてスタートしたので、ソースコードからsubListメソッドを探してみたところ、ArrayListにはsubListメソッドがなく、その親クラスAbstractListに実装があることが判明しました。

つまり、subListメソッドが新しいSubAbstractListオブジェクトを生成するたびに、親リストを参照し、サブの位置を記録するだけの実装になっているのです。

つまり、複数subListの処理は、SubAbstractListオブジェクトの数だけsubすることです。
ArrayList --> SubAbstractList A --> SubAbstractList B --> SubAbstractList C --> ...
4) Collections.sortがクラッシュする理由
再びエラースタックに戻ると、AbstractList$SubAbstractList.listIterator(AbstractList.java:308)は以下のように再帰的な呼び出しを生成して実装しています。

fullListはSubAbstractListの親なので、SubAbstractList Cを反復するときは、当然SubAbstractList C --> SubAbstractList B --> SubAbstractList A --> ArrayListから反復することになります。
5) 解決方法
mNewCommingUsersTemp = mNewCommingUsersTemp.subList(0, NEW_COMMING_LIST_MAX_SIZE) とします。
に変更する。
mNewCommingUsersTemp = new ArrayList<>(mNewCommingUsersTemp.subList(0, NEW_COMMING_LIST_MAX_SIZE)) となります。
SubListをArrayListに変換し、複数のSubが再帰につながる問題を回避しています。

0x3. 考えること、広がること

1) Android 5.0+ (>=JDK 7) はなぜクラッシュしないのですか?
JDK 7+のArrayListはAbstractListのsubListメソッドを再インプリメントしており、依然として新しいSubListオブジェクトを生成しています。

しかし、ArrayList は独自の SubList のセットを実装しており、AbstractList を使用することはもうありません。 S u b A b s t r a c t L i s t . そして A r r a y L i s t SubAbstractListです。とArrayList <スパン S u b A b s t r a c t L i s t . そして A r r a y L i s t サブリスト L i s t I t e r a t o r ほぼ ほとんど レ 新規 実現 実現した l l i s t I t e r a t o r メソッド 方法 異なる とは異なる で A b s t r a c t L i s t ListIteratorはほぼ再実装、AbstractListとlistIteratorメソッドは別物 <スパン L i s t I t e r a t o r 一二 ほぼ 新しい リアル プレゼント その l i s t I t e r a t o r スクエア メソッド いいえ と同じ <スパン A b s t r a c t L i s t SubAbstractList$SubAbstractListIterator は、再帰的でなくなるため、Stack Overflow に戻ることはありません。

2) JDK 7 の ArrayList$SubList.remove メソッドは以下の通りである。

JDK 6 AbstractList$SubAbstractList.remove メソッドは、以下のとおりです。

どちらの方法も基本的に同じものを実装しており、どちらも上方再帰のためにparentを呼び出しますが、これはStack Overflowですか?
回答 はい、そうです。
検証用のデモ(ループ)を書いてください。

One Plus 5T Android 8:スタックが8MになるとStack Overflowになり、この時点で深さは約9万回になります。1秒間に2回subListすると、この深さに到達するのに10時間以上かかる。
ソニーZ1 Android 4.4。深さが500回程度の時にスタックオーバーフローが発生する。

そのため、JDKの高いバージョンでは、メソッドスタッキングのメカニズムが再設計されている可能性があります。
JDKの高いバージョンはクラッシュしにくいのですが、深くなればなるほどSubListオブジェクトの追加や削除に時間がかかり、アプリがラグる原因になるという落とし穴があるのです