É necessário fechar cada OutputStream e Writer aninhado separadamente?

127

Estou escrevendo um pedaço de código:

OutputStream outputStream = new FileOutputStream(createdFile);
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream);
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(gzipOutputStream));

Preciso fechar todos os fluxos ou gravadores como o seguinte?

gzipOutputStream.close();
bw.close();
outputStream.close();

Ou será bom apenas fechar o último fluxo?

bw.close();
Adon Smith
fonte
1
Para obter a pergunta obsoleta correspondente do Java 6, consulte stackoverflow.com/questions/884007/…
Raedwald
2
Observe que seu exemplo possui um bug que pode causar perda de dados, porque você está fechando os fluxos não na ordem em que os abriu. Ao fechar um BufferedWriter, pode ser necessário gravar dados em buffer no fluxo subjacente, que no seu exemplo já está fechado. Evitar esses problemas é outra vantagem das abordagens de tentativa com recursos mostradas nas respostas.
Joe23

Respostas:

150

Supondo que todos os fluxos são criados bem, sim, apenas o encerramento bwé muito bem com essas implementações de fluxo ; mas isso é uma grande suposição.

Eu usaria o try-with-resources ( tutorial ) para que quaisquer problemas na construção dos fluxos subseqüentes que geram exceções não deixem os fluxos anteriores suspensos e, assim, você não precise confiar na implementação do fluxo para encerrar a chamada. o fluxo subjacente:

try (
    OutputStream outputStream = new FileOutputStream(createdFile);
    GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream);
    OutputStreamWriter osw = new OutputStreamWriter(gzipOutputStream);
    BufferedWriter bw = new BufferedWriter(osw)
    ) {
    // ...
}

Observe que você não liga mais close.

Nota importante : Para que a tentativa com recursos os feche, você deve atribuir os fluxos às variáveis ​​ao abri-las, não é possível usar o aninhamento. Se você usar o aninhamento, uma exceção durante a construção de um dos fluxos posteriores (por exemplo, GZIPOutputStream) deixará aberto qualquer fluxo construído pelas chamadas aninhadas dentro dele. Do JLS §14.20.3 :

Uma instrução try-with-resources é parametrizada com variáveis (conhecidas como recursos) que são inicializadas antes da execução do trybloco e fechadas automaticamente, na ordem inversa em que foram inicializadas, após a execução do trybloco.

Observe a palavra "variáveis" (ênfase minha) .

Por exemplo, não faça isso:

// DON'T DO THIS
try (BufferedWriter bw = new BufferedWriter(
        new OutputStreamWriter(
        new GZIPOutputStream(
        new FileOutputStream(createdFile))))) {
    // ...
}

... porque uma exceção do GZIPOutputStream(OutputStream)construtor (que diz que pode ser lançada IOExceptione grava um cabeçalho no fluxo subjacente) deixaria FileOutputStreamaberta. Como alguns recursos têm construtores que podem ser lançados e outros não, é um bom hábito apenas listá-los separadamente.

Podemos verificar novamente nossa interpretação dessa seção JLS com este programa:

public class Example {

    private static class InnerMost implements AutoCloseable {
        public InnerMost() throws Exception {
            System.out.println("Constructing " + this.getClass().getName());
        }

        @Override
        public void close() throws Exception {
            System.out.println(this.getClass().getName() + " closed");
        }
    }

    private static class Middle implements AutoCloseable {
        private AutoCloseable c;

        public Middle(AutoCloseable c) {
            System.out.println("Constructing " + this.getClass().getName());
            this.c = c;
        }

        @Override
        public void close() throws Exception {
            System.out.println(this.getClass().getName() + " closed");
            c.close();
        }
    }

    private static class OuterMost implements AutoCloseable {
        private AutoCloseable c;

        public OuterMost(AutoCloseable c) throws Exception {
            System.out.println("Constructing " + this.getClass().getName());
            throw new Exception(this.getClass().getName() + " failed");
        }

        @Override
        public void close() throws Exception {
            System.out.println(this.getClass().getName() + " closed");
            c.close();
        }
    }

    public static final void main(String[] args) {
        // DON'T DO THIS
        try (OuterMost om = new OuterMost(
                new Middle(
                    new InnerMost()
                    )
                )
            ) {
            System.out.println("In try block");
        }
        catch (Exception e) {
            System.out.println("In catch block");
        }
        finally {
            System.out.println("In finally block");
        }
        System.out.println("At end of main");
    }
}

... que tem a saída:

Exemplo de construção $ InnerMost
Exemplo de construção $ Middle
Exemplo de construção $ OuterMost
No bloco de captura
Finalmente no bloco
No final do principal

Observe que não há chamadas para closelá.

Se corrigirmos main:

