Come posso copiare in sicurezza le raccolte?


9

In passato, ho detto di copiare in sicurezza una raccolta facendo qualcosa del tipo:

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

o

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

Ma questi costruttori "copia", metodi e flussi di creazione statici simili, sono davvero sicuri e dove sono specificate le regole? Con sicurezza intendo che le garanzie di integrità semantica di base offerte dal linguaggio Java e dalle raccolte vengono applicate contro un chiamante malintenzionato, supponendo che siano supportate da un ragionevole SecurityManagere che non vi siano difetti.

Sono felice con il metodo di lancio ConcurrentModificationException, NullPointerException, IllegalArgumentException, ClassCastException, ecc, o forse anche appeso.

Ho scelto Stringcome esempio di argomento di tipo immutabile. Per questa domanda, non mi interessano le copie profonde per raccolte di tipi mutabili che hanno i propri gotchas.

(Per essere chiari, ho esaminato il codice sorgente OpenJDK e ho una sorta di risposta per ArrayListe TreeSet.)


2
Cosa intendi con sicuro ? In generale, le classi nel framework delle raccolte tendono a funzionare in modo simile, con le eccezioni specificate nei javadocs. I costruttori di copie sono "sicuri" come qualsiasi altro costruttore. C'è una cosa particolare che hai in mente, perché chiedere se un costruttore di copie da collezione è sicuro suona molto specifico?
Kayaman,

1
Bene, NavigableSete altre Comparableraccolte basate possono talvolta rilevare se una classe non si implementa compareTo()correttamente e generare un'eccezione. Non è chiaro cosa intendi con argomenti non attendibili. Vuoi dire che un malfattore crea una collezione di stringhe cattive e quando le copi nella tua collezione succede qualcosa di brutto? No, il framework delle collezioni è piuttosto solido, è in circolazione dall'1.2.
Kayaman,

1
@JesseWilson puoi compromettere molte delle raccolte standard senza hackerare i loro interni, HashSet(e tutte le altre raccolte di hashing in generale) si basano sulla correttezza / integrità hashCodedell'implementazione degli elementi TreeSete PriorityQueuedipendono dal Comparator(e non puoi nemmeno crea una copia equivalente senza accettare il comparatore personalizzato se ce n'è uno), si EnumSetfida dell'integrità del enumtipo particolare che non viene mai verificata dopo la compilazione, quindi un file di classe, non generato con javaco creato a mano, può sovvertirlo.
Holger,

1
Nei tuoi esempi, hai new TreeSet<>(strs)dov'è strsa NavigableSet. Questa non è una copia in blocco, poiché il risultante TreeSetutilizzerà il comparatore della fonte, che è persino necessario per conservare la semantica. Se stai bene solo elaborando gli elementi contenuti, toArray()è la strada da percorrere; manterrà anche l'ordine di iterazione. Quando stai bene con "prendi elemento, convalida elemento, usa elemento", non hai nemmeno bisogno di fare una copia. I problemi iniziano quando si desidera verificare tutti gli elementi, seguiti dall'uso di tutti gli elementi. Quindi, non puoi fidarti di una TreeSetcopia con un comparatore personalizzato
Holger

1
L'unica operazione di copia bulk che ha l'effetto di a checkcastper ciascun elemento è toArraycon un tipo specifico. Lo finiamo sempre. Le raccolte generiche non conoscono nemmeno il loro tipo di elemento effettivo, quindi i loro costruttori di copie non possono fornire una funzionalità simile. Certo, puoi rinviare qualsiasi controllo al giusto uso precedente, ma poi non so a cosa mirino le tue domande. Non hai bisogno di "integrità semantica", quando stai bene controllando e fallendo immediatamente prima di usare gli elementi.
Holger,

Risposte:


12

Non esiste una protezione reale contro il codice intenzionalmente dannoso in esecuzione all'interno della stessa JVM nelle normali API, come l'API Collection.

