CodeGym /Cursos /JAVA 25 SELF /DataInputStream, DataOutputStream: trabalhando com tipos ...

DataInputStream, DataOutputStream: trabalhando com tipos primitivos

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

1. Para que servem DataInputStream e DataOutputStream?

Ao trabalhar com arquivos, às vezes é necessário armazenar não apenas texto, mas dados estruturados: números, valores booleanos, arrays de primitivos. Por exemplo, imagine que você escreve um jogo simples e quer salvar o progresso do usuário: quantidade de pontos (int), nível atual (int), tempo de jogo (double), status de vencedor (boolean). É possível, claro, gravar isso em texto:

12345
5
67.5
true

Mas isso é inconveniente e pouco seguro: as strings precisam ser analisadas, o formato pode “sair do controle”, e os números ocupam mais espaço.

A ideia é gravar os dados em arquivo em formato “bruto” (binário), sem converter para texto. Para isso, o Java oferece duas classes excelentes:

  • DataOutputStream — consegue gravar tipos primitivos em um stream.
  • DataInputStream — consegue ler tipos primitivos de um stream.

Eles trabalham sobre streams de bytes comuns (OutputStream/InputStream). Ou seja, são como “camadas” que não apenas escrevem bytes, mas sabem como montar a partir desses bytes um int, double, boolean e até String.

Como isso funciona?

Um FileOutputStream comum pode ser visto como uma esteira na qual você coloca bytes manualmente, um a um. Se você deseja gravar um número inteiro ou uma string, precisa controlar por conta própria quantos bytes cada elemento ocupa.

DataOutputStream facilita a vida: ele age como um robô nessa esteira. Você diz “grave um número” ou “grave uma string”, e ele mesmo empacota os dados na quantidade certa de bytes e envia para o disco. Do outro lado da esteira, um robô semelhante — DataInputStream — sabe remontar esses bytes nos objetos originais.

Por que isso é conveniente? Porque você não precisa pensar na quantidade de bytes para int, double ou boolean. Os dados são armazenados de forma compacta, são lidos e gravados rapidamente e, ao mesmo tempo, não há risco de erros de parsing ou problemas de formato.

2. Exemplo: gravação e leitura de primitivos

Suponha que queiramos salvar os resultados do nosso (hipotético) jogo: nome do jogador (String), quantidade de pontos (int), tempo recorde (double), se o jogador venceu (boolean).

Gravando dados em arquivo

import java.io.*;

public class SaveGameData {
    public static void main(String[] args) {
        String fileName = "savegame.bin";
        String playerName = "Alice";
        int score = 12345;
        double recordTime = 67.5;
        boolean isWinner = true;

        try (DataOutputStream dos = new DataOutputStream(
                new FileOutputStream(fileName))) {
            dos.writeUTF(playerName);    // Gravando a string (UTF-8)
            dos.writeInt(score);         // Gravando int (4 bytes)
            dos.writeDouble(recordTime); // Gravando double (8 bytes)
            dos.writeBoolean(isWinner);  // Gravando boolean (1 byte)
            System.out.println("Dados gravados no arquivo com sucesso!");
        } catch (IOException e) {
            System.out.println("Erro de gravação: " + e.getMessage());
        }
    }
}
  • writeUTF(String) — grava a string no formato UTF-8 (com comprimento no início).
  • writeInt(int) — grava 4 bytes.
  • writeDouble(double) — grava 8 bytes.
  • writeBoolean(boolean) — grava 1 byte (1 ou 0).
  • Todos os métodos “empacotam” automaticamente os dados no formato necessário.

Lendo dados do arquivo

import java.io.*;

public class LoadGameData {
    public static void main(String[] args) {
        String fileName = "savegame.bin";

        try (DataInputStream dis = new DataInputStream(
                new FileInputStream(fileName))) {
            String playerName = dis.readUTF();      // Lendo string
            int score = dis.readInt();              // Lendo int
            double recordTime = dis.readDouble();   // Lendo double
            boolean isWinner = dis.readBoolean();   // Lendo boolean

            System.out.println("Nome do jogador: " + playerName);
            System.out.println("Pontos: " + score);
            System.out.println("Tempo: " + recordTime);
            System.out.println("Vencedor: " + isWinner);
        } catch (IOException e) {
            System.out.println("Erro de leitura: " + e.getMessage());
        }
    }
}

Ponto importante:
A ordem de leitura deve coincidir com a ordem de escrita! Se você primeiro gravou a string, depois o int, depois o double — nessa mesma ordem deve ler. Caso contrário, você obterá um erro ou uma “mistura” de dados.

3. Quais tipos são suportados?

DataOutputStream e DataInputStream suportam todos os principais tipos primitivos do Java:

