¿Cómo puedo copiar colecciones de forma segura?

9

En el pasado, he dicho que copiar una colección de manera segura haga algo como:

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

Pero, ¿son estos constructores de "copia", métodos y flujos de creación estáticos similares, realmente seguros y dónde se especifican las reglas? Por seguro, quiero decir, son las garantías básicas de integridad semántica ofrecidas por el lenguaje Java y las colecciones aplicadas contra un llamador malicioso, suponiendo SecurityManagerque esté respaldado por un razonable y que no haya fallas.

Estoy contento con el lanzamiento método ConcurrentModificationException, NullPointerException, IllegalArgumentException, ClassCastException, etc, o tal vez incluso colgado.

He elegido Stringcomo ejemplo un argumento de tipo inmutable. Para esta pregunta, no me interesan las copias profundas para colecciones de tipos mutables que tienen sus propios trucos.

(Para ser claros, he mirado el código fuente de OpenJDK y tengo algún tipo de respuesta para ArrayListy TreeSet).

Tom Hawtin - tackline
fuente
2
¿Qué quieres decir con seguro ? En general, las clases en el marco de colecciones tienden a funcionar de manera similar, con excepciones especificadas en los javadocs. Los constructores de copia son tan "seguros" como cualquier otro constructor. ¿Hay algo en particular que tenga en mente, porque preguntar si un constructor de copia de colección es seguro suena muy específico?
Kayaman
1
Bueno, NavigableSety otras Comparablecolecciones basadas a veces pueden detectar si una clase no se implementa compareTo()correctamente y lanzar una excepción. No está claro qué quiere decir con argumentos no confiables. ¿Te refieres a un malhechor que crea una colección de cadenas malas y cuando las copias a tu colección, algo malo sucede? No, el marco de colecciones es bastante sólido, existe desde 1.2.
Kayaman
1
@JesseWilson puede comprometer muchas de las colecciones estándar sin piratear sus componentes internos HashSet(y todas las demás colecciones de hash en general) se basa en la corrección / integridad de la hashCodeimplementación de los elementos, TreeSety PriorityQueuedepende de Comparator(y ni siquiera puede crea una copia equivalente sin aceptar el comparador personalizado si lo hay), EnumSetconfía en la integridad del enumtipo particular que nunca se verifica después de la compilación, por lo que un archivo de clase, no generado javaco hecho a mano, puede subvertirlo.
Holger
1
En sus ejemplos, tiene new TreeSet<>(strs)dónde strsestá a NavigableSet. Esta no es una copia masiva, ya que el resultado TreeSetutilizará el comparador de la fuente, que incluso es necesario para mantener la semántica. Si está bien con solo procesar los elementos contenidos, este toArray()es el camino a seguir; incluso mantendrá el orden de iteración. Cuando está bien con "tomar elemento, validar elemento, usar elemento", ni siquiera necesita hacer una copia. Los problemas comienzan cuando desea verificar todos los elementos, seguido de la utilización de todos los elementos. Entonces, no puede confiar en una TreeSetcopia con un comparador personalizado
Holger
1
La única operación de copia masiva que tiene el efecto de a checkcastpara cada elemento, es toArraycon un tipo específico. Siempre terminamos en eso. Las colecciones genéricas ni siquiera conocen su tipo de elemento real, por lo que sus constructores de copias no pueden proporcionar una funcionalidad similar. Por supuesto, puede diferir cualquier verificación al uso previo correcto, pero entonces, no sé a qué apuntan sus preguntas. No necesita "integridad semántica", cuando está de acuerdo con verificar y fallar inmediatamente antes de usar elementos.
Holger

Respuestas:

12

No existe una protección real contra el código intencionalmente malicioso que se ejecuta dentro de la misma JVM en las API normales, como la API de recopilación.

Como se puede demostrar fácilmente:

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 puede ver, esperar un List<String>no garantiza realmente obtener una lista de Stringinstancias. Debido a la eliminación de tipos y tipos sin formato, ni siquiera hay una solución posible en el lado de implementación de la lista.

La otra cosa de la que puede culpar ArrayListal constructor es la confianza en la toArrayimplementación de la colección entrante . TreeMapno se ve afectado de la misma manera, sino solo porque no hay tal ganancia de rendimiento al pasar la matriz, como en la construcción de un ArrayList. Ninguna clase garantiza una protección en el constructor.

Normalmente, no tiene sentido intentar escribir código suponiendo código intencionalmente malicioso en cada esquina. Hay demasiado que puede hacer para protegerse contra todo. Dicha protección solo es útil para el código que realmente encapsula una acción que podría otorgarle a una persona que llama maliciosa acceso a algo, ya que no podría acceder sin este código.

Si necesita seguridad para un código en particular, 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);
    }
}

Luego, puede estar seguro de que newStrssolo contiene cadenas y no puede ser modificado por otro código después de su construcción.

O úselo List<String> newStrs = List.of(strs.toArray(new String[0]));con Java 9 o más reciente
Tenga en cuenta que Java 10's List.copyOf(strs)hace lo mismo, pero su documentación no indica que está garantizado que no confíe en el toArraymétodo de la colección entrante . Entonces List.of(…), llamar , que definitivamente hará una copia en caso de que devuelva una lista basada en una matriz, es más seguro.

