Operaciones Fundamentales de Entrada y Salida en Java

La gestión de entrada y salida (I/O) es un pilar esencial en el desarrollo de aplicaciones Java, permitiendo la interacción con diversas fuentes como archivos, memoria o la red. Java proporciona un conjunto robusto de clases en el paquete java.io para manejar estas operaciones, utilizando el concepto de streams (flujos).

Lectura de Archivos con Búfer

La lectura de archivos de texto se optimiza considerablemente utilizando búferes. La clase BufferedReader, al envolver un FileReader, permite leer líneas completas de manera eficiente, reduciendo el número de accesos al disco. A continuación, se muestra un ejemplo de cómo leer el contenido de un archivo y devolverlo como una cadena.

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;

public class LectorArchivoBuffer {

   public static String leerContenido(String rutaArchivo) throws IOException {
       Path path = Paths.get(rutaArchivo);
       StringBuilder contenido = new StringBuilder();

       // Usamos try-with-resources para asegurar el cierre automático del BufferedReader
       try (BufferedReader lector = new BufferedReader(new FileReader(path.toFile()))) {
           String linea;
           while ((linea = lector.readLine()) != null) {
               contenido.append(linea).append(System.lineSeparator());
           }
       }
       return contenido.toString();
   }

   public static void main(String[] args) throws IOException {
       // Ejemplo: Leer el contenido de este mismo archivo
       String rutaActual = "src/LectorArchivoBuffer.java"; // Ajusta la ruta según tu estructura de proyecto
       System.out.println(leerContenido(rutaActual));
   }
}

Este código leerá el archivo especificado e imprimirá su contenido en la consola, incluyendo saltos de línea.

Entrada de Datos desde la Memoria

No toda la entrada proviene de archivos; a menudo, necesitamos porcesar cadenas de texto ya presentes en la memoria como si fueran un flujo de entrada. La clase StringReader facilita esta tarea, permitiendo leer caracteres uno a uno o en bloques desde una cadena.

import java.io.IOException;
import java.io.StringReader;

public class EntradaDesdeString {

   public static void main(String[] args) throws IOException {
       String textoOrigen = LectorArchivoBuffer.leerContenido("src/EntradaDesdeString.java"); // Usamos el lector anterior
       
       // El StringReader trata la cadena como un flujo de caracteres
       try (StringReader lectorMemoria = new StringReader(textoOrigen)) {
           int caracterLeido;
           // El método read() devuelve un entero que representa el carácter, o -1 si se llega al final
           while ((caracterLeido = lectorMemoria.read()) != -1) {
               System.out.print((char) caracterLeido);
           }
       }
   }
}

Entrada de Datos Formateados

Para leer datos primitivos formateados (como enteros, dobles, etc.), se utiliza DataInputStream. Esta clase es orientada a bytes y requiere un InputStream subyacente. A continuación, se ilustra cómo leer bytes de una cadena convertida en un flujo de entrada de bytes.

import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;

public class LectorDatosFormateados {

   public static void main(String[] args) throws IOException {
       String textoFuente = LectorArchivoBuffer.leerContenido("src/LectorDatosFormateados.java");
       
       try (DataInputStream datosEntrada = new DataInputStream(
               new ByteArrayInputStream(textoFuente.getBytes()))) {
           while (true) {
               // Leer byte a byte
               byte b = datosEntrada.readByte();
               System.out.print((char) b);
           }
       } catch (EOFException e) {
           System.err.println("Fin del flujo de datos.");
       }
   }
}

Verificación de Disponibilidad de Datos

El método available() de InputStream permite estimar el número de bytes que se pueden leer sin bloquear la ejecución. Sin embargo, su comportamiento puede variar significativamente entre diferentes tipos de flujos, por lo que debe usarse con precaución y entenderse que no siempre representa el tamaño total restante del flujo, especialmente en redes.

import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.IOException;

public class VerificadorDisponibilidad {

