Efisiensi Java “Inisialisasi Brace Ganda”?


824

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 .


33
Aroma kode yang saya lihat di sini adalah, bahwa pembaca yang naif akan mengharapkan flavorsmenjadi HashSet, tetapi sayangnya itu adalah subkelas anonim.
Elazar Leibovich

6
Jika Anda mempertimbangkan untuk menjalankan alih-alih memuat kinerja tidak ada perbedaan, lihat jawaban saya.
Peter Lawrey

4
Saya suka Anda membuat ringkasan, saya pikir ini adalah latihan yang bermanfaat bagi Anda untuk meningkatkan pemahaman dan komunitas.
Patrick Murphy

3
Itu tidak jelas menurut saya. Pembaca harus tahu bahwa dobel ... o tunggu, @ElazarLeibovich sudah mengatakan itu dalam komentarnya . Penginisialisasi brace ganda itu sendiri tidak ada sebagai konstruksi bahasa, itu hanya kombinasi dari subkelas anonim dan penginisialisasi instance. Satu-satunya hal adalah, bahwa orang perlu menyadari hal ini.
MC Emperor

8
Java 9 menawarkan Metode Pabrik Statis Immutable Set yang dapat menggantikan penggunaan DCI dalam beberapa situasi:Set<String> flavors = Set.of( "vanilla" , "strawberry" , "chocolate" , "butter pecan" ) ;
Basil Bourque

Jawaban:


607

Inilah masalahnya ketika saya terlalu terbawa oleh kelas batin anonim:

2009/05/27  16:35             1,602 DemoApp2$1.class
2009/05/27  16:35             1,976 DemoApp2$10.class
2009/05/27  16:35             1,919 DemoApp2$11.class
2009/05/27  16:35             2,404 DemoApp2$12.class
2009/05/27  16:35             1,197 DemoApp2$13.class

/* snip */

2009/05/27  16:35             1,953 DemoApp2$30.class
2009/05/27  16:35             1,910 DemoApp2$31.class
2009/05/27  16:35             2,007 DemoApp2$32.class
2009/05/27  16:35               926 DemoApp2$33$1$1.class
2009/05/27  16:35             4,104 DemoApp2$33$1.class
2009/05/27  16:35             2,849 DemoApp2$33.class
2009/05/27  16:35               926 DemoApp2$34$1$1.class
2009/05/27  16:35             4,234 DemoApp2$34$1.class
2009/05/27  16:35             2,849 DemoApp2$34.class

/* snip */

2009/05/27  16:35               614 DemoApp2$40.class
2009/05/27  16:35             2,344 DemoApp2$5.class
2009/05/27  16:35             1,551 DemoApp2$6.class
2009/05/27  16:35             1,604 DemoApp2$7.class
2009/05/27  16:35             1,809 DemoApp2$8.class
2009/05/27  16:35             2,022 DemoApp2$9.class

Ini semua kelas yang dihasilkan ketika saya membuat aplikasi sederhana, dan menggunakan banyak kelas dalam anonim - setiap kelas akan dikompilasi menjadi classfile 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 classfile 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:

List<String> list = new ArrayList<String>() {{
    add("Hello");
    add("World!");
}};

Ini terlihat seperti fitur "tersembunyi" dari Java, tetapi itu hanya penulisan ulang dari:

List<String> list = new ArrayList<String>() {

    // Instance initialization block
    {
        add("Hello");
        add("World!");
    }
};

Jadi itu pada dasarnya contoh inisialisasi blok yang merupakan bagian dari kelas dalam anonim .


Proposal Koleksi Literatur Joshua Bloch untuk Project Coin sejalan:

List<Integer> intList = [1, 2, 3, 4];

Set<String> strSet = {"Apple", "Banana", "Cactus"};

Map<String, Integer> truthMap = { "answer" : 42 };

Sayangnya, itu tidak membuat jalan ke Jawa 7 atau 8 dan disimpan tanpa batas.


Percobaan

Inilah eksperimen sederhana yang telah saya uji - buat 1000 ArrayLists dengan elemen-elemen "Hello"dan "World!"ditambahkan ke mereka melalui addmetode, menggunakan dua metode:

Metode 1: Inisialisasi Brace Ganda

List<String> l = new ArrayList<String>() {{
  add("Hello");
  add("World!");
}};

Metode 2: Instantiate ArrayListdanadd

List<String> l = new ArrayList<String>();
l.add("Hello");
l.add("World!");

