Di masa lalu, saya telah mengatakan untuk menyalin koleksi dengan aman, lakukan sesuatu seperti:
public static void doThing(List<String> strs) {
List<String> newStrs = new ArrayList<>(strs);
atau
public static void doThing(NavigableSet<String> strs) {
NavigableSet<String> newStrs = new TreeSet<>(strs);
Tetapi apakah konstruktor "salin" ini, metode dan aliran penciptaan statis yang serupa, benar - benar aman dan di mana aturan ditentukan? Maksud saya adalah jaminan integritas semantik dasar yang ditawarkan oleh bahasa Jawa dan koleksi yang diberlakukan terhadap penelepon jahat, dengan asumsi didukung oleh alasan yang masuk akal SecurityManager
dan tidak ada kekurangan.
Saya senang dengan metode lempar ConcurrentModificationException
, NullPointerException
, IllegalArgumentException
, ClassCastException
, dll, atau mungkin bahkan menggantung.
Saya telah memilih String
sebagai contoh dari argumen tipe yang tidak dapat diubah. Untuk pertanyaan ini, saya tidak tertarik pada salinan yang dalam untuk koleksi jenis yang bisa berubah yang memiliki gotcha sendiri.
(Untuk lebih jelasnya, saya telah melihat kode sumber OpenJDK dan memiliki beberapa jenis jawaban untuk ArrayList
dan TreeSet
.)
sumber
NavigableSet
danComparable
koleksi berbasis lainnya kadang-kadang dapat mendeteksi jika kelas tidak menerapkancompareTo()
dengan benar dan melemparkan pengecualian. Agak tidak jelas apa yang Anda maksud dengan argumen yang tidak dipercaya. Maksud Anda seorang penjahat membuat koleksi String buruk dan ketika Anda menyalinnya ke koleksi Anda, sesuatu yang buruk terjadi? Tidak, kerangka koleksi cukup solid, sudah ada sejak 1.2.HashSet
(dan semua koleksi hashing lainnya secara umum) bergantung pada kebenaran / integritashashCode
implementasi elemen,TreeSet
danPriorityQueue
bergantung padaComparator
(dan Anda bahkan tidak bisa buat salinan yang setara tanpa menerima komparator khusus jika ada),EnumSet
percaya integritasenum
tipe tertentu yang tidak pernah diverifikasi setelah kompilasi, sehingga file kelas, tidak dihasilkan denganjavac
atau buatan tangan, dapat menumbangkannya.new TreeSet<>(strs)
manastrs
aNavigableSet
. Ini bukan salinan massal, karena hasilnyaTreeSet
akan menggunakan komparator sumber, yang bahkan diperlukan untuk mempertahankan semantik. Jika Anda baik-baik saja dengan hanya memproses elemen yang terkandung,toArray()
adalah cara untuk pergi; bahkan akan menjaga urutan iterasi. Ketika Anda baik-baik saja dengan "mengambil elemen, memvalidasi elemen, menggunakan elemen", Anda bahkan tidak perlu membuat salinan. Masalah dimulai ketika Anda ingin memverifikasi semua elemen, diikuti dengan menggunakan semua elemen. Kemudian, Anda tidak dapat mempercayaiTreeSet
pembanding khusus salinancheckcast
untuk setiap elemen, adalahtoArray
dengan tipe tertentu. Kami selalu mengakhirinya. Koleksi generik bahkan tidak tahu jenis elemen mereka yang sebenarnya, jadi pembuat salinan mereka tidak dapat memberikan fungsionalitas yang serupa. Tentu saja, Anda dapat menunda pemeriksaan apa pun untuk penggunaan yang benar sebelumnya, tetapi kemudian, saya tidak tahu apa tujuan pertanyaan Anda. Anda tidak perlu "integritas semantik", ketika Anda baik-baik saja dengan memeriksa dan gagal segera sebelum menggunakan elemen.Jawaban:
Tidak ada perlindungan nyata terhadap kode jahat yang sengaja dijalankan dalam JVM yang sama di API biasa, seperti API Pengumpulan.
Seperti yang dapat dengan mudah ditunjukkan:
Seperti yang Anda lihat, mengharapkan
List<String>
tidak menjamin untuk benar-benar mendapatkan daftarString
instance. Karena penghapusan tipe dan jenis mentah, bahkan tidak ada perbaikan yang mungkin pada sisi implementasi daftar.Hal lain, Anda dapat menyalahkan
ArrayList
konstruktor, adalah kepercayaan padatoArray
implementasi koleksi yang masuk .TreeMap
tidak terpengaruh dengan cara yang sama, tetapi hanya karena tidak ada keuntungan kinerja seperti itu dari melewati array, seperti dalam konstruksi suatuArrayList
. Kelas tidak menjamin perlindungan pada konstruktor.Biasanya, tidak ada gunanya mencoba menulis kode dengan asumsi kode jahat sengaja ada di setiap sudut. Ada terlalu banyak yang bisa dilakukan, untuk melindungi dari segalanya. Perlindungan semacam itu hanya berguna untuk kode yang benar-benar merangkum tindakan yang dapat memberikan akses penelepon jahat ke sesuatu, itu tidak bisa diakses tanpa kode ini.
Jika Anda membutuhkan keamanan untuk kode tertentu, gunakan
Kemudian, Anda dapat yakin bahwa
newStrs
itu hanya berisi string dan tidak dapat dimodifikasi oleh kode lain setelah konstruksinya.Atau gunakan
List<String> newStrs = List.of(strs.toArray(new String[0]));
dengan Java 9 atau yang lebih baru.Perhatikan bahwa Java 10
List.copyOf(strs)
melakukan hal yang sama, tetapi dokumentasinya tidak menyatakan bahwa ia dijamin tidak mempercayai metode pengumpulan yang masuktoArray
. Jadi meneleponList.of(…)
, yang pasti akan membuat salinan kalau-kalau mengembalikan daftar berbasis array, lebih aman.Karena tidak ada penelepon yang dapat mengubah cara, array bekerja, membuang koleksi yang masuk ke dalam array, diikuti dengan mengisi koleksi baru dengannya, akan selalu membuat salinan aman. Karena koleksi dapat menyimpan referensi ke array yang dikembalikan seperti yang ditunjukkan di atas, itu bisa mengubahnya selama fase salin, tetapi itu tidak dapat mempengaruhi salinan dalam koleksi.
Jadi setiap pemeriksaan konsistensi harus dilakukan setelah elemen tertentu telah diambil dari array atau pada koleksi yang dihasilkan secara keseluruhan.
sumber
AccessController.doPrivileged(…)
dll. Tetapi daftar panjang bug terkait keamanan applet memberi kami petunjuk mengapa teknologi ini telah ditinggalkan ...new ArrayList<>(…)
sebagai copy constructor baik-baik saja dengan asumsi implementasi koleksi yang benar. Bukan tugas Anda untuk memperbaiki masalah keamanan ketika sudah terlambat. Bagaimana dengan perangkat keras yang disusupi? Sistem operasinya? Bagaimana dengan multi-threading?List.copyOf(strs)
tidak bergantung pada kebenaran koleksi yang masuk dalam hal itu, dengan harga yang jelas.ArrayList
adalah kompromi yang wajar untuk sehari-hari.toArray()
diri Anda sendiri, karena array tidak dapat memiliki perilaku yang ditimpa, diikuti dengan membuat salinan kumpulan array, sepertinew ArrayList<>(Arrays.asList( strs.toArray(new String[0])))
atauList.of(strs.toArray(new String[0]))
. Keduanya juga memiliki efek samping menegakkan tipe elemen. Saya, secara pribadi, tidak berpikir mereka akan membiarkancopyOf
kompromi koleksi abadi, tetapi ada alternatif, dalam jawabannya.Saya lebih suka meninggalkan informasi ini dalam komentar, tetapi saya tidak memiliki reputasi yang cukup, maaf :) Saya akan mencoba menjelaskannya sebanyak mungkin.
Alih-alih sesuatu seperti
const
pengubah yang digunakan dalam C ++ untuk menandai fungsi anggota yang tidak seharusnya mengubah konten objek, di Jawa awalnya digunakan konsep "immutability". Enkapsulasi (atau OCP, Prinsip Terbuka-Tertutup) seharusnya melindungi terhadap setiap mutasi (perubahan) objek yang tidak terduga. Tentu saja refleksi API berjalan sekitar ini; akses memori langsung melakukan hal yang sama; itu lebih lanjut tentang menembak kaki sendiri :)java.util.Collection
itu sendiri adalah antarmuka yang bisa berubah: ia memilikiadd
metode yang seharusnya mengubah koleksi. Tentu saja programmer dapat membungkus koleksi menjadi sesuatu yang akan dibuang ... dan semua pengecualian runtime akan terjadi karena programmer lain tidak dapat membaca javadoc yang dengan jelas mengatakan bahwa koleksi tidak dapat diubah.Saya memutuskan untuk menggunakan
java.util.Iterable
tipe untuk mengekspos koleksi abadi di antarmuka saya. SemantikIterable
tidak memiliki karakteristik koleksi seperti "mutabilitas". Anda masih (kemungkinan besar) dapat memodifikasi koleksi yang mendasarinya melalui aliran.JIC, untuk mengekspos peta dengan cara yang tidak
java.util.Function<K,V>
dapat diubah dapat digunakan (get
metode peta sesuai dengan definisi ini)sumber
Iterator
maka itu praktis memaksa salinan elementwise, tapi itu tidak baik. MenggunakanforEachRemaining
/forEach
jelas akan menjadi bencana total. (Saya juga harus menyebutkan bahwaIterator
adaremove
metode.)Iterable
tidak benar-benar abadi, tetapi tidak melihat masalah denganforEach*
)