CodeGym /Cursos /JAVA 25 SELF /Leitura e escrita de arquivos binários: InputStream, Outp...

Leitura e escrita de arquivos binários: InputStream, OutputStream

JAVA 25 SELF
Nível 36 , Lição 2
Disponível

1. Introdução

Vamos começar pelo mais simples: em que um arquivo binário difere de um arquivo de texto? Um arquivo de texto é aquele que você pode abrir em um bloco de notas comum e ver letras, números, espaços e outros símbolos. Por exemplo, my_notes.txt ou poem.txt.

Arquivo binário — é um arquivo que não contém texto, mas bytes arbitrários. Pode ser uma imagem (.jpg, .png), música (.mp3), arquivo compactado (.zip), executável (.exe), vídeo (.mp4), um arquivo de banco de dados e assim por diante. Se você abrir esse tipo de arquivo no bloco de notas, verá algo como ÿØÿà ou uma parede de símbolos estranhos. Isso é normal! O computador “entende” apenas bytes — para ele, texto, imagem e vídeo são apenas uma sequência de bytes. Em arquivos de texto, esses bytes podem ser interpretados como caracteres; em binários, são dados “crus”, não destinados à leitura por pessoas.

Classes principais para trabalhar com arquivos binários

No Java, para trabalhar com arquivos binários são usados fluxos de bytes:

  • InputStream — classe base para leitura de bytes.
  • OutputStream — classe base para gravação de bytes.

Para trabalhar com arquivos existem as implementações específicas:

  • FileInputStream — lê bytes de um arquivo.
  • FileOutputStream — grava bytes em um arquivo.

Se você já ouviu falar de FileReader e FileWriter, saiba: eles trabalham com caracteres e servem apenas para texto. Para arquivos binários use apenas InputStream/OutputStream e seus derivados.

2. Leitura de arquivos binários

Leitura byte a byte

A maneira mais simples é ler o arquivo byte a byte. É didático, mas muito lento.

try (FileInputStream in = new FileInputStream("image.jpg")) {
    int b;
    while ((b = in.read()) != -1) {
        // b é um número de 0 a 255 (byte); -1 indica o fim do arquivo
        // Você pode processar o byte; por exemplo, somar todos os bytes
    }
}

O método read() retorna o próximo byte como int (de 0 a 255) e, quando o arquivo termina, retorna -1. Normalmente lê-se byte a byte apenas quando é necessário algo muito específico (por exemplo, analisar a estrutura do arquivo).

Leitura em blocos (com buffer)

Ler byte a byte é como ir ao mercado buscar cada maçã separadamente. Muito mais eficiente é pegar logo um pacote! No Java, para isso há o método read(byte[] buffer), que preenche um array com bytes do arquivo.

try (FileInputStream in = new FileInputStream("image.jpg")) {
    byte[] buffer = new byte[4096]; // buffer de 4 KB
    int bytesRead;
    while ((bytesRead = in.read(buffer)) != -1) {
        // buffer contém bytesRead bytes do arquivo
        // Você pode processar esses bytes; por exemplo, salvá-los em outro lugar
    }
}

O método read(buffer) retorna quantos bytes foram realmente lidos (pode ser menor que o tamanho do buffer, especialmente na última leitura). Esse modo é muito mais rápido, porque há menos acessos ao disco.

Exemplo: copiando um arquivo

Vamos escrever um programa simples que copia qualquer arquivo binário (por exemplo, uma imagem) de um lugar para outro.

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class BinaryCopyExample {
    public static void main(String[] args) {
        String source = "cat.jpg";
        String dest = "cat_copy.jpg";

        try (FileInputStream in = new FileInputStream(source);
             FileOutputStream out = new FileOutputStream(dest)) {

            byte[] buffer = new byte[8192]; // 8 KB — um tamanho ótimo para a maioria dos casos
            int bytesRead;
            while ((bytesRead = in.read(buffer)) != -1) {
                out.write(buffer, 0, bytesRead);
            }
            System.out.println("Cópia concluída!");
        } catch (IOException e) {
            System.out.println("Erro ao copiar: " + e.getMessage());
        }
    }
}