Saya membuat program sederhana untuk menulis file sumber Java untuk melakukan 1000 inisialisasi menggunakan dua metode:

Tes 1:

class Test1 {
  public static void main(String[] s) {
    long st = System.currentTimeMillis();

    List<String> l0 = new ArrayList<String>() {{
      add("Hello");
      add("World!");
    }};

    List<String> l1 = new ArrayList<String>() {{
      add("Hello");
      add("World!");
    }};

    /* snip */

    List<String> l999 = new ArrayList<String>() {{
      add("Hello");
      add("World!");
    }};

    System.out.println(System.currentTimeMillis() - st);
  }
}

Tes 2:

class Test2 {
  public static void main(String[] s) {
    long st = System.currentTimeMillis();

    List<String> l0 = new ArrayList<String>();
    l0.add("Hello");
    l0.add("World!");

    List<String> l1 = new ArrayList<String>();
    l1.add("Hello");
    l1.add("World!");

    /* snip */

    List<String> l999 = new ArrayList<String>();
    l999.add("Hello");
    l999.add("World!");

    System.out.println(System.currentTimeMillis() - st);
  }
}

Harap dicatat, bahwa waktu yang telah berlalu untuk menginisialisasi 1000 ArrayListdetik dan 1000 kelas dalam anonim yang diperluas ArrayListdiperiksa menggunakan System.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:

Test1 Times (ms)           Test2 Times (ms)
----------------           ----------------
           187                          0
           203                          0
           203                          0
           188                          0
           188                          0
           187                          0
           203                          0
           188                          0
           188                          0
           203                          0

Seperti dapat dilihat, inisialisasi brace ganda memiliki waktu eksekusi yang nyata sekitar 190 ms.

Sementara itu, ArrayListwaktu 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 .classfile yang dihasilkan dengan menyusun Test1program uji inisialisasi brace ganda.


10
"Mungkin" menjadi kata operatif. Kecuali diukur, tidak ada pernyataan tentang kinerja yang berarti.
Instance Hunter

16
Anda telah melakukan pekerjaan yang luar biasa sehingga saya hampir tidak ingin mengatakan ini, tetapi waktu Test1 bisa didominasi oleh beban kelas. Akan menarik untuk melihat seseorang menjalankan instance tunggal dari setiap tes dalam loop for mengatakan 1.000 kali, kemudian jalankan lagi dalam detik untuk loop 1.000 atau 10.000 kali dan mencetak perbedaan waktu (System.nanoTime ()). Yang pertama untuk loop harus dapat melewati semua efek pemanasan (JIT, classload, misalnya). Kedua tes memodelkan use case yang berbeda. Saya akan mencoba menjalankan ini besok di tempat kerja.
Jim Ferrans

8
@ Jim Ferrans: Saya cukup yakin bahwa waktu Test1 berasal dari beban kelas. Tetapi, konsekuensi dari menggunakan inisialisasi brace ganda adalah harus mengatasi beban kelas. Saya percaya sebagian besar menggunakan case untuk double brace init. adalah untuk inisialisasi satu kali, pengujian lebih dekat dalam kondisi dengan kasus penggunaan khas tipe inisialisasi ini. Saya akan percaya bahwa beberapa iterasi dari setiap tes akan membuat kesenjangan waktu eksekusi lebih kecil.
coobird

73
Apa ini membuktikan bahwa a) inisialisasi penjepit ganda lebih lambat, dan b) bahkan jika Anda melakukannya 1000 kali, Anda mungkin tidak akan melihat perbedaannya. Dan tidak seperti ini yang bisa menjadi hambatan dalam loop batin. Ini memberlakukan penalti kecil satu kali DI SANGAT TERBURUK.
Michael Myers

15
Jika menggunakan DBI membuat kode lebih mudah dibaca atau ekspresif, maka gunakan. Fakta bahwa itu meningkatkan sedikit pekerjaan yang harus dilakukan JVM bukanlah argumen yang valid, dengan sendirinya, menentangnya. Jika itu, maka kita juga harus khawatir tentang ekstra pembantu metode / kelas, lebih memilih kelas bukannya besar dengan metode yang lebih sedikit ...
Rogério

105

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).

Pertanyaan kedua: HashSet baru harus menjadi "ini" yang digunakan dalam initializer instance ... adakah yang bisa menjelaskan mekanisme ini? Dengan naif saya berharap "ini" merujuk pada objek yang menginisialisasi "rasa".

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.

