Idioma correto para gerenciar vários recursos encadeados no bloco try-with-resources?

168

A sintaxe do Java 7 try-with-resources (também conhecida como bloco ARM ( Gerenciamento automático de recursos )) é agradável, curta e direta ao usar apenas um AutoCloseablerecurso. No entanto, não tenho certeza de qual é o idioma correto quando preciso declarar vários recursos que dependem um do outro, por exemplo, a FileWritere a BufferedWriterque o envolvem. Obviamente, essa pergunta diz respeito a qualquer caso em que alguns AutoCloseablerecursos são agrupados, não apenas a essas duas classes específicas.

Eu vim com as três alternativas a seguir:

1)

O idioma ingênuo que eu vi é declarar apenas o wrapper de nível superior na variável gerenciada pelo ARM:

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

Isso é bom e curto, mas está quebrado. Como o subjacente FileWriternão é declarado em uma variável, ele nunca será fechado diretamente no finallybloco gerado . Ele será fechado apenas pelo closemétodo de embalagem BufferedWriter. O problema é que, se uma exceção for lançada do bwconstrutor, ela closenão será chamada e, portanto, o subjacente FileWriter não será fechado .

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

Aqui, o recurso subjacente e o empacotamento são declarados nas variáveis ​​gerenciadas pelo ARM, de modo que ambos certamente serão fechados, mas o subjacente fw.close() será chamado duas vezes : não apenas diretamente, mas também através do empacotamento bw.close().

Isso não deve ser um problema para essas duas classes específicas que ambos implementam Closeable(que é um subtipo de AutoCloseable), cujo contrato afirma que várias chamadas para closesão permitidas:

Fecha esse fluxo e libera todos os recursos do sistema associados a ele. Se o fluxo já estiver fechado, a invocação desse método não terá efeito.

No entanto, em um caso geral, posso ter recursos que implementam apenas AutoCloseable(e não Closeable), o que não garante que closepossa ser chamado várias vezes:

Observe que, diferentemente do método close de java.io.Closeable, esse método close não precisa ser idempotente. Em outras palavras, chamar esse método close mais de uma vez pode ter algum efeito colateral visível, ao contrário de Closeable.close, que é necessário para não ter efeito se chamado mais de uma vez. No entanto, os implementadores dessa interface são fortemente encorajados a tornar seus métodos próximos idempotentes.

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

Esta versão deve ser teoricamente correta, porque apenas o fwrepresenta um recurso real que precisa ser limpo. O bwpróprio não possui nenhum recurso, ele apenas delega para o fw, portanto deve ser suficiente fechar apenas o subjacente fw.

Por outro lado, a sintaxe é um pouco irregular e, também, o Eclipse emite um aviso, que acredito ser um alarme falso, mas ainda é um aviso com o qual devemos lidar:

Vazamento de recurso: 'bw' nunca é fechado


Então, qual abordagem usar? Ou eu perdi algum outro idioma que é o correto ?

Natix
fonte
4
Obviamente, se o construtor subjacente do FileWriter lançar uma exceção, ele nem será aberto e tudo estará OK. O que é o primeiro exemplo é o que acontece se o FileWriter for criado, mas o construtor do BufferedWriter lança uma exceção.
Natix 23/09/12
6
Vale a pena notar que BufferedWriter não lançará uma exceção. Existe um exemplo em que você pode pensar onde essa questão não é puramente acadêmica.
Peter Lawrey 23/09/12
10
@PeterLawrey Sim, você está certo que o construtor do BufferedWriter nesse cenário provavelmente não lançará uma exceção, mas, como eu apontei, esta pergunta diz respeito a quaisquer recursos no estilo do decorador. Mas, por exemplo, public BufferedWriter(Writer out, int sz)pode jogar um IllegalArgumentException. Além disso, eu posso estender o BufferedWriter com uma classe que lançaria algo de seu construtor ou criaria qualquer invólucro personalizado que eu precisar.
Natix 23/09
5
O BufferedWriterconstrutor pode facilmente lançar uma exceção. OutOfMemoryErroré provavelmente o mais comum, pois aloca uma boa parte da memória para o buffer (embora possa indicar que você deseja reiniciar todo o processo). / Você precisa flushseu BufferedWriterse você não fechar e quer manter o conteúdo (geralmente única o caso não exceção). FileWriterescolhe o que quer que seja a codificação de arquivo "padrão" - é melhor ser explícito.
Tom Hawtin - defina
10
@ Natix Desejo que todas as perguntas no SO sejam tão bem pesquisadas e claras quanto esta. Eu gostaria de poder votar isso 100 vezes.
22714 Geek

