Ungkapan yang benar untuk mengelola banyak sumber daya berantai dalam blok coba-dengan-sumber daya?

168

Sintaks coba-dengan-sumber daya Java 7 (juga dikenal sebagai blok ARM ( Manajemen Sumber Daya Otomatis )) bagus, pendek dan mudah ketika menggunakan hanya satu AutoCloseablesumber daya. Namun, saya tidak yakin apa idiom yang benar ketika saya perlu mendeklarasikan banyak sumber daya yang saling bergantung, misalnya a FileWriterdan a BufferedWriteryang membungkusnya. Tentu saja, pertanyaan ini menyangkut masalah ketika beberapa AutoCloseablesumber 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 FileWritertidak dideklarasikan dalam variabel, itu tidak akan pernah ditutup langsung di finallyblok yang dihasilkan . Itu akan ditutup hanya melalui closemetode pembungkus BufferedWriter. Masalahnya adalah, bahwa jika pengecualian dilemparkan dari bwkonstruktor, itu closetidak 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 closediizinkan:

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 closedapat 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 fwmewakili sumber daya nyata yang perlu dibersihkan. Itu bwsendiri 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 ?

Natix
sumber
4
Tentu saja, jika konstruktor FileWriter yang mendasarinya melempar pengecualian, itu bahkan tidak bisa dibuka dan semuanya baik-baik saja. Apa contoh pertama tentang, adalah apa yang terjadi jika FileWriter dibuat, tetapi konstruktor BufferedWriter melemparkan pengecualian.
Natix
6
Perlu dicatat bahwa BufferedWriter tidak akan memberikan Pengecualian. Apakah ada contoh yang bisa Anda pikirkan di mana pertanyaan ini bukan murni akademis.
Peter Lawrey
10
@ PeterLawrey Ya, Anda benar bahwa konstruktor BufferedWriter dalam skenario ini kemungkinan besar tidak akan memberikan pengecualian, tapi seperti yang saya tunjukkan, pertanyaan ini menyangkut sumber daya gaya dekorator. Tapi misalnya public BufferedWriter(Writer out, int sz)bisa melempar IllegalArgumentException. Juga, saya dapat memperluas BufferedWriter dengan kelas yang akan membuang sesuatu dari konstruktornya atau membuat bungkus kustom apa pun yang saya butuhkan.
Natix
5
The BufferedWriterkonstruktor dapat dengan mudah melempar pengecualian. OutOfMemoryErrormungkin yang paling umum karena mengalokasikan sebagian besar memori untuk buffer (walaupun mungkin mengindikasikan Anda ingin me-restart seluruh proses). / Anda perlu flushAnda BufferedWriterjika Anda tidak dekat dan ingin menyimpan isi (umumnya hanya kasus non-pengecualian). FileWritermengambil apa pun yang terjadi sebagai pengkodean file "default" - lebih baik untuk menjadi eksplisit.
Tom Hawtin - tackline
10
@ Natix Saya berharap semua pertanyaan dalam SO diteliti dan sejelas seperti ini. Saya berharap saya bisa memilih ini 100 kali.
Geek

Jawaban:

75

Inilah pilihan saya:

1)

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

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, BufferedWritermungkin melemparkan pengecualian di sini. Kehabisan memori tidak akan biasa, misalnya. Untuk dekorator lain, apakah Anda tahu yang mana dari java.iokelas 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 finallyblok dibuat sesingkat dan seandal mungkin. Menambahkan flush tidak membantu sasaran ini. Untuk banyak rilis beberapa kelas buffering di JDK memiliki bug di mana pengecualian dari flushdalam closedisebabkan closepada objek yang didekorasi tidak dipanggil. Sementara itu telah diperbaiki untuk beberapa waktu, harapkan dari implementasi lain.

2)

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

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 gagal flushtidak mencegah pelepasan sumber daya.

3)

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

Ada bug di sini. Seharusnya:

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

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.

Tom Hawtin - tackline
sumber
4
Dalam versi 3, bagaimana Anda tahu bahwa bw tidak perlu dipanggil untuk dipanggil? Dan bahkan jika Anda bisa yakin jika itu, bukankah itu juga "pengetahuan tidak jelas" seperti yang Anda sebutkan untuk versi 1?
TimK
3
" alasan pengembangan perangkat lunak membuat 2 pilihan yang lebih baik " Bisakah Anda menjelaskan pernyataan ini lebih terinci?
Duncan Jones
8
Bisakah Anda memberikan contoh "Jalankan idiom dengan penutupan"
Markus
2
Bisakah Anda menjelaskan apa "sintaks yang lebih jelas dengan penutupan di Java SE 8"?
petertc
1
Contoh "Execute Around idiom" ada di sini: stackoverflow.com/a/342016/258772
mrts
20

