Bagaimana saya bisa menyalin koleksi dengan aman?

9

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 SecurityManagerdan tidak ada kekurangan.

Saya senang dengan metode lempar ConcurrentModificationException, NullPointerException, IllegalArgumentException, ClassCastException, dll, atau mungkin bahkan menggantung.

Saya telah memilih Stringsebagai 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 ArrayListdan TreeSet.)

Tom Hawtin - tackline
sumber
2
Apa yang Anda maksud dengan aman ? Secara umum, kelas-kelas dalam kerangka koleksi cenderung bekerja sama, dengan pengecualian yang ditentukan dalam javadocs. Salin konstruktor sama amannya dengan konstruktor lainnya. Apakah ada hal tertentu yang ada dalam pikiran Anda, karena menanyakan apakah konstruktor salinan koleksi aman terdengar sangat spesifik?
Kayaman
1
Nah, NavigableSetdan Comparablekoleksi berbasis lainnya kadang-kadang dapat mendeteksi jika kelas tidak menerapkan compareTo()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.
Kayaman
1
@JesseWilson Anda dapat berkompromi banyak koleksi standar tanpa meretas ke internal mereka, HashSet(dan semua koleksi hashing lainnya secara umum) bergantung pada kebenaran / integritas hashCodeimplementasi elemen, TreeSetdan PriorityQueuebergantung pada Comparator(dan Anda bahkan tidak bisa buat salinan yang setara tanpa menerima komparator khusus jika ada), EnumSetpercaya integritas enumtipe tertentu yang tidak pernah diverifikasi setelah kompilasi, sehingga file kelas, tidak dihasilkan dengan javacatau buatan tangan, dapat menumbangkannya.
Holger
1
Dalam contoh Anda, Anda memiliki di new TreeSet<>(strs)mana strsa NavigableSet. Ini bukan salinan massal, karena hasilnya TreeSetakan 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 mempercayai TreeSetpembanding khusus salinan
Holger
1
Satu-satunya operasi penyalinan massal yang memiliki efek a checkcastuntuk setiap elemen, adalah toArraydengan 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.
Holger

Jawaban:

12

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:

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]

Seperti yang Anda lihat, mengharapkan List<String>tidak menjamin untuk benar-benar mendapatkan daftar Stringinstance. Karena penghapusan tipe dan jenis mentah, bahkan tidak ada perbaikan yang mungkin pada sisi implementasi daftar.

Hal lain, Anda dapat menyalahkan ArrayListkonstruktor, adalah kepercayaan pada toArrayimplementasi koleksi yang masuk . TreeMaptidak terpengaruh dengan cara yang sama, tetapi hanya karena tidak ada keuntungan kinerja seperti itu dari melewati array, seperti dalam konstruksi suatu ArrayList. 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

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

Kemudian, Anda dapat yakin bahwa newStrsitu 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 masuk toArray. Jadi menelepon List.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.

Holger
sumber
2
Model keamanan Java bekerja dengan memberikan kode persimpangan set izin dari semua kode pada stack, jadi ketika penelepon kode Anda membuat kode Anda melakukan hal-hal yang tidak disengaja, itu masih tidak mendapatkan izin lebih dari yang awalnya. Jadi itu hanya membuat kode Anda melakukan hal-hal yang bisa dilakukan kode berbahaya tanpa kode Anda juga. Anda hanya perlu mengeraskan kode yang ingin Anda jalankan dengan hak istimewa yang ditingkatkan melalui AccessController.doPrivileged(…)dll. Tetapi daftar panjang bug terkait keamanan applet memberi kami petunjuk mengapa teknologi ini telah ditinggalkan ...
Holger
1
Tapi saya harus memasukkan "dalam API biasa seperti API Pengumpulan", karena itulah yang saya fokuskan pada jawabannya.
Holger
2
Mengapa Anda harus mengeraskan kode Anda, yang tampaknya tidak relevan dengan keamanan, terhadap kode istimewa yang memungkinkan implementasi koleksi jahat masuk? Penelepon hipotetis itu masih akan tunduk pada perilaku jahat sebelum dan sesudah memanggil kode Anda. Bahkan tidak akan menyadari bahwa kode Anda adalah satu-satunya yang berperilaku benar. Menggunakan 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?
Holger
2
Saya tidak menganjurkan "tidak ada keamanan", tetapi keamanan di tempat yang tepat, alih-alih mencoba memperbaiki lingkungan yang rusak setelah fakta. Ini adalah klaim yang menarik bahwa " ada banyak koleksi yang tidak menerapkan super-type mereka dengan benar " tetapi sudah terlalu jauh, untuk meminta bukti, memperluas ini lebih jauh. Pertanyaan asli telah dijawab sepenuhnya; poin yang Anda bawa sekarang tidak pernah menjadi bagian dari itu. Seperti yang dikatakan, List.copyOf(strs)tidak bergantung pada kebenaran koleksi yang masuk dalam hal itu, dengan harga yang jelas. ArrayListadalah kompromi yang wajar untuk sehari-hari.
Holger
4
Ini dengan jelas mengatakan bahwa tidak ada spesifikasi seperti itu, untuk semua "metode dan aliran penciptaan statis serupa". Jadi jika Anda ingin benar-benar aman, Anda harus memanggil toArray()diri Anda sendiri, karena array tidak dapat memiliki perilaku yang ditimpa, diikuti dengan membuat salinan kumpulan array, seperti new ArrayList<>(Arrays.asList( strs.toArray(new String[0])))atau List.of(strs.toArray(new String[0])). Keduanya juga memiliki efek samping menegakkan tipe elemen. Saya, secara pribadi, tidak berpikir mereka akan membiarkan copyOfkompromi koleksi abadi, tetapi ada alternatif, dalam jawabannya.
Holger
1

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 constpengubah 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.Collectionitu sendiri adalah antarmuka yang bisa berubah: ia memiliki addmetode 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.Iterabletipe untuk mengekspos koleksi abadi di antarmuka saya. Semantik Iterabletidak 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 ( getmetode peta sesuai dengan definisi ini)

Alexander
sumber
Konsep antarmuka read-only dan imutabilitas adalah ortogonal. Inti dari C ++ dan C adalah bahwa mereka tidak mendukung integritas semantik . Argumen objek / struct juga menyalin - const & adalah optimasi cerdik untuk itu. Jika Anda harus lulus Iteratormaka itu praktis memaksa salinan elementwise, tapi itu tidak baik. Menggunakan forEachRemaining/ forEachjelas akan menjadi bencana total. (Saya juga harus menyebutkan bahwa Iteratorada removemetode.)
Tom Hawtin - tackline
Jika melihat perpustakaan koleksi Scala ada perbedaan ketat antara antarmuka yang bisa berubah dan tidak dapat diubah. Meskipun (saya kira) itu dibuat karena alasan yang sama sekali berbeda, tetapi masih merupakan demonstrasi bagaimana keselamatan dapat dicapai. Antarmuka baca-saja secara semantik mengasumsikan kekekalan, itulah yang ingin saya katakan. (Saya setuju tentang Iterabletidak benar-benar abadi, tetapi tidak melihat masalah dengan forEach*)
Alexander