Respostas:

75

Aqui está a minha opinião sobre as alternativas:

1)

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

Para mim, a melhor coisa que veio do Java tradicional em C ++ há 15 anos foi que você podia confiar no seu programa. Mesmo que as coisas estejam ruins e dando errado, o que geralmente acontecem, quero que o restante do código seja sobre o melhor comportamento e o cheiro de rosas. De fato, o BufferedWriterpode lançar uma exceção aqui. Ficar sem memória não seria incomum, por exemplo. Para outros decoradores, você sabe quais java.ioclasses de wrapper lançam uma exceção verificada de seus construtores? Eu não. O entendimento do código não é muito bom se você confiar nesse tipo de conhecimento obscuro.

Também há a "destruição". Se houver uma condição de erro, provavelmente você não deseja liberar lixo para um arquivo que precisa ser excluído (código para o que não é mostrado). Embora, é claro, excluir o arquivo também seja outra operação interessante para o tratamento de erros.

Geralmente, você deseja que os finallyblocos sejam o mais curtos e confiáveis ​​possível. A adição de liberações não ajuda nesse objetivo. Para muitos lançamentos, algumas das classes de buffer no JDK tiveram um erro em que uma exceção flushinterna closecausada closeno objeto decorado não era chamada. Embora isso tenha sido corrigido por algum tempo, espere de outras implementações.

2)

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

Ainda estamos liberando o bloco implícito finalmente (agora com repetição close- isso piora à medida que você adiciona mais decoradores), mas a construção é segura e temos que implícitar finalmente os blocos para que mesmo uma falha flushnão impeça a liberação de recursos.

3)

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

Há um bug aqui. Deveria estar:

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

