Jak mogę bezpiecznie kopiować kolekcje?

9

W przeszłości mówiłem, aby bezpiecznie skopiować kolekcję i zrobić coś takiego:

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

lub

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

Ale czy te konstruktory „kopiujące”, podobne metody i strumienie tworzenia statycznego są naprawdę bezpieczne i gdzie określone są reguły? Rozumiem przez to, że podstawowe gwarancje integralności semantycznej oferowane przez język Java i kolekcje są wymuszane przeciwko złośliwemu rozmówcy, przy założeniu, że są uzasadnione SecurityManageri że nie ma żadnych wad.

Jestem zadowolony z metody rzucania ConcurrentModificationException, NullPointerException, IllegalArgumentException, ClassCastException, itd., A może nawet powieszenie.

StringJako przykład argumentu niezmiennego wybrałem . W przypadku tego pytania nie interesują mnie głębokie kopie kolekcji zmiennych typów, które mają swoje własne problemy.

(Żeby było jasne, przejrzałem kod źródłowy OpenJDK i mam jakąś odpowiedź na ArrayListi TreeSet.)

Tom Hawtin - tackline
źródło
2
Co rozumiesz przez „ bezpieczny” ? Ogólnie rzecz biorąc, klasy w ramach kolekcji mają tendencję do podobnego działania, z wyjątkami określonymi w javadocs. Konstruktory kopiowania są tak samo „bezpieczne” jak każdy inny konstruktor. Czy masz na myśli konkretną rzecz, ponieważ pytanie, czy konstruktor kopii kolekcji jest bezpieczny, brzmi bardzo konkretnie?
Kayaman
1
Cóż, NavigableSetinne Comparablekolekcje oparte mogą czasem wykryć, czy klasa nie implementuje się compareTo()poprawnie i zgłosić wyjątek. Nie jest jasne, co rozumiesz przez niezaufane argumenty. Masz na myśli, że złoczyńca tworzy kolekcję złych ciągów, a kiedy kopiujesz je do swojej kolekcji, dzieje się coś złego? Nie, struktura kolekcji jest dość solidna, istnieje od 1.2.
Kayaman
1
@JesseWilson możesz skompromitować wiele standardowych kolekcji bez włamywania się do ich wewnętrznych elementów HashSet(i wszystkich innych kolekcji mieszających ogólnie) zależy od poprawności / integralności hashCodeimplementacji elementów TreeSeti PriorityQueuezależy od Comparator(i nie możesz nawet utwórz równoważną kopię bez akceptowania niestandardowego komparatora, jeśli taki istnieje, EnumSetufa integralności określonego enumtypu, który nigdy nie jest weryfikowany po kompilacji, więc plik klasy, który nie został wygenerowany ani utworzony javacręcznie, może go obalić.
Holger
1
W twoich przykładach masz, new TreeSet<>(strs)gdzie strsjest NavigableSet. To nie jest kopia zbiorcza, ponieważ wynikowy TreeSetużyje komparatora źródła, który jest nawet niezbędny do zachowania semantyki. Jeśli wszystko jest w porządku z przetwarzaniem zawartych elementów, toArray()jest to dobra droga; zachowa nawet kolejność iteracji. Kiedy wszystko jest w porządku z „weź element, sprawdź poprawność elementu, użyj elementu”, nie musisz nawet robić kopii. Problemy zaczynają się, gdy chcesz zweryfikować wszystkie elementy, a następnie użyć wszystkich elementów. W takim razie nie można ufać TreeSetkopiowaniu niestandardowego komparatora
Holger
1
Jedyną operacją kopiowania zbiorczego mającą wpływ na checkcastdla każdego elementu jest toArrayokreślony typ. Zawsze na tym kończymy. Kolekcje ogólne nawet nie znają faktycznego typu elementu, więc ich konstruktory kopii nie mogą zapewnić podobnej funkcjonalności. Oczywiście możesz odroczyć każdą kontrolę do właściwego wcześniejszego użycia, ale nie wiem, do czego zmierza twoje pytanie. Nie potrzebujesz „integralności semantycznej”, gdy dobrze Ci jest sprawdzanie i niepowodzenie bezpośrednio przed użyciem elementów.
Holger

Odpowiedzi:

12

Nie ma prawdziwej ochrony przed celowo złośliwym kodem działającym w ramach tej samej maszyny JVM w zwykłych interfejsach API, takich jak Collection API.

Jak można łatwo wykazać:

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]