   public static void main(String[] args) throws IOException {
       String archivoFuente = "src/VerificadorDisponibilidad.java"; // Archivo para leer
       try (DataInputStream entradaDatos = new DataInputStream(
               new BufferedInputStream(
                       new FileInputStream(archivoFuente)))) {
           
           // available() retorna una estimación. Para archivos, suele ser el tamaño restante.
           // Para otros flujos, puede ser solo lo que está en el búfer.
           while (entradaDatos.available() > 0) {
               System.out.print((char) entradaDatos.readByte());
           }
       }
       System.out.println("\nLectura completada.");
   }
}

Escritura Básica en Archivos de Texto

Para escribir datos en un archivo de texto, FileWriter es la clase fundamental. No obstante, para un rendimiento óptimo y la posibilidad de escribir datos formateados (como saltos de línea o tipos primitivos), se suele envolver en un BufferedWriter y luego en un PrintWriter.

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Path;
import java.nio.file.Paths;

public class EscritorArchivoTexto {

   private static final String NOMBRE_ARCHIVO_SALIDA = "salida_proceso.txt";

   public static void main(String[] args) throws IOException {
       String archivoEntrada = "src/EscritorArchivoTexto.java"; // Archivo a leer
       Path rutaSalida = Paths.get(NOMBRE_ARCHIVO_SALIDA);

       // Opción 1: Envoltura completa para control y formato
       try (BufferedReader lectorEntrada = new BufferedReader(new FileReader(archivoEntrada));
            PrintWriter escritorSalida = new PrintWriter(
                    new BufferedWriter(new FileWriter(rutaSalida.toFile())))) {

           String linea;
           int numeroLinea = 1;
           while ((linea = lectorEntrada.readLine()) != null) {
               escritorSalida.println(numeroLinea++ + ": " + linea);
           }
       }
       System.out.println("Contenido procesado y escrito en: " + NOMBRE_ARCHIVO_SALIDA);

       // Opción 2: Atajo para crear PrintWriter que ya incluye buffering
       // Se puede simplificar la creación de PrintWriter directamente con el nombre del archivo.
       try (PrintWriter escritorSimple = new PrintWriter("salida_simple.txt")) {
           escritorSimple.println("Esta es una línea escrita con el atajo de PrintWriter.");
           escritorSimple.printf("El valor de PI es aproximadamente %.2f%n", Math.PI);
       }
       System.out.println("Contenido simple escrito en: salida_simple.txt");
   }
}

Es crucial llamar a close() en los flujos de salida para asegurar que todos los datos en búferes sean volcados y persistidos. El uso de try-with-resources gestiona esto automáticamente.

Almacenamiento y Recuperación de Datos Primitivos

Para persistir y luego recuperar datos primitivos (int, double, boolean, String en formato UTF) en un formato binario legible por Java, se utilizan DataOutputStream y DataInputStream. Estos flujos garantizan la correcta representación y ordenación de los bytes.

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class PersistenciaDatosBinarios {

   private static final String ARCHIVO_BINARIO = "datos_binarios.dat";

   public static void main(String[] args) throws IOException {
       // Escribir datos
       try (DataOutputStream salidaDatos = new DataOutputStream(
               new BufferedOutputStream(new FileOutputStream(ARCHIVO_BINARIO)))) {
           salidaDatos.writeDouble(Math.PI);
           salidaDatos.writeUTF("Valor de Pi"); // writeUTF para cadenas recuperables
           salidaDatos.writeInt(12345);
           salidaDatos.writeUTF("Un número entero");
           salidaDatos.writeBoolean(true);
       }
       System.out.println("Datos escritos en: " + ARCHIVO_BINARIO);

       // Recuperar datos
       try (DataInputStream entradaDatos = new DataInputStream(
               new BufferedInputStream(new FileInputStream(ARCHIVO_BINARIO)))) {
           System.out.println("Recuperando datos:");
           System.out.println("Doble: " + entradaDatos.readDouble());
           System.out.println("Cadena: " + entradaDatos.readUTF());
           System.out.println("Entero: " + entradaDatos.readInt());
           System.out.println("Cadena: " + entradaDatos.readUTF());
           System.out.println("Booleano: " + entradaDatos.readBoolean());
       }
   }
}

