Skuteczność Java „Double Brace Initialization”?


823

W ukrytych funkcjach Javy najlepsza odpowiedź wspomina o inicjalizacji Double Brace z bardzo kuszącą składnią:

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

Ten idiom tworzy anonimową klasę wewnętrzną z tylko inicjatorem instancji, który „może używać dowolnych [...] metod w zakresie zawierającym”.

Główne pytanie: czy to jest tak nieefektywne, jak się wydaje? Czy jego stosowanie powinno być ograniczone do jednorazowych inicjalizacji? (I oczywiście popisywanie się!)

Drugie pytanie: nowy zestaw HashSet musi być „tym” używanym w inicjalizatorze instancji ... czy ktoś może rzucić światło na mechanizm?

Trzecie pytanie: czy ten idiom jest zbyt niejasny, aby go używać w kodzie produkcyjnym?

Podsumowanie: Bardzo, bardzo miłe odpowiedzi, dziękuję wszystkim. W przypadku pytania (3) ludzie uważali, że składnia powinna być jasna (chociaż polecam od czasu do czasu komentarz, szczególnie jeśli kod zostanie przekazany programistom, którzy mogą go nie znać).

W przypadku pytania (1) wygenerowany kod powinien działać szybko. Dodatkowe pliki .class powodują zaśmiecenie plików jar i nieznacznie spowalniają uruchamianie programu (dzięki @coobird za pomiar). @Thilo wskazał, że może to mieć wpływ na wyrzucanie elementów bezużytecznych, a koszt pamięci dla dodatkowych klas obciążeń może być czynnikiem w niektórych przypadkach.

Pytanie 2 okazało się dla mnie najbardziej interesujące. Jeśli rozumiem odpowiedzi, w DBI dzieje się tak, że anonimowa klasa wewnętrzna rozszerza klasę obiektu konstruowanego przez nowy operator, a zatem ma wartość „this” odnoszącą się do budowanej instancji. Bardzo schludny.

Ogólnie rzecz biorąc, DBI wydaje mi się intelektualną ciekawością. Coobird i inni podkreślają, że możesz osiągnąć ten sam efekt dzięki Arrays.asList, metodom varargs, kolekcjom Google i proponowanym literałom kolekcji Java 7. Nowsze języki JVM, takie jak Scala, JRuby i Groovy, oferują również zwięzłe oznaczenia do tworzenia list i dobrze współpracują z Javą. Biorąc pod uwagę, że DBI zaśmieca ścieżkę klasy, spowalnia ładowanie klasy i sprawia, że ​​kod jest nieco bardziej niejasny, prawdopodobnie bym się od tego unikał. Planuję jednak przekazać to znajomemu, który właśnie otrzymał SCJP i uwielbia dobre, naturalne potyczki z semantyką Java! ;-) Dziękuję wszystkim!

7/2017: Baeldung ma dobre podsumowanie inicjalizacji podwójnego nawiasu klamrowego i uważa to za anty-wzór.

12/2017: @Basil Bourque zauważa, że ​​w nowej Javie 9 możesz powiedzieć:

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

To na pewno droga. Jeśli utkniesz z wcześniejszą wersją, spójrz na ImmutableSet kolekcji Google .


33
Zapach kodu, który tu widzę, jest taki, że naiwny czytelnik spodziewałby flavorssię, że jest HashSet, ale niestety jest to anonimowa podklasa.
Elazar Leibovich,

6
Jeśli rozważasz uruchomienie zamiast ładowania wydajności, nie ma różnicy, zobacz moją odpowiedź.
Peter Lawrey

4
Uwielbiam to, że stworzyłeś podsumowanie, myślę, że jest to wartościowe ćwiczenie dla ciebie i dla zwiększenia zrozumienia i dla społeczności.
Patrick Murphy,

3
Moim zdaniem nie jest to niejasne. Czytelnicy powinni wiedzieć, że podwójne ... czekaj, @ElazarLeibovich powiedział to już w swoim komentarzu . Sam inicjator podwójnego nawiasu klamrowego nie istnieje jako konstrukcja językowa, jest jedynie kombinacją anonimowej podklasy i inicjalizatora instancji. Jedyną rzeczą jest to, że ludzie muszą być tego świadomi.
MC Emperor

