コレクションを安全にコピーするにはどうすればよいですか?


9

以前は、コレクションを安全にコピーすると次のようなことをするように言っていました。

public static void doThing(List<String> strs) {
    List<String> newStrs = new ArrayList<>(strs);

または

public static void doThing(NavigableSet<String> strs) {
    NavigableSet<String> newStrs = new TreeSet<>(strs);

しかし、これらの「コピー」コンストラクター、同様の静的作成メソッドおよびストリームは本当に安全で、ルールはどこに指定されていますか?安全とは、Java言語によって提供される基本セマンティック整合性の保証と、悪意のある呼び出し元に対して適用されるコレクションSecurityManagerであり、妥当な手段でバックアップされ、欠陥がないことを前提としています。

私はこの方法を投げると幸せConcurrentModificationExceptionNullPointerExceptionIllegalArgumentExceptionClassCastException、など、またはおそらくハング。

String不変の型引数の例として選択しました。この質問については、私は独自の落とし穴を持つ変更可能な型のコレクションのディープコピーには興味がありません。

(明確にするために、私はOpenJDKのソースコードを見て、とに対して何らかの回答をArrayListしましたTreeSet。)


2
安全とはどういう意味ですか?一般に、コレクションフレームワークのクラスは、javadocsで指定されている例外を除いて、同様に機能する傾向があります。コピーコンストラクターは、他のコンストラクターと同じように「安全」です。コレクションコピーコンストラクターが安全であるかどうかを確認することは非常に具体的に聞こえるので、あなたが心に留めている特定のことはありますか?
カヤマン

1
まあ、NavigableSet他のComparableベースのコレクションは、クラスがcompareTo()正しく実装されていないことを検出して例外をスローすることがあります。信頼できない議論によってあなたが何を意味するかは少し不明確です。悪人が悪い文字列のコレクションを作成し、それらをコレクションにコピーすると、何か悪いことが起こりますか?いいえ、コレクションフレームワークはかなりしっかりしていて、1.2から存在しています。
カヤマン

1
@JesseWilsonは、内部をハッキングすることなく、多くの標準コレクションを危険にさらす可能性がありますHashSet(そして、一般に他のすべてのハッシュコレクション)はhashCode、要素の実装の正確性/完全性に依存しており、TreeSetそれにPriorityQueue依存していますComparator(そして、カスタムコンパレータがある場合はそれを受け入れずに同等のコピーを作成します)、コンパイル後に検証されないEnumSet特定のenum型の整合性を信頼するため、生成javacまたは手作りされていないクラスファイルがそれを覆す可能性があります。
Holger

1
あなたの例new TreeSet<>(strs)strsは、どこにがありNavigableSetますか?結果TreeSetはソースのコンパレータを使用するため、これは一括コピーではありません。これは、セマンティクスを保持するためにも必要です。含まれている要素を処理するだけで問題がなければtoArray()、次の方法です。反復順序も保持します。「要素の取得、要素の検証、要素の使用」で問題がなければ、コピーを作成する必要さえありません。問題は、すべての要素を検証するときに始まり、その後すべての要素を使用します。次に、TreeSetカスタムコンパレータ付きのコピーを信頼することはできません
Holger

1
checkcast各要素に対してaの効果を持つ唯一の一括コピー操作はtoArray、特定のタイプを使用したものです。私たちは常にそれで終わります。ジェネリックコレクションは実際の要素の型さえも知らないため、コピーコンストラクターは同様の機能を提供できません。もちろん、チェックはいつでも延期することができますが、質問の目的がわかりません。要素を使用する直前にチェックして失敗することに問題がなければ、「セマンティックインテグリティ」は必要ありません。
ホルガー

回答:


12

コレクションAPIのような通常のAPIの同じJVM内で意図的に悪意のあるコードが実行されることに対する実際の保護はありません。

簡単に示すことができるように:

public static void main(String[] args) throws InterruptedException {
    Object[] array = { "foo", "bar", "baz", "and", "another", "string" };
    array[array.length - 1] = new Object() {
        @Override
        public String toString() {
            Collections.shuffle(Arrays.asList(array));
            return "string";
        }
    };
    doThing(new ArrayList<String>() {
        @Override public Object[] toArray() {
            return array;
        }
    });
}

public static void doThing(List<String> strs) {
    List<String> newStrs = new ArrayList<>(strs);

    System.out.println("made a safe copy " + newStrs);
    for(int i = 0; i < 10; i++) {
        System.out.println(newStrs);
    }
}
made a safe copy [foo, bar, baz, and, another, string]
[bar, and, string, string, another, foo]
[and, baz, bar, string, string, string]
[another, baz, and, foo, bar, string]
[another, bar, and, foo, string, and]
[another, baz, string, another, and, foo]
[string, and, another, foo, string, foo]
[baz, string, foo, and, baz, string]
[bar, another, string, and, another, baz]
[bar, string, foo, string, baz, and]
[bar, string, bar, another, and, foo]

ご覧のとおりList<String>Stringインスタンスのリストを実際に取得することは保証されていません。型の消去とraw型のため、リストの実装側で可能な修正さえありません。

もう1つArrayListは、のコンストラクターのせいにすることができますが、これは受信コレクションのtoArray実装への信頼です。TreeMap同じように影響を受けることはありませんが、の構築のように配列を渡すことによるパフォーマンスの向上がないためだけArrayListです。どちらのクラスも、コンストラクターでの保護を保証しません。

通常、故意に悪意のあるコードを隅々まで想定してコードを記述しようとしても意味がありません。すべてに対して保護するには、実行できることが多すぎます。このような保護は、悪意のある発信者に何かへのアクセスを許可する可能性のあるアクションを実際にカプセル化するコードにのみ役立ちます。このコードがないと、アクセスできません。

特定のコードの安全性が必要な場合は、

public static void doThing(List<String> strs) {
    String[] content = strs.toArray(new String[0]);
    List<String> newStrs = new ArrayList<>(Arrays.asList(content));

    System.out.println("made a safe copy " + newStrs);
    for(int i = 0; i < 10; i++) {
        System.out.println(newStrs);
    }
}

そうすればnewStrs、に文字列のみが含まれ、構築後に他のコードによって変更されないようにすることができます。

または使用List<String> newStrs = List.of(strs.toArray(new String[0]));するJava 9以降で
Javaの10のことに注意してくださいList.copyOf(strs)同じことを行いますが、そのドキュメントは、入ってくるコレクションの信用しないことが保証されている状態ではないんtoArray方法を。そのList.of(…)ため、配列ベースのリストを返す場合に必ずコピーを作成するを呼び出す方が安全です。

呼び出し元は方法を変更できないため、配列は機能し、着信コレクションを配列にダンプしてから、新しいコレクションに配列を投入すると、常にコピーが安全になります。上記のように、コレクションは返された配列への参照を保持できるため、コピーフェーズ中に配列を変更できますが、コレクション内のコピーには影響しません。

そのため、特定の要素が配列から取得された後、または結果として得られたコレクション全体に対して、整合性チェックを実行する必要があります。


2
Javaのセキュリティモデルは、スタック上のすべてのコードのアクセス権セットの共通部分をコードに付与することで機能します。そのため、コードの呼び出し元がコードに意図しないことをさせた場合でも、当初よりも多くのアクセス権を取得しません。したがって、悪意のあるコードがあなたのコードなしでも実行できたことをコードに実行させるだけです。昇格された特権で実行するコードを強化するだけで済みますAccessController.doPrivileged(…)。しかし、アプレットのセキュリティ関連のバグの長いリストは、このテクノロジーが放棄された理由を私たちに教えてくれます…
Holger

1
しかし、私は「コレクションAPIのような通常のAPIに」挿入する必要がありました。
Holger

2
悪意のあるコレクションの実装を許可する特権コードに対して、どうやらセキュリティに関係のないコードを強化する必要があるのですか?その架空の呼び出し元は、コードを呼び出す前後に悪意のある動作の影響を受けます。あなたのコードが正しく動作する唯一のものであることに気付くことさえありません。new ArrayList<>(…)コピーコンストラクターとしての使用は、正しいコレクションの実装を前提として問題ありません。手遅れになったときにセキュリティの問題を修正するのはあなたの義務ではありません。侵害されたハードウェアはどうですか?オペレーティングシステム?マルチスレッドはどうですか?
Holger

2
私は「セキュリティなし」を主張するのではなく、適切な場所でのセキュリティを主張します。代わりに、事後に壊れた環境を修正しようとはしません。「スーパータイプを正しく実装していないコレクションがたくさんある」というのは興味深い主張ですが、証明を求めるにはあまりに遠すぎて、これをさらに拡大しました。元の質問は完全に回答されています。あなたが今持っているポイントは決してその一部ではありませんでした。言わList.copyOf(strs)れたように、明白な価格で、その点で入ってくるコレクションの正確さに依存しません。ArrayList日常的に妥当な妥協案です。
Holger

4
すべての「類似の静的作成メソッドとストリーム」について、そのような仕様はないことは明確に述べています。だから、絶対に安全にしたいのならtoArray()、配列を振る舞わせることができないので、自分自身を呼び出さなければなりません。その後、new ArrayList<>(Arrays.asList( strs.toArray(new String[0])))またはのように配列のコレクションコピーを作成しますList.of(strs.toArray(new String[0]))。どちらにも、要素タイプを強制するという副作用があります。個人的にはcopyOf、不変のコレクションを危険にさらすことは許されないと思いますが、代わりに選択肢があります。
ホルガー

1

私はこの情報をコメントに残したいのですが、十分な評判がありません。申し訳ありません:)それでは、できるだけ詳細に説明します。

constオブジェクトの内容を変更することを想定していないメンバー関数をマークするためにC ++で使用される修飾子のようなものの代わりに、Javaではもともと「不変性」の概念が使用されていました。カプセル化(またはOCP、Open-Closed Principle)は、オブジェクトの予期しない変更(変更)から保護することになっています。もちろん、リフレクションAPIはこれを処理します。ダイレクトメモリアクセスも同様です。それは自分の足を撃つことについての詳細です:)

