Wie kann ich Sammlungen sicher kopieren?

9

In der Vergangenheit habe ich gesagt, eine Sammlung sicher zu kopieren, um Folgendes zu tun:

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

oder

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

Aber sind diese "Kopier" -Konstruktoren, ähnliche statische Erstellungsmethoden und Streams, wirklich sicher und wo sind die Regeln angegeben? Mit sicher meine ich die grundlegenden semantischen Integritätsgarantien , die von der Java-Sprache und den Sammlungen angeboten werden, die gegen einen böswilligen Anrufer erzwungen werden, vorausgesetzt, sie werden von einem vernünftigen unterstützt SecurityManagerund es gibt keine Fehler.

Ich bin glücklich mit dem Verfahren werfen ConcurrentModificationException, NullPointerException, IllegalArgumentException, ClassCastException, usw., oder vielleicht sogar hängen.

Ich habe Stringals Beispiel für ein unveränderliches Typargument gewählt. Für diese Frage interessieren mich keine tiefen Kopien für Sammlungen veränderlicher Typen, die ihre eigenen Fallstricke haben.

(Um klar zu sein, habe ich mir den OpenJDK-Quellcode angesehen und habe eine Antwort auf ArrayListund TreeSet.)

Tom Hawtin - Tackline
quelle
2
Was meinst du mit sicher ? Im Allgemeinen funktionieren die Klassen im Sammlungsframework ähnlich, mit Ausnahmen, die in den Javadocs angegeben sind. Die Kopierkonstruktoren sind genauso "sicher" wie alle anderen Konstruktoren. Gibt es eine bestimmte Sache, an die Sie denken, weil die Frage, ob ein Sammlungskopiekonstruktor sicher ist, sehr spezifisch klingt?
Kayaman
1
Nun, NavigableSetund andere Comparablebasierte Sammlungen können manchmal erkennen, ob eine Klasse nicht compareTo()korrekt implementiert ist , und eine Ausnahme auslösen. Es ist ein bisschen unklar, was Sie unter nicht vertrauenswürdigen Argumenten verstehen. Du meinst, ein Übeltäter bastelt eine Sammlung von schlechten Saiten und wenn du sie in deine Sammlung kopierst, passiert etwas Schlimmes? Nein, das Sammlungs-Framework ist ziemlich solide, es gibt es seit 1.2.
Kayaman
1
@JesseWilson Sie können viele der Standard-Sammlungen kompromittieren, ohne sich in ihre Interna zu hacken HashSet(und alle anderen Hashing-Sammlungen im Allgemeinen). Dies hängt von der Richtigkeit / Integrität der hashCodeImplementierung der Elemente ab TreeSetund PriorityQueuehängt von der ab Comparator(und Sie können nicht einmal Erstellen Sie eine äquivalente Kopie, ohne den benutzerdefinierten Komparator zu akzeptieren (falls vorhanden), und EnumSetvertrauen Sie auf die Integrität des bestimmten enumTyps, der nach der Kompilierung nie überprüft wird, sodass eine Klassendatei, die nicht mit javacoder in Handarbeit erstellt wurde, diese untergraben kann.
Holger
1
In Ihren Beispielen haben Sie, new TreeSet<>(strs)wo strsist ein NavigableSet. Dies ist keine Massenkopie, da das Ergebnis TreeSetden Komparator der Quelle verwendet, der sogar erforderlich ist, um die Semantik beizubehalten. Wenn Sie nur die enthaltenen Elemente verarbeiten können, toArray()ist dies der richtige Weg. Es wird sogar die Iterationsreihenfolge beibehalten. Wenn Sie mit "Element nehmen, Element validieren, Element verwenden" einverstanden sind, müssen Sie nicht einmal eine Kopie erstellen. Die Probleme beginnen, wenn Sie alle Elemente überprüfen und anschließend alle Elemente verwenden möchten. Dann können Sie einer TreeSetKopie mit benutzerdefiniertem Komparator nicht vertrauen
Holger
1
Die einzige Massenkopieroperation mit der Wirkung von a checkcastfür jedes Element erfolgt toArraymit einem bestimmten Typ. Wir enden immer damit. Die generischen Sammlungen kennen nicht einmal ihren tatsächlichen Elementtyp, sodass ihre Kopierkonstruktoren keine ähnliche Funktionalität bereitstellen können. Natürlich können Sie jede Überprüfung auf die richtige vorherige Verwendung verschieben, aber dann weiß ich nicht, worauf Ihre Fragen abzielen. Sie benötigen keine "semantische Integrität", wenn Sie unmittelbar vor der Verwendung von Elementen prüfen und fehlschlagen können.
Holger

Antworten:

12

Es gibt keinen wirklichen Schutz vor absichtlich bösartigem Code, der in gewöhnlichen APIs wie der Collection-API in derselben JVM ausgeführt wird.

Wie leicht gezeigt werden kann:

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]