8
Java 9 oferuje niezmienne statyczne metody fabryczne, które mogą zastąpić użycie DCI w niektórych sytuacjach:Set<String> flavors = Set.of( "vanilla" , "strawberry" , "chocolate" , "butter pecan" ) ;
Basil Bourque,

Odpowiedzi:


607

Oto problem, kiedy zbyt mnie pociągają anonimowe klasy wewnętrzne:

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

Są to wszystkie klasy, które zostały wygenerowane, gdy tworzyłem prostą aplikację i wykorzystałem wiele anonimowych klas wewnętrznych - każda klasa zostanie skompilowana w osobny classplik.

„Inicjacja podwójnego nawiasu klamrowego”, jak już wspomniano, jest anonimową klasą wewnętrzną z blokiem inicjalizacji instancji, co oznacza, że ​​dla każdej „inicjalizacji” tworzona jest nowa klasa, wszystko w celu zwykle utworzenia pojedynczego obiektu.

Biorąc pod uwagę, że wirtualna maszyna Java będzie musiała odczytać wszystkie te klasy podczas ich używania, co może zająć trochę czasu w procesie weryfikacji kodu bajtowego i tym podobne. Nie wspominając o zwiększeniu potrzebnego miejsca na dysku do przechowywania wszystkich tych classplików.

Wydaje się, że podczas inicjalizacji podwójnego nawiasu jest trochę narzutu, więc prawdopodobnie nie jest to zbyt dobry pomysł, aby przesadzić z tym zbyt daleko. Ale jak zauważył Eddie w komentarzach, nie można być absolutnie pewnym wpływu.


Dla porównania, inicjalizacja podwójnego nawiasu klamrowego jest następująca:

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

Wygląda jak „ukryta” funkcja Java, ale jest to po prostu przepisanie:

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

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

Jest to w zasadzie blok inicjujący instancję, który jest częścią anonimowej klasy wewnętrznej .


Joshua Bloch Collection Literówki propozycja dla projektu Coin była wzdłuż linii:

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

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

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

Niestety nie dostał się ani do Javy 7, ani do 8 i został odłożony na czas nieokreślony.


Eksperyment

Oto prosty eksperyment, który przetestowałem - zrób 1000 ArrayLists elementami "Hello"i "World!"dodaj do nich za pomocą addmetody, używając dwóch metod:

Metoda 1: Inicjalizacja podwójnego nawiasu klamrowego

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

Metoda 2: Utwórz ArrayListiadd

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

Stworzyłem prosty program do wypisania pliku źródłowego Java w celu wykonania 1000 inicjalizacji przy użyciu dwóch metod:

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

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

Należy pamiętać, że czas, który upłynął do zainicjowania 1000 ArrayLists oraz rozszerzenie 1000 anonimowych klas wewnętrznych ArrayListjest sprawdzane za pomocą System.currentTimeMillis, więc timer nie ma bardzo wysokiej rozdzielczości. W moim systemie Windows rozdzielczość wynosi około 15-16 milisekund.

Wyniki dla 10 przebiegów dwóch testów były następujące:

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

Jak można zauważyć, inicjalizacja podwójnego nawiasu klamrowego ma zauważalny czas wykonania około 190 ms.

Tymczasem ArrayListczas wykonania inicjalizacji wyniósł 0 ms. Oczywiście należy wziąć pod uwagę rozdzielczość timera, ale prawdopodobnie będzie to mniej niż 15 ms.

Wydaje się więc, że istnieje zauważalna różnica w czasie wykonywania obu metod. Wygląda na to, że dwie metody inicjalizacji rzeczywiście mają narzut.

Tak, .classwygenerowano 1000 plików, kompilując Test1program testowy inicjujący podwójny nawias klamrowy.


10
„Prawdopodobnie” jest słowem operatywnym. O ile nie zmierzone, żadne stwierdzenia dotyczące wydajności nie mają znaczenia.
Instance Hunter