Come si può facilmente dimostrare:

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]

Come puoi vedere, aspettarsi List<String>che a non garantisca effettivamente un elenco di Stringistanze. A causa della cancellazione dei tipi e dei tipi non elaborati, non è nemmeno possibile una correzione sul lato dell'implementazione dell'elenco.

L'altra cosa, di cui puoi incolpare ArrayListil costruttore, è la fiducia nell'implementazione della raccolta in entrata toArray. TreeMapnon è influenzato allo stesso modo, ma solo perché non c'è un tale guadagno in termini di prestazioni dal passaggio dell'array, come nella costruzione di un ArrayList. Nessuna delle due classi garantisce una protezione nel costruttore.

Normalmente, non ha senso tentare di scrivere codice assumendo un codice intenzionalmente dannoso dietro ogni angolo. C'è troppo che può fare per proteggersi da tutto. Tale protezione è utile solo per il codice che incapsula realmente un'azione che potrebbe consentire a un chiamante malintenzionato di accedere a qualcosa, a cui non potrebbe già accedere senza questo codice.

Se hai bisogno di sicurezza per un determinato codice, usa

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

Quindi, puoi essere sicuro che newStrscontiene solo stringhe e non può essere modificato da altro codice dopo la sua costruzione.

Oppure utilizzare List<String> newStrs = List.of(strs.toArray(new String[0]));con Java 9 o versioni successive
Si noti che Java 10 List.copyOf(strs)fa lo stesso, ma la sua documentazione non afferma che è garantito che non si fidi del toArraymetodo della raccolta in entrata . Quindi chiamare List.of(…), che sicuramente farà una copia nel caso restituisca un elenco basato su array, è più sicuro.

Poiché nessun chiamante può modificare il modo in cui funziona, le matrici funzionano, scaricando la raccolta in arrivo in una matrice, seguita dal popolamento della nuova raccolta con essa, renderà sempre sicura la copia. Poiché la raccolta può contenere un riferimento all'array restituito, come dimostrato sopra, potrebbe modificarlo durante la fase di copia, ma non può influire sulla copia nella raccolta.

Pertanto, eventuali controlli di coerenza devono essere eseguiti dopo che l'elemento particolare è stato recuperato dall'array o sull'insieme risultante nel suo insieme.


2
Il modello di sicurezza di Java funziona concedendo al codice l'intersezione dei set di autorizzazioni di tutto il codice nello stack, quindi quando il chiamante del tuo codice fa sì che il tuo codice faccia cose indesiderate, non ottiene ancora più autorizzazioni di quelle che aveva inizialmente. Quindi fa sì che il tuo codice faccia solo cose che il codice dannoso avrebbe potuto fare senza il tuo codice. Devi solo rafforzare il codice che intendi eseguire con privilegi elevati tramite AccessController.doPrivileged(…)ecc. Ma il lungo elenco di bug relativi alla sicurezza dell'applet ci dà un suggerimento sul perché questa tecnologia è stata abbandonata ...
Holger

1
Ma avrei dovuto inserire "in normali API come l'API di raccolta", poiché è quello su cui mi ero concentrato nella risposta.
Holger

2
Perché dovresti rafforzare il tuo codice, che apparentemente non è rilevante per la sicurezza, contro il codice privilegiato che consente l'implementazione di una raccolta malevola? L'ipotetico chiamante sarebbe comunque soggetto a comportamenti dannosi prima e dopo aver chiamato il tuo codice. Non si accorgerebbe nemmeno che il tuo codice è l'unico a comportarsi correttamente. L'uso new ArrayList<>(…)come costruttore di copie va bene presupponendo che le implementazioni di raccolta siano corrette. Non è tuo dovere risolvere i problemi di sicurezza quando è già troppo tardi. Che dire dell'hardware compromesso? Il sistema operativo? Che ne dici di multi-threading?
Holger il

