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 .
źródło
flavors
się, że jestHashSet
, ale niestety jest to anonimowa podklasa.Set<String> flavors = Set.of( "vanilla" , "strawberry" , "chocolate" , "butter pecan" ) ;
Odpowiedzi:
Oto problem, kiedy zbyt mnie pociągają anonimowe klasy wewnętrzne:
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
class
plik.„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
class
plikó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:
Wygląda jak „ukryta” funkcja Java, ale jest to po prostu przepisanie:
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:
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
ArrayList
s elementami"Hello"
i"World!"
dodaj do nich za pomocąadd
metody, używając dwóch metod:Metoda 1: Inicjalizacja podwójnego nawiasu klamrowego
Metoda 2: Utwórz
ArrayList
iadd
Stworzyłem prosty program do wypisania pliku źródłowego Java w celu wykonania 1000 inicjalizacji przy użyciu dwóch metod:
Test 1:
Test 2:
Należy pamiętać, że czas, który upłynął do zainicjowania 1000
ArrayList
s oraz rozszerzenie 1000 anonimowych klas wewnętrznychArrayList
jest 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:
Jak można zauważyć, inicjalizacja podwójnego nawiasu klamrowego ma zauważalny czas wykonania około 190 ms.
Tymczasem
ArrayList
czas 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,
.class
wygenerowano 1000 plików, kompilującTest1
program testowy inicjujący podwójny nawias klamrowy.źródło
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).
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ą.Aby wyjaśnić anonimową tworzoną podklasę, możesz również zdefiniować tam metody. Na przykład zastąpienie
HashSet.add()
źródło
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:
... stworzy te klasy:
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:
Zwrócony
Map
będzie teraz zawierał odwołanie do załączającej instancjiReallyHeavyObject
. Prawdopodobnie nie chcesz ryzykować, że: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:
Niektóre osoby mogą uznać to za stymulujące syntaktycznie.
źródło
Biorąc następującą klasę testową:
a następnie dekompilując plik klasy, widzę:
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):
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.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: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:
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
flavors
będzie przeciwne,HashSet
ale przeciwneMyHashSet
. 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ętrznejHashSet
.Jeśli masz pytania dotyczące szczegółów inicjatorów instancji, sprawdź szczegóły w dokumentacji JLS .
źródło
super()
drugą linię niejawnego konstruktora, ale musi być na pierwszym miejscu. (Przetestowałem to i nie można go skompilować.)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ć.
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 jaknew LinkedHashSet(Arrays.asList("xxx","YYY"));
źródło
Ł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.
odciski
źródło
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.Aby utworzyć zestawy, możesz użyć fabrycznej metody varargs zamiast inicjalizacji podwójnego nawiasu:
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.
źródło
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:Ograniczeniem tego podejścia jest oczywiście to, że nie można kontrolować konkretnego typu listy, która ma zostać wygenerowana.
źródło
new ArrayList<String>(Arrays.asList("vanilla", "strawberry", "chocolate"))
aby obejść ten problem.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.
źródło
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:
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 .
źródło
static
kontekstach). Link podatny na błędy na dole mojego pytania omawia to dalej.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
Powiedz mi co myślisz :)
źródło
Drugą odpowiedź Nat, z wyjątkiem tego, że użyłbym pętli zamiast tworzyć i natychmiast podrzucać niejawną listę z asList (elements):
źródło
HashSet
o sugerowanej pojemności - pamiętaj o współczynniku obciążenia).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).
źródło
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ę:
i używa go w ten sposób:
Kolekcje Google, teraz będące częścią Guava, obsługują podobny pomysł na tworzenie listy. W tym wywiadzie Jared Levy mówi:
7/10/2014: Gdyby to mogło być tak proste jak Python:
21.02.2020: W Javie 11 możesz teraz powiedzieć:
źródło
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.Wydaje mi się, że kontekstem jest zwracany obiekt
new
, którym jestHashSet
.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.
źródło