public static final void main(String[] args) {
    try (
        InnerMost im = new InnerMost();
        Middle m = new Middle(im);
        OuterMost om = new OuterMost(m)
        ) {
        System.out.println("In try block");
    }
    catch (Exception e) {
        System.out.println("In catch block");
    }
    finally {
        System.out.println("In finally block");
    }
    System.out.println("At end of main");
}

então recebemos as closechamadas apropriadas :

Exemplo de construção $ InnerMost
Exemplo de construção $ Middle
Exemplo de construção $ OuterMost
Exemplo $ Middle closed
Exemplo $ InnerMost fechado
Exemplo $ InnerMost fechado
No bloco de captura
Finalmente no bloco
No final do principal

(Sim, duas chamadas para InnerMost#closeestão corretas; uma é de Middlee a outra de tentativa com recursos.)

TJ Crowder
fonte
7
+1 por observar que exceções podem ser lançadas durante a construção dos fluxos, embora eu observe que, realisticamente, você receberá uma exceção de falta de memória ou algo igualmente sério (nesse ponto, realmente não importa) se você fechar seus fluxos, porque seu aplicativo está prestes a sair) ou será GZIPOutputStream que lança uma IOException; o restante dos construtores não possui exceções verificadas e não há outras circunstâncias que possam produzir uma exceção de tempo de execução.
Jules
5
@ Jules: Sim, para esses fluxos específicos, de fato. É mais sobre bons hábitos.
TJ Crowder 02/02
2
@ PeterLawrey: Não concordo plenamente com o uso de maus hábitos ou não, dependendo da implementação do fluxo. :-) Esta não é uma distinção YAGNI / no-YAGNI, trata-se de padrões que criam códigos confiáveis.
TJ Crowder
2
@ PeterLawrey: Também não há nada acima em não confiar java.io. Alguns fluxos - generalizando, alguns recursos - são lançados pelos construtores. Portanto, garantir que vários recursos sejam abertos individualmente para que possam ser fechados com segurança se um recurso subsequente for lançado é apenas um bom hábito, na minha opinião. Você pode optar por não fazer isso se não concordar, tudo bem.
TJ Crowder
2
@PeterLawrey: Então você recomenda reservar um tempo para analisar o código-fonte de uma implementação em busca de algo que documente uma exceção, caso a caso, e depois dizer "Oh, bem, na verdade não é assim. .. "e salvando alguns caracteres de digitação? Nós nos separamos por lá, grande momento. :-) Além disso, acabei de olhar, e isso não é teórico: GZIPOutputStreamo construtor escreve um cabeçalho no fluxo. E assim pode jogar. Então agora a posição é se eu acho que vale a pena se preocupar em tentar fechar o fluxo depois de escrever jogado. Sim: eu abri, eu deveria pelo menos tentar fechá-lo.
TJ Crowder
12

Você pode fechar o fluxo mais externo, na verdade, não precisa reter todos os fluxos agrupados e pode usar o Java 7 try-with-resources.

try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(
                     new GZIPOutputStream(new FileOutputStream(createdFile)))) {
     // write to the buffered writer
}

Se você se inscrever no YAGNI, ou não precisar, deve adicionar apenas o código que realmente precisa. Você não deve adicionar o código que imagina ser necessário, mas na realidade não faz nada de útil.

Pegue este exemplo e imagine o que poderia dar errado se você não fizesse isso e qual seria o impacto?

try (
    OutputStream outputStream = new FileOutputStream(createdFile);
    GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream);
    OutputStreamWriter osw = new OutputStreamWriter(gzipOutputStream);
    BufferedWriter bw = new BufferedWriter(osw)
    ) {
    // ...
}

Vamos começar com FileOutputStream, que chama openpara fazer todo o trabalho real.

/**
 * Opens a file, with the specified name, for overwriting or appending.
 * @param name name of file to be opened
 * @param append whether the file is to be opened in append mode
 */
private native void open(String name, boolean append)
    throws FileNotFoundException;

Se o arquivo não for encontrado, não há recurso subjacente para fechar, portanto, fechá-lo não fará nenhuma diferença. Se o arquivo existir, ele deve lançar uma FileNotFoundException. Portanto, não há nada a ganhar tentando fechar o recurso somente dessa linha.

O motivo pelo qual você precisa fechar o arquivo é quando o arquivo é aberto com êxito, mas você mais tarde recebe um erro.

Vamos ver o próximo stream GZIPOutputStream

Há código que pode gerar uma exceção

private void writeHeader() throws IOException {
    out.write(new byte[] {
                  (byte) GZIP_MAGIC,        // Magic number (short)
                  (byte)(GZIP_MAGIC >> 8),  // Magic number (short)
                  Deflater.DEFLATED,        // Compression method (CM)
                  0,                        // Flags (FLG)
                  0,                        // Modification time MTIME (int)
                  0,                        // Modification time MTIME (int)
                  0,                        // Modification time MTIME (int)
                  0,                        // Modification time MTIME (int)
                  0,                        // Extra flags (XFLG)
                  0                         // Operating system (OS)
              });
}

