Как я могу безопасно копировать коллекции? - PullRequest
9 голосов
/ 09 марта 2020

Раньше я говорил, что для безопасного копирования коллекции нужно сделать что-то вроде:

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

Но являются ли эти "копирующие" конструкторы похожими на c методы и потоки создания, действительно безопасно и где указаны правила? Под безопасностью я подразумеваю, что базовые c semanti c целостность гарантии, предлагаемые языком Java и коллекциями, применяются против злоумышленника, при условии, что резервное копирование выполнено разумным SecurityManager, и что нет fl aws.

Я доволен методом броска ConcurrentModificationException, NullPointerException, IllegalArgumentException, ClassCastException, et c., или, возможно, даже повешением.

Я выбрал String в качестве примера аргумента неизменяемого типа. На этот вопрос меня не интересуют глубокие копии коллекций изменяемых типов, которые имеют свои собственные ошибки.

(Для ясности я посмотрел исходный код OpenJDK и у меня есть какой-то ответ для ArrayList и TreeSet.)

Ответы [ 2 ]

12 голосов
/ 09 марта 2020

Реальной защиты от преднамеренно вредоносного кода, работающего в той же JVM, в обычных API, например, API-интерфейсе сбора, нет.

Как легко показать:

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 экземпляров. Из-за стирания типов и необработанных типов на стороне реализации списка даже исправить невозможно.

Другая вещь, которую вы можете обвинить в конструкторе 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(…), который определенно сделает копию в случае, если он возвращает список на основе массива, является более безопасным.

Поскольку ни один вызывающий объект не может изменить способ, массивы работают, сбрасывая входящую коллекцию в массив, а затем заполняя новую коллекцию им, всегда будет делать копию безопасной. Поскольку коллекция может содержать ссылку на возвращенный массив, как показано выше, она может изменить ее на этапе копирования, но не может повлиять на копию в коллекции.

Поэтому любые проверки согласованности следует выполнять после конкретный элемент был извлечен из массива или из результирующей коллекции в целом.

1 голос
/ 14 марта 2020

Я бы предпочел оставить эту информацию в комментарии, но мне не хватает репутации, извините :) Я постараюсь объяснить это настолько многословно, насколько смогу.

Вместо чего-то вроде * Модификатор 1003 *, используемый в C ++ для обозначения функций-членов, которые не должны изменять содержимое объекта, в Java изначально использовалось понятие «неизменяемости». Инкапсуляция (или OCP, принцип Open-Closed) должна была защищать от любых неожиданных мутаций (изменений) объекта. Конечно, API отражения идет вокруг этого; прямой доступ к памяти делает то же самое; это больше о стрельбе по собственной ноге:)

java.util.Collection сам по себе является изменяемым интерфейсом: у него есть метод add, который должен модифицировать коллекцию. Конечно, программист может обернуть коллекцию во что-то, что выбросит ... и все исключения во время выполнения произойдут, потому что другой программист не смог прочитать javado c, который ясно говорит, что коллекция неизменна.

Я решил используйте тип java.util.Iterable для отображения неизменяемой коллекции в моих интерфейсах. Семантически Iterable не имеет такой характеристики c коллекции, как «изменчивость». Тем не менее, вы (скорее всего) сможете изменять базовые коллекции с помощью потоков.


JI C, для предоставления карт неизменным образом можно использовать java.util.Function<K,V> (метод карты get подходит для этого определение)

...