Czy powinienem używać bloków inicjalizujących w Javie?

16

Ostatnio natknąłem się na konstrukcję Java, której nigdy wcześniej nie widziałem i zastanawiałem się, czy powinienem jej użyć. Wydaje się, że nazywa się to blokami inicjalizacyjnymi .

public class Test {
  public Test() { /* first constructor */ }
  public Test(String s) { /* second constructor */ }

  // Non-static initializer block - copied into every constructor:
  {
    doStuff();
  }
}

Blok kodu zostanie skopiowany do każdego konstruktora, tzn. Jeśli masz wiele konstruktorów, nie musisz przepisywać kodu.

Widzę jednak trzy główne wady wykorzystujące tę składnię:

  1. Jest to jeden z nielicznych przypadków w Javie, w których ważna jest kolejność kodu, ponieważ można zdefiniować wiele bloków kodu i będą one wykonywane w kolejności, w jakiej zostały zapisane. Wydaje mi się to szkodliwe, ponieważ zmiana kolejności bloków kodu spowoduje zmianę kodu.
  2. Używając go, nie widzę żadnych korzyści. W większości przypadków konstruktory będą wywoływać się z pewnymi predefiniowanymi wartościami. Nawet jeśli tak nie jest, kod można po prostu umieścić w prywatnej metodzie i wywołać z każdego konstruktora.
  3. Zmniejsza to czytelność, ponieważ można umieścić blok na końcu klasy, a konstruktor zwykle znajduje się na początku klasy. Spoglądanie na zupełnie inną część pliku kodu jest zupełnie sprzeczne z intuicją, jeśli nie spodziewasz się, że będzie to konieczne.

Jeśli moje powyższe stwierdzenia są prawdziwe, dlaczego (i kiedy) wprowadzono ten konstrukt językowy? Czy istnieją uzasadnione przypadki użycia?

Przywróć Monikę - sztylet
źródło
3
W opublikowanym przykładzie nie ma niczego, co wygląda jak blok inicjujący.
Simon B
6
@SimonBarker spójrz ponownie - { doStuff(); }na poziomie klasy jest blok inicjujący.
amon
@SimonBarker Otaczający blok kodudoStuff()
Przywróć Monikę - dirkk
2
„[S] oznacza, że ​​zmiana kolejności bloków kodu faktycznie zmieni kod”. A czym to się różni od zmiany kolejności inicjalizatorów zmiennych lub poszczególnych wierszy kodu? Jeśli nie ma zależności, to nie występuje szkoda, a jeśli istnieją zależności, to umieszczenie zależności poza kolejnością jest takie samo, jak błędne zależności dla poszczególnych linii kodu. To, że Java pozwala odwoływać się do metod i klas przed ich zdefiniowaniem, nie oznacza, że ​​kod zależny od kolejności jest rzadki w Javie.
JAB

Odpowiedzi:

20

Są dwa przypadki, w których używam bloków inicjalizujących.

Pierwszy służy do inicjowania członków końcowych. W Javie możesz zainicjować element końcowy albo zgodny z deklaracją, albo możesz zainicjować go w konstruktorze. W metodzie zabrania się przypisywania do końcowego członka.

Jest to ważne:

final int val = 2;

Jest to również ważne:

final int val;

MyClass() {
    val = 2;
}

To jest nieprawidłowe:

final int val;

MyClass() {
    init();
}

void init() {
    val = 2;  // cannot assign to 'final' field in a method
}

Jeśli masz wiele konstruktorów i nie możesz zainicjować wbudowanego elementu końcowego (ponieważ logika inicjalizacji jest zbyt złożona) lub jeśli konstruktory nie mogą się wywołać, możesz skopiować / wkleić kod inicjujący lub użyć blok inicjalizujący.

final int val;
final int squareVal;

MyClass(int v, String s) {
    this.val = v;
    this.s = s;
}

MyClass(Point p, long id) {
    this.val = p.x;
    this.id = id;
}

{
    squareVal = val * val;
}