Isso grava o cabeçalho do arquivo. Agora, seria muito incomum você poder abrir um arquivo para gravação, mas não conseguir escrever nem 8 bytes, mas vamos imaginar que isso poderia acontecer e não fecharemos o arquivo posteriormente. O que acontece com um arquivo se ele não estiver fechado?

Você não recebe gravações não liberadas, elas são descartadas e, nesse caso, não há bytes gravados com sucesso no fluxo que não sejam armazenados em buffer neste momento. Mas um arquivo que não está fechado não fica para sempre, mas o FileOutputStream tem

protected void finalize() throws IOException {
    if (fd != null) {
        if (fd == FileDescriptor.out || fd == FileDescriptor.err) {
            flush();
        } else {
            /* if fd is shared, the references in FileDescriptor
             * will ensure that finalizer is only called when
             * safe to do so. All references using the fd have
             * become unreachable. We can call close()
             */
            close();
        }
    }
}

Se você não fechar um arquivo, ele será fechado de qualquer maneira, mas não imediatamente (e, como eu disse, os dados deixados em um buffer serão perdidos dessa maneira, mas não há nenhum neste momento)

Qual é a consequência de não fechar o arquivo imediatamente? Em condições normais, você potencialmente perde alguns dados e os descritores de arquivos ficam em potencial. Mas se você possui um sistema em que pode criar arquivos, mas não pode gravar nada neles, há um problema maior. ou seja, é difícil imaginar por que você está tentando criar esse arquivo repetidamente, apesar de estar falhando.

OutputStreamWriter e BufferedWriter não lançam IOException em seus construtores, portanto, não está claro qual problema eles causariam. No caso do BufferedWriter, você pode obter um OutOfMemoryError. Nesse caso, ele acionará imediatamente um GC, que, como vimos, fechará o arquivo de qualquer maneira.

Peter Lawrey
fonte
1
Veja a resposta de TJ Crowder para situações em que isso pode falhar.
TimK 03/02
O @TimK pode fornecer um exemplo de onde o arquivo é criado, mas o fluxo falha mais tarde e qual é a consequência. O risco de falha é extremamente baixo e o impacto é trivial. Não precisa tornar o mais complicado do que precisa ser.
Peter Lawrey
1
GZIPOutputStream(OutputStream)documentos IOExceptione, olhando a fonte, na verdade escreve um cabeçalho. Portanto, não é teórico que o construtor possa lançar. Você pode achar que não há problema em deixar o subjacente em FileOutputStreamaberto depois de escrever para ele. Eu não.
TJ Crowder
1
@TJCrowder Qualquer um que seja um desenvolvedor profissional experiente de JavaScript (e em outros idiomas além) eu tiro meu chapéu. Eu não pude fazer isso. ;)
Peter Lawrey
1
Apenas para revisitar isso, a outra questão é que, se você estiver usando um GZIPOutputStream em um arquivo e não ligar explicitamente para concluir, ele será chamado em sua implementação próxima. Isso não é uma tentativa ... finalmente, se o acabamento / liberação lançar uma exceção, o identificador de arquivo subjacente nunca será fechado.
31517 Robert_difalco 31/08
6

Se todos os fluxos tiverem sido instanciados, o fechamento apenas do lado externo será bom.

A documentação na Closeableinterface afirma que método fechado:

Fecha esse fluxo e libera todos os recursos do sistema associados a ele.

Os recursos do sistema de liberação incluem fluxos de fechamento.

Também afirma que:

Se o fluxo já estiver fechado, a invocação desse método não terá efeito.

Portanto, se você fechá-los explicitamente depois, nada de errado acontecerá.

Grzegorz Żur
fonte
2
Isso não pressupõe erros na construção dos fluxos, o que pode ou não ser verdadeiro para os listados, mas não é confiável em geral.
TJ Crowder
6

Eu prefiro usar try(...)sintaxe (Java 7), por exemplo

try (OutputStream outputStream = new FileOutputStream(createdFile)) {
      ...
}
Dmitry Bychenko
fonte
4
Enquanto eu concordo com você, você pode destacar os benefícios dessa abordagem e responder ao ponto se o OP precisar fechar os fluxos filho / interno
MadProgrammer
5

Tudo bem se você fechar apenas o último fluxo - a chamada final também será enviada para os fluxos subjacentes.

Codeversum
fonte
1
Veja o comentário na resposta de Grzegorzur.
TJ Crowder
5

Não, o nível mais alto Streamou readergarantirá que todos os fluxos / leitores subjacentes sejam fechados.

Verifique a implementação do close()método do seu fluxo de nível superior.

TheLostMind
fonte
5

No Java 7, há um recurso try-with-resources . Você não precisa fechar explicitamente seus fluxos, ele cuidará disso.

Sivakumar
fonte