Die Java 7- Try-with-Resources- Syntax (auch als ARM-Block ( Automatic Resource Management ) bezeichnet) ist nett, kurz und unkompliziert, wenn nur eine AutoCloseable
Ressource 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 FileWriter
und a BufferedWriter
, die sie umschließen. Diese Frage betrifft natürlich jeden Fall, in dem einige AutoCloseable
Ressourcen 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 FileWriter
nicht in einer Variablen deklariert ist, wird er niemals direkt im generierten finally
Block geschlossen. Es wird nur durch die close
Methode des Wickelns geschlossen BufferedWriter
. Das Problem ist, dass wenn eine Ausnahme vom bw
Konstruktor des Konstruktors ausgelöst close
wird, 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 close
zulä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 close
dies 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 fw
eine echte Ressource darstellt, die bereinigt werden muss. Das bw
selbst 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 ?
quelle
public BufferedWriter(Writer out, int sz)
kann man einen werfenIllegalArgumentException
. Außerdem kann ich BufferedWriter um eine Klasse erweitern, die etwas von ihrem Konstruktor abwirft oder einen benutzerdefinierten Wrapper erstellt, den ich benötige.BufferedWriter
Konstruktor kann leicht eine Ausnahme auslösen.OutOfMemoryError
ist 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üssenflush
Ihre,BufferedWriter
wenn Sie nicht schließen und den Inhalt behalten möchten (in der Regel nur der Nicht-Ausnahmefall).FileWriter
nimmt auf, was auch immer die "Standard" -Dateicodierung ist - es ist besser, explizit zu sein.Antworten:
Hier ist meine Sicht auf die Alternativen:
1)
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
BufferedWriter
kö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
finally
Blö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 vonflush
innen,close
dieclose
fü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)
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 Fehlerflush
die Ressourcenfreigabe nicht verhindert.3)
Hier gibt es einen Fehler. Sollte sein:
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.
quelle
Der erste Stil ist der von Oracle vorgeschlagene .
BufferedWriter
Wirft 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.
quelle
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):
Nicht gut, da fw nicht geschlossen ist, wenn Ausnahme beim Erstellen eines BufferedWriter. (kann wieder nicht passieren, aber in Ihrem hypothetischen Beispiel):
quelle
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 ...
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.
quelle
try
als auch Ihrefinally
Blöcke Ausnahmen auslösen, verliert dieses Konstrukt die erste (und möglicherweise nützlichere).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 habenAutoCloseable
, können Sie sie in eine andere (verschachtelte) Klasse einschließen, die nur prüft, ob sieclose
nur einmal aufgerufen wird (Fassadenmuster), zprivate 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:
Sie können es dann als einzelne Ressource in einer Try-with-Resources-Klausel verwenden:
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
(hierfileWriter
), 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:
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.
quelle
try (BufferedWriter writer = <BufferedWriter, FileWriter>createChainedResource(file)) { /* work with writer */ }
?Da Ihre Ressourcen verschachtelt sind, sollten Ihre Try-With-Klauseln auch lauten:
quelle
close
zweimal aufgerufen.Ich würde sagen, benutze ARM nicht und fahre mit Closeable fort. Verwenden Sie Methode wie,
Sie sollten auch in Betracht ziehen, close of aufzurufen,
BufferedWriter
da es nicht nur das close von delegiertFileWriter
, sondern auch einige Bereinigungsvorgänge ausführtflushBuffer
.quelle
Meine Lösung besteht darin, ein Refactoring der "Extraktionsmethode" wie folgt durchzuführen:
printToFile
kann entweder geschrieben werdenoder
Für Klassenbibliothek-Designer werde ich vorschlagen, dass sie die
AutoClosable
Schnittstelle 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,
@SuppressWarning
da dasBufferedWriter
Innere der Funktion erfordertclose()
.Wie in einem Kommentar vorgeschlagen
flush()
, müssen wir dies tun, bevor wir den Writer schließen, bevorreturn
(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 werdenwriteFileWriter
.UPDATE WIEDER
Das obige Update macht
@SuppressWarning
unnö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,
AutoClosable
dass die Unterstreichung bei jedem Schließen bearbeitetBufferedWriter
wirdflush()
. Tatsächlich zeigt uns dies eine andere Möglichkeit, die Warnung zu umgehen, da die WarnungBufferWriter
in keiner Weise geschlossen wird.quelle
bw
Daten 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 istflush()
undclose()
Methoden). Ich denke, dieflush()
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.flush()
Anruf erforderlich ist, geschieht dies immer dann, wenn der Anrufer beschlossen hat, das Telefon zu schließenFileWriter
. Dies sollte alsoprintToFile
unmittelbar vorreturn
s im try-Block geschehen . Dies darf kein Teil von sein,writeFileWriter
und 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.