Dalam Fitur Tersembunyi di Jawa , jawaban teratas menyebutkan Inisialisasi Brace Ganda , dengan sintaks yang sangat menarik:
Set<String> flavors = new HashSet<String>() {{
add("vanilla");
add("strawberry");
add("chocolate");
add("butter pecan");
}};
Ungkapan ini membuat kelas dalam anonim hanya dengan inisialisasi instance di dalamnya, yang "dapat menggunakan metode [...] dalam lingkup yang berisi".
Pertanyaan utama: Apakah ini tidak efisien karena kedengarannya? Haruskah penggunaannya terbatas pada inisialisasi sekali saja? (Dan tentu saja pamer!)
Pertanyaan kedua: HashSet baru harus menjadi "ini" yang digunakan dalam initializer instance ... adakah yang bisa menjelaskan mekanisme ini?
Pertanyaan ketiga: Apakah idiom ini terlalu tidak jelas untuk digunakan dalam kode produksi?
Ringkasan: Jawaban yang sangat, sangat bagus, terima kasih semuanya. Pada pertanyaan (3), orang-orang merasa sintaksinya harus jelas (walaupun saya akan merekomendasikan komentar sesekali, terutama jika kode Anda akan diteruskan ke pengembang yang mungkin tidak akrab dengannya).
Pada pertanyaan (1), kode yang dihasilkan harus berjalan dengan cepat. File .class tambahan memang menyebabkan kekacauan file jar, dan sedikit memperlambat startup program (terima kasih kepada @coobird untuk mengukurnya). @ Thilo menunjukkan bahwa pengumpulan sampah dapat terpengaruh, dan biaya memori untuk kelas-kelas tambahan mungkin menjadi faktor dalam beberapa kasus.
Pertanyaan (2) ternyata paling menarik bagi saya. Jika saya mengerti jawabannya, apa yang terjadi di DBI adalah bahwa kelas dalam anonim memperluas kelas objek yang sedang dibangun oleh operator baru, dan karenanya memiliki nilai "ini" yang merujuk pada instance yang sedang dibangun. Sangat rapi.
Secara keseluruhan, DBI menganggap saya sebagai suatu keingintahuan intelektual. Coobird dan yang lainnya menunjukkan bahwa Anda dapat mencapai efek yang sama dengan Arrays.asList, metode varargs, Google Collections, dan literal Java 7 Collection yang diusulkan. Bahasa JVM yang lebih baru seperti Scala, JRuby, dan Groovy juga menawarkan notasi ringkas untuk konstruksi daftar, dan beroperasi dengan baik dengan Java. Mengingat bahwa DBI mengacaukan classpath, memperlambat memuat kelas sedikit, dan membuat kode sedikit lebih jelas, saya mungkin akan menghindarinya. Namun, saya berencana untuk menceritakan hal ini pada seorang teman yang baru saja mendapatkan SCJP-nya dan suka sekali dorongan sifat alami tentang semantik Jawa! ;-) Terimakasih semuanya!
7/2017: Baeldung memiliki ringkasan yang bagus untuk inisialisasi penjepit ganda dan menganggapnya sebagai anti-pola.
12/2017: @Basil Bourque mencatat bahwa di Java 9 yang baru Anda dapat mengatakan:
Set<String> flavors = Set.of("vanilla", "strawberry", "chocolate", "butter pecan");
Itu pasti cara untuk pergi. Jika Anda terjebak dengan versi yang lebih lama, lihat ImmutableSet dari Google Collections .
sumber
flavors
menjadiHashSet
, tetapi sayangnya itu adalah subkelas anonim.Set<String> flavors = Set.of( "vanilla" , "strawberry" , "chocolate" , "butter pecan" ) ;
Jawaban:
Inilah masalahnya ketika saya terlalu terbawa oleh kelas batin anonim:
Ini semua kelas yang dihasilkan ketika saya membuat aplikasi sederhana, dan menggunakan banyak kelas dalam anonim - setiap kelas akan dikompilasi menjadi
class
file terpisah ."Inisialisasi penjepit ganda", sebagaimana telah disebutkan, adalah kelas dalam anonim dengan blok inisialisasi instance, yang berarti bahwa kelas baru dibuat untuk setiap "inisialisasi", semua untuk tujuan biasanya membuat objek tunggal.
Mempertimbangkan bahwa Java Virtual Machine perlu membaca semua kelas ketika menggunakannya, yang dapat menyebabkan beberapa waktu dalam proses verifikasi bytecode dan semacamnya. Belum lagi peningkatan ruang disk yang dibutuhkan untuk menyimpan semua
class
file tersebut.Sepertinya ada sedikit overhead ketika menggunakan inisialisasi double-brace, jadi mungkin bukan ide yang baik untuk terlalu berlebihan dengan itu. Tapi seperti yang dicatat Eddie di komentar, tidak mungkin untuk benar-benar yakin akan dampaknya.
Hanya untuk referensi, inisialisasi penjepit ganda adalah sebagai berikut:
Ini terlihat seperti fitur "tersembunyi" dari Java, tetapi itu hanya penulisan ulang dari:
Jadi itu pada dasarnya contoh inisialisasi blok yang merupakan bagian dari kelas dalam anonim .
Proposal Koleksi Literatur Joshua Bloch untuk Project Coin sejalan:
Sayangnya, itu tidak membuat jalan ke Jawa 7 atau 8 dan disimpan tanpa batas.
Percobaan
Inilah eksperimen sederhana yang telah saya uji - buat 1000
ArrayList
s dengan elemen-elemen"Hello"
dan"World!"
ditambahkan ke mereka melaluiadd
metode, menggunakan dua metode:Metode 1: Inisialisasi Brace Ganda
Metode 2: Instantiate
ArrayList
danadd
Saya membuat program sederhana untuk menulis file sumber Java untuk melakukan 1000 inisialisasi menggunakan dua metode:
Tes 1:
Tes 2:
Harap dicatat, bahwa waktu yang telah berlalu untuk menginisialisasi 1000
ArrayList
detik dan 1000 kelas dalam anonim yang diperluasArrayList
diperiksa menggunakanSystem.currentTimeMillis
, sehingga timer tidak memiliki resolusi yang sangat tinggi. Pada sistem Windows saya, resolusinya sekitar 15-16 milidetik.Hasil untuk 10 tes kedua adalah sebagai berikut:
Seperti dapat dilihat, inisialisasi brace ganda memiliki waktu eksekusi yang nyata sekitar 190 ms.
Sementara itu,
ArrayList
waktu pelaksanaan inisialisasi keluar menjadi 0 ms. Tentu saja, resolusi pengatur waktu harus diperhitungkan, tetapi kemungkinan waktunya di bawah 15 ms.Jadi, tampaknya ada perbedaan nyata dalam waktu pelaksanaan kedua metode. Tampaknya memang ada beberapa overhead dalam dua metode inisialisasi.
Dan ya, ada 1000
.class
file yang dihasilkan dengan menyusunTest1
program uji inisialisasi brace ganda.sumber
Salah satu properti dari pendekatan ini yang belum ditunjukkan sejauh ini adalah bahwa karena Anda membuat kelas dalam, seluruh kelas yang mengandung ditangkap dalam cakupannya. Ini berarti bahwa selama Set Anda masih hidup, itu akan mempertahankan pointer ke instance yang berisi (
this$0
) dan menjaga agar tidak dikumpulkan sampah, yang bisa menjadi masalah.Ini, dan fakta bahwa kelas baru akan dibuat di tempat pertama meskipun HashSet biasa akan bekerja dengan baik (atau bahkan lebih baik), membuat saya tidak ingin menggunakan konstruk ini (meskipun saya benar-benar merindukan gula sintaksis).
Beginilah cara kerja kelas batin. Mereka mendapatkan milik mereka sendiri
this
, tetapi mereka juga memiliki pointer ke instance induk, sehingga Anda dapat memanggil metode pada objek yang berisi juga. Dalam kasus konflik penamaan, kelas dalam (dalam kasus Anda HashSet) diutamakan, tetapi Anda bisa mengawali "ini" dengan nama kelas untuk mendapatkan metode luar juga.Untuk lebih jelas tentang subkelas anonim yang sedang dibuat, Anda bisa menentukan metode di sana juga. Misalnya menimpa
HashSet.add()
sumber
Setiap kali seseorang menggunakan inisialisasi penjepit ganda, anak kucing terbunuh.
Terlepas dari sintaks yang agak tidak biasa dan tidak benar-benar idiomatis (rasanya bisa diperdebatkan, tentu saja), Anda tidak perlu menciptakan dua masalah signifikan dalam aplikasi Anda, yang baru-baru ini saya blogging tentang lebih detail di sini .
1. Anda membuat terlalu banyak kelas anonim
Setiap kali Anda menggunakan inisialisasi brace ganda, kelas baru dibuat. Misalnya contoh ini:
... akan menghasilkan kelas-kelas ini:
Itu sedikit overhead untuk classloader Anda - gratis! Tentu saja tidak akan memakan banyak waktu inisialisasi jika Anda melakukannya sekali. Tetapi jika Anda melakukan ini 20'000 kali di seluruh aplikasi perusahaan Anda ... semua itu menumpuk memori hanya untuk sedikit "sintaks gula"?
2. Anda berpotensi membuat kebocoran memori!
Jika Anda mengambil kode di atas dan mengembalikan peta itu dari suatu metode, penelepon dari metode itu mungkin secara tidak curiga berpegangan pada sumber daya yang sangat berat yang tidak dapat dikumpulkan dari sampah. Perhatikan contoh berikut:
Yang dikembalikan
Map
sekarang akan berisi referensi ke instance terlampirReallyHeavyObject
. Anda mungkin tidak ingin mengambil risiko itu:Gambar dari http://blog.jooq.org/2014/12/08/dont-be-clever-the-double-curly-braces-anti-pattern/
3. Anda dapat berpura-pura bahwa Java memiliki peta literal
Untuk menjawab pertanyaan Anda yang sebenarnya, orang-orang telah menggunakan sintaks ini untuk berpura-pura bahwa Java memiliki sesuatu seperti literal peta, mirip dengan literal array yang ada:
Beberapa orang mungkin merasa ini merangsang secara sintaksis.
sumber
Mengambil kelas tes berikut:
dan kemudian mendekompilasi file kelas, saya melihat:
Ini tidak terlihat sangat tidak efisien bagi saya. Jika saya khawatir tentang kinerja untuk sesuatu seperti ini, saya akan menggambarkannya. Dan pertanyaan Anda # 2 dijawab oleh kode di atas: Anda berada di dalam konstruktor implisit (dan instance initializer) untuk kelas batin Anda, jadi "
this
" merujuk ke kelas batin ini.Ya, sintaks ini tidak jelas, tetapi komentar dapat menjelaskan penggunaan sintaksis yang tidak jelas. Untuk mengklarifikasi sintaksis, kebanyakan orang terbiasa dengan blok penginisialisasi statis (JLS 8.7 Static Inisialisasi):
Anda juga dapat menggunakan sintaksis yang mirip (tanpa kata "
static
") untuk penggunaan konstruktor (JLS 8.6 Inisialisasi Instance), meskipun saya belum pernah melihat ini digunakan dalam kode produksi. Ini jauh kurang dikenal.Jika Anda tidak memiliki konstruktor default, maka blok kode antara
{
dan}
diubah menjadi konstruktor oleh kompiler. Dengan mengingat hal ini, uraikan kode penjepit ganda:Blok kode antara kawat gigi paling dalam diubah menjadi konstruktor oleh kompiler. Kawat gigi paling luar membatasi kelas dalam anonim. Untuk mengambil ini langkah terakhir untuk membuat semuanya menjadi non-anonim:
Untuk keperluan inisialisasi, saya akan mengatakan tidak ada overhead apa pun (atau sangat kecil sehingga dapat diabaikan). Namun, setiap penggunaan tidak
flavors
akan bertentanganHashSet
tetapi sebaliknyaMyHashSet
. Mungkin ada overhead kecil (dan sangat mungkin diabaikan) untuk ini. Tapi sekali lagi, sebelum saya khawatir tentang itu, saya akan profil itu.Sekali lagi, untuk pertanyaan Anda # 2, kode di atas adalah ekuivalen logis dan eksplisit dari inisialisasi penjepit ganda, dan membuatnya jelas di mana "
this
" merujuk: Untuk kelas dalam yang diperluasHashSet
.Jika Anda memiliki pertanyaan tentang detail inisialisasi contoh, lihat detail dalam dokumentasi JLS .
sumber
super()
sebagai baris kedua dari konstruktor implisit, tetapi harus didahulukan. (Saya sudah mengujinya, dan itu tidak akan dikompilasi.)rawan bocor
Saya telah memutuskan untuk berpadu. Dampak kinerja meliputi: operasi disk + unzip (untuk toples), verifikasi kelas, ruang perm-gen (untuk Sun's Hotspot JVM). Namun, yang terburuk: rawan bocor. Anda tidak bisa kembali begitu saja.
Jadi jika set lolos ke bagian lain yang dimuat oleh classloader yang berbeda dan referensi disimpan di sana, seluruh pohon kelas + classloader akan bocor. Untuk menghindari itu, salinan ke HashMap diperlukan
new LinkedHashSet(new ArrayList(){{add("xxx);add("yyy");}})
,. Tidak lagi lucu. Saya tidak menggunakan idiom itu sendiri, melainkan sepertinew LinkedHashSet(Arrays.asList("xxx","YYY"));
sumber
Memuat banyak kelas dapat menambahkan beberapa milidetik ke awal. Jika startup tidak begitu penting dan Anda melihat efisiensi kelas setelah startup tidak ada perbedaan.
cetakan
sumber
compareCollections
digabungkan dengan||
bukan&&
? Menggunakan&&
tampaknya tidak hanya salah secara semantik, itu meniadakan niat untuk mengukur kinerja, karena hanya kondisi pertama yang akan diuji sama sekali. Selain itu, pengoptimal yang pintar dapat mengenali bahwa kondisi tidak akan pernah berubah selama iterasi.Untuk membuat set, Anda dapat menggunakan metode pabrik varargs alih-alih inisialisasi brace ganda:
Pustaka Google Collections memiliki banyak metode kenyamanan seperti ini, serta banyak fungsi berguna lainnya.
Adapun ketidakjelasan idiom itu, saya temui dan menggunakannya dalam kode produksi sepanjang waktu. Saya akan lebih khawatir tentang programmer yang bingung dengan idiom yang diizinkan untuk menulis kode produksi.
sumber
Selain efisiensi, saya jarang menemukan diri saya berharap untuk pembuatan koleksi deklaratif di luar unit test. Saya percaya bahwa sintaks penjepit ganda sangat mudah dibaca.
Cara lain untuk mencapai konstruksi deklaratif daftar secara khusus adalah menggunakan
Arrays.asList(T ...)
seperti:Keterbatasan pendekatan ini tentu saja adalah Anda tidak dapat mengontrol jenis daftar spesifik yang akan dihasilkan.
sumber
new ArrayList<String>(Arrays.asList("vanilla", "strawberry", "chocolate"))
mengatasi masalah ini.Biasanya tidak ada yang tidak efisien tentang hal itu. Pada umumnya tidak penting bagi JVM bahwa Anda telah membuat subkelas dan menambahkan konstruktor ke dalamnya - itu hal biasa yang harus dilakukan sehari-hari dalam bahasa berorientasi objek. Saya dapat memikirkan kasus-kasus yang cukup dibuat di mana Anda dapat menyebabkan inefisiensi dengan melakukan ini (misalnya Anda memiliki metode yang berulang kali disebut yang akhirnya mengambil campuran kelas yang berbeda karena subkelas ini, sedangkan kelas biasa lewat akan benar-benar dapat diprediksi - - dalam kasus terakhir, kompiler JIT dapat membuat optimisasi yang tidak layak pada awalnya). Tapi sungguh, saya pikir kasus-kasus di mana itu akan menjadi masalah sangat dibuat-buat.
Saya akan melihat masalah lebih dari sudut pandang apakah Anda ingin "mengacaukan segalanya" dengan banyak kelas anonim. Sebagai panduan kasar, pertimbangkan untuk menggunakan idiom tidak lebih dari yang akan Anda gunakan, katakanlah, kelas anonim untuk penangan acara.
Di (2), Anda berada di dalam konstruktor suatu objek, jadi "ini" mengacu pada objek yang Anda buat. Itu tidak berbeda dengan konstruktor lain.
Adapun (3), itu benar-benar tergantung pada siapa yang menjaga kode Anda, saya kira. Jika Anda tidak tahu ini sebelumnya, maka tolok ukur yang saya sarankan gunakan adalah "apakah Anda melihat ini dalam kode sumber ke JDK?" (dalam hal ini, saya tidak ingat melihat banyak inisialisasi anonim, dan tentu saja tidak dalam kasus di mana itu satu - satunya konten dari kelas anonim). Dalam sebagian besar proyek berukuran sedang, saya berpendapat Anda benar-benar akan membutuhkan programmer Anda untuk memahami sumber JDK di beberapa titik atau lainnya, sehingga setiap sintaks atau idiom yang digunakan ada "permainan yang adil". Di luar itu, saya katakan, latih orang-orang tentang sintaksis itu jika Anda memiliki kendali atas siapa yang menjaga kode, komentar lain atau hindari.
sumber
Inisialisasi double-brace adalah peretasan yang tidak perlu yang dapat menyebabkan kebocoran memori dan masalah lainnya
Tidak ada alasan yang sah untuk menggunakan "trik" ini. Jambu memberikan koleksi abadi bagus yang mencakup pabrik dan pembangun statis, memungkinkan Anda untuk mengisi koleksi Anda di tempat yang dinyatakan dalam bersih, mudah dibaca, dan aman sintaksis .
Contoh dalam pertanyaan menjadi:
Tidak hanya lebih pendek dan lebih mudah dibaca, tetapi juga menghindari banyak masalah dengan pola kurung ganda yang dijelaskan dalam jawaban lain . Tentu, ia melakukan mirip dengan yang dibangun langsung
HashMap
, tetapi berbahaya dan rawan kesalahan, dan ada opsi yang lebih baik.Setiap kali Anda menemukan diri Anda mempertimbangkan inisialisasi penguat ganda, Anda harus memeriksa ulang API Anda atau memperkenalkan yang baru untuk mengatasi masalah dengan benar, daripada memanfaatkan trik sintaksis.
Rawan Kesalahan sekarang menandai anti-pola ini .
sumber
static
konteks). Tautan Rawan Kesalahan di bagian bawah pertanyaan saya membahas hal ini lebih lanjut.Saya sedang meneliti ini dan memutuskan untuk melakukan tes yang lebih mendalam daripada yang diberikan oleh jawaban yang valid.
Ini kodenya: https://gist.github.com/4368924
dan inilah kesimpulan saya
Biarkan aku tahu apa yang Anda pikirkan :)
sumber
Saya menjawab Nat kedua, kecuali saya akan menggunakan loop daripada membuat dan segera melemparkan Daftar implisit dari asList (elemen):
sumber
HashSet
kapasitas yang disarankan - ingat faktor beban).Meskipun sintaks ini bisa nyaman, ia juga menambahkan banyak referensi $ 0 ini karena ini menjadi bersarang dan mungkin sulit untuk melangkah debug ke inisialisasi kecuali jika breakpoint ditetapkan pada masing-masing. Untuk alasan itu, saya hanya merekomendasikan menggunakan ini untuk setter dangkal, terutama diatur ke konstanta, dan tempat-tempat di mana subclass anonim tidak penting (seperti tidak ada serialisasi yang terlibat).
sumber
Mario Gleichman menjelaskan cara menggunakan fungsi generik Java 1.5 untuk mensimulasikan literal Daftar Scala, meskipun sayangnya Anda berakhir dengan kekal Daftar yang .
Dia mendefinisikan kelas ini:
dan menggunakannya sebagai berikut:
Google Collections, sekarang bagian dari Guava mendukung ide serupa untuk konstruksi daftar. Dalam wawancara ini , Jared Levy mengatakan:
7/10/2014: Kalau saja bisa sesederhana Python:
2/21/2020: Di Java 11 Anda sekarang dapat mengatakan:
sumber
Ini akan memanggil
add()
untuk setiap anggota. Jika Anda bisa menemukan cara yang lebih efisien untuk memasukkan item ke dalam hash set, maka gunakan itu. Perhatikan bahwa kelas dalam kemungkinan akan menghasilkan sampah, jika Anda sensitif tentang hal itu.Sepertinya saya seolah-olah konteksnya adalah objek yang dikembalikan oleh
new
, yaituHashSet
.Jika Anda perlu bertanya ... Kemungkinan besar: akankah orang-orang yang datang setelah Anda mengetahui hal ini atau tidak? Apakah mudah dimengerti dan dijelaskan? Jika Anda bisa menjawab "ya" untuk keduanya, jangan ragu untuk menggunakannya.
sumber