Como ninguna persona que llama puede alterar la forma, las matrices funcionan, volcar la colección entrante en una matriz, seguido de llenar la nueva colección con ella, siempre hará que la copia sea segura. Dado que la colección puede contener una referencia a la matriz devuelta como se demostró anteriormente, podría alterarla durante la fase de copia, pero no puede afectar la copia en la colección.

Por lo tanto, cualquier verificación de coherencia debe realizarse después de que el elemento particular se haya recuperado de la matriz o de la colección resultante en su conjunto.

Holger
fuente
2
El modelo de seguridad de Java funciona al otorgarle al código la intersección de los conjuntos de permisos de todo el código en la pila, por lo que cuando el llamante de su código hace que su código haga cosas no intencionadas, aún no obtiene más permisos de los que inicialmente tenía. Por lo tanto, solo hace que su código haga cosas que el código malicioso podría haber hecho sin su código también. Solo tiene que endurecer el código que intenta ejecutar con privilegios elevados a través de AccessController.doPrivileged(…)etc. Pero la larga lista de errores relacionados con la seguridad del applet nos da una pista de por qué se ha abandonado esta tecnología ...
Holger
1
Pero debería haber insertado "en API normales como la API de colección", ya que eso es en lo que me estaba centrando en la respuesta.
Holger
2
¿Por qué debería endurecer su código, que aparentemente no es relevante para la seguridad, contra el código privilegiado que permite que se introduzca una implementación de recopilación maliciosa? Esa persona que llama hipotética aún estaría sujeta al comportamiento malicioso antes y después de llamar a su código. Ni siquiera se daría cuenta de que su código es el único que se comporta correctamente. Usar new ArrayList<>(…)como constructor de copias está bien asumiendo implementaciones de colección correctas. No es su deber solucionar los problemas de seguridad cuando ya es demasiado tarde. ¿Qué pasa con el hardware comprometido? ¿El sistema operativo? ¿Qué tal multihilo?
Holger
2
No estoy abogando por "no seguridad", sino seguridad en los lugares correctos, en lugar de tratar de arreglar un entorno roto después del hecho. Es una afirmación interesante que " hay muchas colecciones que no implementan correctamente sus supertipos ", pero ya fue demasiado lejos, para pedir pruebas, expandiendo esto aún más. La pregunta original ha sido respondida completamente; los puntos que traes ahora nunca fueron parte de eso. Como se dijo, List.copyOf(strs)no se basa en la corrección de la colección entrante en ese sentido, al precio obvio. ArrayListes un compromiso razonable para todos los días.
Holger
44
Dice claramente que no existe tal especificación para todos los "métodos y flujos de creación estática similares". Entonces, si desea estar absolutamente seguro, debe llamarse a toArray()sí mismo, porque las matrices no pueden tener un comportamiento anulado, seguido de la creación de una copia de recopilación de la matriz, como new ArrayList<>(Arrays.asList( strs.toArray(new String[0])))o List.of(strs.toArray(new String[0])). Ambos también tienen el efecto secundario de aplicar el tipo de elemento. Yo, personalmente, no creo que alguna vez permitan copyOfcomprometer las colecciones inmutables, pero las alternativas están ahí, en la respuesta.
Holger
1

Preferiría dejar esta información en el comentario, pero no tengo suficiente reputación, lo siento :) Trataré de explicarla tan detalladamente como pueda.

En lugar de algo así como un constmodificador utilizado en C ++ para marcar funciones miembro que no deben modificar el contenido del objeto, en Java originalmente se utilizó el concepto de "inmutabilidad". Se suponía que la encapsulación (u OCP, principio abierto-cerrado) protegía contra cualquier mutación inesperada (cambio) de un objeto. Por supuesto, la API de reflexión explica esto; el acceso directo a la memoria hace lo mismo; eso es más sobre disparar una pierna :)

java.util.Collectionen sí es una interfaz mutable: tiene un addmétodo que se supone que modifica la colección. Por supuesto, el programador puede envolver la colección en algo que arrojará ... y todas las excepciones de tiempo de ejecución sucederán porque otro programador no pudo leer javadoc, que claramente dice que la colección es inmutable.

Decidí usar java.util.Iterabletype para exponer una colección inmutable en mis interfaces. Semánticamente Iterableno tiene una característica de colección como la "mutabilidad". Aún así (muy probablemente) podrá modificar colecciones subyacentes a través de secuencias.


java.util.Function<K,V>Se puede usar JIC para exponer mapas de manera inmutable (el getmétodo del mapa se ajusta a esta definición)

Alejandro
fuente
Los conceptos de interfaces de solo lectura e inmutabilidad son ortogonales. El punto de C ++ y C es que no admiten integridad semántica . El también copia argumentos de objeto / estructura - const & es una optimización poco fiable para eso. Si Iteratortuviera que pasar un entonces, eso prácticamente obliga a una copia por elementos, pero eso no es bueno. Usar forEachRemaining/ forEachobviamente será un completo desastre. (También tengo que mencionar que Iteratortiene un removemétodo.)
Tom Hawtin - tackline
Si mira la biblioteca de colecciones Scala, hay una distinción estricta entre interfaces mutables e inmutables. Aunque (supongo) se hizo así por razones completamente diferentes, pero aún así es una demostración de cómo se puede lograr la seguridad. La interfaz de solo lectura asume semánticamente la inmutabilidad, eso es lo que estoy tratando de decir. (Estoy de acuerdo con respecto a que Iterableno sea realmente inmutable, pero no veo ningún problema con forEach*)
Alexander