Método de escrita Método de leitura Tipo de dado Tamanho (bytes)
writeBoolean(boolean)
readBoolean()
boolean 1
writeByte(int)
readByte()
byte 1
writeShort(int)
readShort()
short 2
writeChar(int)
readChar()
char 2
writeInt(int)
readInt()
int 4
writeLong(long)
readLong()
long 8
writeFloat(float)
readFloat()
float 4
writeDouble(double)
readDouble()
double 8
writeUTF(String)
readUTF()
String (UTF) variável

Observações:
- Para strings, usa-se com mais frequência writeUTF/readUTF (grava-se o comprimento da string e os próprios bytes em UTF-8).
- Se quiser gravar um array, primeiro grave o seu tamanho e, em seguida, os elementos um a um.

4. Exemplo avançado: salvando arrays de primitivos

Gravando um array

int[] scores = {100, 200, 300, 400, 500};
try (DataOutputStream dos = new DataOutputStream(
        new FileOutputStream("scores.bin"))) {
    dos.writeInt(scores.length); // Primeiro escrevemos o tamanho do array
    for (int score : scores) {
        dos.writeInt(score);     // Depois, cada elemento
    }
}

Lendo um array

try (DataInputStream dis = new DataInputStream(
        new FileInputStream("scores.bin"))) {
    int length = dis.readInt();      // Lemos o tamanho
    int[] scores = new int[length];
    for (int i = 0; i < length; i++) {
        scores[i] = dis.readInt();   // Lemos os elementos
    }
    // Exibimos o array
    for (int score : scores) {
        System.out.println(score);
    }
}

Por que primeiro o tamanho?
Porque, na leitura, não sabemos quantos números foram gravados. Ao gravar o tamanho no início, tornamos o formato do arquivo autodescritivo.

5. Nuances e particularidades importantes

Quando usar DataInputStream/DataOutputStream?

  • Quando for necessário salvar/carregar dados estruturados compostos por primitivos.
  • Para troca de dados binários entre programas em Java (ou até entre linguagens diferentes, se você conhecer o formato).
  • Quando a compactação e a velocidade forem importantes (por exemplo, logs, resultados de cálculos, grandes arrays de números).

Quando não usar:

  • Se você precisa de um formato legível por humanos (CSV, JSON, XML) — use formatos de texto.
  • Para objetos complexos com aninhamento — prefira a serialização com ObjectOutputStream/ObjectInputStream (tema à parte).

Bufferização

DataOutputStream e DataInputStream não fazem buffer por si sós. Se você quiser aumentar o desempenho ao trabalhar com arquivos grandes, envolva-os em BufferedOutputStream/BufferedInputStream:

try (DataOutputStream dos = new DataOutputStream(
        new BufferedOutputStream(new FileOutputStream("data.bin")))) {
    // ...
}

Codificação de strings

Os métodos writeUTF/readUTF usam um formato especial: primeiro escreve-se o comprimento da string (em bytes), depois o conteúdo em UTF-8. Não confunda com simplesmente gravar um array de bytes!

Exceções

Operações de leitura/gravação podem lançar IOException se o arquivo estiver indisponível, corrompido ou terminar antes do esperado. Ao tentar ler mais do que foi gravado, é comum surgir EOFException. Sempre use a construção try-with-resources ou trate a exceção com try-catch.

Ordem de leitura e escrita

O erro mais comum — a ordem de leitura não corresponder à ordem de escrita. Se você gravou: int, double, boolean, mas lê como double, int, boolean, você obterá dados incorretos ou uma exceção.

6. Erros comuns

Erro nº 1: Quebrar a ordem de escrita e leitura. Se você mudar a ordem dos métodos, os dados serão lidos incorretamente ou será lançada uma exceção. Por exemplo, se primeiro gravou uma string e depois um número, mas na leitura tenta primeiro ler um número — você terá um erro de formato.

Erro nº 2: Esquecer de gravar o tamanho do array. Se você grava um array de primitivos, mas não registra o seu tamanho, na leitura não saberá quantos elementos precisa ler. Isso leva a um erro ou a “dados sobrando” no final.

Erro nº 3: Tentar ler após o fim do arquivo. Se você ler mais dados do que foi gravado, receberá EOFException (end of file).

Erro nº 4: Usar DataInputStream/DataOutputStream para arquivos de texto. Essas classes não são destinadas à leitura de arquivos de texto comuns, criados, por exemplo, no Bloco de Notas. Se você tentar ler tal arquivo com readInt() — obterá dados sem sentido ou um erro.

Erro nº 5: Não fechar o stream. Se você não usar try-with-resources ou não fechar os streams manualmente, o arquivo pode permanecer indisponível para outros programas ou não ser gravado completamente.

1
Tarefa
JAVA 25 SELF, nível 36, lição 3
Bloqueado
Codificação e decodificação da mensagem secreta ✉️
Codificação e decodificação da mensagem secreta ✉️
1
Tarefa
JAVA 25 SELF, nível 36, lição 3
Bloqueado
Inventário de artefatos mágicos 🧙
Inventário de artefatos mágicos 🧙
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION