Como posso copiar coleções com segurança?

9

No passado, eu disse para copiar com segurança uma coleção, algo como:

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

ou

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

Mas esses construtores de "cópia", métodos e fluxos de criação estática semelhantes, são realmente seguros e onde as regras são especificadas? Por segurança, quero dizer, são as garantias básicas de integridade semântica oferecidas pela linguagem Java e coleções impostas a um chamador mal-intencionado, assumindo o backup de um razoável SecurityManagere que não há falhas.

Estou feliz com o lançamento método ConcurrentModificationException, NullPointerException, IllegalArgumentException, ClassCastException, etc., ou talvez mesmo pendurado.

Eu escolhi Stringcomo exemplo de um argumento de tipo imutável. Para esta pergunta, não estou interessado em cópias profundas para coleções de tipos mutáveis ​​que têm suas próprias dicas.

(Para ser claro, observei o código-fonte do OpenJDK e tenho algum tipo de resposta para ArrayListe TreeSet.)

Tom Hawtin - linha de orientação
fonte
2
O que você quer dizer com seguro ? De um modo geral, as classes na estrutura de coleções tendem a funcionar de maneira semelhante, com exceções especificadas nos javadocs. Os construtores de cópia são tão "seguros" quanto qualquer outro construtor. Você pensa em algo específico, porque perguntar se um construtor de cópias de coleção é seguro parece muito específico?
Kayaman 9/03
11
Bem, NavigableSetoutras Comparablecoleções baseadas podem às vezes detectar se uma classe não é implementada compareTo()corretamente e gerar uma exceção. Não está claro o que você quer dizer com argumentos não confiáveis. Você quer dizer que um malfeitor cria uma coleção de Strings ruins e quando você as copia para sua coleção algo ruim acontece? Não, a estrutura de coleções é bastante sólida, existe desde a versão 1.2.
Kayaman 9/03
11
@JesseWilson, você pode comprometer muitas das coleções padrão sem invadir suas partes internas HashSet(e todas as outras coleções de hash em geral) depende da correção / integridade da hashCodeimplementação dos elementos TreeSete PriorityQueuedepende da Comparator(e você nem pode crie uma cópia equivalente sem aceitar o comparador personalizado, se houver), EnumSetconfie na integridade do enumtipo específico que nunca é verificado após a compilação, para que um arquivo de classe, não gerado com javacou artesanal, possa subvertê-lo.
Holger
11
Nos seus exemplos, você tem new TreeSet<>(strs)onde strsestá um NavigableSet. Esta não é uma cópia em massa, pois o resultado TreeSetusará o comparador da fonte, que é até necessário para manter a semântica. Se você está bem apenas processando os elementos contidos, toArray()é o caminho a percorrer; ele ainda manterá a ordem da iteração. Quando você está bem com “pegar elemento, validar elemento, usar elemento”, você nem precisa fazer uma cópia. Os problemas começam quando você deseja verificar todos os elementos, seguido pelo uso de todos os elementos. Então você não pode confiar em uma TreeSetcópia com comparador personalizado
Holger
11
A única operação de cópia em massa que tem o efeito de a checkcastpara cada elemento é toArraycom um tipo específico. Estamos sempre terminando. As coleções genéricas nem conhecem seu tipo de elemento real, portanto, seus construtores de cópias não podem fornecer uma funcionalidade semelhante. Obviamente, você pode adiar qualquer verificação para o uso anterior correto, mas não sei o que suas perguntas estão buscando. Você não precisa de "integridade semântica" quando estiver bem em verificar e falhar imediatamente antes de usar elementos.
Holger

Respostas:

12

Não há proteção real contra códigos maliciosos intencionalmente executados na mesma JVM em APIs comuns, como a API Collection.

Como pode ser facilmente demonstrado:

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]

Como você pode ver, esperar que um List<String>não garanta uma lista de Stringinstâncias. Devido à eliminação de tipos e tipos brutos, não há nem mesmo uma correção possível no lado da implementação da lista.

A outra coisa pela qual você pode culpar ArrayListo construtor é a confiança na toArrayimplementação da coleção recebida . TreeMapnão é afetado da mesma maneira, mas apenas porque não há ganho de desempenho ao passar a matriz, como na construção de um ArrayList. Nenhuma classe garante uma proteção no construtor.

Normalmente, não faz sentido tentar escrever código assumindo intencionalmente código malicioso em cada esquina. Há muito o que fazer, para proteger contra tudo. Essa proteção é útil apenas para códigos que realmente encapsulam uma ação que pode dar a um chamador mal-intencionado acesso a algo que já não podia acessar sem esse código.

Se você precisar de segurança para um código específico, use

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

Então, você pode ter certeza de que newStrscontém apenas cadeias e não pode ser modificado por outro código após sua construção.

Ou use List<String> newStrs = List.of(strs.toArray(new String[0]));com o Java 9 ou mais recente
Observe que o Java 10 List.copyOf(strs)faz o mesmo, mas sua documentação não afirma que é garantido que não confie no toArraymétodo da coleção recebida . Assim List.of(…), chamar , que definitivamente fará uma cópia caso retorne uma lista baseada em array, é mais seguro.

