Kaynaklarla deneme bloğunda birden çok zincirleme kaynağı yönetme deyimi doğru mu?


168

Java 7 , kaynaklarla deneme sözdizimi (ARM bloğu ( Otomatik Kaynak Yönetimi ) olarak da bilinir ), yalnızca bir AutoCloseablekaynak kullanılırken güzel, kısa ve anlaşılır . Ancak, birbirine bağlı birden fazla kaynak, örneğin a FileWriterve BufferedWriteronu saran birden fazla kaynak bildirmek gerektiğinde doğru deyimin ne olduğundan emin değilim . Tabii ki, bu soru AutoCloseable, sadece bu iki özel sınıfla değil, bazı kaynakların sarıldığı herhangi bir durumla ilgilidir .

Aşağıdaki üç alternatif buldum:

1)

Gördüğüm naif deyim, sadece ARM tarafından yönetilen değişkende en üst düzey sarmalayıcıyı bildirmektir:

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

Bu güzel ve kısa, ama kırık. Temel alınan FileWriterbir değişkente bildirilmediğinden, hiçbir zaman doğrudan üretilen finallyblokta kapatılmaz . Sadece closesarma yöntemi ile kapatılacaktır BufferedWriter. Sorun şu ki, bwyapıcıdan bir istisna atılırsa, closeçağrılmayacak ve bu nedenle altta yatan FileWriter kapanmayacaktır .

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

Burada, hem temel hem de sarma kaynağı ARM tarafından yönetilen değişkenlerde bildirilir, bu nedenle her ikisi de kesinlikle kapatılacaktır, ancak altta yatan fw.close() iki kez çağrılacaktır : sadece doğrudan değil, aynı zamanda sarma yoluyla bw.close().

Bu, her ikisinin de uyguladığı Closeable(bir alt türü olan AutoCloseable) bu iki özel sınıf için, sorun birden fazla çağrıya closeizin verildiğini belirten bir sorun olmamalıdır :

Bu akışı kapatır ve onunla ilişkili tüm sistem kaynaklarını serbest bırakır. Akış zaten kapalıysa, bu yöntemin başlatılmasının bir etkisi yoktur.

Bununla birlikte, genel bir durumda, birden fazla kez çağrılabileceğini garanti etmeyen, sadece AutoCloseable(ve değil Closeable) uygulayan kaynaklara sahip closeolabilirim:

Java.io.Closeable kapat yönteminin aksine, bu kapat yönteminin idempotent olması gerekmez. Başka bir deyişle, bu kapat yöntemini bir kereden fazla çağırmak, birden fazla çağrılırsa hiçbir etkisi olmaması gereken Closeable.close öğesinin aksine bazı görünür yan etkilere sahip olabilir. Bununla birlikte, bu arayüzün uygulayıcılarının yakın yöntemlerini idempotent yapmaları şiddetle tavsiye edilir.

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

Bu sürüm teorik olarak doğru olmalıdır, çünkü sadece fwtemizlenmesi gereken gerçek bir kaynağı temsil eder. bwİçin, sadece delegeler herhangi bir kaynak tutamaz kendisi vermez fwsadece yakın yatan için yeterli olmalıdır, böylece fw.

Öte yandan, sözdizimi biraz düzensizdir ve ayrıca, Eclipse yanlış bir alarm olduğuna inandığım bir uyarı yayınlar, ancak yine de ilgilenilmesi gereken bir uyarıdır:

Kaynak sızıntısı: 'bw' asla kapatılmaz


Peki, hangi yaklaşımı benimsemek? Yoksa doğru olan başka bir deyimi kaçırdım mı?


4
Tabii ki, temeldeki FileWriter'ın yapıcısı bir istisna atarsa, açılmaz ve her şey yolundadır. İlk örnek, FileWriter oluşturulursa, ancak BufferedWriter'ın yapıcısı bir istisna atarsa ​​ne olacağıdır.
Natix

6
BufferedWriter'ın bir İstisna atmayacağını belirtmek gerekir. Bu sorunun saf akademik olmayan nerede olduğunu düşünebileceğiniz bir örnek var mı?
Peter Lawrey

10
@PeterLawrey Evet, bu senaryoda BufferedWriter'in yapıcısının büyük olasılıkla bir istisna atmayacağı konusunda haklısınız, ancak işaret ettiğim gibi, bu soru dekoratör tarzı kaynaklarla ilgilidir. Ama mesela public BufferedWriter(Writer out, int sz)bir IllegalArgumentException. Ayrıca, BufferedWriter'ı yapıcısından bir şey atacak veya ihtiyacım olan herhangi bir özel sarmalayıcı oluşturacak bir sınıfla genişletebilirim.
Natix

