Sekali lagi Tentang ImmutableList di Jawa

Dalam artikel saya sebelumnya, “ Mengalami ImmutableList di Jawa, ” saya mengusulkan solusi untuk masalah tidak adanya daftar yang tidak dapat diubah di Jawa, yang tidak diperbaiki , baik sekarang maupun sebelumnya , di Jawa.


Solusinya kemudian dikerjakan hanya pada tingkat "ada ide seperti itu", dan implementasi dalam kode itu bengkok, karena itu semuanya dianggap agak skeptis. Pada artikel ini, saya mengusulkan solusi yang dimodifikasi. Logika penggunaan dan API dibawa ke tingkat yang dapat diterima. Implementasi dalam kode hingga tingkat beta.


Pernyataan masalah


Kami akan menggunakan definisi dari artikel asli. Secara khusus, ini berarti ImmutableList adalah daftar referensi yang tidak dapat diubah untuk beberapa objek. Jika objek-objek ini berubah menjadi tidak berubah, maka daftar juga tidak akan menjadi objek abadi, terlepas dari namanya. Dalam praktiknya, ini tidak mungkin menyakiti siapa pun, tetapi untuk menghindari harapan yang tidak dapat dibenarkan perlu disebutkan.


Juga jelas bahwa ketidakmampuan daftar dapat "diretas" melalui refleksi, atau dengan membuat kelas Anda sendiri dalam paket yang sama, diikuti dengan naik ke bidang yang dilindungi daftar, atau sesuatu yang serupa.


Berbeda dengan artikel asli, kami tidak akan berpegang pada prinsip "semua atau tidak sama sekali": penulis di sana tampaknya percaya bahwa jika masalahnya tidak dapat diselesaikan di tingkat JDK, maka tidak ada yang harus dilakukan. (Sebenarnya, pertanyaan lain, "tidak dapat diselesaikan" atau "penulis Java tidak memiliki keinginan untuk menyelesaikannya." Sepertinya saya masih mungkin dengan menambahkan antarmuka, kelas, dan metode tambahan untuk membawa koleksi yang ada lebih dekat ke penampilan yang diinginkan, meskipun kurang cantik daripada jika Anda langsung memikirkannya, tapi sekarang ini bukan tentang itu.)


Kami akan membuat perpustakaan yang berhasil hidup berdampingan dengan koleksi yang ada di Jawa.


Gagasan utama perpustakaan:


  • Ada MutableList dan MutableList . Dengan casting type tidak mungkin untuk mendapatkan satu dari yang lain.
  • Dalam proyek kami, yang ingin kami tingkatkan menggunakan perpustakaan, kami mengganti semua List dengan salah satu dari dua antarmuka ini. Jika suatu saat Anda tidak dapat melakukannya tanpa List , maka pada kesempatan pertama kami akan mengonversi List dari / ke salah satu dari dua antarmuka. Hal yang sama berlaku untuk saat-saat menerima / mengirimkan data ke perpustakaan pihak ketiga menggunakan List .
  • Konversi timbal balik antara MutableList , MutableList , List harus dilakukan secepat mungkin (yaitu, tanpa menyalin daftar, jika mungkin). Tanpa konversi pulang-pergi "murah", seluruh ide mulai tampak meragukan.

Perlu dicatat bahwa hanya daftar yang dipertimbangkan, karena saat ini hanya diterapkan di perpustakaan. Tapi tidak ada yang mencegah perpustakaan dari melengkapi dengan Set dan Map .


API


Daftar Abadi


ImmutableList adalah penerus dari ReadOnlyList (yang, seperti pada artikel sebelumnya, adalah antarmuka List disalin dari mana semua metode bermutasi dilemparkan). Metode ditambahkan:


 List<E> toList(); MutableList<E> mutable(); boolean contentEquals(Iterable<? extends E> iterable); 

Metode toList memberikan kemampuan untuk meneruskan ImmutableList ke potongan kode yang menunggu List . Wrapper dikembalikan di mana semua metode modifikasi mengembalikan UnsupportedOperationException , dan metode yang tersisa diarahkan ke ImmutableList asli.


Metode mutable mengubah MutableList menjadi MutableList . Wrapper dikembalikan di mana semua metode dialihkan ke ImmutableList asli hingga perubahan pertama. Sebelum perubahan, pembungkus dilepaskan dari ImmutableList asli, menyalin isinya ke ArrayList internal, di mana semua operasi kemudian dialihkan.