Alguns decoradores mal implementados são de fato um recurso e precisarão ser fechados de maneira confiável. Além disso, alguns fluxos podem precisar ser fechados de uma maneira específica (talvez eles estejam fazendo compressão e precisem escrever bits para finalizar, e não podem simplesmente liberar tudo.

Veredito

Embora 3 seja uma solução tecnicamente superior, os motivos de desenvolvimento de software tornam 2 a melhor escolha. No entanto, o try-with-resource ainda é uma correção inadequada e você deve seguir o idioma Execute Around , que deve ter uma sintaxe mais clara com os fechamentos no Java SE 8.

Tom Hawtin - linha de orientação
fonte
4
Na versão 3, como você sabe que o bw não exige que seu próximo seja chamado? E mesmo que você possa ter certeza, não é também esse "conhecimento obscuro", como você mencionou na versão 1?
TimK 26/03
3
" razões de desenvolvimento de software tornam a 2 a melhor escolha " Você pode explicar essa afirmação em mais detalhes?
Duncan Jones
8
Você pode dar um exemplo do idioma "Execute Around with closures"?
Markus
2
Você pode explicar o que é "sintaxe mais clara com fechamentos no Java SE 8"?
petertc
1
O exemplo de "Execute Around Idiom" está aqui: stackoverflow.com/a/342016/258772
mrts
20

O primeiro estilo é o sugerido pela Oracle . BufferedWriternão lança exceções verificadas, portanto, se alguma exceção for lançada, não se espera que o programa se recupere, fazendo com que a recuperação de recursos seja quase sempre discutível.

Principalmente porque isso pode ocorrer em um encadeamento, com o encadeamento morrendo, mas o programa ainda continua - digamos, houve uma falta temporária de memória que não foi longa o suficiente para prejudicar seriamente o restante do programa. É um caso bastante interessante, e se isso acontece com frequência suficiente para tornar um vazamento de recursos um problema, a tentativa com recursos é o menor dos seus problemas.

Daniel C. Sobral
fonte
2
É também a abordagem recomendada na 3ª edição do Effective Java.
shmosel 18/03/19
5

Opção 4

Altere seus recursos para Closeable, não AutoClosable, se possível. O fato de os construtores poderem ser encadeados implica que não é inédito fechar o recurso duas vezes. (Isso também ocorreu antes do ARM.) Mais sobre isso abaixo.

Opção 5

Não use o ARM e o código com muito cuidado para garantir que close () não seja chamado duas vezes!

Opção 6

Não use o ARM e receba as chamadas finalmente close () em uma tentativa / captura.

Por que não acho que esse problema seja exclusivo do ARM

Em todos esses exemplos, as chamadas finalmente close () devem estar em um bloco catch. Deixado para legibilidade.

Não é bom porque o fw pode ser fechado duas vezes. (o que é bom para o FileWriter, mas não no seu exemplo hipotético):

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

Não é bom porque fw não fechou se exceção na construção de um BufferedWriter. (novamente, não pode acontecer, mas no seu exemplo hipotético):

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

Eu só queria aproveitar a sugestão de Jeanne Boyarsky de não usar o ARM, mas garantir que o FileWriter esteja sempre fechado exatamente uma vez. Não pense que há problemas aqui ...

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

Acho que, como o ARM é apenas açúcar sintático, nem sempre podemos usá-lo para substituir finalmente os blocos. Assim como nem sempre podemos usar um loop for-each para fazer algo possível com os iteradores.

AmyGamy
fonte
5
Se o seu trye o finallybloco lançarem exceções, essa construção perderá a primeira (e potencialmente mais útil).
rxg
3

Para concordar com os comentários anteriores: o mais simples é (2) usar os Closeablerecursos e declará-los em ordem na cláusula try-with-resources. Se você tiver apenas AutoCloseable, poderá agrupá-los em outra classe (aninhada) que apenas verifica a closechamada apenas uma vez (Padrão de fachada), por exemplo, tendo private bool isClosed;. Na prática, mesmo o Oracle apenas (1) encadeia os construtores e não lida corretamente com exceções no meio da cadeia.

Como alternativa, você pode criar manualmente um recurso encadeado, usando um método estático de fábrica; isso encapsula a cadeia e manipula a limpeza se ela falhar no meio do caminho:

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

Você pode usá-lo como um único recurso em uma cláusula try-with-resources:

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

A complexidade vem do tratamento de várias exceções; caso contrário, são apenas "recursos próximos que você adquiriu até agora". Uma prática comum parece ser primeiro inicializar a variável que contém o objeto que contém o recurso null(aqui fileWriter) e depois incluir uma verificação nula na limpeza, mas isso parece desnecessário: se o construtor falhar, não há nada para limpar, para que possamos deixar essa exceção se propagar, o que simplifica um pouco o código.

Você provavelmente poderia fazer isso genericamente:

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

Da mesma forma, você pode encadear três recursos, etc.

Como um aspecto matemático, você pode encadear três vezes encadeando dois recursos de cada vez, e seria associativo, o que significa que você obteria o mesmo objeto com êxito (porque os construtores são associativos) e as mesmas exceções, se houver uma falha em qualquer um dos construtores. Supondo que você tenha adicionado um S à cadeia acima (para começar com um V e terminar com um S , aplicando U , T e S por sua vez), você obtém o mesmo se primeiro encadear S e T e depois U , correspondente a (ST) U , ou se você primeiro encadear T e U , entãoS , correspondente a S (TU) . No entanto, seria mais claro apenas escrever uma cadeia tripla explícita em uma única função de fábrica.

Nils von Barth
fonte
Estou percebendo corretamente que ainda é necessário usar o try-with-resource, como em try (BufferedWriter writer = <BufferedWriter, FileWriter>createChainedResource(file)) { /* work with writer */ }?
ErikE
@ErikE Sim, você ainda precisaria usar a tentativa com recursos, mas só precisaria usar uma única função para o recurso encadeado: a função de fábrica encapsula o encadeamento. Eu adicionei um exemplo de uso; obrigado!
Nils von Barth
2

Como seus recursos estão aninhados, suas cláusulas try-with também devem ser:

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
}
Poção
fonte
5
Isso é muito parecido com o meu segundo exemplo. Se nenhuma exceção ocorrer, o FileWriter closeserá chamado duas vezes.
Natix 26/09/12
0

