java-io-streams-lectura-escritura-bytes-char

Lectura y escritura en Java: bytes y caracteres

  • 4 min

Los streams de E/S transportan bytes o caracteres entre el programa y una fuente o destino.

En el artículo anterior usamos Files.readString() y Files.readAllLines(), dos métodos cómodos para ficheros de texto pequeños.

Pero, ¿qué pasa si quieres copiar una imagen JPG? ¿O un PDF? ¿O si el fichero de texto ocupa 50GB y no cabe en la RAM? Ahí los métodos de utilidad se quedan cortos. Necesitamos el control manual. Necesitamos los Streams (Flujos).

En Java I/O hay una división básica:

  1. Bytes (Binario): Para imágenes, sonidos, PDFs, ejecutables.
  2. Caracteres (Texto): Para .txt, .json, .xml, .java.

Guía para elegir una clase de E/S

Tipo de DatoOperaciónClase Base (Abstracta)Implementación de FicheroBuffer (Recomendado)
TextoLecturaReaderFileReaderBufferedReader
TextoEscrituraWriterFileWriterBufferedWriter
BinarioLecturaInputStreamFileInputStreamBufferedInputStream
BinarioEscrituraOutputStreamFileOutputStreamBufferedOutputStream

Trabajando con texto (caracteres)

Cuando trabajas con texto, Java necesita saber qué codificación usar (UTF-8, ISO-8859-1…). Las clases encargadas de esto se llaman Readers (Lectores) y Writers (Escritores).

La herramienta clave: BufferedReader y BufferedWriter

Leer carácter a carácter es ineficiente (cada letra implica un viaje al disco duro). Para evitarlo, usamos Buffers. Un buffer lee un trozo grande de golpe en memoria y te lo va dando poco a poco.

Lectura eficiente línea a línea:

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;

Path ruta = Path.of("quijote.txt");

// Usamos try-with-resources para cerrar el flujo automáticamente
// Files.newBufferedReader crea un Reader optimizado
try (BufferedReader reader = Files.newBufferedReader(ruta)) {

    String linea;
    // readLine() devuelve null cuando llega al final del fichero
    while ((linea = reader.readLine()) != null) {
        System.out.println(linea);
    }

} catch (IOException e) {
    e.printStackTrace();
}
Copied!

Escritura eficiente:

Path salida = Path.of("salida.txt");

try (BufferedWriter writer = Files.newBufferedWriter(salida)) {

    writer.write("Hola Mundo");
    writer.newLine(); // Salto de línea compatible con el SO (\n o \r\n)
    writer.write("Segunda línea");

} catch (IOException e) { ... }
Copied!

¡Cuidado con el Encoding! Por defecto Java usa UTF-8 (en versiones modernas). Si tienes que leer un fichero antiguo de Windows, especifícalo: Files.newBufferedReader(ruta, Charset.forName("windows-1252"));

Trabajando con binarios (bytes)

Si intentas leer una imagen usando un Reader de texto, la romperás. El Reader intentará interpretar los bytes de la foto como letras, cambiará cosas que no entiende, y el archivo resultante será basura corrupta.

Para datos “crudos” (Raw Data), usamos InputStream y OutputStream. Aquí no hay letras, solo números del 0 al 255.

Ejemplo: Copiar una imagen (Byte a Byte)

import java.io.*;

File origen = new File("foto.jpg");
File destino = new File("copia_foto.jpg");

try (
    FileInputStream in = new FileInputStream(origen);
    FileOutputStream out = new FileOutputStream(destino)
) {
    // Buffer manual de 1KB (leemos de 1024 en 1024 bytes)
    byte[] buffer = new byte[1024];
    int bytesLeidos;

    // in.read(buffer) llena el array y devuelve cuántos ha leído
    // Devuelve -1 si se acaba el fichero
    while ((bytesLeidos = in.read(buffer)) != -1) {
        // Escribimos en el destino SOLO los bytes leídos
        out.write(buffer, 0, bytesLeidos);
    }

} catch (IOException e) { ... }
Copied!

Este es el mecanismo base de cómo funcionan las descargas de internet o las copias de ficheros en el sistema operativo.

El puente: InputStreamReader

A veces tienes un flujo de bytes (como la entrada del teclado System.in o una conexión de red Socket) y quieres leerlo como texto.

Necesitas un adaptador que convierta Bytes -> Caracteres. Ese es el InputStreamReader.

// System.in es un InputStream (Bytes)
InputStream flujoBytes = System.in;

// Lo convertimos a Reader (Caracteres)
InputStreamReader conversor = new InputStreamReader(flujoBytes);

// Lo envolvemos en Buffer para leer líneas cómodamente
BufferedReader lectorTeclado = new BufferedReader(conversor);

System.out.print("Escribe algo: ");
String texto = lectorTeclado.readLine();
Copied!

Esta es la razón por la que en los cursos antiguos de Java se veía esa línea monstruosa: new BufferedReader(new InputStreamReader(System.in)) (Para una entrada de consola sencilla solemos usar Scanner, que ofrece una API más cómoda y permite convertir tokens a distintos tipos).

Scanner frente a BufferedReader

Es una duda común. Ambos leen texto.

  • Scanner: Es potente analizando texto. Puede buscar enteros (nextInt), booleanos, o usar expresiones regulares. Es un poco más lento porque hace mucho “parsing”.
  • BufferedReader: Es simple y puro. Solo lee Strings o líneas completas. Es muy rápido.

Veredicto:

  • Si lees comandos de consola o un CSV complejo: Scanner.
  • Si lees un fichero de texto gigante línea a línea: BufferedReader.