2
Non sto sostenendo "nessuna sicurezza", ma la sicurezza nei posti giusti, invece di cercare di riparare un ambiente rotto dopo il fatto. È un'affermazione interessante che " ci sono molte raccolte che non implementano correttamente i loro supertipi " ma è già andata troppo oltre, per chiedere prove, espandendo ulteriormente questo. Alla domanda originale è stata data una risposta completa; i punti che porti ora non ne fanno mai parte. Come detto, List.copyOf(strs)non si basa sulla correttezza della raccolta in arrivo a tale proposito, al prezzo ovvio. ArrayListè un compromesso ragionevole per tutti i giorni.
Holger,

4
Dice chiaramente che non esiste tale specifica, per tutti i "metodi e flussi di creazione statici simili". Quindi, se si desidera essere assolutamente sicuri, è necessario chiamare toArray()se stessi, poiché gli array non possono avere un comportamento ignorato, seguito dalla creazione di una copia di raccolta dell'array, come new ArrayList<>(Arrays.asList( strs.toArray(new String[0])))o List.of(strs.toArray(new String[0])). Entrambi hanno anche l'effetto collaterale di applicare il tipo di elemento. Personalmente, non credo che permetteranno mai copyOfdi compromettere le collezioni immutabili, ma le alternative ci sono, nella risposta.
Holger,

1

Preferirei lasciare queste informazioni nei commenti, ma non ho abbastanza reputazione, scusa :) Cercherò di spiegarle il più dettagliatamente possibile.

Invece di qualcosa come il constmodificatore usato in C ++ per contrassegnare le funzioni membro che non dovrebbero modificare il contenuto degli oggetti, in Java originariamente veniva usato il concetto di "immutabilità". L'incapsulamento (o OCP, principio aperto-chiuso) doveva proteggere da eventuali mutazioni (cambiamenti) inattese di un oggetto. Ovviamente l'API di riflessione fa un passo avanti; l'accesso diretto alla memoria fa lo stesso; si tratta di sparare alla propria gamba :)

java.util.Collectionè di per sé un'interfaccia mutabile: ha un addmetodo che dovrebbe modificare la raccolta. Naturalmente il programmatore può racchiudere la raccolta in qualcosa che genererà ... e tutte le eccezioni di runtime accadranno perché un altro programmatore non è stato in grado di leggere javadoc, il che afferma chiaramente che la raccolta è immutabile.

Ho deciso di utilizzare il java.util.Iterabletipo per esporre la raccolta immutabile nelle mie interfacce. Semanticamente Iterablenon ha una tale caratteristica di raccolta come la "mutabilità". Tuttavia (molto probabilmente) sarai in grado di modificare le raccolte sottostanti attraverso gli stream.


JIC, per esporre le mappe in modo immutabile si java.util.Function<K,V>può usare (il getmetodo della mappa si adatta a questa definizione)


I concetti di interfacce di sola lettura e immutabilità sono ortogonali. Il punto di C ++ e C è che non supportano l'integrità semantica . Copia anche gli argomenti object / struct - const & è un'ottimizzazione per questo. Se dovessi passare un, Iteratorallora praticamente forza una copia elementally, ma non è carino. L'uso di forEachRemaining/ forEachsarà ovviamente un disastro completo. (Devo anche menzionare che Iteratorha un removemetodo.)
Tom Hawtin - affronta il

Se si guarda alla biblioteca delle collezioni Scala, esiste una stretta distinzione tra interfacce mutabili e immutabili. Anche se (suppongo) è stato fatto così per motivi completamente diversi, ma è ancora una dimostrazione di come si può raggiungere la sicurezza. L'interfaccia di sola lettura presuppone semanticamente l' immutabilità, è quello che sto cercando di dire. (Concordo sul Iterablefatto che in realtà non è immutabile, ma non vedo alcun problema con forEach*)
Alexander
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.