public class Test {

    public void add(Object o) {
    }

    public Set<String> makeSet() {
        return new HashSet<String>() {
            {
              add("hello"); // HashSet
              Test.this.add("hello"); // outer instance 
            }
        };
    }
}

Untuk lebih jelas tentang subkelas anonim yang sedang dibuat, Anda bisa menentukan metode di sana juga. Misalnya menimpaHashSet.add()

    public Set<String> makeSet() {
        return new HashSet<String>() {
            {
              add("hello"); // not HashSet anymore ...
            }

            @Override
            boolean add(String s){

            }

        };
    }

5
Poin yang sangat bagus tentang referensi tersembunyi ke kelas yang mengandung. Dalam contoh asli, initializer instance memanggil metode add () dari HashSet baru <String>, bukan Test.this.add (). Itu menunjukkan kepada saya bahwa sesuatu yang lain sedang terjadi. Apakah ada kelas dalam anonim untuk <String> HashSet, seperti yang disarankan Nathan Kitchen?
Jim Ferrans

Referensi ke kelas yang mengandung mungkin juga berbahaya jika serialisasi dari datastructure terlibat. Kelas yang mengandung juga akan serial dan dengan demikian harus Serializable. Ini dapat menyebabkan kesalahan yang tidak jelas.
Hanya programmer Java

56

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:

Map source = new HashMap(){{
    put("firstName", "John");
    put("lastName", "Smith");
    put("organizations", new HashMap(){{
        put("0", new HashMap(){{
            put("id", "1234");
        }});
        put("abc", new HashMap(){{
            put("id", "5678");
        }});
    }});
}};

... akan menghasilkan kelas-kelas ini:

Test$1$1$1.class
Test$1$1$2.class
Test$1$1.class
Test$1.class
Test.class

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:

public class ReallyHeavyObject {

    // Just to illustrate...
    private int[] tonsOfValues;
    private Resource[] tonsOfResources;

    // This method almost does nothing
    public Map quickHarmlessMethod() {
        Map source = new HashMap(){{
            put("firstName", "John");
            put("lastName", "Smith");
            put("organizations", new HashMap(){{
                put("0", new HashMap(){{
                    put("id", "1234");
                }});
                put("abc", new HashMap(){{
                    put("id", "5678");
                }});
            }});
        }};

        return source;
    }
}

Yang dikembalikan Mapsekarang akan berisi referensi ke instance terlampir ReallyHeavyObject. Anda mungkin tidak ingin mengambil risiko itu:

Memori Kebocoran Di Sini

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:

String[] array = { "John", "Doe" };
Map map = new HashMap() {{ put("John", "Doe"); }};

Beberapa orang mungkin merasa ini merangsang secara sintaksis.


7
Simpan anak-anak kucing! Jawaban yang bagus!
Aris2World

36

Mengambil kelas tes berikut:

public class Test {
  public void test() {
    Set<String> flavors = new HashSet<String>() {{
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
    }};
  }
}

dan kemudian mendekompilasi file kelas, saya melihat:

public class Test {
  public void test() {
    java.util.Set flavors = new HashSet() {

      final Test this$0;

      {
        this$0 = Test.this;
        super();
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
      }
    };
  }
}

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):

public class Sample1 {
    private static final String someVar;
    static {
        String temp = null;
        ..... // block of code setting temp
        someVar = temp;
    }
}

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.

public class Sample2 {
    private final String someVar;

    // This is an instance initializer
    {
        String temp = null;
        ..... // block of code setting temp
        someVar = temp;
    }
}

Jika Anda tidak memiliki konstruktor default, maka blok kode antara {dan }diubah menjadi konstruktor oleh kompiler. Dengan mengingat hal ini, uraikan kode penjepit ganda:

public void test() {
  Set<String> flavors = new HashSet<String>() {
      {
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
      }
  };
}

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:

public void test() {
  Set<String> flavors = new MyHashSet();
}

class MyHashSet extends HashSet<String>() {
    public MyHashSet() {
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
    }
}

Untuk keperluan inisialisasi, saya akan mengatakan tidak ada overhead apa pun (atau sangat kecil sehingga dapat diabaikan). Namun, setiap penggunaan tidak flavorsakan bertentangan HashSettetapi sebaliknya MyHashSet. 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 diperluas HashSet.