Como nenhum chamador pode alterar a maneira, as matrizes funcionam, despejar a coleção recebida em uma matriz e, em seguida, preencher a nova coleção, sempre tornará a cópia segura. Como a coleção pode conter uma referência à matriz retornada, como demonstrado acima, ela pode alterá-la durante a fase de cópia, mas não pode afetar a cópia na coleção.

Portanto, qualquer verificação de consistência deve ser feita após o elemento específico ter sido recuperado da matriz ou da coleção resultante como um todo.

Holger
fonte
2
O modelo de segurança do Java funciona concedendo ao código a interseção dos conjuntos de permissões de todo o código na pilha; portanto, quando o chamador do seu código faz com que o código faça coisas não intencionais, ele ainda não recebe mais permissões do que tinha inicialmente. Portanto, ele apenas faz com que seu código faça coisas que o código malicioso poderia ter feito sem o seu código também. Você só precisa fortalecer o código que pretende executar com privilégios elevados via AccessController.doPrivileged(…)etc. Mas a longa lista de erros relacionados à segurança de applets nos dá uma dica de por que essa tecnologia foi abandonada…
Holger
11
Mas eu deveria ter inserido "em APIs comuns como a API Collection", pois era nisso que eu estava focado na resposta.
Holger
2
Por que você deve proteger seu código, que aparentemente não é relevante para a segurança, contra código privilegiado que permite a implementação de uma coleção maliciosa? Esse chamador hipotético ainda estaria sujeito ao comportamento malicioso antes e depois de chamar seu código. Nem notaria que seu código é o único que está se comportando corretamente. Usar new ArrayList<>(…)como construtor de cópia é bom, assumindo implementações de coleção corretas. Não é seu dever corrigir problemas de segurança quando já é tarde demais. E quanto ao hardware comprometido? O sistema operacional? Que tal multi-threading?
Holger
2
Não estou defendendo "nenhuma segurança", mas a segurança nos lugares certos, em vez de tentar consertar um ambiente quebrado após o fato. É uma afirmação interessante de que “ existem muitas coleções que não implementam corretamente seus supertipos ”, mas já foi longe demais, pedir provas, expandindo ainda mais isso. A pergunta original foi respondida completamente; os pontos que você está trazendo agora nunca fizeram parte disso. Como já foi dito, List.copyOf(strs)não depende da exatidão da coleção recebida a esse respeito, pelo preço óbvio. ArrayListé um compromisso razoável para o dia a dia.
Holger
4
Diz claramente que não existe essa especificação para todos os "métodos e fluxos de criação estática similares". Portanto, se você quiser estar absolutamente seguro, precisará se chamar toArray(), porque as matrizes não podem ter um comportamento substituído, seguido pela criação de uma cópia de coleção da matriz, como new ArrayList<>(Arrays.asList( strs.toArray(new String[0])))ou List.of(strs.toArray(new String[0])). Ambos também têm o efeito colateral de impor o tipo de elemento. Pessoalmente, acho que nunca permitirão copyOfcomprometer as coleções imutáveis, mas as alternativas estão lá, na resposta.
Holger
1

Eu preferiria deixar essas informações em comentário, mas não tenho reputação suficiente, desculpe :) Vou tentar explicá-las o mais detalhadamente possível.

Em vez de algo como o constmodificador usado em C ++ para marcar funções-membro que não deveriam modificar o conteúdo do objeto, originalmente em Java era usado o conceito de "imutabilidade". O encapsulamento (ou OCP, Princípio Aberto-Fechado) deveria proteger contra quaisquer mutações inesperadas (mudanças) de um objeto. É claro que a API de reflexão anda por aí; o acesso direto à memória faz o mesmo; isso é mais sobre fotografar a própria perna :)

java.util.Collectionem si é uma interface mutável: possui um addmétodo que deve modificar a coleção. É claro que o programador pode agrupar a coleção em algo que será lançado ... e todas as exceções de tempo de execução acontecerão porque outro programador não conseguiu ler o javadoc, o que claramente diz que a coleção é imutável.

Decidi usar java.util.Iterabletype para expor coleção imutável em minhas interfaces. Semanticamente Iterable, não possui tal característica de coleção como "mutabilidade". Ainda (provavelmente) você poderá modificar coleções subjacentes por meio de fluxos.


JIC, para expor mapas de maneira imutável java.util.Function<K,V>pode ser usado (o getmétodo do mapa se encaixa nessa definição)

Alexander
fonte
Os conceitos de interfaces somente leitura e imutabilidade são ortogonais. O ponto de C ++ e C é que eles não suportam integridade semântica . Os argumentos também copiar objeto / estrutura - const & é uma otimização desonesta para isso. Se você passar uma Iteratornota, isso praticamente força uma cópia elementar, mas isso não é legal. Usar forEachRemaining/ forEachobviamente será um desastre completo. (Eu também tenho que mencionar que Iteratortem um removemétodo.)
Tom Hawtin - tackline
Se olhar para a biblioteca de coleções Scala, existe uma distinção estrita entre interfaces mutáveis ​​e imutáveis. Embora (suponho) tenha sido feito por razões completamente diferentes, mas ainda é uma demonstração de como a segurança pode ser alcançada. A interface somente leitura pressupõe semanticamente a imutabilidade, é o que estou tentando dizer. (Eu concordo sobre Iterableé não ser verdade imutável, mas não vê qualquer problema com forEach*)
Alexander