Richtige Redewendung für die Verwaltung mehrerer verketteter Ressourcen im Try-with-Resources-Block?

168

Die Java 7- Try-with-Resources- Syntax (auch als ARM-Block ( Automatic Resource Management ) bezeichnet) ist nett, kurz und unkompliziert, wenn nur eine AutoCloseableRessource verwendet wird. Ich bin mir jedoch nicht sicher, was die richtige Redewendung ist, wenn ich mehrere voneinander abhängige Ressourcen deklarieren muss, z. B. a FileWriterund a BufferedWriter, die sie umschließen. Diese Frage betrifft natürlich jeden Fall, in dem einige AutoCloseableRessourcen eingeschlossen sind, nicht nur diese beiden spezifischen Klassen.

Ich habe mir die folgenden drei Alternativen ausgedacht:

1)

Die naive Redewendung, die ich gesehen habe, besteht darin, nur den Wrapper der obersten Ebene in der von ARM verwalteten Variablen zu deklarieren:

static void printToFile1(String text, File file) {
    try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
        bw.write(text);
    } catch (IOException ex) {
        // handle ex
    }
}

Das ist schön kurz, aber es ist kaputt. Da der Basiswert FileWriternicht in einer Variablen deklariert ist, wird er niemals direkt im generierten finallyBlock geschlossen. Es wird nur durch die closeMethode des Wickelns geschlossen BufferedWriter. Das Problem ist, dass wenn eine Ausnahme vom bwKonstruktor des Konstruktors ausgelöst closewird, diese nicht aufgerufen wird und daher der zugrunde liegende FileWriter Wert nicht geschlossen wird .

2)

static void printToFile2(String text, File file) {
    try (FileWriter fw = new FileWriter(file);
            BufferedWriter bw = new BufferedWriter(fw)) {
        bw.write(text);
    } catch (IOException ex) {
        // handle ex
    }
}

Hier werden sowohl die zugrunde liegende als auch die Wrapping-Ressource in den von ARM verwalteten Variablen deklariert, sodass beide sicher geschlossen werden, aber der zugrunde liegende fw.close() wird zweimal aufgerufen : nicht nur direkt, sondern auch über den Wrapping bw.close().

Dies sollte kein Problem für diese beiden spezifischen Klassen sein, die beide implementieren Closeable(was ein Subtyp von ist AutoCloseable), deren Vertrag besagt, dass mehrere Aufrufe closezulässig sind:

Schließt diesen Stream und gibt alle damit verbundenen Systemressourcen frei. Wenn der Stream bereits geschlossen ist, hat das Aufrufen dieser Methode keine Auswirkung.

Im Allgemeinen kann ich jedoch Ressourcen haben, die nur implementiert werden AutoCloseable(und nicht Closeable), was nicht garantiert, dass closedies mehrmals aufgerufen werden kann:

Beachten Sie, dass diese close-Methode im Gegensatz zur close-Methode von java.io.Closeable nicht idempotent sein muss. Mit anderen Worten, das mehrmalige Aufrufen dieser close-Methode kann im Gegensatz zu Closeable.close, das bei mehrmaligem Aufruf keine Wirkung haben muss, sichtbare Nebenwirkungen haben. Implementierern dieser Schnittstelle wird jedoch dringend empfohlen, ihre engen Methoden idempotent zu machen.

3)

static void printToFile3(String text, File file) {
    try (FileWriter fw = new FileWriter(file)) {
        BufferedWriter bw = new BufferedWriter(fw);
        bw.write(text);
    } catch (IOException ex) {
        // handle ex
    }
}

Diese Version sollte theoretisch korrekt sein, da nur die fweine echte Ressource darstellt, die bereinigt werden muss. Das bwselbst enthält keine Ressource, es delegiert nur an das fw, daher sollte es ausreichen, nur den Basiswert zu schließen fw.

Andererseits ist die Syntax etwas unregelmäßig und Eclipse gibt eine Warnung aus, die meines Erachtens ein Fehlalarm ist, aber es ist immer noch eine Warnung, mit der man sich befassen muss:

Ressourcenleck: 'bw' wird niemals geschlossen


Also, welchen Ansatz sollte man wählen? Oder habe ich eine andere Redewendung verpasst, die die richtige ist ?

Natix
quelle
4
Wenn der zugrunde liegende FileWriter-Konstruktor eine Ausnahme auslöst, wird er natürlich nicht einmal geöffnet und alles ist in Ordnung. Im ersten Beispiel geht es darum, was passiert, wenn der FileWriter erstellt wird, der Konstruktor des BufferedWriter jedoch eine Ausnahme auslöst.
Natix
6
Es ist erwähnenswert, dass BufferedWriter keine Ausnahme auslöst. Gibt es ein Beispiel, bei dem Sie sich vorstellen können, wo diese Frage nicht rein akademisch ist?
Peter Lawrey
10
@PeterLawrey Ja, Sie haben Recht, dass der Konstruktor des BufferedWriter in diesem Szenario höchstwahrscheinlich keine Ausnahme auslöst, aber wie ich bereits erwähnt habe, betrifft diese Frage alle Ressourcen im Dekorationsstil. Aber zum Beispiel public BufferedWriter(Writer out, int sz)kann man einen werfen IllegalArgumentException. Außerdem kann ich BufferedWriter um eine Klasse erweitern, die etwas von ihrem Konstruktor abwirft oder einen benutzerdefinierten Wrapper erstellt, den ich benötige.
Natix
5
Der BufferedWriterKonstruktor kann leicht eine Ausnahme auslösen. OutOfMemoryErrorist wahrscheinlich die häufigste, da sie dem Speicher einen angemessenen Speicherplatz zuweist (obwohl dies möglicherweise darauf hinweist, dass Sie den gesamten Prozess neu starten möchten). / Sie müssen flushIhre, BufferedWriterwenn Sie nicht schließen und den Inhalt behalten möchten (in der Regel nur der Nicht-Ausnahmefall). FileWriternimmt auf, was auch immer die "Standard" -Dateicodierung ist - es ist besser, explizit zu sein.
Tom Hawtin - Tackline
10
@ Natix Ich wünschte, alle Fragen in SO sind so gut recherchiert und klar wie diese. Ich wünschte, ich könnte dies 100 Mal abstimmen.
Geek

Antworten:

75

Hier ist meine Sicht auf die Alternativen:

1)

try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
    bw.write(text);
}

Für mich war das Beste, was vor 15 Jahren von traditionellem C ++ zu Java kam, dass Sie Ihrem Programm vertrauen konnten. Selbst wenn die Dinge im Dreck liegen und schief gehen, was sie oft tun, möchte ich, dass der Rest des Codes sich bestens verhält und nach Rosen riecht. In der Tat BufferedWriterkönnte das hier eine Ausnahme auslösen. Zum Beispiel wäre es nicht ungewöhnlich, wenn der Speicher knapp wird. Wissen Sie für andere Dekorateure, welche derjava.io Wrapper-Klassen eine geprüfte Ausnahme von ihren Konstruktoren auslösen? Ich nicht. Ist für die Verständlichkeit von Code nicht sehr gut, wenn Sie sich auf diese Art von obskurem Wissen verlassen.

Es gibt auch die "Zerstörung". Wenn eine Fehlerbedingung vorliegt, möchten Sie wahrscheinlich keinen Müll in eine Datei spülen, die gelöscht werden muss (Code dafür wird nicht angezeigt). Das Löschen der Datei ist natürlich auch eine weitere interessante Operation zur Fehlerbehandlung.

Im Allgemeinen möchten Sie, dass finallyBlöcke so kurz und zuverlässig wie möglich sind. Das Hinzufügen von Flushes hilft diesem Ziel nicht. Bei vielen Releases hatten einige der Pufferklassen im JDK einen Fehler, bei dem eine Ausnahme von flushinnen, closedie closefür das dekorierte Objekt verursacht wurde, nicht aufgerufen werden konnte. Während dies seit einiger Zeit behoben wurde, erwarten Sie es von anderen Implementierungen.

2)

try (
    FileWriter fw = new FileWriter(file);
    BufferedWriter bw = new BufferedWriter(fw)
) {
    bw.write(text);
}

Wir leeren immer noch den impliziten finally-Block (jetzt mit wiederholt close- dies wird schlimmer, wenn Sie weitere Dekoratoren hinzufügen), aber die Konstruktion ist sicher und wir müssen implizite finally-Blöcke implizieren, damit selbst ein Fehler flushdie Ressourcenfreigabe nicht verhindert.

3)

try (FileWriter fw = new FileWriter(file)) {
    BufferedWriter bw = new BufferedWriter(fw);
    bw.write(text);
}

Hier gibt es einen Fehler. Sollte sein:

try (FileWriter fw = new FileWriter(file)) {
    BufferedWriter bw = new BufferedWriter(fw);
    bw.write(text);
    bw.flush();
}

Einige schlecht implementierte Dekorateure sind in der Tat Ressourcen und müssen zuverlässig geschlossen werden. Außerdem müssen einige Streams möglicherweise auf eine bestimmte Weise geschlossen werden (möglicherweise führen sie eine Komprimierung durch und müssen Bits schreiben, um den Vorgang abzuschließen, und können nicht einfach alles leeren.

Urteil

Obwohl 3 eine technisch überlegene Lösung ist, ist 2 aus Gründen der Softwareentwicklung die bessere Wahl. Try-with-Resource ist jedoch immer noch eine unzureichende Lösung, und Sie sollten sich an die Execute Around-Sprache halten , die eine klarere Syntax mit Schließungen in Java SE 8 haben sollte.

Tom Hawtin - Tackline
quelle
4
Woher wissen Sie in Version 3, dass bw nicht erfordert, dass sein Abschluss aufgerufen wird? Und selbst wenn Sie sicher sein können, ist das nicht auch "obskures Wissen", wie Sie es für Version 1 erwähnt haben?
TimK
3
" Gründe für die Softwareentwicklung machen 2 zur besseren Wahl " Können Sie diese Aussage genauer erläutern?
Duncan Jones
8
Können Sie ein Beispiel für die "Execute Around Idiom with Closures"
Markus
2
Können Sie erklären, was "klarere Syntax mit Schließungen in Java SE 8" ist?
Petertc
1
Ein Beispiel für "Execute Around idiom" finden Sie hier: stackoverflow.com/a/342016/258772
mrts
20

Der erste Stil ist der von Oracle vorgeschlagene . BufferedWriterWirft keine aktivierten Ausnahmen aus. Wenn also eine Ausnahme ausgelöst wird, wird nicht erwartet, dass das Programm sie wiederherstellt, sodass die Wiederherstellung der Ressourcen größtenteils umstritten ist.

Meistens, weil es in einem Thread passieren könnte, während der Thread stirbt, das Programm aber weiterhin läuft - sagen wir, es gab einen vorübergehenden Speicherausfall, der nicht lang genug war, um den Rest des Programms ernsthaft zu beeinträchtigen. Es ist jedoch ein eher eckiger Fall, und wenn es häufig genug vorkommt, um das Auslaufen von Ressourcen zu einem Problem zu machen, ist der Versuch mit Ressourcen das geringste Ihrer Probleme.

Daniel C. Sobral
quelle
2
Dies ist auch der empfohlene Ansatz in der 3. Ausgabe von Effective Java.
Shmosel
5

Option 4

Ändern Sie Ihre Ressourcen so, dass sie geschlossen und nicht automatisch geschlossen werden können, wenn Sie können. Die Tatsache, dass die Konstruktoren verkettet werden können, impliziert, dass es nicht ungewöhnlich ist, die Ressource zweimal zu schließen. (Dies galt auch vor ARM.) Mehr dazu weiter unten.

Option 5

Verwenden Sie ARM und Code nicht sehr sorgfältig, um sicherzustellen, dass close () nicht zweimal aufgerufen wird!

Option 6

Verwenden Sie ARM nicht und lassen Sie Ihre finally close () -Aufrufe versuchen / fangen.

Warum ich nicht denke, dass dieses Problem nur bei ARM auftritt

In all diesen Beispielen sollten sich die Aufrufe von finally close () in einem catch-Block befinden. Aus Gründen der Lesbarkeit weggelassen.

Nicht gut, weil fw zweimal geschlossen werden kann. (Das ist in Ordnung für FileWriter, aber nicht in Ihrem hypothetischen Beispiel):

FileWriter fw = null;
BufferedWriter bw = null;
try {
  fw = new FileWriter(file);
  bw = new BufferedWriter(fw);
  bw.write(text);
} finally {
  if ( fw != null ) fw.close();
  if ( bw != null ) bw.close();
}

Nicht gut, da fw nicht geschlossen ist, wenn Ausnahme beim Erstellen eines BufferedWriter. (kann wieder nicht passieren, aber in Ihrem hypothetischen Beispiel):

FileWriter fw = null;
BufferedWriter bw = null;
try {
  fw = new FileWriter(file);
  bw = new BufferedWriter(fw);
  bw.write(text);
} finally {
  if ( bw != null ) bw.close();
}
Jeanne Boyarsky
quelle
3

Ich wollte nur auf Jeanne Boyarskys Vorschlag aufbauen, ARM nicht zu verwenden, aber sicherstellen, dass der FileWriter immer genau einmal geschlossen wird. Denken Sie nicht, dass es hier irgendwelche Probleme gibt ...

FileWriter fw = null;
BufferedWriter bw = null;
try {
    fw = new FileWriter(file);
    bw = new BufferedWriter(fw);
    bw.write(text);
} finally {
    if (bw != null) bw.close();
    else if (fw != null) fw.close();
}

Ich denke, da ARM nur syntaktischer Zucker ist, können wir ihn nicht immer verwenden, um endgültige Blöcke zu ersetzen. Genauso wie wir nicht immer eine for-each-Schleife verwenden können, um etwas zu tun, was mit Iteratoren möglich ist.

AmyGamy
quelle
5
Wenn sowohl Ihre tryals auch Ihre finallyBlöcke Ausnahmen auslösen, verliert dieses Konstrukt die erste (und möglicherweise nützlichere).
RXG
3

Um früheren Kommentaren zuzustimmen: Am einfachsten ist es, (2)Closeable Ressourcen zu verwenden und sie in der Try-with-Resources-Klausel der Reihe nach zu deklarieren. Wenn Sie nur haben AutoCloseable, können Sie sie in eine andere (verschachtelte) Klasse einschließen, die nur prüft, ob sie closenur einmal aufgerufen wird (Fassadenmuster), z private bool isClosed;. In der Praxis verkettet sogar Oracle nur (1) die Konstruktoren und behandelt Ausnahmen während der gesamten Kette nicht korrekt.

Alternativ können Sie eine verkettete Ressource mithilfe einer statischen Factory-Methode manuell erstellen. Dadurch wird die Kette gekapselt und die Bereinigung durchgeführt, wenn sie teilweise fehlschlägt:

static BufferedWriter createBufferedWriterFromFile(File file)
  throws IOException {
  // If constructor throws an exception, no resource acquired, so no release required.
  FileWriter fileWriter = new FileWriter(file);
  try {
    return new BufferedWriter(fileWriter);  
  } catch (IOException newBufferedWriterException) {
    try {
      fileWriter.close();
    } catch (IOException closeException) {
      // Exceptions in cleanup code are secondary to exceptions in primary code (body of try),
      // as in try-with-resources.
      newBufferedWriterException.addSuppressed(closeException);
    }
    throw newBufferedWriterException;
  }
}

Sie können es dann als einzelne Ressource in einer Try-with-Resources-Klausel verwenden:

try (BufferedWriter writer = createBufferedWriterFromFile(file)) {
  // Work with writer.
}

Die Komplexität ergibt sich aus der Behandlung mehrerer Ausnahmen. Andernfalls handelt es sich nur um "nahe Ressourcen, die Sie bisher erworben haben". Eine gängige Praxis scheint darin zu bestehen, zuerst die Variable zu initialisieren, die das Objekt enthält, in dem sich die Ressource befindet null(hier fileWriter), und dann eine Nullprüfung in die Bereinigung aufzunehmen. Dies scheint jedoch unnötig: Wenn der Konstruktor fehlschlägt, gibt es nichts zu bereinigen. Wir können diese Ausnahme also einfach verbreiten lassen, was den Code ein wenig vereinfacht.

Sie könnten dies wahrscheinlich generisch tun:

static <T extends AutoCloseable, U extends AutoCloseable, V>
    T createChainedResource(V v) throws Exception {
  // If constructor throws an exception, no resource acquired, so no release required.
  U u = new U(v);
  try {
    return new T(u);  
  } catch (Exception newTException) {
    try {
      u.close();
    } catch (Exception closeException) {
      // Exceptions in cleanup code are secondary to exceptions in primary code (body of try),
      // as in try-with-resources.
      newTException.addSuppressed(closeException);
    }
    throw newTException;
  }
}

Ebenso können Sie drei Ressourcen usw. verketten.

Abgesehen von der Mathematik könnten Sie sogar dreimal verketten, indem Sie zwei Ressourcen gleichzeitig verketten. Dies wäre assoziativ, was bedeutet, dass Sie beim Erfolg dasselbe Objekt erhalten (weil die Konstruktoren assoziativ sind) und bei einem Fehler dieselben Ausnahmen in einem der Konstruktoren. Angenommen, Sie haben der obigen Kette ein S hinzugefügt (also beginnen Sie mit einem V und enden mit einem S , indem Sie U , T und S der Reihe nach anwenden ), erhalten Sie dasselbe, wenn Sie zuerst S und T , dann U , verketten . entsprechend (ST) U , oder wenn Sie zuerst T und U verkettet haben , dannS , entsprechend S (TU) . Es wäre jedoch klarer, nur eine explizite dreifache Kette in einer einzigen Fabrikfunktion zu schreiben.

Nils von Barth
quelle
Sammle ich richtig, dass man noch Try-with-Resource verwenden muss, wie in try (BufferedWriter writer = <BufferedWriter, FileWriter>createChainedResource(file)) { /* work with writer */ }?
ErikE
@ErikE Ja, Sie müssten immer noch Try-with-Resources verwenden, aber Sie müssten nur eine einzige Funktion für die verkettete Ressource verwenden: Die Factory-Funktion kapselt die Verkettung. Ich habe ein Anwendungsbeispiel hinzugefügt. Vielen Dank!
Nils von Barth
2

Da Ihre Ressourcen verschachtelt sind, sollten Ihre Try-With-Klauseln auch lauten:

try (FileWriter fw=new FileWriter(file)) {
    try (BufferedWriter bw=new BufferedWriter(fw)) {
        bw.write(text);
    } catch (IOException ex) {
        // handle ex
    }
} catch (IOException ex) {
    // handle ex
}
Gift
quelle
5
Dies ist meinem zweiten Beispiel ziemlich ähnlich. Wenn keine Ausnahme auftritt, wird der FileWriter closezweimal aufgerufen.
Natix
0

Ich würde sagen, benutze ARM nicht und fahre mit Closeable fort. Verwenden Sie Methode wie,

public void close(Closeable... closeables) {
    for (Closeable closeable: closeables) {
       try {
           closeable.close();
         } catch (IOException e) {
           // you can't much for this
          }
    }

}

Sie sollten auch in Betracht ziehen, close of aufzurufen, BufferedWriterda es nicht nur das close von delegiert FileWriter, sondern auch einige Bereinigungsvorgänge ausführt flushBuffer.

Sakthisundar
quelle
0

Meine Lösung besteht darin, ein Refactoring der "Extraktionsmethode" wie folgt durchzuführen:

static AutoCloseable writeFileWriter(FileWriter fw, String txt) throws IOException{
    final BufferedWriter bw  = new BufferedWriter(fw);
    bw.write(txt);
    return new AutoCloseable(){

        @Override
        public void close() throws IOException {
            bw.flush();
        }

    };
}

printToFile kann entweder geschrieben werden

static void printToFile(String text, File file) {
    try (FileWriter fw = new FileWriter(file)) {
        AutoCloseable w = writeFileWriter(fw, text);
        w.close();
    } catch (Exception ex) {
        // handle ex
    }
}

oder

static void printToFile(String text, File file) {
    try (FileWriter fw = new FileWriter(file);
        AutoCloseable w = writeFileWriter(fw, text)){

    } catch (Exception ex) {
        // handle ex
    }
}

Für Klassenbibliothek-Designer werde ich vorschlagen, dass sie die AutoClosableSchnittstelle um eine zusätzliche Methode erweitern, um das Schließen zu unterdrücken. In diesem Fall können wir dann das Schließverhalten manuell steuern.

Für Sprachdesigner bedeutet die Lektion, dass das Hinzufügen einer neuen Funktion das Hinzufügen vieler anderer Funktionen bedeuten kann. In diesem Java-Fall funktioniert die ARM-Funktion offensichtlich besser mit einem Mechanismus zur Übertragung des Ressourcenbesitzes.

AKTUALISIEREN

Ursprünglich erfordert der obige Code, @SuppressWarningda das BufferedWriterInnere der Funktion erfordert close().

Wie in einem Kommentar vorgeschlagen flush(), müssen wir dies tun, bevor wir den Writer schließen, bevor return(implizite oder explizite) Anweisungen innerhalb des try-Blocks ausgeführt werden. Ich denke, es gibt derzeit keine Möglichkeit, sicherzustellen, dass der Anrufer dies tut. Daher muss dies dokumentiert werden writeFileWriter.

UPDATE WIEDER

Das obige Update macht @SuppressWarningunnötig, da die Funktion die Ressource an den Aufrufer zurückgeben muss, sodass sie selbst nicht geschlossen werden muss. Dies bringt uns leider zurück zum Anfang der Situation: Die Warnung wird jetzt zurück auf die Anruferseite verschoben.

Um dies richtig zu lösen, benötigen wir eine angepasste, AutoClosabledass die Unterstreichung bei jedem Schließen bearbeitet BufferedWriterwird flush(). Tatsächlich zeigt uns dies eine andere Möglichkeit, die Warnung zu umgehen, da die Warnung BufferWriterin keiner Weise geschlossen wird.

Erdmotor
quelle
Die Warnung hat ihre Bedeutung: Können wir hier sicher sein, dass die bwDaten tatsächlich ausgeschrieben werden? Es wird immerhin gepuffert, so dass es nur manchmal auf die Festplatte schreiben muss (wenn der Puffer voll und / oder eingeschaltet ist flush()und close()Methoden). Ich denke, die flush()Methode sollte aufgerufen werden. Aber dann ist es sowieso nicht nötig, den gepufferten Writer zu verwenden, wenn wir ihn sofort in einem Stapel ausschreiben. Und wenn Sie den Code nicht korrigieren, werden möglicherweise Daten in falscher Reihenfolge in die Datei geschrieben oder gar nicht in die Datei geschrieben.
Petr Janeček
Wenn ein flush()Anruf erforderlich ist, geschieht dies immer dann, wenn der Anrufer beschlossen hat, das Telefon zu schließen FileWriter. Dies sollte also printToFileunmittelbar vor returns im try-Block geschehen . Dies darf kein Teil von sein, writeFileWriterund daher bezieht sich die Warnung nicht auf irgendetwas innerhalb dieser Funktion, sondern auf den Aufrufer dieser Funktion. Wenn wir eine Anmerkung @LiftWarningToCaller("wanrningXXX")haben, hilft dies in diesem Fall und ähnlichem.
Earth Engine