Wie Sie sehen können, List<String>garantiert das Erwarten von a nicht, dass tatsächlich eine Liste von StringInstanzen angezeigt wird. Aufgrund des Löschens von Typen und der Rohtypen ist auf der Seite der Listenimplementierung nicht einmal eine Korrektur möglich.

Die andere Sache, für die Sie den ArrayListKonstruktor verantwortlich machen können, ist das Vertrauen in die toArrayImplementierung der eingehenden Sammlung . TreeMapwird nicht auf die gleiche Weise beeinflusst, sondern nur, weil es keinen solchen Leistungsgewinn durch das Übergeben des Arrays gibt, wie bei der Konstruktion eines ArrayList. Keine der Klassen garantiert einen Schutz im Konstruktor.

Normalerweise macht es keinen Sinn, Code zu schreiben, der absichtlich bösartigen Code an jeder Ecke voraussetzt. Es kann zu viel tun, um sich vor allem zu schützen. Ein solcher Schutz ist nur für Code nützlich, der eine Aktion wirklich kapselt, die einem böswilligen Anrufer Zugriff auf etwas gewähren könnte. Ohne diesen Code könnte er nicht bereits darauf zugreifen.

Wenn Sie Sicherheit für einen bestimmten Code benötigen, verwenden Sie

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);
    }
}

Dann können Sie sicher sein, dass newStrsdas nur Zeichenfolgen enthält und nach seiner Erstellung nicht durch anderen Code geändert werden kann.

Oder List<String> newStrs = List.of(strs.toArray(new String[0]));mit Java 9 oder neuer verwenden
Beachten Sie, dass Java 10 List.copyOf(strs)dasselbe tut, in der Dokumentation jedoch nicht angegeben ist, dass die toArrayMethode der eingehenden Sammlung garantiert nicht vertrauenswürdig ist . Ein Aufruf List.of(…), der definitiv eine Kopie erstellt, falls eine Array-basierte Liste zurückgegeben wird, ist also sicherer.

Da kein Aufrufer die Art und Weise ändern kann, funktionieren Arrays. Wenn Sie die eingehende Sammlung in ein Array kopieren und anschließend die neue Sammlung damit füllen, wird die Kopie immer sicher. Da die Auflistung einen Verweis auf das zurückgegebene Array enthalten kann, wie oben gezeigt, kann sie ihn während der Kopierphase ändern, die Kopie in der Auflistung kann jedoch nicht beeinflusst werden.

Daher sollten Konsistenzprüfungen durchgeführt werden, nachdem das bestimmte Element aus dem Array oder der resultierenden Sammlung als Ganzes abgerufen wurde.

Holger
quelle
2
Das Sicherheitsmodell von Java gewährt dem Code den Schnittpunkt der Berechtigungssätze des gesamten Codes auf dem Stapel. Wenn also der Aufrufer Ihres Codes Ihren Code dazu veranlasst, unbeabsichtigte Aktionen auszuführen, erhält er immer noch nicht mehr Berechtigungen als ursprünglich. Ihr Code kann also nur Dinge tun, die der Schadcode auch ohne Ihren Code hätte tun können. Sie müssen nur den Code härten, den Sie mit erhöhten Berechtigungen über AccessController.doPrivileged(…)usw. ausführen möchten . Die lange Liste der Fehler im Zusammenhang mit der Applet-Sicherheit gibt uns jedoch einen Hinweis darauf, warum diese Technologie aufgegeben wurde…
Holger
1
Aber ich hätte "in gewöhnliche APIs wie die Collection-API" einfügen sollen, da ich mich in der Antwort darauf konzentriert habe.
Holger
2
Warum sollten Sie Ihren Code, der anscheinend nicht sicherheitsrelevant ist, gegen privilegierten Code absichern, der die Implementierung einer böswilligen Sammlungsimplementierung ermöglicht? Dieser hypothetische Anrufer ist vor und nach dem Aufrufen Ihres Codes immer noch dem böswilligen Verhalten ausgesetzt. Es würde nicht einmal bemerken, dass Ihr Code der einzige ist, der sich richtig verhält. Die Verwendung new ArrayList<>(…)als Kopierkonstruktor ist unter der Annahme korrekter Sammlungsimplementierungen in Ordnung. Es ist nicht Ihre Pflicht, Sicherheitsprobleme zu beheben, wenn es bereits zu spät ist. Was ist mit kompromittierter Hardware? Das Betriebssystem? Wie wäre es mit Multithreading?
Holger
2
Ich befürworte nicht "keine Sicherheit", sondern Sicherheit an den richtigen Stellen, anstatt zu versuchen, eine kaputte Umgebung nachträglich zu reparieren. Es ist eine interessante Behauptung, dass „ es viele Sammlungen gibt, die ihre Supertypen nicht korrekt implementieren “, aber es ging bereits zu weit, um nach Beweisen zu fragen, und erweiterte dies noch weiter. Die ursprüngliche Frage wurde vollständig beantwortet; Die Punkte, die Sie jetzt bringen, waren nie Teil davon. Wie gesagt, List.copyOf(strs)verlässt sich diesbezüglich nicht auf die Richtigkeit der eingehenden Sammlung zum offensichtlichen Preis. ArrayListist ein alltäglicher Kompromiss.
Holger
4
Es wird klargestellt, dass es keine solche Spezifikation für alle „ähnlichen statischen Erstellungsmethoden und -ströme“ gibt. Wenn Sie also absolut sicher sein möchten, müssen Sie sich toArray()selbst aufrufen , da Arrays kein überschriebenes Verhalten aufweisen können, und anschließend eine Sammlungskopie des Arrays wie new ArrayList<>(Arrays.asList( strs.toArray(new String[0])))oder erstellen List.of(strs.toArray(new String[0])). Beide haben auch den Nebeneffekt, den Elementtyp zu erzwingen. Ich persönlich glaube nicht, dass sie jemals zulassen werden copyOf, die unveränderlichen Sammlungen zu kompromittieren, aber die Alternativen sind in der Antwort enthalten.
Holger
1

