Soll ich Initialisierungsblöcke in Java verwenden?

16

Ich bin kürzlich auf ein Java-Konstrukt gestoßen, das ich noch nie gesehen habe, und habe mich gefragt, ob ich es verwenden soll. Es scheint Initialisierungsblöcke zu heißen .

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

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

Der Codeblock wird in jeden Konstruktor kopiert, dh wenn Sie mehrere Konstruktoren haben, müssen Sie den Code nicht umschreiben.

Ich sehe jedoch drei Hauptnachteile bei der Verwendung dieser Syntax:

  1. Es ist einer der wenigen Fälle in Java, in denen die Reihenfolge Ihres Codes wichtig ist, da Sie mehrere Codeblöcke definieren können, die in der Reihenfolge ausgeführt werden, in der sie geschrieben wurden. Dies scheint mir schädlich zu sein, da eine einfache Änderung der Reihenfolge der Codeblöcke den Code tatsächlich ändert.
  2. Ich sehe keine wirklichen Vorteile, wenn ich es benutze. In den meisten Fällen rufen sich die Konstruktoren gegenseitig mit vordefinierten Werten auf. Auch wenn dies nicht der Fall ist, kann der Code einfach in eine private Methode gestellt und von jedem Konstruktor aufgerufen werden.
  3. Dies verringert die Lesbarkeit, da Sie den Block am Ende der Klasse einfügen können und der Konstruktor normalerweise am Anfang der Klasse steht. Es ist ziemlich kontraintuitiv, einen völlig anderen Teil einer Codedatei zu betrachten, wenn Sie nicht erwarten, dass dies erforderlich ist.

Wenn meine obigen Aussagen wahr sind, warum (und wann) wurde dieses Sprachkonstrukt eingeführt? Gibt es legitime Anwendungsfälle?

Setzen Sie Monica - dirkk wieder ein
quelle
3
Das Beispiel, das Sie gepostet haben, enthält nichts, das wie ein Initialisierungsblock aussieht.
Simon B
6
@ SimonBarker nochmal schauen - das { doStuff(); }auf der Klassenebene ist ein Initialisierungsblock.
amon
@ SimonBarker Der Code-Block, der umgibtdoStuff()
Reinstate Monica - dirkk
2
"[S] bedeutet, dass die Reihenfolge der Codeblöcke den Code tatsächlich ändert." Und wie unterscheidet sich das von der Änderung der Reihenfolge von Variableninitialisierern oder einzelnen Codezeilen? Wenn es keine Abhängigkeiten gibt, tritt kein Schaden auf, und wenn es Abhängigkeiten gibt, ist das Aufheben der Reihenfolge der Abhängigkeiten dasselbe wie das falsche Ordnen von Abhängigkeiten für einzelne Codezeilen. Nur weil Sie in Java auf Methoden und Klassen verweisen können, bevor sie definiert werden, bedeutet dies nicht, dass auftragsabhängiger Code in Java selten vorkommt.
JAB

Antworten:

20

Es gibt zwei Fälle, in denen ich Initialisierungsblöcke verwende.

Die erste dient zum Initialisieren der endgültigen Mitglieder. In Java können Sie ein endgültiges Element entweder direkt in der Deklaration oder im Konstruktor initialisieren. In einer Methode ist es verboten, ein endgültiges Mitglied zuzuweisen.

Das ist gültig:

final int val = 2;

Dies gilt auch:

final int val;

MyClass() {
    val = 2;
}

Das ist ungültig:

final int val;

MyClass() {
    init();
}

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

Wenn Sie mehrere Konstruktoren haben und ein letztes Element nicht inline initialisieren können (weil die Initialisierungslogik zu komplex ist) oder wenn die Konstruktoren sich nicht selbst aufrufen können, können Sie den Initialisierungscode entweder kopieren / einfügen oder verwenden ein Initialisierungsblock.

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

Der andere Anwendungsfall, den ich für Initialisierungsblöcke habe, ist das Erstellen kleiner Hilfsdatenstrukturen. Ich deklariere ein Member und füge Werte direkt nach den Deklarationen in den eigenen Initialisierungsblock ein.

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
quelle
Es ist nicht der Methodenaufruf, der ungültig ist. Es ist der Code in der init-Methode, der ungültig ist. Nur Konstruktoren und Initalizer-Blöcke können einer finalen Member-Variablen zugewiesen werden, daher wird die Zuweisung in init nicht kompiliert.
Barjak
Ihr vierter Codeblock wird nicht kompiliert. Initialisierungsblöcke werden vor allen Konstruktoren ausgeführt und squareVal = val * valbeschweren sich daher über den Zugriff auf nicht initialisierte Werte. Initialisierungsblöcke können möglicherweise nicht von Argumenten abhängen, die an den Konstruktor übergeben werden. Die übliche Lösung für dieses Problem besteht darin, einen einzelnen "Basis" -Konstruktor mit der komplexen Logik zu definieren und alle anderen Konstruktoren in Bezug auf diesen zu definieren. Tatsächlich können die meisten Verwendungen von Instanzinitialisierern durch dieses Muster ersetzt werden.
Malnormalulo
11

Verwenden Sie im Allgemeinen keine nicht statischen Initialisierungsblöcke (und vermeiden Sie möglicherweise auch statische).

