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 AutoCloseable
recurso. 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 FileWriter
e a BufferedWriter
que o envolvem. Obviamente, essa pergunta diz respeito a qualquer caso em que alguns AutoCloseable
recursos 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 FileWriter
não é declarado em uma variável, ele nunca será fechado diretamente no finally
bloco gerado . Ele será fechado apenas pelo close
método de embalagem BufferedWriter
. O problema é que, se uma exceção for lançada do bw
construtor, ela close
nã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 close
sã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 close
possa 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 fw
representa um recurso real que precisa ser limpo. O bw
pró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 ?
fonte
public BufferedWriter(Writer out, int sz)
pode jogar umIllegalArgumentException
. 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.BufferedWriter
construtor 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ê precisaflush
seuBufferedWriter
se você não fechar e quer manter o conteúdo (geralmente única o caso não exceção).FileWriter
escolhe o que quer que seja a codificação de arquivo "padrão" - é melhor ser explícito.Respostas:
Aqui está a minha opinião sobre as alternativas:
1)
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
BufferedWriter
pode lançar uma exceção aqui. Ficar sem memória não seria incomum, por exemplo. Para outros decoradores, você sabe quaisjava.io
classes 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
finally
blocos 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çãoflush
internaclose
causadaclose
no objeto decorado não era chamada. Embora isso tenha sido corrigido por algum tempo, espere de outras implementações.2)
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 falhaflush
não impeça a liberação de recursos.3)
Há um bug aqui. Deveria estar:
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.
fonte
O primeiro estilo é o sugerido pela Oracle .
BufferedWriter
nã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.
fonte
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):
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):
fonte
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 ...
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.
fonte
try
e ofinally
bloco lançarem exceções, essa construção perderá a primeira (e potencialmente mais útil).Para concordar com os comentários anteriores: o mais simples é (2) usar os
Closeable
recursos e declará-los em ordem na cláusula try-with-resources. Se você tiver apenasAutoCloseable
, poderá agrupá-los em outra classe (aninhada) que apenas verifica aclose
chamada apenas uma vez (Padrão de fachada), por exemplo, tendoprivate 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:
Você pode usá-lo como um único recurso em uma cláusula try-with-resources:
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
(aquifileWriter
) 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:
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.
fonte
try (BufferedWriter writer = <BufferedWriter, FileWriter>createChainedResource(file)) { /* work with writer */ }
?Como seus recursos estão aninhados, suas cláusulas try-with também devem ser:
fonte
close
será chamado duas vezes.Eu diria que não use o ARM e continue com o Closeable. Use um método como,
Além disso, considere ligar para perto,
BufferedWriter
pois não é apenas delegar o próximoFileWriter
, mas faz alguma limpeza comoflushBuffer
.fonte
Minha solução é fazer uma refatoração de "método de extração", da seguinte maneira:
printToFile
pode ser escritoou
Para designers de bibliotecas de classes, sugiro que estendam a
AutoClosable
interface 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,
@SuppressWarning
pois oBufferedWriter
interior da função exigeclose()
.Conforme sugerido por um comentário, se
flush()
for chamado antes de fechar o gravador, precisamos fazê-lo antes de qualquerreturn
declaraçã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 documentadowriteFileWriter
.ATUALIZAR NOVAMENTE
A atualização acima torna
@SuppressWarning
desnecessá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
AutoClosable
que, sempre que for fechado, o sublinhadoBufferedWriter
sejaflush()
editado. Na verdade, isso nos mostra uma outra maneira de ignorar o aviso, uma vez queBufferWriter
nunca é fechado de qualquer maneira.fonte
bw
realmente 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 ativadoflush()
eclose()
métodos). Eu acho que oflush()
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.flush()
precisar ser chamado, isso deve acontecer sempre que o chamador decidir fechar oFileWriter
. Portanto, isso deve acontecerprintToFile
logo antes de qualquerreturn
s no bloco try. Isso não deve fazer partewriteFileWriter
e, 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.