Jika Anda memiliki pertanyaan tentang detail inisialisasi contoh, lihat detail dalam dokumentasi JLS .


Eddie, penjelasan yang sangat bagus. Jika kode byte JVM sebersih dekompilasi, kecepatan eksekusi akan cukup cepat, meskipun saya agak khawatir tentang kekacauan file .class ekstra. Saya masih penasaran mengapa konstruktor initializer instance melihat "ini" sebagai instance <String> HashSet baru dan bukan instance Test. Apakah ini hanya perilaku yang ditentukan secara eksplisit dalam Spesifikasi Bahasa Jawa terbaru untuk mendukung idiom?
Jim Ferrans

Saya memperbarui jawaban saya. Saya meninggalkan pelat baja dari kelas Uji, yang menyebabkan kebingungan. Saya memasukkannya ke dalam jawaban saya untuk membuat segalanya lebih jelas. Saya juga menyebutkan bagian JLS untuk blok initializer contoh yang digunakan dalam idiom ini.
Eddie

1
@ Jim Penafsiran "ini" bukan kasus khusus; itu hanya merujuk pada instance dari kelas terlampir terdalam, yang merupakan subclass anonim dari HashSet <String>.
Nathan Kitchen

Maaf melompat empat setengah tahun kemudian. Tetapi hal yang menyenangkan tentang file kelas yang didekompilasi (blok kode kedua Anda) adalah bahwa itu bukan Java yang valid! Ini super()sebagai baris kedua dari konstruktor implisit, tetapi harus didahulukan. (Saya sudah mengujinya, dan itu tidak akan dikompilasi.)
chiastic-security

1
@ chiastic-security: Terkadang decompiler menghasilkan kode yang tidak dapat dikompilasi.
Eddie

35

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.

Set<String> getFlavors(){
  return Collections.unmodifiableSet(flavors)
}

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 seperti new LinkedHashSet(Arrays.asList("xxx","YYY"));


3
Untungnya, pada Java 8, PermGen tidak lagi menjadi masalah. Masih ada dampak, saya kira, tetapi tidak satu pun dengan pesan kesalahan yang berpotensi sangat tidak jelas.
Joey

2
@ Joey, tidak ada bedanya jika memori dikelola langsung oleh GC (perm gen) atau tidak. Kebocoran dalam metaspace masih bocor, kecuali meta terbatas, tidak akan ada OOM (keluar dari perm gen) oleh hal-hal seperti oom_killer di linux yang akan
dimulai

19

Memuat banyak kelas dapat menambahkan beberapa milidetik ke awal. Jika startup tidak begitu penting dan Anda melihat efisiensi kelas setelah startup tidak ada perbedaan.

package vanilla.java.perfeg.doublebracket;

import java.util.*;

/**
 * @author plawrey
 */
public class DoubleBracketMain {
    public static void main(String... args) {
        final List<String> list1 = new ArrayList<String>() {
            {
                add("Hello");
                add("World");
                add("!!!");
            }
        };
        List<String> list2 = new ArrayList<String>(list1);
        Set<String> set1 = new LinkedHashSet<String>() {
            {
                addAll(list1);
            }
        };
        Set<String> set2 = new LinkedHashSet<String>();
        set2.addAll(list1);
        Map<Integer, String> map1 = new LinkedHashMap<Integer, String>() {
            {
                put(1, "one");
                put(2, "two");
                put(3, "three");
            }
        };
        Map<Integer, String> map2 = new LinkedHashMap<Integer, String>();
        map2.putAll(map1);

        for (int i = 0; i < 10; i++) {
            long dbTimes = timeComparison(list1, list1)
                    + timeComparison(set1, set1)
                    + timeComparison(map1.keySet(), map1.keySet())
                    + timeComparison(map1.values(), map1.values());
            long times = timeComparison(list2, list2)
                    + timeComparison(set2, set2)
                    + timeComparison(map2.keySet(), map2.keySet())
                    + timeComparison(map2.values(), map2.values());
            if (i > 0)
                System.out.printf("double braced collections took %,d ns and plain collections took %,d ns%n", dbTimes, times);
        }
    }

    public static long timeComparison(Collection a, Collection b) {
        long start = System.nanoTime();
        int runs = 10000000;
        for (int i = 0; i < runs; i++)
            compareCollections(a, b);
        long rate = (System.nanoTime() - start) / runs;
        return rate;
    }

    public static void compareCollections(Collection a, Collection b) {
        if (!a.equals(b) && a.hashCode() != b.hashCode() && !a.toString().equals(b.toString()))
            throw new AssertionError();
    }
}

cetakan

double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 34 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns

2
Tidak ada perbedaan kecuali bahwa ruang PermGen Anda akan menguap jika DBI digunakan secara berlebihan. Setidaknya, itu akan kecuali Anda menetapkan beberapa opsi JVM tidak jelas untuk memungkinkan pembongkaran ruang kelas dan pengumpulan sampah ruang PermGen. Mengingat prevalensi Java sebagai bahasa sisi server, masalah memori / PermGen menjamin setidaknya menyebutkan.
Agustus

1
@ Aroth, ini adalah poin yang bagus. Saya akui bahwa dalam 16 tahun bekerja di Jawa saya tidak pernah bekerja pada sistem di mana Anda harus menyetel PermGen (atau Metaspace). Untuk sistem saya telah bekerja pada ukuran kode selalu disimpan cukup kecil.
Peter Lawrey

2
Bukankah seharusnya kondisi di compareCollectionsdigabungkan 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.
Holger

@aroth hanya sebagai pembaruan: karena Java 8, VM tidak lagi menggunakan perm-gen.
Angel O'Sphere

16

Untuk membuat set, Anda dapat menggunakan metode pabrik varargs alih-alih inisialisasi brace ganda:

public static Set<T> setOf(T ... elements) {
    return new HashSet<T>(Arrays.asList(elements));
}

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.


Hah! ;-) Saya sebenarnya adalah Rip van Winkle yang kembali ke Jawa sejak 1,2 hari (saya menulis peramban web suara VoiceXML di evolution.voxeo.com di Jawa). Sudah menyenangkan mempelajari generik, tipe parameter, Koleksi, java.util.concurrent, baru untuk sintaks loop, dll. Ini bahasa yang lebih baik sekarang. Untuk maksud Anda, meskipun mekanisme di balik DBI mungkin tampak tidak jelas pada awalnya, arti kode harus cukup jelas.
Jim Ferrans

10

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:

List<String> aList = Arrays.asList("vanilla", "strawberry", "chocolate");

Keterbatasan pendekatan ini tentu saja adalah Anda tidak dapat mengontrol jenis daftar spesifik yang akan dihasilkan.


1
Arrays.asList () adalah apa yang biasanya saya gunakan, tetapi Anda benar, situasi ini muncul terutama dalam tes unit; kode nyata akan membangun daftar dari permintaan DB, XML, dan sebagainya.
Jim Ferrans

7
Waspadalah terhadap asList, daftar yang dikembalikan tidak mendukung penambahan atau penghapusan elemen. Setiap kali saya menggunakan asList, saya meneruskan daftar yang dihasilkan ke konstruktor ingin new ArrayList<String>(Arrays.asList("vanilla", "strawberry", "chocolate"))mengatasi masalah ini.
Michael Myers

7

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.


5

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:

Set<String> flavors = ImmutableSet.of(
    "vanilla", "strawberry", "chocolate", "butter pecan");

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 langsungHashMap , 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 .


-1. Meskipun ada beberapa poin yang valid, jawaban ini bermuara pada "Bagaimana menghindari menghasilkan kelas anonim yang tidak perlu? Gunakan kerangka kerja, dengan kelas yang lebih banyak lagi!"
Agent_L

1
Saya akan mengatakan itu bermuara pada "menggunakan alat yang tepat untuk pekerjaan itu, daripada hack yang dapat merusak aplikasi Anda". Guava adalah pustaka yang cukup umum untuk dimasukkan oleh aplikasi (Anda pasti kehilangan jika tidak menggunakannya), tetapi bahkan jika Anda tidak ingin menggunakannya, Anda dapat dan masih harus menghindari inisialisasi penjepit ganda.
dimo414

Dan bagaimana tepatnya inisialisasi penjepit ganda menyebabkan kebocoran Meory?
Angel O'Sphere

@ AngelO'Sphere DBI adalah cara membingungkan menciptakan kelas dalam , dan mempertahankan referensi implisit untuk kelas penutupnya (kecuali hanya digunakan dalam statickonteks). Tautan Rawan Kesalahan di bagian bawah pertanyaan saya membahas hal ini lebih lanjut.
dimo414

Saya akan mengatakan itu adalah masalah selera. Dan tidak ada yang benar-benar membingungkan tentang hal itu.
Angel O'Sphere