Es fundamental leer los datos en el mismo orden y con el mismo tipo con el que fueron escritos. writeUTF() y readUTF() son cruciales para el manejo correcto de cadenas en este contexto.

Archivos de Acceso Aleatorio

La clase RandomAccessFile permite leer y escribir datos en cualquier posición dentro de un archivo. Implementa las interfaces DataInput y DataOutput, lo que significa que puede manejar datos primitivos de la misma manera que DataInputStream y DataOutputStream. Además, su método seek() permite navegar por el archivo.

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.file.Path;
import java.nio.file.Paths;

public class GestorAccesoAleatorio {

   private static final String ARCHIVO_ALEATORIO = "acceso_aleatorio.dat";

   private static void mostrarContenido() throws IOException {
       Path ruta = Paths.get(ARCHIVO_ALEATORIO);
       try (RandomAccessFile raf = new RandomAccessFile(ruta.toFile(), "r")) { // Modo "r" para solo lectura
           System.out.println("\nContenido del archivo:");
           for (int i = 0; i < 5; i++) {
               System.out.printf("Valor %d: %.3f%n", i, raf.readDouble());
           }
           System.out.println("Mensaje final: " + raf.readUTF());
       }
   }

   public static void main(String[] args) throws IOException {
       Path ruta = Paths.get(ARCHIVO_ALEATORIO);
       try (RandomAccessFile raf = new RandomAccessFile(ruta.toFile(), "rw")) { // Modo "rw" para lectura/escritura
           // Escribir algunos valores dobles
           for (int i = 0; i < 5; i++) {
               raf.writeDouble(i * 0.75);
           }
           raf.writeUTF("Fin de los datos numéricos.");
       }
       mostrarContenido();

       // Modificar un valor específico
       try (RandomAccessFile raf = new RandomAccessFile(ruta.toFile(), "rw")) {
           // Cada double ocupa 8 bytes. Para el tercer double (índice 2), la posición es 2 * 8.
           raf.seek(2 * 8); // Mover el puntero al inicio del tercer double
           raf.writeDouble(99.999); // Sobreescribir el valor
       }
       System.out.println("\nDespués de modificar el tercer valor:");
       mostrarContenido();
   }
}

La capacidad de seek() para posicionar el puntero en cualquier byte del archivo hace que RandomAccessFile sea invaluable para bases de datos simples o manipulación de archivos estructurados.

Clases de Utilidad para Archivos de Texto y Binairos

Aunque las clases java.io son potentes, a menudo se requiere código repetitivo para tareas comunes como leer un archivo completo en una cadena o en una lista de líneas. Crear clases de utilidad simplifica estas operaciones.

Utilidad para Archivos de Texto

La siguiente clase GestionadorArchivoTexto proporciona métodos estáticos para leer y escribir archivos como una sola cadena o como una lista de líneas, lo que facilita su manipulación.

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.TreeSet;
import java.util.stream.Collectors;

public class GestionadorArchivoTexto extends ArrayList<String> {

   // Lee un archivo completo como una sola cadena
   public static String leerTodo(String nombreArchivo) {
       try {
           return Files.readString(Paths.get(nombreArchivo));
       } catch (IOException e) {
           throw new RuntimeException("Error al leer el archivo: " + nombreArchivo, e);
       }
   }

   // Escribe una cadena en un archivo
   public static void escribirTodo(String nombreArchivo, String texto) {
       try {
           Files.writeString(Paths.get(nombreArchivo), texto);
       } catch (IOException e) {
           throw new RuntimeException("Error al escribir en el archivo: " + nombreArchivo, e);
       }
   }

