Cloak Around ImmutableList di Jawa

Saya membaca artikel “Tidak akan ada koleksi abadi di Jawa - tidak sekarang, tidak akan pernah” dan berpikir bahwa masalah tidak adanya daftar abadi di Jawa, yang membuat penulis sedih, cukup dapat dipecahkan dalam skala terbatas. Saya menawarkan pemikiran dan potongan kode saya tentang hal ini.


(Ini adalah artikel jawaban, baca artikel aslinya terlebih dahulu.)


UnmodifiableList vs ImmutableList


Pertanyaan pertama yang muncul adalah: mengapa saya memerlukan UnmodifiableList , jika ada ImmutableList ? Sebagai hasil dari diskusi, dua ide mengenai makna UnmodifiableList terlihat di komentar dari artikel asli:


  • metode menerima Daftar UnmodifiableList , tidak dapat mengubahnya sendiri, tetapi tahu bahwa isinya dapat diubah oleh utas lain (dan tahu bagaimana menanganinya dengan benar)
  • utas lainnya tidak memengaruhi, UnmodifiableList dan ImmutableList setara untuk suatu metode, tetapi UnmodifiableList digunakan sebagai yang lebih "ringan".

Opsi pertama tampaknya terlalu jarang dalam praktik. Dengan demikian, jika Anda dapat membuat implementasi ImmutableList “mudah”, maka UnmodifiableList menjadi tidak terlalu diperlukan. Karenanya, di masa mendatang kami akan melupakannya dan hanya akan mengimplementasikan ImmutableList .


Pernyataan masalah


Kami akan menerapkan opsi ImmutableList :


  • API harus identik dengan API List biasa di bagian "membaca". Bagian "menulis" harus tidak ada.
  • List dan List ImmutableList tidak boleh dikaitkan dengan hubungan pewarisan. Kenapa begitu - mengerti artikel aslinya.
  • Masuk akal untuk melakukan implementasi dengan analogi dengan ArrayList . Ini adalah opsi termudah.
  • Implementasi harus menghindari menyalin array jika memungkinkan.

Implementasi ImmutableList


Pertama, kami berurusan dengan API. Kami memeriksa antarmuka Collection and List dan menyalin bagian "membaca" dari mereka ke antarmuka baru kami.


 public interface ReadOnlyCollection<E> extends Iterable<E> { int size(); boolean isEmpty(); boolean contains(Object o); Object[] toArray(); <T> T[] toArray(T[] a); boolean containsAll(Collection<?> c); } public interface ReadOnlyList<E> extends ReadOnlyCollection<E> { E get(int index); int indexOf(Object o); int lastIndexOf(Object o); ListIterator<E> listIterator(); ListIterator<E> listIterator(int index); ReadOnlyList<E> subList(int fromIndex, int toIndex); } 

Selanjutnya, buat kelas ImmutableList . Tanda tangan ini mirip dengan ArrayList (tetapi mengimplementasikan antarmuka ReadOnlyList bukan List ).


 public class ImmutableList<E> implements ReadOnlyList<E>, RandomAccess, Cloneable, Serializable 

Kami menyalin implementasi kelas dari ArrayList dan dengan tegas refactor, membuang semua yang berhubungan dengan bagian "menulis", memeriksa modifikasi bersamaan, dll.


Konstruktor akan sebagai berikut:


 public ImmutableList() public ImmutableList(E[] original) public ImmutableList(Collection<? extends E> original) 

Yang pertama membuat daftar kosong. Yang kedua membuat daftar dengan menyalin array. Kami tidak dapat melakukannya tanpa menyalin jika kami ingin mencapai yang abadi. Yang ketiga lebih menarik. Konstruktor ArrayList serupa juga menyalin data dari koleksi. Kami akan melakukan hal yang sama, kecuali jika orginal adalah turunan dari ArrayList atau Arrays$ArrayList (inilah yang dikembalikan oleh metode Arrays.asList() ). Kita dapat dengan aman berasumsi bahwa kasing ini akan mencakup 90% dari panggilan konstruktor.