Eu diria que não use o ARM e continue com o Closeable. Use um método como,

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

}

Além disso, considere ligar para perto, BufferedWriterpois não é apenas delegar o próximo FileWriter, mas faz alguma limpeza como flushBuffer.

sakthisundar
fonte
0

Minha solução é fazer uma refatoração de "método de extração", da seguinte maneira:

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 pode ser escrito

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

ou

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

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

Para designers de bibliotecas de classes, sugiro que estendam a AutoClosableinterface com um método adicional para suprimir o fechamento. Nesse caso, podemos controlar manualmente o comportamento próximo.

Para designers de idiomas, a lição é que adicionar um novo recurso pode significar adicionar muitos outros. Nesse caso Java, obviamente, o recurso ARM funcionará melhor com um mecanismo de transferência de propriedade de recursos.

ATUALIZAR

Originalmente, o código acima requer, @SuppressWarningpois o BufferedWriterinterior da função exige close().

Conforme sugerido por um comentário, se flush()for chamado antes de fechar o gravador, precisamos fazê-lo antes de qualquer returndeclaração (implícita ou explícita) dentro do bloco try. No momento, não há como garantir que o responsável pela chamada esteja fazendo isso, portanto, isso deve ser documentado writeFileWriter.

ATUALIZAR NOVAMENTE

A atualização acima torna @SuppressWarningdesnecessária, pois exige que a função retorne o recurso ao chamador, portanto ela não precisa ser fechada. Infelizmente, isso nos leva de volta ao início da situação: o aviso agora é retornado ao lado do chamador.

Portanto, para resolver adequadamente isso, precisamos de um personalizado AutoClosableque, sempre que for fechado, o sublinhado BufferedWriterseja flush()editado. Na verdade, isso nos mostra uma outra maneira de ignorar o aviso, uma vez que BufferWriternunca é fechado de qualquer maneira.

Earth Engine
fonte
O aviso tem seu significado: Podemos ter certeza aqui de que bwrealmente gravará os dados? Ele é armazenado em buffer no final das contas, portanto, apenas precisa gravar no disco algumas vezes (quando o buffer está cheio e / ou ativado flush()e close()métodos). Eu acho que o flush()método deve ser chamado. Mas não há necessidade de usar o gravador em buffer, de qualquer maneira, quando estamos escrevendo imediatamente em um lote. E se você não corrigir o código, poderá acabar com os dados gravados no arquivo em uma ordem errada ou até mesmo não gravados no arquivo.
Petr Janeček
Se flush()precisar ser chamado, isso deve acontecer sempre que o chamador decidir fechar o FileWriter. Portanto, isso deve acontecer printToFilelogo antes de qualquer returns no bloco try. Isso não deve fazer parte writeFileWritere, portanto, o aviso não se refere a nada dentro dessa função, mas ao chamador dessa função. Se tivermos uma anotação, @LiftWarningToCaller("wanrningXXX")isso ajudará neste caso e semelhante.
Earth Engine