Gaya pertama adalah yang disarankan oleh Oracle . BufferedWritertidak 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.

Daniel C. Sobral
sumber
2
Ini juga merupakan pendekatan yang direkomendasikan dalam Java Efektif edisi ke-3.
shmosel
5

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):

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

Tidak bagus karena fw tidak ditutup jika pengecualian membangun BufferedWriter. (sekali lagi, tidak bisa terjadi, tetapi dalam contoh hipotetis Anda):

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
sumber
3

Saya hanya ingin membangun saran Jeanne Boyarsky untuk tidak menggunakan ARM tetapi memastikan FileWriter selalu ditutup tepat sekali. Jangan berpikir ada masalah di sini ...

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

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.

AmyGamy
sumber
5
Jika Anda trydan finallyblok Anda melempar pengecualian, konstruk ini akan kehilangan yang pertama (dan berpotensi lebih bermanfaat).
rxg
3

Untuk menyetujui komentar sebelumnya: paling sederhana adalah (2) menggunakan Closeablesumber daya dan mendeklarasikannya dalam klausa coba-dengan-sumber daya. Jika hanya punya AutoCloseable, Anda bisa membungkusnya dengan kelas lain (bersarang) yang baru saja memeriksanyaclose hanya dipanggil sekali (Pola Fasad), misalnya dengan memiliki private 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:

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

Anda kemudian dapat menggunakannya sebagai sumber daya tunggal dalam klausa coba-dengan-sumber daya:

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

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 dayanull (di sini fileWriter), 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:

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

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.

Nils von Barth
sumber
Apakah saya mengumpulkan dengan benar bahwa seseorang masih perlu menggunakan try-with-resource, seperti pada try (BufferedWriter writer = <BufferedWriter, FileWriter>createChainedResource(file)) { /* work with writer */ }?
ErikE
@ErikE Ya, Anda masih perlu menggunakan coba-dengan-sumber daya, tetapi Anda hanya perlu menggunakan satu fungsi untuk sumber daya berantai: fungsi pabrik merangkum rantai . Saya telah menambahkan contoh penggunaan; Terima kasih!
Nils von Barth
2

Karena sumber daya Anda bersarang, klausa coba-coba Anda juga harus:

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
}
meracuni
sumber
5
Ini sangat mirip dengan contoh ke-2 saya. Jika tidak ada pengecualian terjadi, FileWriter closeakan dipanggil dua kali.
Natix
0

Saya akan mengatakan jangan gunakan ARM dan lanjutkan dengan Closeable. Gunakan metode seperti,

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

}

Anda juga harus mempertimbangkan untuk menutup BufferedWriterkarena tidak hanya mendelegasikan yang dekat dengan FileWriter, tetapi melakukan pembersihan seperti flushBuffer.

sakthisundar
sumber
0

Solusi saya adalah melakukan refactoring "ekstrak metode", sebagai berikut:

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 dapat ditulis baik

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

atau

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

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

Untuk desainer lib kelas, saya akan menyarankan mereka memperluas AutoClosableantarmuka 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 @SuppressWarningkarena di BufferedWriterdalam fungsi memerlukanclose() .

Seperti yang disarankan oleh komentar, jika flush()dipanggil sebelum menutup penulis, kita perlu melakukannya sebelum returnpernyataan (implisit atau eksplisit) apa pun di dalam blok try. Saat ini tidak ada cara untuk memastikan penelepon melakukan ini menurut saya, jadi ini harus didokumentasikan writeFileWriter.

PEMBARUAN LAGI

Pembaruan di atas @SuppressWarningtidak 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 AutoClosablebahwa setiap kali ditutup, garis bawah BufferedWriterakan flush()disunting. Sebenarnya, ini menunjukkan kepada kita cara lain untuk memotong peringatan, karena BufferWritertidak pernah ditutup dengan cara baik.

Mesin Tanah
sumber
Peringatan itu memiliki maknanya: Bisakah kita yakin di sini bahwa surat bwwasiat itu benar-benar akan menulis data? Hal ini buffered afterall, sehingga hanya memiliki untuk menulis ke disk kadang-kadang (ketika buffer penuh dan / atau flush()dan close()metode). Saya kira flush()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.
Petr Janeček
Jika flush()diharuskan dipanggil, itu akan terjadi kapan saja sebelum penelepon memutuskan untuk menutup FileWriter. Jadi ini harus terjadi printToFiletepat sebelum ada returndi blok percobaan. Ini tidak akan menjadi bagian dari writeFileWriterdan 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.
Earth Engine