Jak widać, oczekiwanie, że List<String>nie zagwarantuje otrzymania listy Stringinstancji. Z powodu usuwania typów i typów surowych, nie jest nawet możliwa poprawka po stronie implementacji listy.

Inną rzeczą, za którą można winić ArrayListkonstruktora, jest zaufanie do toArrayimplementacji kolekcji przychodzącej . TreeMapnie wpływa to w ten sam sposób, ale tylko dlatego, że nie ma takiego wzrostu wydajności po przekazaniu tablicy, jak w konstrukcji an ArrayList. Żadna klasa nie gwarantuje ochrony w konstruktorze.

Zwykle nie ma sensu pisać kodu zakładającego celowo złośliwy kod za każdym rogiem. Jest zbyt wiele, co może zrobić, aby chronić się przed wszystkim. Taka ochrona jest użyteczna tylko dla kodu, który naprawdę zawiera w sobie akcję, która mogłaby dać szkodliwemu rozmówcy dostęp do czegoś, do czego nie byłaby w stanie uzyskać dostępu bez tego kodu.

Jeśli potrzebujesz bezpieczeństwa dla konkretnego kodu, użyj

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

Następnie możesz być pewien, że newStrszawiera on tylko łańcuchy i nie może zostać zmodyfikowany przez inny kod po jego zbudowaniu.

Lub użyj List<String> newStrs = List.of(strs.toArray(new String[0]));z Javą 9 lub nowszą
Zauważ, że Java 10 List.copyOf(strs)robi to samo, ale w jej dokumentacji nie stwierdzono, że nie można ufać toArraymetodzie kolekcji przychodzącej . Wywołanie List.of(…), które z pewnością utworzy kopię w przypadku, gdy zwróci listę opartą na tablicy, jest bezpieczniejsze.

Ponieważ żaden dzwoniący nie może zmienić sposobu, tablice działają, zrzucenie kolekcji przychodzącej do tablicy, a następnie zapełnienie jej nową kolekcją, zawsze zapewni bezpieczeństwo kopii. Ponieważ kolekcja może zawierać odniesienie do zwróconej tablicy, jak pokazano powyżej, może to zmienić podczas fazy kopiowania, ale nie może wpływać na kopię w kolekcji.

Tak więc wszelkie kontrole spójności powinny być wykonywane po pobraniu konkretnego elementu z tablicy lub w wynikowej kolekcji jako całości.

Holger
źródło
2
Model bezpieczeństwa Javy działa poprzez przyznanie kodowi przecięcia zestawów uprawnień całego kodu na stosie, więc kiedy program wywołujący twój kod powoduje, że kod robi niezamierzone rzeczy, nadal nie otrzymuje więcej uprawnień niż początkowo miał. Dzięki temu twój kod robi tylko to, co złośliwy kod mógłby zrobić bez twojego kodu. Musisz tylko zahartować kod, który chcesz uruchomić z podwyższonymi uprawnieniami za pośrednictwem AccessController.doPrivileged(…)itp. Ale długa lista błędów związanych z bezpieczeństwem apletów daje nam wskazówkę, dlaczego ta technologia została porzucona…
Holger
1
Ale powinienem był wstawić „do zwykłych interfejsów API, takich jak Collection API”, ponieważ na tym skupiłem się w odpowiedzi.
Holger
2
Dlaczego warto zahartować swój kod, który najwyraźniej nie jest istotny z punktu widzenia bezpieczeństwa, przed kodem uprzywilejowanym, który pozwala na włożenie złośliwej kolekcji? Ten hipotetyczny dzwoniący nadal podlegałby złośliwemu zachowaniu przed i po wywołaniu kodu. Nie zauważyłby nawet, że Twój kod działa tylko poprawnie. Używanie new ArrayList<>(…)jako konstruktora kopii jest w porządku, zakładając prawidłowe implementacje kolekcji. Naprawienie bezpieczeństwa nie jest twoim obowiązkiem, gdy jest już za późno. Co z zainfekowanym sprzętem? System operacyjny? Co powiesz na wielowątkowość?
Holger
2
Nie opowiadam się za „brakiem bezpieczeństwa”, ale za bezpieczeństwem we właściwych miejscach, zamiast próbować naprawić zepsute środowisko po fakcie. Interesujące jest twierdzenie, że „ istnieje wiele kolekcji, które nie implementują poprawnie swoich nadtypów ”, ale już za daleko posunęło się prosić o dowody, rozszerzając to jeszcze bardziej. Odpowiedź na pierwotne pytanie jest kompletna; punkty, które przynosisz teraz, nigdy nie były tego częścią. Jak już wspomniano, List.copyOf(strs)nie opiera się na poprawności przychodzącej kolekcji pod tym względem, po oczywistej cenie. ArrayListto rozsądny kompromis na co dzień.
Holger
4
Wyraźnie mówi, że nie ma takiej specyfikacji dla wszystkich „podobnych metod i strumieni tworzenia statycznego”. Więc jeśli chcesz być całkowicie bezpieczny, musisz zadzwonić do toArray()siebie, ponieważ tablice nie mogą mieć nadpisanego zachowania, a następnie utworzyć kopię kolekcji tablicy, np . new ArrayList<>(Arrays.asList( strs.toArray(new String[0])))Lub List.of(strs.toArray(new String[0])). Oba mają również efekt uboczny wymuszania typu elementu. Ja osobiście nie sądzę, że kiedykolwiek pozwolą copyOfna kompromis niezmiennych kolekcji, ale w odpowiedzi są alternatywy.
Holger
1

Wolę zostawić tę informację w komentarzu, ale nie mam wystarczającej reputacji, przepraszam :) Postaram się wtedy wyjaśnić tyle, ile potrafię.

Zamiast czegoś podobnego do constmodyfikatora używanego w C ++ do oznaczania funkcji składowych, które nie powinny modyfikować zawartości obiektu, w Javie pierwotnie zastosowano koncepcję „niezmienności”. Encapsulation (lub OCP, Open-Closed Principle) miał chronić przed wszelkimi nieoczekiwanymi mutacjami (zmianami) obiektu. Oczywiście API do refleksji to obchodzi; bezpośredni dostęp do pamięci robi to samo; to więcej o strzelaniu własną nogą :)

java.util.Collectionsam jest interfejsem zmiennym: ma addmetodę, która ma modyfikować kolekcję. Oczywiście programista może zawinąć kolekcję w coś, co rzuci ... i zdarzają się wszystkie wyjątki środowiska wykonawczego, ponieważ inny programista nie był w stanie odczytać javadoc, co wyraźnie mówi, że kolekcja jest niezmienna.

Zdecydowałem się użyć java.util.Iterabletypu, aby ujawnić niezmienną kolekcję w moich interfejsach. Semantycznie Iterablenie ma takiej cechy kolekcji, jak „zmienność”. Nadal będziesz (najprawdopodobniej) modyfikować bazowe kolekcje za pomocą strumieni.


java.util.Function<K,V>Można użyć JIC, aby eksponować mapy w niezmienny sposób ( getmetoda mapy pasuje do tej definicji)

Alexander
źródło
Pojęcia interfejsów tylko do odczytu i niezmienności są ortogonalne. Chodzi o to, że C ++ i C nie obsługują integralności semantycznej . Również skopiuj argumenty object / struct - const & jest do tego niejasną optymalizacją. Jeśli zdasz, Iteratorto praktycznie zmusza elementarną kopię, ale to nie jest miłe. Używanie forEachRemaining/ forEachbędzie oczywiście całkowitą katastrofą. (Muszę również wspomnieć, że Iteratorma removemetodę.)
Tom Hawtin - sfałszował
Jeśli spojrzymy na bibliotekę kolekcji Scala, istnieje ścisłe rozróżnienie między interfejsami zmiennymi i niezmiennymi. Chociaż (tak przypuszczam) powstał z zupełnie innych powodów, ale wciąż jest demonstracją tego, jak można osiągnąć bezpieczeństwo. Interfejs tylko do odczytu semantycznie zakłada niezmienność, to właśnie próbuję powiedzieć. (Zgadzam się co do Iterabletego, że tak naprawdę nie jest niezmienny, ale nie widzę żadnych problemów forEach*)
Alexander