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 SecurityManager
i że nie ma żadnych wad.
Jestem zadowolony z metody rzucania ConcurrentModificationException
, NullPointerException
, IllegalArgumentException
, ClassCastException
, itd., A może nawet powieszenie.
String
Jako 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 ArrayList
i TreeSet
.)
źródło
NavigableSet
inneComparable
kolekcje 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.HashSet
(i wszystkich innych kolekcji mieszających ogólnie) zależy od poprawności / integralnościhashCode
implementacji elementówTreeSet
iPriorityQueue
zależy odComparator
(i nie możesz nawet utwórz równoważną kopię bez akceptowania niestandardowego komparatora, jeśli taki istnieje,EnumSet
ufa integralności określonegoenum
typu, który nigdy nie jest weryfikowany po kompilacji, więc plik klasy, który nie został wygenerowany ani utworzonyjavac
ręcznie, może go obalić.new TreeSet<>(strs)
gdziestrs
jestNavigableSet
. To nie jest kopia zbiorcza, ponieważ wynikowyTreeSet
uż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ćTreeSet
kopiowaniu niestandardowego komparatoracheckcast
dla każdego elementu jesttoArray
okreś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.Odpowiedzi:
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ć:
Jak widać, oczekiwanie, że
List<String>
nie zagwarantuje otrzymania listyString
instancji. 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ć
ArrayList
konstruktora, jest zaufanie dotoArray
implementacji kolekcji przychodzącej .TreeMap
nie wpływa to w ten sam sposób, ale tylko dlatego, że nie ma takiego wzrostu wydajności po przekazaniu tablicy, jak w konstrukcji anArrayList
. Ż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
Następnie możesz być pewien, że
newStrs
zawiera 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ćtoArray
metodzie kolekcji przychodzącej . WywołanieList.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.
źródło
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…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ść?List.copyOf(strs)
nie opiera się na poprawności przychodzącej kolekcji pod tym względem, po oczywistej cenie.ArrayList
to rozsądny kompromis na co dzień.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])))
LubList.of(strs.toArray(new String[0]))
. Oba mają również efekt uboczny wymuszania typu elementu. Ja osobiście nie sądzę, że kiedykolwiek pozwolącopyOf
na kompromis niezmiennych kolekcji, ale w odpowiedzi są alternatywy.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
const
modyfikatora 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.Collection
sam jest interfejsem zmiennym: maadd
metodę, 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.Iterable
typu, aby ujawnić niezmienną kolekcję w moich interfejsach. SemantycznieIterable
nie 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 (get
metoda mapy pasuje do tej definicji)źródło
Iterator
to praktycznie zmusza elementarną kopię, ale to nie jest miłe. UżywanieforEachRemaining
/forEach
będzie oczywiście całkowitą katastrofą. (Muszę również wspomnieć, żeIterator
maremove
metodę.)Iterable
tego, że tak naprawdę nie jest niezmienny, ale nie widzę żadnych problemówforEach*
)