5
BufferedWriterYapıcı kolayca istisna atabilir. OutOfMemoryErrorarabellek için adil bir bellek ayırdığı için en yaygın olanıdır (ancak tüm işlemi yeniden başlatmak istediğinizi gösterebilir). / Sen gerek flushSİZİN BufferedWriterdeğil yakın yapmak ve içerikleri (genellikle tutmak istiyorsanız sadece sigara istisna durum). FileWriter"varsayılan" dosya kodlaması ne olursa olsun alır - açık olması daha iyidir.
Tom Hawtin - çakmak hattı

10
@Natix Keşke SO tüm soru bu kadar iyi araştırılmış ve açık. Keşke bunu 100 kez oylayabilseydim.
Geek

Yanıtlar:


75

Alternatifleri ele alalım:

1)

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

Benim için, 15 yıl önce geleneksel C ++ 'dan Java'ya gelen en iyi şey, programınıza güvenebileceğinizdi. Her ne kadar işler çetede ve yanlış gidiyor olsalar da, sık sık yaptıkları, kodun geri kalanının en iyi davranış ve gül kokusu üzerinde olmasını istiyorum. Gerçekten de, BufferedWriterburada bir istisna getirebilir. Örneğin, hafızanın tükenmesi olağandışı olmazdı. Diğer dekoratörler için, hangi java.iosarmalayıcı sınıflarının yapıcılarından denetimli bir istisna attığını biliyor musunuz ? Yapmıyorum. Bu tür belirsiz bilgilere güvenirseniz, kod anlaşılırlığını çok iyi yapmaz.

Bir de "yıkım" var. Bir hata koşulu varsa, muhtemelen silinmesi gereken bir dosyaya çöp gösterilmesini istemezsiniz (bunun için kod gösterilmez). Tabii ki, dosyayı silmek de hata işleme olarak yapmak ilginç bir işlemdir.

Genellikle finallyblokların mümkün olduğunca kısa ve güvenilir olmasını istersiniz . Yıkama eklemek bu hedefe yardımcı olmaz. Birçok bültenleri için JDK tamponlama sınıflardan bazılarının istisna bir hata vardı flushiçinde closeneden closedekore nesne üzerinde değil çağrılabilir. Bu bir süredir düzeltilmiş olsa da, diğer uygulamalardan beklemenizi sağlar.

2)

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

Örtülü nihayet bloğunda hala kızartıyoruz (şimdi tekrarlanan ile close- daha fazla dekoratör ekledikçe daha da kötüleşiyor), ancak inşaat güvenlidir ve nihayet engellemeliyiz, böylece başarısız bile flushkaynak çıkışını engellemez.

3)

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

Burada bir hata var. Olmalı:

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

