1. ¿Para qué sirven DataInputStream y DataOutputStream?
Cuando trabajas con archivos, a veces necesitas almacenar no solo texto, sino datos estructurados: números, valores booleanos, arrays de primitivos. Por ejemplo, imagina que escribes un juego sencillo y quieres guardar el progreso del usuario: cantidad de puntos (int), nivel actual (int), tiempo de juego (double), estado de ganador (boolean). Por supuesto, podrías escribirlo en formato de texto:
12345
5
67.5
true
Pero eso es incómodo e inseguro: hay que parsear las cadenas, el formato puede «descuadrarse», y los números ocupan más espacio.
La idea es escribir los datos en el archivo en formato «en crudo» (binario), sin convertirlos a texto. Para ello, Java ofrece dos clases estupendas:
- DataOutputStream — sabe escribir tipos primitivos en un flujo.
- DataInputStream — sabe leer tipos primitivos de un flujo.
Funcionan sobre flujos de bytes normales (OutputStream/InputStream). Es decir, son una «capa» superior que no solo escribe bytes, sino que sabe cómo ensamblar a partir de esos bytes un int, double, boolean e incluso un String.
¿Cómo funciona?
Un FileOutputStream normal puede imaginarse como una cinta transportadora en la que vas colocando bytes manualmente uno tras otro. Si quieres escribir un número entero o una cadena, tienes que controlar por tu cuenta cuántos bytes ocupa cada elemento.
DataOutputStream te simplifica la vida: actúa como un robot en esa cinta. Le dices «escribe un número» o «escribe una cadena», y él empaqueta los datos en la cantidad de bytes necesaria y los envía al disco. En el otro extremo de la cinta, otro robot —DataInputStream— sabe reconstruir esos bytes de vuelta a los objetos originales.
¿Por qué es útil? Porque no necesitas pensar en el número de bytes de un int, double o boolean. Los datos se almacenan de forma compacta, se leen y escriben rápido, y además evitas errores de parseo o problemas de formato.
2. Ejemplo: escritura y lectura de primitivos
Supongamos que queremos guardar los resultados de nuestro (hipotético) juego: nombre del jugador (String), cantidad de puntos (int), tiempo récord (double), si el jugador ganó (boolean).
Escritura de datos en un archivo
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); // Escribimos la cadena (UTF-8)
dos.writeInt(score); // Escribimos int (4 bytes)
dos.writeDouble(recordTime); // Escribimos double (8 bytes)
dos.writeBoolean(isWinner); // Escribimos boolean (1 byte)
System.out.println("¡Los datos se han escrito correctamente en el archivo!");
} catch (IOException e) {
System.out.println("Error de escritura: " + e.getMessage());
}
}
}
- writeUTF(String) — escribe una cadena en formato UTF-8 (con la longitud al principio).
- writeInt(int) — escribe 4 bytes.
- writeDouble(double) — escribe 8 bytes.
- writeBoolean(boolean) — escribe 1 byte (1 o 0).
- Todos los métodos «empaquetan» automáticamente los datos en el formato correcto.
Lectura de datos desde el archivo
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(); // Leemos la cadena
int score = dis.readInt(); // Leemos int
double recordTime = dis.readDouble(); // Leemos double
boolean isWinner = dis.readBoolean(); // Leemos boolean
System.out.println("Nombre del jugador: " + playerName);
System.out.println("Puntos: " + score);
System.out.println("Tiempo: " + recordTime);
System.out.println("Ganador: " + isWinner);
} catch (IOException e) {
System.out.println("Error de lectura: " + e.getMessage());
}
}
}
Punto importante:
¡El orden de lectura debe coincidir con el orden de escritura! Si primero escribiste una cadena, luego un int y después un double, debes leer en ese mismo orden. De lo contrario, obtendrás un error o datos corruptos.
3. ¿Qué tipos se admiten?
DataOutputStream y DataInputStream admiten todos los tipos primitivos principales de Java:
| Método de escritura | Método de lectura | Tipo de datos | Tamaño (bytes) |
|---|---|---|---|
|
|
boolean | 1 |
|
|
byte | 1 |
|
|
short | 2 |
|
|
char | 2 |
|
|
int | 4 |
|
|
long | 8 |
|
|
float | 4 |
|
|
double | 8 |
|
|
String (UTF) | variable |
Notas:
- Para cadenas se suele usar writeUTF/readUTF (se escribe la longitud de la cadena y luego los bytes en UTF-8).
- Si quieres escribir un array, primero escribe su longitud y después los elementos uno a uno.
4. Ejemplo avanzado: guardamos arrays de primitivos
Escritura de un array
int[] scores = {100, 200, 300, 400, 500};
try (DataOutputStream dos = new DataOutputStream(
new FileOutputStream("scores.bin"))) {
dos.writeInt(scores.length); // Primero escribimos la longitud del array
for (int score : scores) {
dos.writeInt(score); // Luego cada elemento
}
}
Lectura de un array
try (DataInputStream dis = new DataInputStream(
new FileInputStream("scores.bin"))) {
int length = dis.readInt(); // Leemos la longitud
int[] scores = new int[length];
for (int i = 0; i < length; i++) {
scores[i] = dis.readInt(); // Leemos los elementos
}
// Imprimimos el array
for (int score : scores) {
System.out.println(score);
}
}
¿Por qué la longitud primero?
Porque al leer no sabemos cuántos números se han escrito. Al escribir la longitud al principio, hacemos que el formato del archivo sea autodocumentado.
5. Matices y particularidades importantes
¿Cuándo conviene usar DataInputStream/DataOutputStream?
- Cuando necesitas guardar/cargar datos estructurados compuestos por primitivos.
- Para intercambiar datos binarios entre programas en Java (o incluso entre distintos lenguajes, si conoces el formato).
- Cuando importan la compacidad y la velocidad (por ejemplo, logs, resultados de cálculos, grandes arrays de números).
Cuándo no usar:
- Si necesitas un formato legible por humanos (CSV, JSON, XML), usa formatos de texto.
- Para objetos complejos con anidamiento, es mejor utilizar la serialización mediante ObjectOutputStream/ObjectInputStream (tema aparte).
Bufferización
DataOutputStream y DataInputStream no hacen buffering por sí mismos. Si quieres aumentar el rendimiento al trabajar con archivos grandes, envuélvelos en BufferedOutputStream/BufferedInputStream:
try (DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(new FileOutputStream("data.bin")))) {
// ...
}
Codificación de cadenas
Los métodos writeUTF/readUTF usan un formato especial: primero se escribe la longitud de la cadena (en bytes) y luego el contenido en UTF-8. ¡No lo confundas con escribir simplemente un array de bytes!
Excepciones
Las operaciones de lectura/escritura pueden lanzar IOException si el archivo no está disponible, está dañado o se termina antes de tiempo. Al intentar leer más de lo que se escribió, a menudo aparece EOFException. Utiliza siempre la construcción try-with-resources o maneja la excepción con try-catch.
Orden de lectura y escritura
El error más común es la discrepancia entre el orden de lectura y el de escritura. Si escribes: int, double, boolean, pero lees como double, int, boolean, obtendrás datos incorrectos o una excepción.
6. Errores típicos
Error n.º 1: romper el orden de escritura y lectura. Si cambias el orden de los métodos, los datos se leerán incorrectamente o se lanzará una excepción. Por ejemplo, si primero escribiste una cadena y luego un número, pero al leer intentas leer primero el número, obtendrás un error de formato.
Error n.º 2: olvidar escribir la longitud del array. Si escribes un array de primitivos pero no escribes su longitud, al leer no sabrás cuántos elementos tienes que leer. Esto conduce a un error o a «datos sobrantes» al final.
Error n.º 3: intentar leer después del final del archivo. Si lees más datos de los que se escribieron, obtendrás EOFException (end of file).
Error n.º 4: usar DataInputStream/DataOutputStream para archivos de texto. Estas clases no están pensadas para leer archivos de texto normales creados, por ejemplo, en el Bloc de notas. Si intentas leer tal archivo con readInt(), obtendrás datos sin sentido o un error.
Error n.º 5: no cerrar el flujo. Si no usas try-with-resources o no cierras los flujos manualmente, el archivo puede quedar inaccesible para otros programas o no guardarse por completo.
GO TO FULL VERSION