Как я могу безопасно копировать коллекции?

9

В прошлом я говорил, что для безопасного копирования коллекции сделайте что-то вроде:

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

Но действительно ли эти «копирующие» конструкторы, подобные статические методы создания и потоки действительно безопасны и где указаны правила? Под безопасностью я подразумеваю базовые гарантии семантической целостности, предлагаемые языком Java и коллекциями, которые применяются против злонамеренного вызывающего, при условии, что они подкреплены разумными данными SecurityManagerи что в них нет недостатков.

Я доволен метод метания ConcurrentModificationException, NullPointerException, IllegalArgumentException, ClassCastExceptionи т.д., или , возможно , даже висит.

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

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

Том Хотин - Tackline
источник
2
Что вы подразумеваете под сейфом ? Вообще говоря, классы в структуре коллекций, как правило, работают аналогично, с исключениями, указанными в javadocs. Конструкторы копирования так же «безопасны», как и любые другие конструкторы. Есть что-то особенное, что вы имеете в виду, потому что вопрос о том, безопасен ли конструктор копирования коллекции, звучит очень специфично?
Каяман
1
Ну, NavigableSetи другие Comparableоснованные коллекции могут иногда обнаруживать, что класс не реализуется compareTo()правильно, и генерировать исключение. Немного неясно, что вы подразумеваете под ненадежными аргументами. Вы имеете в виду, что злодей создает коллекцию плохих строк, и когда вы копируете их в свою коллекцию, происходит что-то плохое? Нет, структура коллекций довольно прочная, она существует примерно с 1.2.
Каяман
1
@JesseWilson, вы можете скомпрометировать многие стандартные коллекции, не взламывая их внутренние компоненты HashSet(и все другие коллекции хеширования в целом), полагаясь на правильность / целостность hashCodeреализации элементов, TreeSetи PriorityQueueзависеть от Comparator(и вы даже не можете создает эквивалентную копию без принятия пользовательского компаратора, если таковой имеется), EnumSetдоверяет целостности определенного enumтипа, который никогда не проверяется после компиляции, поэтому файл класса, не созданный с помощью javacили созданный вручную, может подорвать его.
Хольгер
1
В ваших примерах у вас есть new TreeSet<>(strs)где strsнаходится NavigableSet. Это не массовая копия, поскольку в результате TreeSetбудет использован компаратор источника, который даже необходим для сохранения семантики. Если вам достаточно просто обработать содержащиеся элементы, toArray()это путь; он даже будет поддерживать порядок итераций. Когда у вас все в порядке с «взять элемент, проверить элемент, использовать элемент», вам даже не нужно делать копию. Проблемы начинаются, когда вы хотите проверить все элементы, после чего используются все элементы. Тогда вы не можете доверять TreeSetкопии с пользовательским компаратором
Хольгер
1
Единственная операция массового копирования, имеющая эффект a checkcastдля каждого элемента, имеет toArrayопределенный тип. Мы всегда на этом заканчиваем. Родовые коллекции даже не знают своего фактического типа элемента, поэтому их конструкторы копирования не могут предоставить аналогичную функциональность. Конечно, вы можете отложить проверку до правильного использования, но тогда я не знаю, на что направлены ваши вопросы. Вам не нужна «семантическая целостность», когда вы хорошо справляетесь с проверкой и сбоем непосредственно перед использованием элементов.
Хольгер

Ответы:

12

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

Как легко можно продемонстрировать:

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

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

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

Holger
источник
2
Модель безопасности Java работает, предоставляя коду пересечение наборов разрешений всего кода в стеке, поэтому, когда вызывающий ваш код заставляет ваш код выполнять непреднамеренные действия, он все равно не получает больше разрешений, чем было изначально. Таким образом, он только заставляет ваш код делать то, что мог бы сделать злонамеренный код без вашего кода. Вам нужно только укрепить код, который вы намерены запускать с повышенными привилегиями через AccessController.doPrivileged(…)и т. Д. Но длинный список ошибок, связанных с безопасностью апплета, дает нам подсказку, почему эта технология была заброшена ...
Хольгер,
1
Но я должен был вставить «в обычные API, такие как Collection API», на чем я сосредоточился в ответе.
Хольгер
2
Почему вы должны защищать свой код, который, очевидно, не имеет отношения к безопасности, от привилегированного кода, который позволяет внедрить вредоносную коллекцию? Этот гипотетический вызывающий объект все еще будет подвержен вредоносному поведению до и после вызова вашего кода. Он даже не заметит, что ваш код - единственный, который ведет себя правильно. Использование в new ArrayList<>(…)качестве конструктора копирования вполне допустимо при условии правильной реализации коллекции. Вы не обязаны исправлять проблемы безопасности, когда уже слишком поздно. Как насчет скомпрометированного оборудования? Операционная система? Как насчет многопоточности?
Хольгер
2
Я не защищаю «отсутствие безопасности», но безопасность в нужных местах, вместо того, чтобы пытаться исправить нарушенную обстановку после факта. Это интересное утверждение, что « есть много коллекций, которые неправильно реализуют свои супертипы », но это уже зашло слишком далеко, чтобы просить доказательств, расширяя это еще дальше. На первоначальный вопрос был дан полный ответ; Точки, которые вы приносите сейчас, никогда не были частью этого. Как сказано, List.copyOf(strs)не полагается на правильность поступающей коллекции в этом отношении, по очевидной цене. ArrayListэто разумный компромисс на каждый день.
Хольгер
4
В нем четко сказано, что такой спецификации нет для всех «похожих статических методов и потоков создания». Поэтому, если вы хотите абсолютно безопасно, вы должны вызывать toArray()себя, потому что массивы не могут иметь переопределенного поведения, а затем создавать коллекционную копию массива, например new ArrayList<>(Arrays.asList( strs.toArray(new String[0])))или List.of(strs.toArray(new String[0])). У обоих также есть побочный эффект применения типа элемента. Лично я не думаю, что они когда-либо позволят copyOfскомпрометировать неизменные коллекции, но альтернативы в ответе есть.
Хольгер
1

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

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

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

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


JIC, для показа карт в неизменном виде java.util.Function<K,V>можно использовать ( getметод map соответствует этому определению)

Александр
источник
Понятия интерфейсов только для чтения и неизменяемости являются ортогональными. Смысл C ++ и C в том, что они не поддерживают семантическую целостность . Также скопируйте аргументы объекта / структуры - const & - хитрая оптимизация для этого. Если вы должны передать Iteratorтогда, то это фактически вызывает поэлементное копирование, но это не хорошо. Использование forEachRemaining/ forEach, очевидно, будет полной катастрофой. (Я также должен упомянуть, что у Iteratorнего есть removeметод.)
Том Хотин - tackline
Если взглянуть на библиотеку коллекций Scala, существует строгое различие между изменяемым и неизменным интерфейсами. Хотя (я полагаю) это было сделано по совершенно другим причинам, но все же это демонстрация того, как можно достичь безопасности. Интерфейс только для чтения семантически предполагает неизменность, вот что я пытаюсь сказать. (Я согласен Iterableс тем, что я не являюсь неизменным, но не вижу проблем forEach*)
Александр