Ich würde diese Informationen lieber im Kommentar hinterlassen, aber ich habe nicht genug Ruf, sorry :) Ich werde versuchen, sie so ausführlich wie möglich zu erklären.

Anstelle eines constModifikators, der in C ++ zum Markieren von Elementfunktionen verwendet wird, die den Objektinhalt nicht ändern sollen, wurde in Java ursprünglich das Konzept der "Unveränderlichkeit" verwendet. Die Einkapselung (oder OCP, Open-Closed-Prinzip) sollte vor unerwarteten Mutationen (Änderungen) eines Objekts schützen. Natürlich geht die Reflection API darum herum; Der direkte Speicherzugriff macht dasselbe. das ist mehr über das eigene Bein schießen :)

java.util.Collectionselbst ist eine veränderbare Schnittstelle: Es hat eine addMethode, die die Sammlung ändern soll. Natürlich kann der Programmierer die Sammlung in etwas einwickeln, das auslöst ... und alle Laufzeitausnahmen werden auftreten, weil ein anderer Programmierer Javadoc nicht lesen konnte, was eindeutig besagt, dass die Sammlung unveränderlich ist.

Ich habe mich für java.util.Iterabletype entschieden, um unveränderliche Sammlungen in meinen Schnittstellen verfügbar zu machen. Semantisch Iterablehat keine Sammlungseigenschaft wie "Veränderlichkeit". Dennoch können Sie (höchstwahrscheinlich) zugrunde liegende Sammlungen über Streams ändern.


JIC, um Karten unveränderlich freizulegen, java.util.Function<K,V>kann verwendet werden (die getMethode der Karte entspricht dieser Definition).

Alexander
quelle
Die Konzepte der schreibgeschützten Schnittstellen und der Unveränderlichkeit sind orthogonal. Der Punkt von C ++ und C ist, dass sie keine semantische Integrität unterstützen . Das auch Kopieren von Objekt- / Strukturargumenten - const & ist eine zweifelhafte Optimierung dafür. Wenn Sie eine übergeben Iterator, erzwingt dies praktisch eine elementweise Kopie, aber das ist nicht schön. Die Verwendung von forEachRemaining/ forEachwird offensichtlich eine völlige Katastrophe sein. (Ich muss auch erwähnen, dass dies Iteratoreine removeMethode hat.)
Tom Hawtin - Tackline
Wenn Sie sich die Scala-Sammlungsbibliothek ansehen, gibt es eine strikte Unterscheidung zwischen veränderlichen und unveränderlichen Schnittstellen. Obwohl (ich nehme an) es aus ganz anderen Gründen gemacht wurde, ist es dennoch eine Demonstration, wie Sicherheit erreicht werden kann. Die schreibgeschützte Schnittstelle setzt semantisch Unveränderlichkeit voraus, das versuche ich zu sagen. (Ich stimme zu Iterable, dass es nicht wirklich unveränderlich ist, sehe aber keine Probleme mit forEach*)
Alexander