Як я можу безпечно копіювати колекції?


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


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копію з користувацьким компаратором
Holger

1
Єдина операція масової копії, що має ефект checkcastдля кожного елемента, - це toArrayз певним типом. Ми завжди на цьому закінчуємося. Узагальнені колекції навіть не знають свого фактичного типу елементів, тому їх конструктори копій не можуть забезпечити подібну функціональність. Звичайно, ви можете відкласти будь-яку перевірку до прямого використання, але тоді я не знаю, на що спрямовані ваші запитання. Вам не потрібна "смислова цілісність", коли ви добре перевіряєте і не працюєте безпосередньо перед використанням елементів.
Холгер

Відповіді:


12

Немає реального захисту від навмисно зловмисного коду, що працює в межах одного JVM у звичайних API, як 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(…) , який обов'язково зробить копію у випадку, якщо він поверне список на основі масиву, є більш безпечним.

Оскільки жоден абонент не може змінити спосіб, масиви працюють, скидання вхідної колекції в масив з подальшим заповненням нової колекції завжди зробить копію безпечною. Оскільки колекція може містити посилання на повернений масив, як показано вище, це може змінити його під час фази копіювання, але це не може вплинути на копію в колекції.

Таким чином, будь-які перевірки узгодженості повинні проводитися після отримання конкретного елемента з масиву або з отриманої колекції в цілому.


2
Модель безпеки Java працює, надаючи коду перетин наборів дозволів усього коду на стеку, тому коли абонент вашого коду змушує ваш код робити непередбачувані речі, він все одно не отримує більше дозволів, ніж це було раніше. Тому він змушує ваш код робити те, що зловмисний код міг зробити і без вашого коду. Вам потрібно лише посилити код, який ви збираєтеся запустити з підвищеними привілеями через AccessController.doPrivileged(…)тощо. Але довгий список помилок, пов’язаних із безпекою аплетів, дає нам підказку, чому від цієї технології було відмовлено…
Holger

1
Але я мав би вставити "у звичайні API, такі як 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, відкритий закритий принцип) повинна була захищати від будь-яких несподіваних мутацій (змін) об'єкта. Звичайно, API роздумів обходить це; прямий доступ до пам'яті робить те саме; це більше про відстріл власною ногою :)

java.util.CollectionСам по собі є змінним інтерфейсом: він має addметод, який повинен модифікувати колекцію. Звичайно, програміст може перетворити колекцію на щось, що кине ... і всі винятки з виконання будуть відбуватися, оскільки інший програміст не зміг прочитати javadoc, що чітко говорить, що колекція незмінна.

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


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


Поняття інтерфейсів лише для читання та незмінність є ортогональними. Сенс C ++ і C полягає в тому, що вони не підтримують смислову цілісність . Також копіюйте об'єкти / структури аргументів - const & - це хитра оптимізація для цього. Якби вам здали Iteratorтоді, це практично змушує стихійну копію, але це не приємно. Використання forEachRemaining/ forEachочевидно стане повною катастрофою. (Я також мушу зазначити, що Iteratorє removeметод.)
Том Хоутін - смуга

Якщо подивитися на бібліотеку колекцій Scala, існує чітка різниця між змінними та незмінними інтерфейсами. Хоча (я вважаю) це було зроблено з абсолютно різних причин, але все ж є демонстрацією того, як можна досягти безпеки. Інтерфейс лише для читання семантично передбачає незмінність, ось що я намагаюся сказати. (Я згоден з приводу Iterable«S не є фактично залишається незмінною, але не бачить ніяких проблем з forEach*)
Олександром
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.