Verwirrende Syntax

Wenn Sie sich diese Frage ansehen, gibt es 3 Antworten, aber Sie haben 4 Personen mit dieser Syntax getäuscht. Ich war einer von ihnen und schreibe seit 16 Jahren Java! Natürlich ist die Syntax möglicherweise fehleranfällig! Ich würde mich davon fernhalten.

Teleskopbauer

Für wirklich einfache Dinge können Sie "teleskopierende" Konstruktoren verwenden, um diese Verwirrung zu vermeiden:

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

Builder-Muster

Wenn Sie doStuff () am Ende eines jeden Konstruktors oder einer anderen raffinierten Initialisierung benötigen, ist möglicherweise ein Builder-Muster am besten geeignet. Josh Bloch nennt mehrere Gründe, warum Bauherren eine gute Idee sind. Bauherren nehmen sich etwas Zeit zum Schreiben, aber richtig geschrieben ist es eine Freude, sie zu benutzen.

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

Statische Initialisierungsschleifen

Früher habe ich häufig statische Initialisierer verwendet, aber gelegentlich kam es zu Schleifen, in denen zwei Klassen davon abhingen, ob die statischen Initialisiererblöcke des jeweils anderen aufgerufen wurden, bevor die Klasse vollständig geladen werden konnte. Dies erzeugte eine "Klasse konnte nicht geladen werden" oder eine ähnlich vage Fehlermeldung. Ich musste Dateien mit der letzten bekannten Arbeitsversion in der Quellcodeverwaltung vergleichen, um herauszufinden, wo das Problem lag. Gar kein Spaß.

Lazy Initialization

Vielleicht sind statische Initialisierer aus Performancegründen gut, wenn sie funktionieren und nicht zu verwirrend. Aber im Allgemeinen bevorzuge ich in diesen Tagen die verzögerte Initialisierung gegenüber statischen Initialisierern. Es ist klar, was sie tun, ich habe noch keinen Fehler beim Laden von Klassen mit ihnen entdeckt, und sie funktionieren in mehr Initialisierungssituationen als Initialisierungsblöcke.

Datendefinition

Anstelle der statischen Initialisierung zum Erstellen von Datenstrukturen (vergleiche die Beispiele in den anderen Antworten) verwende ich jetzt die unveränderlichen Hilfefunktionen für die Datendefinition von 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"));

Erschütterung

Zu Beginn von Java waren Initialisierungsblöcke die einzige Möglichkeit, einige Dinge zu tun, aber jetzt sind sie verwirrend, fehleranfällig und wurden in den meisten Fällen durch bessere Alternativen ersetzt (siehe oben). Es ist interessant, Informationen zu Initialisierungsblöcken zu erhalten, falls Sie sie im Legacy-Code sehen oder wenn sie in einem Test auftauchen. Wenn ich jedoch eine Codeüberprüfung durchführe und einen neuen Code sehe, möchte ich Sie bitten, zu begründen, warum keiner der Blöcke vorhanden ist Die oben genannten Alternativen waren geeignet, bevor Sie Ihrem Code die Daumen hoch gaben.

GlenPeterson
quelle
3

Zusätzlich zur Initialisierung einer Instanzvariablen, die als deklariert ist final(siehe Antwort von Barjak ), würde ich auch den staticInitialisierungsblock erwähnen .

Sie können sie als eine Art "statischer Konstruktor" verwenden.

Auf diese Weise können Sie komplexe Initialisierungen für eine statische Variable ausführen, wenn zum ersten Mal auf die Klasse verwiesen wird.

Hier ist ein Beispiel, das von Barjaks inspiriert wurde:

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. Champagner
quelle
1

Was nicht statische Initialisierungsblöcke betrifft, besteht ihre reine Funktion darin, in anonymen Klassen als Standardkonstruktor zu fungieren. Das ist im Grunde ihr einziges Existenzrecht.

Nico
quelle
0

Ich stimme den Aussagen 1, 2 und 3 voll und ganz zu. Aus diesen Gründen verwende ich auch nie Blockinitialisierer, und ich weiß nicht, warum sie in Java existieren.

Ich bin jedoch gezwungen, den statischen Blockinitialisierer in einem Fall zu verwenden: Wenn ich ein statisches Feld instanziieren muss, dessen Konstruktor eine aktivierte Ausnahme auslösen kann.

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

Aber stattdessen müssen Sie Folgendes tun:

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

Ich finde dieses Idiom sehr hässlich (es verhindert auch das Markieren contextals final), aber dies ist die einzige von Java unterstützte Möglichkeit, solche Felder zu initialisieren.

Beschmutzt
quelle
Ich denke, wenn Sie context = null;in Ihrem catch-Block festlegen , können Sie möglicherweise den Kontext als endgültig deklarieren.
GlenPeterson
@ GlenPeterson Ich habe versucht, aber es nicht kompiliert:The final field context may already have been assigned
Spotted
Hoppla! Ich wette, Sie können Ihren Kontext endgültig machen, wenn Sie eine lokale Variable innerhalb des statischen Blocks static { JAXBContext tempCtx = null; try { tempCtx = JAXBContext.newInstance(Foo.class); } catch (JAXBException ignored) { ; } context = tempCtx; }
einführen