java.util.Collectionそれ自体は変更可能なインターフェイスですadd。コレクションを変更することになっているメソッドがあります。もちろん、プログラマはコレクションをスローするものにラップすることもできます...そして、別のプログラマがコレクションが不変であることを明確に示すjavadocを読み取ることができなかったため、すべての実行時例外が発生します。

java.util.Iterableは、インターフェイスで不変のコレクションを公開するためにtype を使用することを決定しました。意味的にIterableは、「可変性」のようなコレクションの特性はありません。それでも、(おそらく)ストリームを通じて基になるコレクションを変更することができます。


JIC、不変の方法でマップを公開java.util.Function<K,V>するために使用できます(マップのgetメソッドはこの定義に適合します)


読み取り専用インターフェースの概念と不変性は直交しています。C ++とCのポイントは、セマンティックインテグリティをサポートしていないことです。オブジェクト/構造体の引数もコピーします-const&はそのための危険な最適化です。あなたが通過した場合、Iteratorそれは実質的に要素ごとのコピーを強制しますが、それは良いことではありません。forEachRemaining/の使用forEachは明らかに完全な災害となります。(私はそれにも方法Iteratorがあることを言及しなければなりませんremove。)
トム・ホーティン-

Scalaコレクションライブラリを見ると、変更可能なインターフェースと不変のインターフェースが厳密に区別されています。(たぶん)完全に異なる理由のために作られたと思いますが、いかにして安全を達成できるかを示すデモです。読み取り専用インターフェースは、意味的に不変性を前提としています。それが私が言いたいことです。(私Iterableはが実際に不変ではないことについては同意しますが、問題は発生しませんforEach*
Alexander
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.