   // Constructor que lee un archivo y lo divide por un delimitador (por defecto, salto de línea)
   public GestionadorArchivoTexto(String nombreArchivo, String delimitador) {
       super(Arrays.asList(leerTodo(nombreArchivo).split(delimitador)));
       // Elimina la primera cadena vacía si existe debido al split
       if (size() > 0 && get(0).isEmpty()) {
           remove(0);
       }
   }

   // Constructor que lee un archivo línea por línea
   public GestionadorArchivoTexto(String nombreArchivo) {
       this(nombreArchivo, System.lineSeparator());
   }
   
   // Escribe el contenido de esta lista en un archivo
   public void escribirALista(String nombreArchivo) {
       try (PrintWriter escritor = new PrintWriter(nombreArchivo)) {
           for (String item : this) {
               escritor.println(item);
           }
       } catch (IOException e) {
           throw new RuntimeException("Error al escribir la lista en el archivo: " + nombreArchivo, e);
       }
   }

   public static void main(String[] args) {
       String archivoOrigen = "src/GestionadorArchivoTexto.java";
       String archivoTemp1 = "temp_texto1.txt";
       String archivoTemp2 = "temp_texto2.txt";

       String contenido = leerTodo(archivoOrigen);
       System.out.println("Contenido leído del archivo de origen.");
       escribirTodo(archivoTemp1, contenido);
       System.out.println("Contenido escrito en " + archivoTemp1);

       GestionadorArchivoTexto lineas = new GestionadorArchivoTexto(archivoTemp1);
       System.out.println("Contenido de " + archivoTemp1 + " como lista: " + lineas.size() + " líneas.");
       lineas.add("Esta es una línea adicional.");
       lineas.escribirALista(archivoTemp2);
       System.out.println("Contenido modificado escrito en " + archivoTemp2);

       // Ejemplo avanzado: extraer palabras únicas y ordenadas
       // \W+ divide por uno o más caracteres no-palabra (espacios, puntuación, etc.)
       TreeSet<String> palabrasUnicas = new TreeSet<>(
           new GestionadorArchivoTexto(archivoOrigen, "\\W+")
               .stream()
               .filter(s -> !s.isEmpty())
               .map(String::toLowerCase)
               .collect(Collectors.toSet()) // Convertir a Set para unicidad
       );
       System.out.println("\nPrimeras palabras únicas del archivo de origen (minúsculas):");
       // Mostrar las palabras que empiezan antes de 'b' para una muestra
       System.out.println(palabrasUnicas.headSet("b")); 
   }
}

Utilidad para Archivos Binarios

De manera similar, para archivos binarios, una utilidad simplifica la lectura de su contenido en un array de bytes.

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class GestionadorArchivoBinario {

   // Lee un archivo binario y devuelve su contenido como un array de bytes
   public static byte[] leerBytes(File archivo) throws IOException {
       Path ruta = archivo.toPath();
       return Files.readAllBytes(ruta); // java.nio.file es más directo aquí
   }

   // Sobrecarga para aceptar una ruta como String
   public static byte[] leerBytes(String rutaArchivo) throws IOException {
       return leerBytes(Paths.get(rutaArchivo).toFile());
   }

   public static void main(String[] args) {
       String archivoFuente = "src/GestionadorArchivoBinario.java";
       try {
           byte[] datos = leerBytes(archivoFuente);
           System.out.println("Archivo " + archivoFuente + " leído. Tamaño: " + datos.length + " bytes.");
           // Imprimir los primeros 50 bytes (o menos si el archivo es pequeño)
           System.out.print("Primeros bytes: ");
           for (int i = 0; i < Math.min(50, datos.length); i++) {
               System.out.printf("%02X ", datos[i]);
           }
           System.out.println();
       } catch (IOException e) {
           System.err.println("Error al leer el archivo binario: " + e.getMessage());
       }
   }
}

El uso de Files.readAllBytes() de java.nio.file es a menudo la forma más simple y eficiente de leer un archivo binario completo en memoria.

Etiquetas: Java I/O Streams archivos BufferedReader PrintWriter

Publicado el 7-5 06:28