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) |
|---|---|---|---|
|
|
boolean | 1 |
|
|
byte | 1 |
|
|
short | 2 |
|
|
char | 2 |
|
|
int | 4 |
|
|
long | 8 |
|
|
float | 4 |
|
|
double | 8 |
|
|
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.
GO TO FULL VERSION