16
Wykonałeś tak świetną robotę, że prawie nie chcę tego mówić, ale czasy Test1 mogą być zdominowane przez obciążenia klasowe. Byłoby interesujące zobaczyć, jak ktoś uruchamia jedno wystąpienie każdego testu w pętli for powiedzmy 1000 razy, a następnie uruchom go ponownie w sekundę dla pętli for 1000 lub 10 000 razy i wydrukuj różnicę czasu (System.nanoTime ()). Pierwsza pętla for powinna ominąć wszystkie efekty rozgrzewki (JIT, obciążenie klasy, np.). Oba testy modelują jednak różne przypadki użycia. Spróbuję uruchomić to jutro w pracy.
Jim Ferrans

8
@Jim Ferrans: Jestem całkiem pewien, że czasy Test1 pochodzą z obciążeń klasowych. Jednak konsekwencją użycia inicjalizacji podwójnego nawiasu klamrowego jest konieczność radzenia sobie z obciążeniami klas. Wierzę, że większość przypadków użycia dla inicjacji podwójnego nawiasu klamrowego. jest dla jednorazowej inicjalizacji, test jest bliższy w warunkach do typowego przypadku zastosowania tego typu inicjalizacji. Sądzę, że wielokrotne iteracje każdego testu zmniejszą lukę czasową wykonania.
coobird

73
Dowodzi to, że a) inicjalizacja podwójnego nawiasu klamrowego przebiega wolniej, i b) nawet jeśli zrobisz to 1000 razy, prawdopodobnie nie zauważysz różnicy. I to nie tak, że może to być wąskie gardło w wewnętrznej pętli. Nakłada niewielką jednorazową karę NA BARDZO NAJGORSZY.
Michael Myers

15
Jeśli użycie DBI sprawia, że ​​kod jest bardziej czytelny lub ekspresyjny, użyj go. Fakt, że nieco zwiększa pracę JVM, nie jest sam w sobie uzasadnionym argumentem przeciwko niemu. Gdyby tak było, powinniśmy również martwić się dodatkowymi metodami / klasami pomocniczymi, preferując zamiast tego ogromne klasy z mniejszą liczbą metod ...
Rogério

105

Jedną z właściwości tego podejścia, która nie została do tej pory wskazana, jest to, że ponieważ tworzysz klasy wewnętrzne, cała klasa zawierająca zostaje ujęta w jej zakresie. Oznacza to, że tak długo, jak Twój zestaw żyje, zachowa wskaźnik do zawierającej instancji ( this$0) i zapobiegnie gromadzeniu śmieci, co może być problemem.

To i fakt, że tworzona jest nowa klasa, mimo że zwykły zestaw HashSet działałby dobrze (lub nawet lepiej), sprawia, że ​​nie chcę używać tego konstruktu (mimo że naprawdę tęsknię za cukrem syntaktycznym).

Drugie pytanie: nowy zestaw HashSet musi być „tym” używanym w inicjalizatorze instancji ... czy ktoś może rzucić światło na mechanizm? Naiwnie oczekiwałbym, że „to” odnosi się do obiektu inicjującego „smaki”.

Właśnie tak działają klasy wewnętrzne. Otrzymują własne this, ale mają także wskaźniki do instancji nadrzędnej, dzięki czemu można wywoływać metody również na obiekcie zawierającym. W przypadku konfliktu nazw, klasa wewnętrzna (w twoim przypadku HashSet) ma pierwszeństwo, ale możesz poprzedzić „to” nazwą klasy, aby uzyskać również metodę zewnętrzną.

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

Aby wyjaśnić anonimową tworzoną podklasę, możesz również zdefiniować tam metody. Na przykład zastąpienieHashSet.add()

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

            @Override
            boolean add(String s){

            }

        };
    }

5
Bardzo dobry punkt dotyczący ukrytego odwołania do zawierającej klasy. W oryginalnym przykładzie inicjator instancji wywołuje metodę add () nowego HashSet <String>, a nie Test.this.add (). To sugeruje mi, że dzieje się coś innego. Czy istnieje anonimowa klasa wewnętrzna dla HashSet <String>, jak sugeruje Nathan Kitchen?
Jim Ferrans

Odwołanie do klasy zawierającej może być również niebezpieczne, jeśli zaangażowana jest serializacja struktury danych. Klasa zawierająca również zostanie serializowana, a zatem musi być możliwa do serializacji. Może to prowadzić do niejasnych błędów.
Kolejny programista Java

56

Za każdym razem, gdy ktoś używa inicjalizacji podwójnego nawiasu klamrowego, kotek zostaje zabity.

Oprócz tego, że składnia jest dość niezwykła i nie jest tak naprawdę idiomatyczna (smak jest oczywiście dyskusyjny), niepotrzebnie stwarzasz dwa znaczące problemy w swojej aplikacji, o których niedawno pisałem bardziej szczegółowo tutaj .

1. Tworzysz zbyt wiele anonimowych klas

Za każdym razem, gdy używasz inicjalizacji podwójnego nawiasu klamrowego, tworzona jest nowa klasa. Np. Ten przykład:

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

... stworzy te klasy:

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

To dość duże obciążenie dla twojego programu ładującego klasy - na darmo! Oczywiście nie zajmie to dużo czasu inicjalizacji, jeśli zrobisz to raz. Ale jeśli robisz to 20 000 razy w całej aplikacji korporacyjnej ... cała ta pamięć sterty tylko na odrobinę „cukru składniowego”?

2. Potencjalnie tworzysz wyciek pamięci!

Jeśli weźmiesz powyższy kod i zwrócisz mapę z metody, osoby wywołujące tę metodę mogą nieświadomie trzymać się bardzo ciężkich zasobów, których nie można wyrzucać. Rozważ następujący przykład:

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

Zwrócony Mapbędzie teraz zawierał odwołanie do załączającej instancji ReallyHeavyObject. Prawdopodobnie nie chcesz ryzykować, że:

Przeciek pamięci tutaj

Zdjęcie z http://blog.jooq.org/2014/12/08/dont-be-clever-the-double-curly-braces-anti-pattern/

3. Możesz udawać, że Java ma literały map

Aby odpowiedzieć na twoje pytanie, ludzie używali tej składni, by udawać, że Java ma coś w rodzaju literałów map, podobnych do istniejących literałów tablic:

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

Niektóre osoby mogą uznać to za stymulujące syntaktycznie.


7
Uratuj kocięta! Dobra odpowiedź!
Aris2World

36

Biorąc następującą klasę testową:

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

a następnie dekompilując plik klasy, widzę:

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

Nie wydaje mi się to okropnie nieefektywne. Gdybym martwił się wydajnością dla czegoś takiego, profilowałbym to. A na twoje pytanie nr 2 odpowiada powyższy kod: Jesteś wewnątrz niejawnego konstruktora (i inicjatora instancji) dla swojej wewnętrznej klasy, więc „ this” odnosi się do tej wewnętrznej klasy.

Tak, ta składnia jest niejasna, ale komentarz może wyjaśnić niejasne użycie składni. Aby wyjaśnić składnię, większość osób zna statyczny blok inicjalizujący (inicjalizatory statyczne JLS 8.7):

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

Możesz także użyć podobnej składni (bez słowa „ static”) do użycia konstruktora (Inicjatory instancji JLS 8.6), chociaż nigdy nie widziałem, aby była używana w kodzie produkcyjnym. Jest to o wiele mniej znane.

public class Sample2 {
    private final String someVar;

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

Jeśli nie posiada konstruktora domyślnego, a następnie blok kodu pomiędzy {i }zamienia się w konstruktora przez kompilator. Mając to na uwadze, rozwiąż kod podwójnego nawiasu klamrowego:

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

Blok kodu między najbardziej wewnętrznymi nawiasami klamrowymi jest kompilator zamieniany w konstruktor. Najbardziej zewnętrzne nawiasy klamrowe ograniczają anonimową klasę wewnętrzną. Aby zrobić to ostatni krok, aby uczynić wszystko nieanonimowym:

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

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

Dla celów inicjalizacji powiedziałbym, że nie ma żadnych kosztów ogólnych (lub tak małych, że można je pominąć). Jednak każde użycie nie flavorsbędzie przeciwne, HashSetale przeciwne MyHashSet. Jest to prawdopodobnie niewielki (i być może nieistotny) narzut związany z tym. Ale znowu, zanim się tym martwię, profilowałbym to.

Ponownie, w pytaniu nr 2 powyższy kod jest logicznym i wyraźnym odpowiednikiem inicjalizacji podwójnego nawiasu klamrowego i pokazuje, gdzie „ this” odnosi się do rozszerzonej klasy wewnętrznej HashSet.

Jeśli masz pytania dotyczące szczegółów inicjatorów instancji, sprawdź szczegóły w dokumentacji JLS .


Eddie, bardzo miłe wytłumaczenie. Jeśli kody bajtów JVM są tak czyste jak dekompilacja, szybkość wykonywania będzie wystarczająco szybka, choć nieco martwi mnie dodatkowy bałagan w pliku .class. Nadal jestem ciekawy, dlaczego konstruktor inicjalizatora instancji widzi „to” jako nową instancję HashSet <String>, a nie instancję Test. Czy to właśnie jawnie określone zachowanie w najnowszej specyfikacji języka Java do obsługi tego idiomu?
Jim Ferrans

Zaktualizowałem swoją odpowiedź. Pominąłem płytę kotłową klasy Test, co spowodowało zamieszanie. Włożyłem to do mojej odpowiedzi, aby wszystko stało się bardziej oczywiste. Wspominam także o sekcji JLS dla bloków inicjalizujących instancję używanych w tym idiomie.
Eddie

1
@ Jim Interpretacja „tego” nie jest szczególnym przypadkiem; odnosi się po prostu do instancji najbardziej wewnętrznej klasy zamykającej, która jest anonimową podklasą HashSet <String>.
Nathan Kitchen

Przepraszam, że wskoczyłem za cztery i pół roku później. Ale miłą rzeczą w zdekompilowanym pliku klasy (twój drugi blok kodu) jest to, że nie jest to poprawna Java! Ma super()drugą linię niejawnego konstruktora, ale musi być na pierwszym miejscu. (Przetestowałem to i nie można go skompilować.)
Chiastic-Security

1
@ chiastic-security: Czasami dekompilatory generują kod, który się nie kompiluje.
Eddie,

35

podatny na wyciek

Zdecydowałem się włączyć. Wpływ na wydajność obejmuje: działanie dysku + rozpakowanie (dla jar), weryfikację klasy, przestrzeń perm-gen (dla JVM Hotspot firmy Sun). Jednak najgorsze: podatne na wycieki. Nie możesz po prostu wrócić.

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

Więc jeśli zestaw ucieka do dowolnej innej części załadowanej przez inny moduł ładujący klasy i zachowane jest tam odniesienie, całe drzewo klas + moduł ładujący zostanie wyciekły. Aby tego uniknąć, kopia do HashMap jest konieczne new LinkedHashSet(new ArrayList(){{add("xxx);add("yyy");}}). Już nie taki słodki. Ja sam nie używam tego idiomu, to jest jak new LinkedHashSet(Arrays.asList("xxx","YYY"));


3
Na szczęście od wersji Java 8 PermGen nie jest już czymś. Sądzę, że nadal występuje wpływ, ale nie ma on potencjalnie bardzo niejasnego komunikatu o błędzie.
Joey,

2
@Joey, robi zerową różnicę, jeśli pamięć jest zarządzana bezpośrednio przez GC (perm gen), czy nie. Wyciek w metaspace wciąż jest wyciek, chyba że meta jest ograniczony, nie pojawi się OOM (poza perm gen) przez rzeczy takie jak oom_killer w Linuksie.
bestsss

19

Ładowanie wielu klas może dodać kilka milisekund do początku. Jeśli uruchomienie nie jest tak ważne, a patrzysz na efektywność zajęć po uruchomieniu, nie ma różnicy.

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

odciski

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
Nie ma różnicy, z wyjątkiem tego, że przestrzeń PermGen wyparuje, jeśli DBI zostanie użyte nadmiernie. Przynajmniej tak, chyba że ustawisz jakieś niejasne opcje JVM, aby umożliwić rozładowywanie klas i odśmiecanie przestrzeni PermGen. Biorąc pod uwagę powszechność języka Java jako języka po stronie serwera, problem pamięci / PermGen wymaga co najmniej wzmianki.
aroth

1
@aroth to jest dobra uwaga. Przyznaję, że w ciągu 16 lat pracy nad Javą nigdy nie pracowałem na systemie, w którym musiałeś dostroić PermGen (lub Metaspace). W systemach, w których pracowałem, rozmiar kodu zawsze był dość mały.
Peter Lawrey,

2
Czy warunki nie powinny być compareCollectionsłączone ||raczej niż &&? Używanie &&wydaje się nie tylko semantycznie złe, ale przeciwdziała zamiarowi mierzenia wydajności, ponieważ tylko pierwszy warunek zostanie w ogóle przetestowany. Poza tym inteligentny optymalizator może rozpoznać, że warunki nigdy się nie zmienią podczas iteracji.
Holger

@aroth tylko jako aktualizacja: od wersji Java 8 maszyna wirtualna nie korzysta już z żadnych uprawnień.
Angel O'Sphere

16

Aby utworzyć zestawy, możesz użyć fabrycznej metody varargs zamiast inicjalizacji podwójnego nawiasu:

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

Biblioteka Kolekcje Google ma wiele podobnych metod wygody, a także mnóstwo innych przydatnych funkcji.

Jeśli chodzi o niejasność tego idiomu, to spotykam go i cały czas używam w kodzie produkcyjnym. Byłbym bardziej zaniepokojony, że programiści, którzy są zdezorientowani tym idiomem, mogą pisać kod produkcyjny.


Hah! ;-) Jestem właściwie Rip van Winkle wracającym do Javy od 1.2 dni (napisałem przeglądarkę głosową VoiceXML na stronie evolution.voxeo.com w Javie). Fajnie było uczyć się ogólnych, sparametryzowanych typów, kolekcji, java.util.concurrent, nowej składni pętli itp. Teraz jest to lepszy język. W twoim odczuciu, chociaż mechanizm stojący za DBI może wydawać się na początku niejasny, znaczenie kodu powinno być dość jasne.
Jim Ferrans

10

Pomijając efektywność, rzadko zdarza mi się pragnąć tworzenia deklaratywnych kolekcji poza testami jednostkowymi. Wierzę, że składnia podwójnego nawiasu jest bardzo czytelna.

Innym sposobem na osiągnięcie deklaratywnej konstrukcji list jest użycie Arrays.asList(T ...)takiego:

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

Ograniczeniem tego podejścia jest oczywiście to, że nie można kontrolować konkretnego typu listy, która ma zostać wygenerowana.


1
Arrays.asList () jest tym, czego normalnie użyłbym, ale masz rację, ta sytuacja występuje głównie w testach jednostkowych; prawdziwy kod konstruuje listy z zapytań DB, XML i tak dalej.
Jim Ferrans

7
Uważaj jednak na asList: zwrócona lista nie obsługuje dodawania ani usuwania elementów. Ilekroć używam asList, przekazuję wynikową listę do konstruktora, new ArrayList<String>(Arrays.asList("vanilla", "strawberry", "chocolate"))aby obejść ten problem.
Michael Myers

7

Na ogół nie ma w tym nic szczególnie nieefektywnego. Zasadniczo dla JVM nie ma znaczenia, że ​​utworzyłeś podklasę i dodałeś do niej konstruktor - to normalne, codzienne zadanie w języku zorientowanym obiektowo. Mogę wymyślić całkiem wymyślone przypadki, w których możesz spowodować nieefektywność, robiąc to (np. Masz wielokrotnie wywoływaną metodę, która kończy się mieszaniem różnych klas z powodu tej podklasy, podczas gdy zwykła przekazana klasa byłaby całkowicie przewidywalna- - w tym drugim przypadku kompilator JIT może dokonać optymalizacji, które nie są możliwe w pierwszym przypadku). Ale tak naprawdę uważam, że przypadki, w których będzie to miało znaczenie, są bardzo zmyślone.

Widziałbym ten problem bardziej z punktu widzenia tego, czy chcesz „zaśmiecać” wieloma anonimowymi klasami. Jako przybliżony przewodnik, rozważ użycie idiomu bardziej niż, powiedzmy, anonimowe klasy dla programów obsługi zdarzeń.

W (2) jesteś wewnątrz konstruktora obiektu, więc „to” odnosi się do budowanego obiektu. Nie różni się niczym od żadnego innego konstruktora.

Jeśli chodzi o (3), to tak naprawdę zależy od tego, kto utrzymuje Twój kod. Jeśli nie wiesz o tym z góry, to punktem odniesienia, którego sugerowałbym użycie, jest „widzisz to w kodzie źródłowym JDK?” (w tym przypadku nie przypominam sobie, aby widziałem wiele anonimowych inicjatorów, a już na pewno nie w przypadkach, gdy jest to jedyna treść anonimowej klasy). W większości projektów o średniej wielkości argumentowałbym, że naprawdę potrzebujesz programistów, aby w pewnym momencie zrozumieli źródło JDK, więc każda użyta tam składnia lub idiom to „uczciwa gra”. Poza tym, powiedziałbym, trenuj ludzi na tej składni, jeśli masz kontrolę nad tym, kto utrzymuje kod, w przeciwnym razie komentuj lub unikaj.


5

Inicjacja podwójnego nawiasu klamrowego to niepotrzebny hack, który może powodować wycieki pamięci i inne problemy

Nie ma uzasadnionego powodu, aby używać tej „sztuczki”. Guava zapewnia niezmienne niezmienne kolekcje, które obejmują zarówno statyczne fabryki, jak i konstruktory, co pozwala zapełnić kolekcję tam, gdzie jest zadeklarowana w czystej, czytelnej i bezpiecznej składni.

Przykład w pytaniu brzmi:

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

Jest to nie tylko krótsze i łatwiejsze do odczytania, ale pozwala także uniknąć licznych problemów związanych z podwójnie wzmocnionym wzorem opisanym w innych odpowiedziach . Jasne, działa podobnie jak bezpośrednio skonstruowana HashMap, ale jest niebezpieczna i podatna na błędy, a są lepsze opcje.

Za każdym razem, gdy zastanawiasz się nad podwójną inicjalizacją, powinieneś ponownie sprawdzić swoje interfejsy API lub wprowadzić nowe, aby właściwie rozwiązać problem, zamiast korzystać ze sztuczek składniowych.

Podatne na błędy oznacza teraz ten anty-wzór .


-1. Pomimo pewnych ważnych punktów, ta odpowiedź sprowadza się do: „Jak uniknąć generowania niepotrzebnych anonimowych klas? Użyj frameworka, z jeszcze większą liczbą klas!”
Agent_L,

1
Powiedziałbym, że sprowadza się to do „użycia odpowiedniego narzędzia do pracy, a nie włamania, które może spowodować awarię aplikacji”. Guava jest dość powszechną biblioteką do dołączania aplikacji (zdecydowanie nie możesz jej pominąć, jeśli jej nie używasz), ale nawet jeśli nie chcesz jej używać, możesz i powinieneś unikać inicjalizacji podwójnego nawiasu klamrowego.
dimo414,

W jaki sposób inicjalizacja podwójnego nawiasu klamrowego spowodowałaby wyciek mięśni?
Angel O'Sphere

@ AngelO'Sphere DBI jest zaciemnionym sposobem tworzenia wewnętrznej klasy , dlatego zachowuje niejawne odniesienie do otaczającej go klasy (chyba że jest używane tylko w statickontekstach). Link podatny na błędy na dole mojego pytania omawia to dalej.
dimo414,

Powiedziałbym, że to kwestia gustu. I nie ma w tym nic tak naprawdę zaciemniającego.
Angel O'Sphere

4

Badałem to i postanowiłem przeprowadzić bardziej szczegółowy test niż ten, który podał poprawna odpowiedź.

Oto kod: https://gist.github.com/4368924

i to jest mój wniosek

Byłem zaskoczony, gdy stwierdziłem, że w większości testów uruchamiania inicjacja wewnętrzna była w rzeczywistości szybsza (w niektórych przypadkach prawie dwukrotnie). Podczas pracy z dużymi liczbami korzyść wydaje się zanikać.

Co ciekawe, skrzynka, która tworzy 3 obiekty w pętli, traci swoją korzyść szybciej niż w innych przypadkach. Nie jestem pewien, dlaczego tak się dzieje i należy przeprowadzić więcej testów, aby dojść do jakichkolwiek wniosków. Tworzenie konkretnych implementacji może pomóc uniknąć ponownego załadowania definicji klasy (jeśli tak się dzieje)

Oczywiste jest jednak, że w większości przypadków nie zaobserwowano zbyt dużego obciążenia związanego z budową pojedynczego przedmiotu, nawet przy dużych liczbach.

Jednym cofnięciem byłby fakt, że każda inicjacja podwójnego nawiasu klamrowego tworzy nowy plik klasy, który dodaje cały blok dysku do rozmiaru naszej aplikacji (lub około 1k po skompresowaniu). Mały ślad, ale jeśli jest używany w wielu miejscach, może potencjalnie mieć wpływ. Użyj tego 1000 razy, a potencjalnie dodajesz całą MiB do swojej aplikacji, co może dotyczyć środowiska osadzonego.

Mój wniosek? Można używać, o ile nie jest to nadużywane.

Powiedz mi co myślisz :)


2
To nie jest poprawny test. Kod tworzy obiekty bez ich używania, co pozwala optymalizatorowi pomijać całe tworzenie instancji. Jedynym pozostałym efektem ubocznym jest przyspieszenie losowej sekwencji liczb, której koszty i tak przewyższają wszystko inne w tych testach.
Holger

3

Drugą odpowiedź Nat, z wyjątkiem tego, że użyłbym pętli zamiast tworzyć i natychmiast podrzucać niejawną listę z asList (elements):

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

1
Dlaczego? Nowy obiekt zostanie utworzony w przestrzeni eden, dlatego do utworzenia instancji potrzeba tylko dwóch lub trzech dodatków wskaźnika. JVM może zauważyć, że nigdy nie ucieka poza zakres metody, więc przydziel go na stosie.
Nat

Tak, prawdopodobnie skończy się to wydajniejszym kodem (chociaż możesz go poprawić, informując HashSeto sugerowanej pojemności - pamiętaj o współczynniku obciążenia).
Tom Hawtin - tackline

Konstruktor HashSet i tak musi wykonać iterację, więc nie będzie mniej wydajna. Kod biblioteki stworzony do ponownego użycia powinien zawsze starać się być jak najlepszy .
Lawrence Dol

3

Chociaż ta składnia może być wygodna, dodaje również wiele odwołań do 0 USD, ponieważ zostają one zagnieżdżone, a debugowanie w inicjalizatorach może być trudne, chyba że dla każdego z nich zostaną ustawione punkty przerwania. Z tego powodu zalecam używanie tego tylko do banalnych ustawień, szczególnie ustawionych na stałe i miejsca, w których anonimowe podklasy nie mają znaczenia (jak brak serializacji).


3

Mario Gleichman opisuje, jak używać ogólnych funkcji Java 1.5 do symulacji literałów Scala List, choć niestety kończy się na niezmiennych Listach.

Definiuje tę klasę:

package literal;

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

i używa go w ten sposób:

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

Kolekcje Google, teraz będące częścią Guava, obsługują podobny pomysł na tworzenie listy. W tym wywiadzie Jared Levy mówi:

[...] najczęściej używanymi funkcjami, które pojawiają się w prawie każdej klasie Java, którą piszę, są metody statyczne, które zmniejszają liczbę powtarzających się naciśnięć klawiszy w kodzie Java. Wygodne jest wprowadzanie następujących poleceń:

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

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

7/10/2014: Gdyby to mogło być tak proste jak Python:

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

21.02.2020: W Javie 11 możesz teraz powiedzieć:

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


2
  1. To zadzwoni add()do każdego członka. Jeśli możesz znaleźć bardziej skuteczny sposób umieszczania przedmiotów w zestawie skrótów, skorzystaj z niego. Zauważ, że klasa wewnętrzna prawdopodobnie wygeneruje śmieci, jeśli jesteś na to wrażliwy.

  2. Wydaje mi się, że kontekstem jest zwracany obiekt new, którym jest HashSet.

  3. Jeśli musisz zapytać ... Bardziej prawdopodobne: czy ludzie, którzy przyjdą po tobie, wiedzą to, czy nie? Czy łatwo to zrozumieć i wyjaśnić? Jeśli możesz odpowiedzieć „tak” na oba, skorzystaj z nich.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.