Innym przypadkiem użycia, jaki mam dla bloków inicjalizacyjnych, jest budowanie małych struktur danych pomocniczych. Deklaruję element członkowski i umieszczam w nim wartości zaraz po jego deklaracjach we własnym bloku inicjującym.

private Map<String, String> days = new HashMap<String, String>();
{
    days.put("mon", "monday");
    days.put("tue", "tuesday");
    days.put("wed", "wednesday");
    days.put("thu", "thursday");
    days.put("fri", "friday");
    days.put("sat", "saturday");
    days.put("sun", "sunday");
}
Barjak
źródło
To nie wywołanie metody jest nieprawidłowe. Niepoprawny jest kod w metodzie init. Tylko konstruktory i bloki inicjalizujące mogą przypisywać do końcowej zmiennej składowej, dlatego przypisanie w init nie zostanie skompilowane.
barjak
Czwarty blok kodu nie jest kompilowany. Bloki inicjalizatora działają przed wszystkimi konstruktorami, dlatego squareVal = val * valbędą narzekać na dostęp do niezainicjowanych wartości. Bloki inicjalizujące nie mogą zależeć od argumentów przekazanych do konstruktora. Typowym rozwiązaniem tego rodzaju problemu jest zdefiniowanie jednego konstruktora „podstawowego” o złożonej logice i zdefiniowanie wszystkich innych konstruktorów pod tym względem. W rzeczywistości większość zastosowań inicjatorów instancji można zastąpić tym wzorcem.
Malnormalulo
11

Zasadniczo nie używaj niestatycznych bloków inicjalizacyjnych (i być może unikaj również bloków statycznych).

Myląca składnia

Patrząc na to pytanie, istnieją 3 odpowiedzi, ale oszukałeś 4 osoby tą składnią. Byłem jednym z nich i pisałem w Javie od 16 lat! Oczywiście składnia jest potencjalnie podatna na błędy! Trzymałbym się od tego z daleka.

Konstruktory teleskopowe

W przypadku naprawdę prostych rzeczy można użyć konstruktorów „teleskopowych”, aby uniknąć tego zamieszania:

public class Test {
    private String something;

    // Default constructor does some things
    public Test() { doStuff(); }

    // Other constructors call the default constructor
    public Test(String s) {
        this(); // Call default constructor
        something = s;
    }
}

Wzór konstruktora

Jeśli potrzebujesz doStuff () na końcu każdego konstruktora lub innej zaawansowanej inicjalizacji, być może wzorzec konstruktora byłby najlepszy. Josh Bloch wymienia kilka powodów, dla których budowniczowie są dobrym pomysłem. Konstruktorzy poświęcają trochę czasu na pisanie, ale właściwie napisane, są przyjemnością w użyciu.

public class Test {
    // Value can be final (immutable)
    private final String something;

    // Private constructor.
    private Test(String s) { something = s; }

    // Static method to get a builder
    public static Builder builder() { return new Builder(); }

    // builder class accumulates values until a valid Test object can be created. 
    private static class Builder {
        private String tempSomething;
        public Builder something(String s) {
            tempSomething = s;
            return this;
        }
        // This is our factory method for a Test class.
        public Test build() {
            Test t = new Test(tempSomething);
            // Here we do your extra initialization after the
            // Test class has been created.
            doStuff();
            // Return a valid, potentially immutable Test object.
            return t;
        }
    }
}

// Now you can call:
Test t = Test.builder()
             .setString("Utini!")
             .build();

Statyczne pętle inicjujące

Często używałem statycznych inicjatorów, ale czasami wpadałem w pętle, w których 2 klasy zależały od siebie nawzajem bloków statycznego inicjatora, zanim klasa mogła zostać w pełni załadowana. Powodowało to „nie udało się załadować klasy” lub podobnie niejasny komunikat o błędzie. Musiałem porównać pliki z ostatnią znaną działającą wersją w kontroli źródła, aby dowiedzieć się, na czym polega problem. W ogóle nie ma zabawy.

Leniwa inicjalizacja