Kötü uygulanmış bazı dekoratörler aslında kaynaktır ve güvenilir bir şekilde kapatılması gerekir. Ayrıca bazı akışların belirli bir şekilde kapatılması gerekebilir (belki de sıkıştırma yapıyorlar ve bitirmek için bitler yazıyorlar ve her şeyi temizleyemiyorlar.

Karar

3 teknik olarak üstün bir çözüm olmasına rağmen, yazılım geliştirme nedenleri 2'yi daha iyi bir seçim haline getirmektedir. Bununla birlikte, kaynakla deneme hala yetersiz bir çözümdür ve Java SE 8'de kapaklarla daha açık bir sözdizimine sahip olması gereken Execute Around deyimine uymalısınız .


4
Sürüm 3'te, bw'nin yakınının çağrılmasını gerektirmediğini nasıl anlarsınız? Ve eğer emin olsanız bile, bu sürüm 1 için de bahsettiğiniz gibi "karanlık bilgi" de değil mi?
TimK

3
" yazılım geliştirme nedenleri 2'yi daha iyi bir seçim haline getiriyor " Bu ifadeyi daha ayrıntılı olarak açıklayabilir misiniz?
Duncan Jones

8
"Kapatmak ile deyim etrafında yürütmek" bir örnek verebilir misiniz
Markus

2
"Java SE 8'de kapaklarla daha açık sözdizimi" nin ne olduğunu açıklayabilir misiniz?
petertc

1
"Deyim Etrafında Yürüt" örneği burada: stackoverflow.com/a/342016/258772
mrts

20

İlk stil Oracle tarafından önerilen stildir . BufferedWriterkontrol edilen istisnaları atmaz, bu nedenle herhangi bir istisna atılırsa, programın bundan kurtulması beklenmez, bu da kaynak kurtarmayı çoğunlukla tartışmalıdır.

Çoğunlukla bir iş parçacığında gerçekleşebileceğinden, iş parçacığı ölüyor ancak program devam ediyor - örneğin, programın geri kalanını ciddi şekilde bozacak kadar uzun olmayan geçici bir bellek kesintisi vardı. Bu oldukça köşe bir durumdur ve eğer kaynak sızıntısını bir problem haline getirecek kadar sık ​​olursa, kaynaklarla denemek sorunlarınızın en azıdır.


2
Aynı zamanda Etkili Java'nın 3. baskısında önerilen yaklaşımdır.
shmosel

5

Seçenek 4

Kaynaklarınızı Kapanabilir olacak şekilde değiştirin; Yapıcıların zincirlenebilmesi, kaynağı iki kez kapatmanın duyulmadığı anlamına gelir. (Bu, ARM'den önce de geçerliydi.) Aşağıda daha fazlası.

Seçenek 5

Close () işlevinin iki kez çağrılmadığından emin olmak için ARM ve kodu çok dikkatli kullanmayın!

Seçenek 6

ARM kullanmayın ve nihayet yakın () çağrılarınızı kendiniz deneyin / yakalayın.

Neden bu problemin ARM'ye özgü olduğunu düşünmüyorum

Tüm bu örneklerde, nihayet close () çağrıları bir catch bloğunda olmalıdır. Okunabilirlik için dışarıda bırakıldı.

İyi değil çünkü fw iki kez kapatılabilir. (FileWriter için iyidir ancak varsayımsal örneğinizde yoktur):

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

BufferedWriter oluşturma istisnası varsa fw kapatılmadı. (yine, olamaz, ama varsayımsal örneğinizde):

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

3

Jeanne Boyarsky'nin ARM kullanmama önerisi üzerine inşa etmek istedim, ancak FileWriter'ın her zaman tam olarak bir kez kapalı olduğundan emin olmak istedim. Burada herhangi bir sorun olduğunu düşünmeyin ...

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

Sanırım ARM sadece sözdizimsel şeker olduğundan, nihayet blokları değiştirmek için her zaman kullanamayız. Tıpkı yineleyicilerle mümkün olan bir şeyi yapmak için her zaman for-loop'u kullanamayacağımız gibi.


5
Hem senin ederse tryve finallybloklar istisnalar atmak, bu yapı bir ilk (ve potansiyel olarak daha yararlı) kaybedecektir.
rxg

3

Daha önceki yorumlarla aynı fikirde olmak için: en basit olanı (2)Closeable kaynakları kullanmak ve bunları kaynaklarla deneme fıkrasında sırayla beyan etmektir. Yalnızca varsa AutoCloseable, bunları closeyalnızca bir kez (Cephe Deseni) olarak adlandırılan, örneğin sahip olan denetimleri başka bir (iç içe) sınıfa sarabilirsiniz private bool isClosed;. Pratikte Oracle bile sadece (1) yapıcıları zincirliyor ve zincir boyunca kısmen istisnaları doğru şekilde işlemiyor.

Alternatif olarak, statik bir fabrika yöntemini kullanarak zincirleme bir kaynağı elle oluşturabilirsiniz; bu zinciri sarar ve kısmen başarısız olursa temizleme işlemlerini gerçekleştirir:

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

Daha sonra bunu, kaynaklarla dene yan tümcesinde tek bir kaynak olarak kullanabilirsiniz:

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

Karmaşıklık birden fazla istisnayı ele almaktan kaynaklanır; Aksi takdirde "şimdiye kadar edindiğiniz yakın kaynaklar". Yaygın bir uygulama, önce kaynağı tutan nesneyi null(burada fileWriter) tutan değişkeni başlatmak ve ardından temizlemeye boş bir denetim eklemek gibi görünüyor, ancak bu gereksiz görünüyor: Oluşturucu başarısız olursa, temizlenmesi gereken bir şey yok, bu yüzden kodu istisna eden istisnanın yayılmasına izin verebiliriz.

Muhtemelen bunu genel olarak yapabilirsiniz:

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

Benzer şekilde, üç kaynağı vb. Zincirleyebilirsiniz.

Matematiksel bir yana, aynı anda iki kaynağı zincirleyerek üç kez bile zincirleyebilirsiniz ve bu ilişkilendirici olur, yani aynı nesneyi başarıya alırsınız (inşaatçılar ilişkilendirici olduğu için) ve bir hata varsa aynı istisnalar inşaatçıların herhangi birinde. Yukarıdaki zincire bir S eklediğinizi varsayarsak (böylece bir V ile başlayıp bir S ile bitirirsiniz , sırayla U , T ve S uygulayarak ), ilk önce S ve T , sonra U , (ST) U'ya karşılık gelir veya T ve U'yu ilk zincirlediyseniz ,S , S (TU) 'ya karşılık gelir . Bununla birlikte, tek bir fabrika fonksiyonunda açık bir üç katlı zincir yazmak daha açık olacaktır.


Birinin hala olduğu gibi, kaynakla deneme özelliğini kullanması gerektiği gibi toplanıyor muyum try (BufferedWriter writer = <BufferedWriter, FileWriter>createChainedResource(file)) { /* work with writer */ }?
ErikE

@ErikE Evet, hala kullanımına denemek-ile-kaynaklar gerekir, ancak yalnızca zincirleme kaynak için tek bir işlevi kullanmak gerekir: fabrika işlevi kapsüller zincirleme. Bir kullanım örneği ekledim; Teşekkürler!
Nils von Barth

2

Kaynaklarınız iç içe olduğundan, deneme yan tümceleriniz de şöyle olmalıdır:

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
}

5
Bu benim 2. örneğime çok benziyor. İstisna olmazsa, FileWriter closeiki kez çağrılır.
Natix

0

ARM kullanmayın ve Closeable ile devam edin. Gibi yöntemi kullanın,

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

}

