Składnia try-with-resources Java 7 (znana również jako blok ARM ( Automatyczne zarządzanie zasobami )) jest przyjemna, krótka i prosta, gdy używasz tylko jednego AutoCloseable
zasobu. Nie jestem jednak pewien, jaki jest poprawny idiom, gdy muszę zadeklarować wiele zasobów, które są od siebie zależne, na przykład a FileWriter
i a, BufferedWriter
które to zawijają. Oczywiście to pytanie dotyczy każdego przypadku, gdy niektóre AutoCloseable
zasoby są opakowane, a nie tylko te dwie konkretne klasy.
Wymyśliłem trzy następujące alternatywy:
1)
Naiwny idiom, który widziałem, polega na zadeklarowaniu tylko opakowania najwyższego poziomu w zmiennej zarządzanej przez ARM:
static void printToFile1(String text, File file) {
try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
bw.write(text);
} catch (IOException ex) {
// handle ex
}
}
To jest ładne i krótkie, ale jest zepsute. Ponieważ element bazowy FileWriter
nie jest zadeklarowany w zmiennej, nigdy nie zostanie zamknięty bezpośrednio w wygenerowanym finally
bloku. Zostanie zamknięty tylko close
metodą zawijania BufferedWriter
. Problem polega na tym, że jeśli wyjątek zostanie wyrzucony z bw
konstruktora, close
to nie zostanie on wywołany i dlatego element bazowy FileWriter
nie zostanie zamknięty .
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
}
}
Tutaj zarówno zasób bazowy, jak i zasób opakowujący są zadeklarowane w zmiennych zarządzanych przez ARM, więc oba z pewnością zostaną zamknięte, ale element bazowy fw.close()
zostanie wywołany dwukrotnie : nie tylko bezpośrednio, ale także poprzez opakowanie bw.close()
.
Nie powinno to stanowić problemu dla tych dwóch konkretnych klas, które obie implementują Closeable
(co jest podtypem AutoCloseable
), których umowa stanowi, że close
dozwolone jest wielokrotne wywoływanie :
Zamyka ten strumień i zwalnia wszystkie powiązane z nim zasoby systemowe. Jeśli strumień jest już zamknięty, wywołanie tej metody nie ma żadnego efektu.
Jednak w ogólnym przypadku mogę mieć zasoby, które implementują tylko AutoCloseable
(i nie Closeable
), co nie gwarantuje, że close
można to wywołać wiele razy:
Zauważ, że w przeciwieństwie do metody close java.io.Closeable, ta metoda close nie musi być idempotentna. Innymi słowy, wywołanie tej metody close więcej niż raz może mieć widoczny efekt uboczny, w przeciwieństwie do Closeable.close, które jest wymagane, aby nie wywoływać żadnego efektu, jeśli zostanie wywołane więcej niż raz. Jednak implementujących ten interfejs zdecydowanie zachęca się, aby ich bliskie metody były idempotentne.
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
}
}
Ta wersja powinna być teoretycznie poprawna, ponieważ tylko fw
reprezentuje prawdziwy zasób, który należy wyczyścić. Samo bw
nie zawiera żadnego zasobu, a jedynie deleguje go do fw
, więc powinno wystarczyć tylko zamknięcie bazowego fw
.
Z drugiej strony składnia jest nieco nieregularna, a także Eclipse generuje ostrzeżenie, które uważam za fałszywy alarm, ale nadal jest to ostrzeżenie, z którym trzeba sobie poradzić:
Wyciek zasobów: „bw” nigdy nie jest zamykany
Więc jakie podejście wybrać? A może przegapiłem jakiś inny idiom, który jest właściwy ?
źródło
public BufferedWriter(Writer out, int sz)
może rzucićIllegalArgumentException
. Mogę również rozszerzyć BufferedWriter o klasę, która wyrzuci coś ze swojego konstruktora lub utworzy dowolne niestandardowe opakowanie, którego potrzebuję.BufferedWriter
Konstruktor łatwo wyrzucić wyjątek.OutOfMemoryError
jest prawdopodobnie najczęstszym, ponieważ przydziela sporą część pamięci dla bufora (chociaż może wskazywać, że chcesz ponownie uruchomić cały proces). / Trzebaflush
TWOJEJBufferedWriter
jeśli nie blisko i chcesz zachować zawartość (generalnie tylko i uzasadnienie bez wyjątku).FileWriter
przejmuje wszystko, co jest „domyślnym” kodowaniem pliku - lepiej jest to wyraźnie określić.Odpowiedzi:
Oto moje podejście do alternatyw:
1)
Dla mnie najlepszą rzeczą, która pojawiła się w Javie z tradycyjnego C ++ 15 lat temu, było to, że możesz zaufać swojemu programowi. Nawet jeśli coś jest w błocie i idzie źle, co często się zdarza, chcę, aby reszta kodu dotyczyła najlepszego zachowania i zapachu róż. Rzeczywiście,
BufferedWriter
może to rzucić tutaj wyjątek. Na przykład brak pamięci nie byłby niczym niezwykłym. W przypadku innych dekoratorów, czy wiesz, która zjava.io
klas opakowania rzuca sprawdzony wyjątek od swoich konstruktorów? Ja nie. Zrozumiałość kodu nie jest zbyt dobra, jeśli polegasz na tego rodzaju niejasnej wiedzy.Jest też „zniszczenie”. Jeśli wystąpi stan błędu, prawdopodobnie nie chcesz wyrzucać śmieci do pliku, który wymaga usunięcia (kod do tego nie jest wyświetlany). Chociaż oczywiście usunięcie pliku jest również kolejną interesującą operacją do wykonania jako obsługa błędów.
Ogólnie rzecz biorąc, chcesz, aby
finally
bloki były jak najkrótsze i niezawodne. Dodawanie kolorów nie pomaga w osiągnięciu tego celu. W wielu wydaniach niektóre z klas buforujących w JDK miały błąd polegający na tym, że wyjątek odflush
wewnątrzclose
spowodowanyclose
na dekorowanym obiekcie nie był wywoływany. Chociaż zostało to naprawione od jakiegoś czasu, spodziewaj się tego po innych implementacjach.2)
Nadal opróżniamy niejawny blok końcowy (teraz z powtórzeniem
close
- to pogarsza się, gdy dodajesz więcej dekoratorów), ale konstrukcja jest bezpieczna i musimy ostatecznie zablokować niejawne, więc nawet niepowodzenieflush
nie zapobiega uwolnieniu zasobów.3)
Tu jest błąd. Powinien być:
Niektóre źle wdrożone dekoratory są w rzeczywistości zasobami i będą wymagały niezawodnego zamknięcia. Ponadto niektóre strumienie mogą wymagać zamknięcia w określony sposób (być może używają kompresji i muszą pisać bity, aby zakończyć, i nie mogą po prostu opróżnić wszystkiego.
Werdykt
Chociaż 3 jest technicznie lepszym rozwiązaniem, ze względu na rozwój oprogramowania 2 jest lepszym wyborem. Jednak próba z zasobem jest nadal niewystarczająca i należy trzymać się idiomu Execute Around , który powinien mieć jaśniejszą składnię z zamknięciami w Javie SE 8.
źródło
Pierwszy styl to ten, który sugeruje Oracle .
BufferedWriter
nie zgłasza sprawdzonych wyjątków, więc jeśli zostanie zgłoszony jakikolwiek wyjątek, program nie powinien się z niego odzyskać, przez co odzyskiwanie zasobów jest głównie dyskusyjne.Głównie dlatego, że mogło się to zdarzyć w wątku, gdy wątek umierał, ale program nadal działał - powiedzmy, wystąpiła tymczasowa awaria pamięci, która nie była wystarczająco długa, aby poważnie zakłócić resztę programu. Jest to jednak raczej wątpliwy przypadek i jeśli zdarza się to wystarczająco często, aby spowodować wyciek zasobów, próba użycia zasobów jest najmniejszym z twoich problemów.
źródło
Opcja 4
Zmień zasoby, aby były zamykalne, a nie AutoClosable, jeśli możesz. Fakt, że konstruktory można połączyć w łańcuch, oznacza, że nie jest niczym niezwykłym dwukrotne zamknięcie zasobu. (Było to prawdą również przed ARM.) Więcej na ten temat poniżej.
Opcja 5
Nie używaj ARM i kodu bardzo ostrożnie, aby upewnić się, że close () nie zostanie wywołana dwukrotnie!
Opcja 6
Nie używaj ARM i wykonuj w końcu wywołania close () w trybie try / catch.
Dlaczego nie sądzę, że ten problem jest unikalny dla ARM
We wszystkich tych przykładach wywołania last close () powinny znajdować się w bloku catch. Pominięto ze względu na czytelność.
Nic dobrego, ponieważ fw można zamknąć dwukrotnie. (co jest dobre w przypadku FileWriter, ale nie w twoim hipotetycznym przykładzie):
Nic dobrego, ponieważ fw nie jest zamknięty, jeśli wyjątek podczas konstruowania BufferedWriter. (znowu nie może się zdarzyć, ale w twoim hipotetycznym przykładzie):
źródło
Chciałem tylko oprzeć się na sugestii Jeanne Boyarsky, aby nie używać ARM, ale upewnić się, że FileWriter jest zawsze zamykany dokładnie raz. Nie myśl, że są tu jakieś problemy ...
Wydaje mi się, że ponieważ ARM to tylko cukier syntaktyczny, nie zawsze możemy go użyć do zastąpienia ostatecznie bloków. Tak jak nie zawsze możemy użyć pętli for-each, aby zrobić coś, co jest możliwe za pomocą iteratorów.
źródło
try
ifinally
bloki rzucają wyjątki, ta konstrukcja utraci pierwszy (i potencjalnie bardziej przydatny).Zgadzając się z wcześniejszymi komentarzami: najprościej jest (2) użyć
Closeable
zasobów i zadeklarować je w kolejności w klauzuli try-with-resources. Jeśli masz tylkoAutoCloseable
, możesz umieścić je w innej (zagnieżdżonej) klasie, która sprawdzaclose
tylko raz wywołaną (wzorzec elewacji), np. Przez posiadanieprivate bool isClosed;
. W praktyce nawet Oracle tylko (1) łączy w łańcuch konstruktory i nie obsługuje poprawnie wyjątków w części łańcucha.Alternatywnie możesz ręcznie utworzyć połączony zasób, używając statycznej metody fabryki; to hermetyzuje łańcuch i obsługuje czyszczenie, jeśli zawiedzie częściowo:
Następnie możesz użyć go jako pojedynczego zasobu w klauzuli try-with-resources:
Złożoność wynika z obsługi wielu wyjątków; w przeciwnym razie jest to po prostu „zamknij zasoby, które zdobyłeś do tej pory”. Powszechną praktyką wydaje się być najpierw zainicjowanie zmiennej, która przechowuje obiekt, który przechowuje zasób
null
(tutajfileWriter
), a następnie uwzględnienie zerowego sprawdzenia w czyszczeniu, ale wydaje się to niepotrzebne: jeśli konstruktor zawiedzie, nie ma nic do wyczyszczenia, więc możemy po prostu pozwolić na propagację tego wyjątku, co trochę upraszcza kod.Prawdopodobnie możesz to zrobić ogólnie:
Podobnie możesz połączyć trzy zasoby itp.
Na marginesie matematycznym możesz nawet trzykrotnie łączyć w łańcuch, łącząc dwa zasoby naraz, i byłoby to asocjacyjne, co oznacza, że otrzymałeś ten sam obiekt w przypadku sukcesu (ponieważ konstruktory są asocjacyjne) i te same wyjątki, jeśli wystąpi awaria w dowolnym konstruktorze. Zakładając, że dodałeś S do powyższego łańcucha (więc zaczynasz od V i kończysz na S , stosując kolejno U , T i S ), otrzymasz to samo, jeśli najpierw połączysz S i T , a następnie U , odpowiadające (ST) U lub jeśli najpierw połączyłeś T i U , toS , odpowiadające S (TU) . Jednak byłoby jaśniejsze, gdybyśmy po prostu napisali jawny potrójny łańcuch w jednej funkcji fabryki.
źródło
try (BufferedWriter writer = <BufferedWriter, FileWriter>createChainedResource(file)) { /* work with writer */ }
?Ponieważ zasoby są zagnieżdżone, klauzule try-with powinny również wyglądać następująco:
źródło
close
zostanie wywołany dwukrotnie.Powiedziałbym, że nie używaj ARM i kontynuuj z Closeable. Użyj metody takiej jak,
Powinieneś także rozważyć wywołanie close of,
BufferedWriter
ponieważ nie jest to tylko delegowanie zamknięciaFileWriter
, ale także porządkowanieflushBuffer
.źródło
Moim rozwiązaniem jest refaktoryzacja „metody wyodrębniania” w następujący sposób:
printToFile
można zapisaćlub
Projektantom bibliotek klasowych zasugeruję rozszerzenie
AutoClosable
interfejsu o dodatkową metodę, aby pominąć zamknięcie. W takim przypadku możemy ręcznie kontrolować zachowanie zamknięcia.Dla projektantów języków lekcja jest taka, że dodanie nowej funkcji może oznaczać dodanie wielu innych. W tym przypadku Javy, oczywiście, funkcja ARM będzie działać lepiej z mechanizmem przenoszenia własności zasobów.
AKTUALIZACJA
Oryginalnie powyższy kod wymaga,
@SuppressWarning
ponieważBufferedWriter
wnętrze funkcji wymagaclose()
.Jak sugeruje komentarz, jeśli
flush()
chcemy zostać wywołani przed zamknięciem modułu zapisującego, musimy to zrobić przed wszelkimireturn
(niejawnymi lub jawnymi) instrukcjami wewnątrz bloku try. Myślę, że obecnie nie ma sposobu, aby upewnić się, że dzwoniący robi to, więc musi to zostać udokumentowanewriteFileWriter
.ZAKTUALIZUJ PONOWNIE
Powyższa aktualizacja sprawia, że
@SuppressWarning
zbędna, ponieważ wymaga funkcji zwrócenia zasobu wywołującemu, więc sama nie musi być zamykana. Niestety, to ciągnie nas z powrotem do początku sytuacji: ostrzeżenie jest teraz przenoszone z powrotem na stronę dzwoniącego.Aby poprawnie rozwiązać ten problem, potrzebujemy spersonalizowanego ustawienia,
AutoClosable
że za każdym razem, gdy się zamyka, podkreślenieBufferedWriter
zostanieflush()
zredagowane. Właściwie to pokazuje nam inny sposób obejścia ostrzeżenia, ponieważBufferWriter
nigdy nie jest zamknięty w żaden sposób.źródło
bw
faktycznie wypisze dane? W końcu jest buforowany, więc musi tylko czasami zapisywać na dysku (gdy bufor jest pełny i / lub włączonyflush()
iclose()
metody). Myślę, żeflush()
należy nazwać metodę. Ale i tak nie ma potrzeby używania buforowanego programu zapisującego, gdy piszemy go natychmiast w jednej partii. A jeśli nie naprawisz kodu, możesz skończyć z danymi zapisanymi w pliku w złej kolejności lub nawet w ogóle nie zapisanymi w pliku.flush()
wymagane jest wywołanie, nastąpi to zawsze, zanim dzwoniący zdecyduje się zamknąćFileWriter
. Więc powinno to nastąpićprintToFile
tuż przed jakimkolwiekreturn
s w bloku try. Nie powinno to być częścią,writeFileWriter
a zatem ostrzeżenie nie dotyczy niczego wewnątrz tej funkcji, ale wywołującego tę funkcję. Jeśli mamy adnotację@LiftWarningToCaller("wanrningXXX")
, pomoże to w tej sprawie i podobnej.