Metode contentEquals dimaksudkan untuk membandingkan konten daftar dengan konten yang lulus Iterable sewenang-wenang (tentu saja, operasi ini hanya bermakna bagi implementasi Iterable yang memiliki urutan elemen yang berbeda).


Perhatikan bahwa dalam implementasi ReadOnlyList , metode iterator dan listIterator mengembalikan standar java.util.Iterator / java.util.ListIterator . Iterator ini berisi metode modifikasi yang harus ditekan dengan melempar UnsupportedOperationException . Akan lebih baik untuk membuat ReadOnlyIterator kami, tetapi dalam hal ini kami tidak dapat menulis for (Object item : immutableList) , yang akan segera merusak semua kesenangan menggunakan perpustakaan.


MutableList


MutableList adalah turunan dari List reguler. Metode ditambahkan:


 ImmutableList<E> snapshot(); void releaseSnapshot(); boolean contentEquals(Iterable<? extends E> iterable); 

Metode snapshot dirancang untuk mendapatkan "snapshot" dari keadaan MutableList saat ini sebagai MutableList . "Snapshot" disimpan di dalam MutableList , dan jika keadaan belum berubah pada saat pemanggilan metode selanjutnya, instance ImmutableList . "Snapshot" yang disimpan di dalamnya dibuang saat pertama kali metode modifikasi dipanggil, atau ketika releaseSnapshot dipanggil. Metode releaseSnapshot dapat digunakan untuk menghemat memori jika Anda yakin tidak ada yang akan membutuhkan "snapshot", tetapi metode modifikasi tidak akan segera dipanggil.


Mutabor


Kelas Mutabor menyediakan seperangkat metode statis yang merupakan "titik masuk" ke perpustakaan.


Ya, proyek ini sekarang disebut "mutabor" (itu sesuai dengan "bisa berubah", dan dalam terjemahan itu berarti "Saya akan mengubah", yang sesuai dengan ide cepat "mengubah" beberapa jenis koleksi menjadi yang lain).


 public static <E> ImmutableList<E> copyToImmutableList(E[] original); public static <E> ImmutableList<E> copyToImmutableList(Collection<? extends E> original); public static <E> ImmutableList<E> convertToImmutableList(Collection<? extends E> original); public static <E> MutableList<E> copyToMutableList(Collection<? extends E> original); public static <E> MutableList<E> convertToMutableList(List<E> original); 

copyTo* dirancang untuk membuat koleksi yang sesuai dengan menyalin data yang disediakan. Metode convertTo* konversi cepat koleksi yang ditransfer ke jenis yang diinginkan, dan jika tidak memungkinkan untuk dengan cepat dikonversi, mereka melakukan penyalinan lambat. Jika konversi cepat berhasil, maka koleksi asli dihapus, dan diasumsikan bahwa itu tidak akan digunakan di masa depan (meskipun bisa, tetapi ini hampir tidak masuk akal).


Panggilan ke konstruktor ImmutableList implementasi MutableList / MutableList disembunyikan. Diasumsikan bahwa pengguna hanya berurusan dengan antarmuka, ia tidak membuat objek seperti itu, dan menggunakan metode yang dijelaskan di atas untuk mengubah koleksi.


Detail implementasi


ImmutableListImpl


Merangkum berbagai objek. Implementasi kira-kira sesuai dengan implementasi ArrayList , dari mana semua metode modifikasi dan pemeriksaan untuk modifikasi bersamaan dilemparkan.


Implementasi metode toList dan contentEquals juga cukup sepele. Metode toList mengembalikan pembungkus yang mengalihkan panggilan ke ImmutableList diberikan; penyalinan data secara lambat tidak terjadi.


Metode mutable mengembalikan MutableListImpl dibuat berdasarkan MutableListImpl ini. Penyalinan data tidak terjadi sampai metode pengubahan apa pun dipanggil pada MutableList diterima.


MutableListImpl


Meringkas tautan ke List dan List ImmutableList . Saat membuat objek, hanya satu dari dua tautan ini yang selalu diisi, yang lainnya tetap null .


 protected ImmutableList<E> immutable; protected List<E> list; 

Metode Immutable mengarahkan ulang panggilan ke ImmutableList jika bukan null , dan ke List sebaliknya.


Metode memodifikasi mengarahkan panggilan ke List , setelah menginisialisasi:


 protected void beforeChange() { if (list == null) { list = new ArrayList<>(immutable.toList()); } immutable = null; } 