Simples: lemos blocos do arquivo de origem e os gravamos imediatamente no novo arquivo. Esse método funciona com quaisquer arquivos: imagens, compactados, vídeos.

3. Gravação de arquivos binários

Gravação de um array de bytes

Se você tem um array de bytes (por exemplo, que chegou da rede ou foi gerado no programa), pode gravá-lo em um arquivo assim:

byte[] data = new byte[] {1, 2, 3, 4, 5}; // exemplo de array

try (FileOutputStream out = new FileOutputStream("data.bin")) {
    out.write(data); // grava todo o array no arquivo
}

O método write(byte[]) grava todos os bytes do array. Também é possível gravar apenas uma parte do array: out.write(data, offset, length).

Gravação de arquivo em partes (por exemplo, ao copiar)

Assim como na leitura, normalmente se usa um buffer:

try (FileInputStream in = new FileInputStream("source.bin");
     FileOutputStream out = new FileOutputStream("dest.bin")) {

    byte[] buffer = new byte[4096];
    int bytesRead;
    while ((bytesRead = in.read(buffer)) != -1) {
        out.write(buffer, 0, bytesRead);
    }
}

Aqui, tudo o que lemos de um arquivo gravamos imediatamente em outro. Esse tipo de código é comum em aplicativos de compactação, gerenciadores de download, processadores de imagem etc.

4. Detalhes úteis

Por que não usar Reader/Writer para arquivos binários?

Reader e Writer trabalham com caracteres (char), não com bytes. Eles convertem bytes em caracteres automaticamente conforme a codificação (por exemplo, UTF-8). Isso é conveniente para texto, mas para arquivos binários — é fatal!

Se você tentar gravar uma imagem com FileWriter, obterá um arquivo corrompido que não poderá ser aberto. Lembre-se: para quaisquer arquivos não textuais use apenas InputStream/OutputStream!

Diferenças e detalhes importantes ao trabalhar com arquivos binários

  • Tamanho do buffer: Um buffer muito pequeno deixa tudo mais lento (excessivos acessos ao disco), muito grande consome memória desnecessariamente. 416 KB — geralmente é o ideal.
  • Tratamento de erros: Sempre trate IOException — o arquivo pode não existir, estar bloqueado ou o espaço em disco pode ter acabado.
  • Fechamento de fluxos: Use try-with-resources — isso garante o fechamento dos arquivos mesmo em caso de erro.
  • Sobrescrever arquivo: Se você abrir um arquivo com new FileOutputStream("file.bin"), ele será sobrescrito. Para acrescentar no final, use o construtor com o parâmetro append = true.
  • Permissões de acesso: Se o programa não consegue abrir o arquivo, verifique as permissões de leitura/gravação.
  • readAllBytes(): Permite ler o arquivo inteiro em um array de bytes com uma chamada. Para arquivos grandes — não use, para não “consumir” toda a memória!

5. Erros comuns ao trabalhar com arquivos binários

Erro nº 1: Uso de FileReader/FileWriter para arquivos binários. Isso resultará em corrupção de dados, porque essas classes convertem bytes em caracteres (e vice-versa), o que para imagens, arquivos compactados etc. é desastroso.

Erro nº 2: Ignorar o valor de retorno de read(). O método read(byte[]) pode ler menos bytes do que o solicitado, especialmente no último bloco. Sempre use o valor de retorno para saber quantos bytes foram realmente processados.

Erro nº 3: Esquecer de fechar o fluxo. Se o arquivo não for fechado, ele pode permanecer bloqueado e os dados podem não ser gravados completamente (principalmente na gravação!). Use try-with-resources.

Erro nº 4: Tentar ler o arquivo inteiro na memória sem considerar o tamanho. Para arquivos grandes, isso levará a OutOfMemoryError. Use um buffer e leia em partes.

Erro nº 5: Não tratar exceções. Trabalhar com arquivos pode sempre ocasionar erros: arquivo não encontrado, sem permissão, disco cheio. Não deixe de tratar IOException.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION