Sintaks coba-dengan-sumber daya Java 7 (juga dikenal sebagai blok ARM ( Manajemen Sumber Daya Otomatis )) bagus, pendek dan mudah ketika menggunakan hanya satu AutoCloseable
sumber daya. Namun, saya tidak yakin apa idiom yang benar ketika saya perlu mendeklarasikan banyak sumber daya yang saling bergantung, misalnya a FileWriter
dan a BufferedWriter
yang membungkusnya. Tentu saja, pertanyaan ini menyangkut masalah ketika beberapa AutoCloseable
sumber daya dibungkus, tidak hanya dua kelas khusus ini.
Saya datang dengan tiga alternatif berikut:
1)
Ungkapan naif yang saya lihat adalah mendeklarasikan hanya pembungkus tingkat atas dalam variabel yang dikelola ARM:
static void printToFile1(String text, File file) {
try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
bw.write(text);
} catch (IOException ex) {
// handle ex
}
}
Ini bagus dan pendek, tetapi rusak. Karena yang mendasarinya FileWriter
tidak dideklarasikan dalam variabel, itu tidak akan pernah ditutup langsung di finally
blok yang dihasilkan . Itu akan ditutup hanya melalui close
metode pembungkus BufferedWriter
. Masalahnya adalah, bahwa jika pengecualian dilemparkan dari bw
konstruktor, itu close
tidak akan dipanggil dan karena itu yang mendasarinya FileWriter
tidak akan ditutup .
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
}
}
Di sini, sumber daya pokok dan pembungkus dideklarasikan dalam variabel yang dikelola ARM, sehingga keduanya pasti akan ditutup, tetapi dasarnya fw.close()
akan dipanggil dua kali : tidak hanya secara langsung, tetapi juga melalui pembungkus bw.close()
.
Ini seharusnya tidak menjadi masalah untuk dua kelas khusus ini yang keduanya mengimplementasikan Closeable
(yang merupakan subtipe dari AutoCloseable
), yang kontraknya menyatakan bahwa beberapa panggilan close
diizinkan:
Menutup aliran ini dan melepaskan sumber daya sistem apa pun yang terkait dengannya. Jika aliran sudah ditutup maka menjalankan metode ini tidak akan berpengaruh.
Namun, dalam kasus umum, saya dapat memiliki sumber daya yang hanya menerapkan AutoCloseable
(dan tidak Closeable
), yang tidak menjamin yang close
dapat dipanggil beberapa kali:
Perhatikan bahwa tidak seperti metode tutup java.io.Closeable, metode tutup ini tidak diperlukan untuk menjadi idempoten. Dengan kata lain, memanggil metode tutup ini lebih dari sekali mungkin memiliki beberapa efek samping yang terlihat, tidak seperti Closeable.close yang diperlukan untuk tidak memiliki efek jika dipanggil lebih dari sekali. Namun, pelaksana antarmuka ini sangat dianjurkan untuk membuat metode mereka dekat idempoten.
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
}
}
Versi ini harus benar secara teoritis, karena hanya fw
mewakili sumber daya nyata yang perlu dibersihkan. Itu bw
sendiri tidak memiliki sumber daya apa pun, itu hanya mendelegasikan ke fw
, jadi itu harus cukup untuk hanya menutup yang mendasarinya fw
.
Di sisi lain, sintaksnya agak tidak teratur dan juga, Eclipse mengeluarkan peringatan, yang saya percaya merupakan alarm palsu, tetapi itu masih merupakan peringatan bahwa seseorang harus berurusan dengan:
Kebocoran sumber daya: 'bw' tidak pernah ditutup
Jadi, pendekatan mana yang harus ditempuh? Atau apakah saya melewatkan beberapa idiom lain yang benar ?
sumber
public BufferedWriter(Writer out, int sz)
bisa melemparIllegalArgumentException
. Juga, saya dapat memperluas BufferedWriter dengan kelas yang akan membuang sesuatu dari konstruktornya atau membuat bungkus kustom apa pun yang saya butuhkan.BufferedWriter
konstruktor dapat dengan mudah melempar pengecualian.OutOfMemoryError
mungkin yang paling umum karena mengalokasikan sebagian besar memori untuk buffer (walaupun mungkin mengindikasikan Anda ingin me-restart seluruh proses). / Anda perluflush
AndaBufferedWriter
jika Anda tidak dekat dan ingin menyimpan isi (umumnya hanya kasus non-pengecualian).FileWriter
mengambil apa pun yang terjadi sebagai pengkodean file "default" - lebih baik untuk menjadi eksplisit.Jawaban:
Inilah pilihan saya:
1)
Bagi saya, hal terbaik yang datang ke Jawa dari C ++ tradisional 15 tahun lalu adalah Anda bisa mempercayai program Anda. Bahkan jika segala sesuatunya dalam kesalahan dan kesalahan, yang sering mereka lakukan, saya ingin sisa kode untuk perilaku terbaik dan berbau mawar. Memang,
BufferedWriter
mungkin melemparkan pengecualian di sini. Kehabisan memori tidak akan biasa, misalnya. Untuk dekorator lain, apakah Anda tahu yang mana darijava.io
kelas pembungkus melemparkan pengecualian diperiksa dari konstruktor mereka? Bukan saya. Tidak dapat dipahami dengan baik oleh kode jika Anda mengandalkan pengetahuan yang tidak jelas seperti itu.Juga ada "kehancuran". Jika ada kondisi kesalahan, maka Anda mungkin tidak ingin menyiram sampah ke file yang perlu dihapus (kode untuk itu tidak ditampilkan). Meskipun, tentu saja, menghapus file juga merupakan operasi yang menarik untuk dilakukan sebagai penanganan kesalahan.
Secara umum, Anda ingin
finally
blok dibuat sesingkat dan seandal mungkin. Menambahkan flush tidak membantu sasaran ini. Untuk banyak rilis beberapa kelas buffering di JDK memiliki bug di mana pengecualian dariflush
dalamclose
disebabkanclose
pada objek yang didekorasi tidak dipanggil. Sementara itu telah diperbaiki untuk beberapa waktu, harapkan dari implementasi lain.2)
Kami masih membilas blok implisit akhirnya (sekarang dengan berulang
close
- ini semakin buruk ketika Anda menambahkan lebih banyak dekorator), tetapi konstruksi aman dan kami harus implisit akhirnya memblokir sehingga bahkan gagalflush
tidak mencegah pelepasan sumber daya.3)
Ada bug di sini. Seharusnya:
Beberapa dekorator yang diimplementasikan dengan buruk sebenarnya adalah sumber daya dan harus ditutup dengan andal. Juga beberapa aliran mungkin perlu ditutup dengan cara tertentu (mungkin mereka melakukan kompresi dan perlu menulis bit untuk menyelesaikannya, dan tidak bisa hanya menyiram semuanya.
Putusan
Meskipun 3 adalah solusi yang unggul secara teknis, alasan pengembangan perangkat lunak membuat 2 pilihan yang lebih baik. Namun, coba-dengan-sumber daya masih merupakan perbaikan yang tidak memadai dan Anda harus tetap menggunakan idiom Execute Around , yang seharusnya memiliki sintaks yang lebih jelas dengan penutupan di Java SE 8.
sumber
Gaya pertama adalah yang disarankan oleh Oracle .
BufferedWriter
tidak membuang pengecualian yang dicentang, jadi jika ada pengecualian yang dilemparkan, program tidak diharapkan untuk pulih darinya, membuat sumber daya pulih sebagian besar diperdebatkan.Sebagian besar karena itu bisa terjadi di utas, dengan utas yang mati tetapi program masih berlanjut - katakanlah, ada pemadaman sementara memori yang tidak cukup lama untuk secara serius merusak sisa program. Ini adalah kasus yang agak sudut, dan jika itu terjadi cukup sering untuk membuat sumber daya bocor masalah, coba-dengan-sumber daya adalah yang paling sedikit dari masalah Anda.
sumber
Opsi 4
Ubah sumber daya Anda menjadi Closeable, bukan AutoClosable jika Anda bisa. Fakta bahwa konstruktor dapat dirantai menyiratkan itu tidak pernah terdengar untuk menutup sumber daya dua kali. (Ini juga berlaku sebelum ARM.) Lebih lanjut tentang ini di bawah.
Opsi 5
Jangan gunakan ARM dan kode dengan sangat hati-hati untuk memastikan close () tidak dipanggil dua kali!
Opsi 6
Jangan gunakan ARM dan mintalah panggilan tutup () Anda di coba / tangkap sendiri.
Mengapa saya tidak berpikir masalah ini unik untuk ARM
Dalam semua contoh ini, panggilan akhirnya tutup () harus di blok tangkap. Ditinggalkan agar mudah dibaca.
Tidak bagus karena fw bisa ditutup dua kali. (yang baik untuk FileWriter tetapi tidak dalam contoh hipotesis Anda):
Tidak bagus karena fw tidak ditutup jika pengecualian membangun BufferedWriter. (sekali lagi, tidak bisa terjadi, tetapi dalam contoh hipotetis Anda):
sumber
Saya hanya ingin membangun saran Jeanne Boyarsky untuk tidak menggunakan ARM tetapi memastikan FileWriter selalu ditutup tepat sekali. Jangan berpikir ada masalah di sini ...
Saya kira karena ARM hanyalah gula sintaksis, kita tidak bisa selalu menggunakannya untuk mengganti blok akhirnya. Sama seperti kita tidak selalu dapat menggunakan untuk-setiap loop untuk melakukan sesuatu yang mungkin dengan iterator.
sumber
try
danfinally
blok Anda melempar pengecualian, konstruk ini akan kehilangan yang pertama (dan berpotensi lebih bermanfaat).Untuk menyetujui komentar sebelumnya: paling sederhana adalah (2) menggunakan
Closeable
sumber daya dan mendeklarasikannya dalam klausa coba-dengan-sumber daya. Jika hanya punyaAutoCloseable
, Anda bisa membungkusnya dengan kelas lain (bersarang) yang baru saja memeriksanyaclose
hanya dipanggil sekali (Pola Fasad), misalnya dengan memilikiprivate bool isClosed;
. Dalam prakteknya bahkan Oracle hanya (1) mem-chain konstruktor dan tidak benar menangani pengecualian di sebagian rantai.Atau, Anda dapat secara manual membuat sumber daya berantai, menggunakan metode pabrik statis; ini merangkum rantai, dan menangani pembersihan jika gagal sebagian:
Anda kemudian dapat menggunakannya sebagai sumber daya tunggal dalam klausa coba-dengan-sumber daya:
Kompleksitas berasal dari penanganan beberapa pengecualian; kalau tidak, itu hanya "sumber daya dekat yang telah Anda peroleh sejauh ini". Praktek umum tampaknya pertama menginisialisasi variabel yang menyimpan objek yang menyimpan sumber daya
null
(di sinifileWriter
), dan kemudian menyertakan pemeriksaan nol dalam pembersihan, tetapi itu tampaknya tidak perlu: jika konstruktor gagal, tidak ada yang dibersihkan, jadi kita bisa membiarkan pengecualian itu menyebar, yang menyederhanakan kode sedikit.Anda mungkin dapat melakukan ini secara umum:
Demikian pula, Anda dapat rantai tiga sumber daya, dll.
Sebagai matematis samping, Anda bahkan dapat rantai tiga kali dengan merantai dua sumber sekaligus, dan itu akan asosiatif, artinya Anda akan mendapatkan objek yang sama untuk sukses (karena konstruktornya asosiatif), dan pengecualian yang sama jika ada kegagalan di salah satu konstruktor. Dengan asumsi Anda menambahkan S ke rantai di atas (jadi Anda mulai dengan V dan diakhiri dengan S , dengan menerapkan U , T , dan S pada gilirannya), Anda mendapatkan yang sama jika Anda pertama rantai S dan T , lalu U , berkorespondensi dengan (ST) U , atau jika Anda pertama kali menghubungkan T dan U , makaS , berkorespondensi denganS (TU) . Namun, akan lebih jelas untuk hanya menuliskan rantai tiga kali lipat secara eksplisit dalam satu fungsi pabrik.
sumber
try (BufferedWriter writer = <BufferedWriter, FileWriter>createChainedResource(file)) { /* work with writer */ }
?Karena sumber daya Anda bersarang, klausa coba-coba Anda juga harus:
sumber
close
akan dipanggil dua kali.Saya akan mengatakan jangan gunakan ARM dan lanjutkan dengan Closeable. Gunakan metode seperti,
Anda juga harus mempertimbangkan untuk menutup
BufferedWriter
karena tidak hanya mendelegasikan yang dekat denganFileWriter
, tetapi melakukan pembersihan sepertiflushBuffer
.sumber
Solusi saya adalah melakukan refactoring "ekstrak metode", sebagai berikut:
printToFile
dapat ditulis baikatau
Untuk desainer lib kelas, saya akan menyarankan mereka memperluas
AutoClosable
antarmuka dengan metode tambahan untuk menekan penutupan. Dalam hal ini, kita dapat mengontrol perilaku tutup secara manual.Untuk perancang bahasa, pelajarannya adalah menambahkan fitur baru bisa berarti menambahkan banyak fitur lainnya. Dalam kasus Java ini, jelas fitur ARM akan bekerja lebih baik dengan mekanisme transfer kepemilikan sumber daya.
MEMPERBARUI
Awalnya kode di atas memerlukan
@SuppressWarning
karena diBufferedWriter
dalam fungsi memerlukanclose()
.Seperti yang disarankan oleh komentar, jika
flush()
dipanggil sebelum menutup penulis, kita perlu melakukannya sebelumreturn
pernyataan (implisit atau eksplisit) apa pun di dalam blok try. Saat ini tidak ada cara untuk memastikan penelepon melakukan ini menurut saya, jadi ini harus didokumentasikanwriteFileWriter
.PEMBARUAN LAGI
Pembaruan di atas
@SuppressWarning
tidak diperlukan karena memerlukan fungsi untuk mengembalikan sumber daya ke pemanggil, jadi itu sendiri tidak perlu ditutup. Sayangnya, ini menarik kita kembali ke awal situasi: peringatan sekarang dipindahkan kembali ke sisi penelepon.Jadi untuk menyelesaikannya dengan benar, kita perlu disesuaikan
AutoClosable
bahwa setiap kali ditutup, garis bawahBufferedWriter
akanflush()
disunting. Sebenarnya, ini menunjukkan kepada kita cara lain untuk memotong peringatan, karenaBufferWriter
tidak pernah ditutup dengan cara baik.sumber
bw
wasiat itu benar-benar akan menulis data? Hal ini buffered afterall, sehingga hanya memiliki untuk menulis ke disk kadang-kadang (ketika buffer penuh dan / atauflush()
danclose()
metode). Saya kiraflush()
metode ini harus dipanggil. Tapi bagaimanapun, tidak perlu menggunakan penulis buffer, ketika kita sedang menulis itu segera dalam satu batch. Dan jika Anda tidak memperbaiki kode, Anda bisa berakhir dengan data yang ditulis ke file dalam urutan yang salah, atau bahkan tidak ditulis ke file sama sekali.flush()
diharuskan dipanggil, itu akan terjadi kapan saja sebelum penelepon memutuskan untuk menutupFileWriter
. Jadi ini harus terjadiprintToFile
tepat sebelum adareturn
di blok percobaan. Ini tidak akan menjadi bagian dariwriteFileWriter
dan dengan demikian peringatan bukan tentang apa pun di dalam fungsi itu, tetapi penelepon fungsi itu. Jika kami memiliki anotasi,@LiftWarningToCaller("wanrningXXX")
ini akan membantu kasus ini dan sejenisnya.