Dalam kasus ini, kami akan "mencuri" array original melalui refleksi (ada harapan bahwa ini lebih cepat daripada menyalin array gigabyte). Inti dari "pencurian":


  • kita sampai ke bidang pribadi original , yang menyimpan array ( ArrayList.elementData )
  • salin tautan ke array ke diri kita sendiri
  • dimasukkan ke dalam bidang sumber nol

 protected static final Field data_ArrayList; static { try { data_ArrayList = ArrayList.class.getDeclaredField("elementData"); data_ArrayList.setAccessible(true); } catch (NoSuchFieldException | SecurityException e) { throw new IllegalStateException(e); } } public ImmutableList(Collection<? extends E> original) { Object[] arr = null; if (original instanceof ArrayList) { try { arr = (Object[]) data_ArrayList.get(original); data_ArrayList.set(original, null); } catch (@SuppressWarnings("unused") IllegalArgumentException | IllegalAccessException e) { arr = null; } } if (arr == null) { //   ArrayList,      -  arr = original.toArray(); } this.data = arr; } 

Sebagai sebuah kontrak, kami mengasumsikan bahwa ketika konstruktor dipanggil, daftar yang dapat ImmutableList ke ImmutableList . Daftar asli tidak dapat digunakan setelah itu. Saat mencoba menggunakan, NullPointerException tiba. Ini memastikan bahwa array "dicuri" tidak akan berubah dan daftar kami akan benar-benar berubah (kecuali untuk kasus ketika seseorang sampai ke array melalui refleksi).


Kelas lainnya


Misalkan kita memutuskan untuk menggunakan ImmutableList dalam proyek nyata.


Proyek berinteraksi dengan perpustakaan: menerima dari mereka dan mengirimkannya berbagai daftar. Dalam sebagian besar kasus, daftar ini akan menjadi ArrayList . Implementasi ImmutableList dijelaskan memungkinkan ImmutableList untuk dengan cepat mengkonversi ArrayList dihasilkan menjadi ImmutableList . Anda juga harus menerapkan konversi untuk daftar yang dikirim ke perpustakaan: ImmutableList to List . Untuk konversi cepat, Anda memerlukan pembungkus ImmutableList yang mengimplementasikan List , mengeluarkan pengecualian ketika mencoba menulis ke daftar (mirip dengan Collections.unmodifiableList ).


Juga, proyek itu sendiri entah bagaimana memproses daftar. Masuk akal untuk membuat kelas MutableList yang mewakili daftar bisa berubah, dengan implementasi berdasarkan ArrayList . Dalam hal ini, Anda dapat memperbaiki proyek dengan mengganti semua kelas ArrayList yang secara eksplisit menyatakan tujuannya: MutableList atau MutableList .


Perlu konversi cepat dari MutableList ke MutableList dan sebaliknya. Pada saat yang sama, tidak seperti konversi ArrayList ke ImmutableList , kami tidak dapat lagi merusak daftar asli.


Mengkonversi biasanya akan menjadi lambat, dengan menyalin array. Tetapi untuk kasus ketika MutableList diterima tidak selalu berubah, Anda dapat membuat pembungkus: MutableList , yang menyimpan referensi ke ImmutableList dan menggunakannya untuk metode "membaca", dan jika metode "menulis" dipanggil, maka hanya lupa tentang ImmutableList , setelah menyalin isinya array ke dirinya sendiri, dan kemudian sudah berfungsi dengan array-nya (sesuatu yang mirip adalah di CopyOnWriteArrayList ).


Mengubah "kembali" menyiratkan menerima snapshot dari konten MutableList pada saat metode ini dipanggil. Sekali lagi, dalam sebagian besar kasus, Anda tidak dapat melakukannya tanpa menyalin array, tetapi Anda dapat membuat pembungkus untuk mengoptimalkan kasus beberapa konversi di mana konten MutableList tidak berubah. Opsi lain untuk mengonversi "kembali": beberapa data dikumpulkan di MutableList , dan ketika pengumpulan data selesai, MutableList perlu dikonversi selamanya ke ImmutableList . Ini juga diterapkan tanpa masalah dengan pembungkus lain.


Total


Hasil percobaan dalam bentuk kode diposting di sini


ImmutableList sendiri ImmutableList , yang dijelaskan di bagian "Kelas lain" (belum?).


Kita dapat berasumsi bahwa premis artikel asli, “koleksi abadi di Jawa tidak akan” salah.


Jika ada keinginan, maka sangat mungkin untuk menggunakan pendekatan serupa. Ya, dengan kruk kecil. Ya, tidak di dalam keseluruhan sistem, tetapi hanya dalam proyek mereka (meskipun jika banyak menembus, maka secara bertahap akan ditarik ke perpustakaan).


Satu hal: jika ada keinginan ... (Tahiti, Tahiti ... Kami tidak ada di Tahiti! Mereka memberi kami makan dengan baik di sini.)

Source: https://habr.com/ru/post/id470257/


All Articles