4

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

Saya terkejut menemukan bahwa dalam sebagian besar tes yang dijalankan inisiasi internal sebenarnya lebih cepat (hampir dua kali lipat dalam beberapa kasus). Ketika bekerja dengan jumlah besar, manfaatnya tampaknya memudar.

Menariknya, case yang membuat 3 objek pada loop kehilangan manfaatnya lebih cepat daripada pada case lainnya. Saya tidak yakin mengapa ini terjadi dan lebih banyak pengujian harus dilakukan untuk mencapai kesimpulan. Membuat implementasi konkret dapat membantu menghindari definisi kelas untuk dimuat ulang (jika itu yang terjadi)

Namun, jelas bahwa tidak banyak overhead yang diamati dalam banyak kasus untuk bangunan item tunggal, bahkan dengan jumlah besar.

Satu set mundur adalah fakta bahwa masing-masing inisiasi brace ganda menciptakan file kelas baru yang menambahkan seluruh blok disk ke ukuran aplikasi kita (atau sekitar 1rb saat dikompresi). Jejak kecil, tetapi jika digunakan di banyak tempat, hal itu berpotensi berdampak. Gunakan ini 1000 kali dan Anda berpotensi menambahkan seluruh MiB untuk aplikasi Anda, yang mungkin menyangkut lingkungan tertanam.

Kesimpulan saya? Dapat ok untuk digunakan selama tidak disalahgunakan.

Biarkan aku tahu apa yang Anda pikirkan :)


2
Itu bukan tes yang valid. Kode menciptakan objek tanpa menggunakannya yang memungkinkan pengoptimal untuk menghapus seluruh pembuatan instance. Satu-satunya efek samping yang tersisa adalah memajukan urutan nomor acak yang overhead-nya melebihi apa pun dalam tes ini.
Holger

3

Saya menjawab Nat kedua, kecuali saya akan menggunakan loop daripada membuat dan segera melemparkan Daftar implisit dari asList (elemen):

static public Set<T> setOf(T ... elements) {
    Set set=new HashSet<T>(elements.size());
    for(T elm: elements) { set.add(elm); }
    return set;
    }

1
Mengapa? Objek baru akan dibuat di ruang eden, dan hanya membutuhkan dua atau tiga penambahan pointer untuk instantiate. JVM mungkin memperhatikan bahwa ia tidak pernah keluar dari ruang lingkup metode dan mengalokasikannya di stack.
Nat

Ya, itu mungkin berakhir lebih efisien daripada kode itu (meskipun Anda dapat memperbaikinya dengan memberi tahu HashSetkapasitas yang disarankan - ingat faktor beban).
Tom Hawtin - tackline

Nah, konstruktor HashSet harus melakukan iterasi, jadi itu tidak akan menjadi kurang efisien. Kode perpustakaan yang dibuat untuk digunakan kembali harus selalu berusaha menjadi yang terbaik .
Lawrence Dol

3

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).


3

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:

package literal;

public class collection {
    public static <T> List<T> List(T...elems){
        return Arrays.asList( elems );
    }
}

dan menggunakannya sebagai berikut:

import static literal.collection.List;
import static system.io.*;

public class CollectionDemo {
    public void demoList(){
        List<String> slist = List( "a", "b", "c" );
        List<Integer> iList = List( 1, 2, 3 );
        for( String elem : List( "a", "java", "list" ) )
            System.out.println( elem );
    }
}

Google Collections, sekarang bagian dari Guava mendukung ide serupa untuk konstruksi daftar. Dalam wawancara ini , Jared Levy mengatakan:

[...] fitur yang paling banyak digunakan, yang muncul di hampir setiap kelas Java yang saya tulis, adalah metode statis yang mengurangi jumlah penekanan tombol berulang dalam kode Java Anda. Sangat nyaman bisa memasukkan perintah seperti berikut:

Map<OneClassWithALongName, AnotherClassWithALongName> = Maps.newHashMap();

List<String> animals = Lists.immutableList("cat", "dog", "horse");

7/10/2014: Kalau saja bisa sesederhana Python:

animals = ['cat', 'dog', 'horse']

2/21/2020: Di Java 11 Anda sekarang dapat mengatakan:

animals = List.of(“cat”, “dog”, “horse”)


2
  1. 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.

  2. Sepertinya saya seolah-olah konteksnya adalah objek yang dikembalikan oleh new, yaitu HashSet.

  3. 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.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.