Metode snapshot terlihat seperti ini:


 public ImmutableList<E> snapshot() { if (immutable != null) { return immutable; } immutable = InternalUtils.convertToImmutableList(list); if (immutable != null) { //    //   ,  . //     immutable     . list = null; return immutable; } immutable = InternalUtils.copyToImmutableList(list); return immutable; } 

Penerapan contentEquals releaseSnapshot dan contentEquals sepele.


Pendekatan ini memungkinkan Anda untuk meminimalkan jumlah salinan data selama penggunaan "biasa", menggantikan salinan dengan konversi cepat.


Konversi daftar cepat


Konversi cepat dimungkinkan untuk kelas ArrayList atau Arrays$ArrayList (hasil dari metode Arrays.asList() ). Dalam praktiknya, dalam sebagian besar kasus, justru kelas-kelas inilah yang muncul.


Di dalam kelas-kelas ini berisi array elemen. Inti dari konversi cepat adalah untuk mendapatkan referensi ke array ini melalui refleksi (ini adalah bidang pribadi) dan menggantinya dengan referensi ke array kosong. Ini memastikan bahwa satu-satunya referensi ke array tetap dengan objek kita, dan array tetap tidak berubah.


Di versi perpustakaan sebelumnya, konversi cepat dari jenis koleksi dilakukan dengan memanggil konstruktor. Pada saat yang sama, objek koleksi asli memburuk (menjadi tidak cocok untuk digunakan lebih lanjut), yang tidak Anda sadari secara tidak terduga dari perancang. Sekarang, metode statis khusus digunakan untuk konversi, dan koleksi asli tidak rusak, tetapi hanya dihapus. Dengan demikian, perilaku yang tidak biasa yang menakutkan dihilangkan.


Masalah dengan equals / hashCode


Koleksi Java menggunakan pendekatan yang sangat aneh untuk mengimplementasikan metode equals dan hashCode .


Perbandingan dilakukan sesuai dengan konten, yang tampaknya logis, tetapi kelas daftar itu sendiri tidak diperhitungkan. Karenanya, misalnya, ArrayList dan LinkedList dengan konten yang sama akan equals .


Inilah implementasi equals / hashCode dari AbstractList (dari mana ArrayList diwarisi)
 public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof List)) return false; ListIterator<E> e1 = listIterator(); ListIterator e2 = ((List) o).listIterator(); while (e1.hasNext() && e2.hasNext()) { E o1 = e1.next(); Object o2 = e2.next(); if (!(o1==null ? o2==null : o1.equals(o2))) return false; } return !(e1.hasNext() || e2.hasNext()); } public int hashCode() { int hashCode = 1; for (E e : this) hashCode = 31*hashCode + (e==null ? 0 : e.hashCode()); return hashCode; } 

Dengan demikian, sekarang benar-benar semua implementasi List diharuskan memiliki implementasi yang equals (dan, sebagai hasilnya, hashCode ). Kalau tidak, Anda bisa mendapatkan situasi ketika a.equals(b) && !b.equals(a) , yang tidak baik. Situasi serupa terjadi dengan Set dan Map .


Dalam aplikasi ke pustaka, ini berarti bahwa implementasi equals dan hashCode untuk MutableList ditentukan sebelumnya, dan dalam implementasi seperti itu, MutableList dan MutableList dengan konten yang sama tidak dapat equals (karena ImmutableList bukan List ). Oleh karena itu, metode contentEquals telah ditambahkan untuk membandingkan konten.


Implementasi metode equals dan hashCode untuk ImmutableList dibuat sangat mirip dengan versi dari AbstractList , tetapi dengan penggantian List oleh ReadOnlyList .


Total


Sumber dan tes perpustakaan diposting dengan referensi dalam bentuk proyek pakar.


Jika seseorang ingin menggunakan perpustakaan, ia telah membuat grup untuk "umpan balik".


Menggunakan perpustakaan cukup jelas, berikut adalah contoh singkat:


 private boolean myBusinessProcess() { List<Entity> tempFromDb = queryEntitiesFromDatabase("SELECT * FROM my_table"); ImmutableList<Entity> fromDb = Mutabor.convertToImmutableList(tempFromDb); if (fromDb.isEmpty() || !someChecksPassed(fromDb)) { return false; } //... MutableList<Entity> list = fromDb.mutable(); //time to change list.remove(1); ImmutableList<Entity> processed = list.snapshot(); //time to change ended //... if (!callSideLibraryExpectsListParameter(processed.toList())) { return false; } for (Entity entity : processed) { outputToUI(entity); } return true; } 

Semoga beruntung untuk semuanya! Kirim laporan bug!

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


All Articles