Może statyczne inicjalizatory są dobre ze względu na wydajność, gdy działają i nie są zbyt mylące. Ale ogólnie wolę obecnie leniwe inicjowanie niż statyczne inicjalizatory. Jest jasne, co robią, nie spotkałem się jeszcze z błędem ładowania klas i działają one w większej liczbie sytuacji inicjalizacyjnych niż w blokach inicjujących.

Definicja danych

Zamiast statycznej inicjalizacji do budowania struktur danych (porównaj z przykładami w innych odpowiedziach), teraz używam niezmiennych funkcji pomocniczych definicji danych Paguro :

private ImMap<String,String> days =
        map(tup("mon", "monday"),
            tup("tue", "tuesday"),
            tup("wed", "wednesday"),
            tup("thu", "thursday"),
            tup("fri", "friday"),
            tup("sat", "saturday"),
            tup("sun", "sunday"));

Conculsion

Na początku Javy bloki inicjalizacyjne były jedynym sposobem na zrobienie pewnych rzeczy, ale teraz są mylące, podatne na błędy, aw większości przypadków zostały zastąpione lepszymi alternatywami (wyszczególnione powyżej). Interesujące jest wiedzieć o blokach inicjalizujących, na wypadek, gdy zobaczysz je w starszym kodzie lub pojawią się w teście, ale gdybym robił przegląd kodu i widziałem jeden w nowym kodzie, poprosiłbym cię o uzasadnienie, dlaczego żaden z powyższe alternatywy były odpowiednie przed podaniem kciuka do góry.

GlenPeterson
źródło
3

Oprócz inicjalizacji zmiennej instancji zadeklarowanej jako final(patrz odpowiedź barjaka ) wspomniałbym również o staticbloku inicjalizacji.

Możesz użyć ich jako swego rodzaju „statycznego konstruktora”.

W ten sposób można wykonać złożone inicjalizacje zmiennej statycznej przy pierwszym odwołaniu do klasy.

Oto przykład zainspirowany przykładem Barjaka:

public class dayHelper(){
    private static Map<String, String> days = new HashMap<String, String>();
    static {
        days.put("mon", "monday");
        days.put("tue", "tuesday");
        days.put("wed", "wednesday");
        days.put("thu", "thursday");
        days.put("fri", "friday");
        days.put("sat", "saturday");
        days.put("sun", "sunday");
    }
    public static String getLongName(String shortName){
         return days.get(shortName);
    }
}
C. szampan
źródło
1

Jeśli chodzi o statyczne bloki inicjujące, ich podstawową funkcją jest działanie jako domyślny konstruktor w klasach anonimowych. To w zasadzie ich jedyne prawo do istnienia.

Nico
źródło
0

Całkowicie zgadzam się z instrukcjami 1, 2, 3. Z tych powodów nigdy nie używam inicjatorów blokowych i nie wiem, dlaczego istnieje w Javie.

Jestem jednak zmuszony użyć inicjatora bloku statycznego w jednym przypadku: gdy muszę utworzyć instancję pola statycznego, którego konstruktor może zgłosić sprawdzony wyjątek.

private static final JAXBContext context = JAXBContext.newInstance(Foo.class); //doesn't compile

Ale zamiast tego musisz zrobić:

private static JAXBContext context;
static {
    try
    {
        context = JAXBContext.newInstance(Foo.class);
    }
    catch (JAXBException e)
    {
        //seriously...
    }
}

Uważam ten idiom za bardzo brzydki (to także zapobiega oznaczeniu contextjako final), ale jest to jedyny obsługiwany przez Javę sposób inicjowania takich pól.

Cętkowany
źródło
Myślę, że jeśli ustawisz context = null;blok catch, możesz określić kontekst jako ostateczny.
GlenPeterson
@GlenPeterson Próbowałem, ale nie kompilacji:The final field context may already have been assigned
Znaleziono
ups! Założę się, że możesz uczynić swój kontekst ostatecznym, jeśli wprowadzisz zmienną lokalną w bloku statycznym:static { JAXBContext tempCtx = null; try { tempCtx = JAXBContext.newInstance(Foo.class); } catch (JAXBException ignored) { ; } context = tempCtx; }
GlenPeterson