Ayrıca BufferedWritersadece yakın temsilci değil FileWriter, aynı zamanda bazı temizlik yapar gibi yakın çağırmayı düşünmelisiniz flushBuffer.


0

Benim çözüm aşağıdaki gibi bir "özü yöntemi" yeniden düzenleme yapmaktır:

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 ya yazılabilir

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

veya

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

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

Sınıf lib tasarımcıları için, AutoClosable kapatmayı bastırmak için ek bir yöntemle arayüzü . Bu durumda kapatma davranışını manuel olarak kontrol edebiliriz.

Dil tasarımcıları için ders, yeni bir özellik eklemenin diğerlerini eklemek anlamına gelebileceğidir. Bu Java durumunda, ARM özelliğinin bir kaynak sahipliği aktarım mekanizmasıyla daha iyi çalışacağı açıktır.

GÜNCELLEME

Başlangıçta yukarıdaki kod gerektirir @SuppressWarningçünkü BufferedWriterfonksiyonun içindeclose() .

Bir yorum tarafından önerildiği gibi flush(), yazar kapatılmadan önce çağrılırsa, bunu returntry bloğu içindeki (örtük veya açık) ifadelerden önce yapmamız gerekir . Şu anda arayanın bunu yaptığından emin olmanın bir yolu yok, bu yüzden bunun belgelenmesi gerekiyor writeFileWriter.

TEKRAR GÜNCELLEME

Yukarıdaki güncelleştirme @SuppressWarninggereksizdir, çünkü işlevin kaynağı arayana geri döndürmesini gerektirir, bu nedenle kendisinin kapatılması gerekmez. Ne yazık ki, bu bizi durumun başına geri götürüyor: uyarı şimdi arayan tarafa geri taşındı.

Yani düzgün bunu çözmek için, biz özelleştirilmiş ihtiyacımız AutoClosablekapanır zaman, alt çizgi olduğunu BufferedWriterolacaktır flush()ed. Aslında, bu bize uyarıyı atlamanın başka bir yolunu gösterir, çünkü BufferWriterasla hiçbir şekilde kapatılmaz.


Uyarının anlamı var: Burada bwverilerin gerçekten yazılacağından emin olabilir miyiz ? Bu olup sadece (tampon dolu ve / veya ilgili olduğu zaman, bazen diske yazma sahiptir, böylece afterall tamponlanmış flush()ve close()yöntemler). Sanırım flush()yöntem çağrılmalıdır. Ama sonra, tamponlu yazarı kullanmaya gerek yok, her neyse, hemen bir toplu işte yazdığımızda. Kodu düzeltemezseniz, dosyaya yanlış sırada yazılmış veya dosyaya hiç yazılmamış verilerle sonuçlanabilir.
Petr Janeček

Aranması flush()gerekiyorsa, arayan kişi telefonu kapatmaya karar vermeden önce gerçekleşecektir FileWriter. Bu yüzden bu , try bloğundaki printToFileherhangi bir returns'den hemen önce gerçekleşmelidir . Bu bir parçası olmayacak writeFileWriterve bu nedenle uyarı bu işlevin içindeki hiçbir şeyle ilgili değil, o işlevin arayanı ile ilgilidir. Bir ek açıklamamız @LiftWarningToCaller("wanrningXXX")varsa, bu davaya ve benzerlerine yardımcı olacaktır.
Dünya Motoru
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.