Java

Tutorial Java: Leer y escribir archivos

Aprende a leer y escribir archivos en Java 11+ usando Files y BufferedReader/Writer con gestión adecuada de codificación UTF-8.

Aprende Java y certifícate

Files.readAllLines() y Files.readString() (Java 11+)

La lectura de archivos es una operación fundamental en cualquier aplicación que necesite procesar datos externos. Java 11 introdujo métodos simplificados para leer archivos de texto, eliminando gran parte del código repetitivo que era necesario en versiones anteriores.

Los métodos Files.readAllLines() y Files.readString() forman parte del paquete java.nio.file, que proporciona una API moderna para operaciones de entrada/salida. Estos métodos están diseñados para leer archivos de texto completos de manera eficiente y con una sintaxis concisa.

Files.readAllLines()

El método readAllLines() lee todas las líneas de un archivo y las devuelve como una lista de cadenas. Cada elemento de la lista representa una línea individual del archivo.

La sintaxis básica es:

List<String> lines = Files.readAllLines(Path path, Charset charset);

Donde:

  • path es un objeto Path que representa la ubicación del archivo
  • charset es el conjunto de caracteres utilizado para decodificar el archivo (opcional)

Veamos un ejemplo práctico:

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.charset.StandardCharsets;
import java.io.IOException;
import java.util.List;

public class LeerLineas {
    public static void main(String[] args) {
        // Definir la ruta del archivo
        Path ruta = Paths.get("datos.txt");
        
        try {
            // Leer todas las líneas del archivo
            List<String> lineas = Files.readAllLines(ruta, StandardCharsets.UTF_8);
            
            // Procesar cada línea
            for (int i = 0; i < lineas.size(); i++) {
                System.out.println("Línea " + (i + 1) + ": " + lineas.get(i));
            }
            
            // También podemos usar expresiones lambda
            lineas.forEach(linea -> System.out.println("Contenido: " + linea));
            
        } catch (IOException e) {
            System.err.println("Error al leer el archivo: " + e.getMessage());
        }
    }
}

Este método es ideal para archivos de tamaño moderado donde necesitamos procesar el contenido línea por línea. Sin embargo, hay que tener en cuenta que carga todo el archivo en memoria, por lo que no es recomendable para archivos muy grandes.

Files.readString()

Introducido en Java 11, readString() es aún más conciso que readAllLines(). Este método lee todo el contenido de un archivo y lo devuelve como una única cadena de texto.

La sintaxis básica es:

String contenido = Files.readString(Path path, Charset charset);

Veamos un ejemplo:

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.charset.StandardCharsets;
import java.io.IOException;

public class LeerContenidoCompleto {
    public static void main(String[] args) {
        Path ruta = Paths.get("mensaje.txt");
        
        try {
            // Leer todo el contenido como una sola cadena
            String contenido = Files.readString(ruta, StandardCharsets.UTF_8);
            
            // Mostrar el contenido
            System.out.println("Contenido del archivo:");
            System.out.println(contenido);
            
            // Podemos procesar el contenido como cualquier String
            if (contenido.contains("importante")) {
                System.out.println("El archivo contiene información importante");
            }
            
            // Contar palabras
            String[] palabras = contenido.split("\\s+");
            System.out.println("El archivo contiene " + palabras.length + " palabras");
            
        } catch (IOException e) {
            System.err.println("Error al leer el archivo: " + e.getMessage());
        }
    }
}

Este método es perfecto para situaciones donde necesitamos el contenido completo del archivo como una sola cadena, como al leer archivos de configuración, plantillas o cualquier texto que deba procesarse en su totalidad.

Manejo de excepciones

Ambos métodos lanzan una excepción IOException si ocurre algún problema durante la lectura, por lo que es necesario manejarla adecuadamente. Algunas situaciones comunes que pueden causar excepciones son:

  • El archivo no existe
  • No se tienen permisos de lectura
  • El archivo está siendo utilizado por otro proceso
  • Problemas con la codificación de caracteres

Un patrón común para manejar estas situaciones es utilizar un bloque try-catch:

try {
    String contenido = Files.readString(ruta);
    // Procesar el contenido
} catch (NoSuchFileException e) {
    System.err.println("El archivo no existe: " + e.getMessage());
} catch (AccessDeniedException e) {
    System.err.println("No tienes permisos para leer el archivo: " + e.getMessage());
} catch (IOException e) {
    System.err.println("Error al leer el archivo: " + e.getMessage());
}

Uso con try-with-resources

Aunque readAllLines() y readString() gestionan automáticamente el cierre de recursos, en operaciones más complejas podemos combinarlos con el patrón try-with-resources:

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

public class ProcesarArchivo {
    public static void main(String[] args) {
        Path rutaEntrada = Paths.get("entrada.txt");
        Path rutaSalida = Paths.get("salida.txt");
        
        try {
            // Leer el contenido
            String contenido = Files.readString(rutaEntrada);
            
            // Procesar el contenido (por ejemplo, convertir a mayúsculas)
            String resultado = contenido.toUpperCase();
            
            // Escribir el resultado en otro archivo
            try (BufferedWriter writer = Files.newBufferedWriter(rutaSalida)) {
                writer.write(resultado);
            }
            
            System.out.println("Procesamiento completado con éxito");
            
        } catch (IOException e) {
            System.err.println("Error durante el procesamiento: " + e.getMessage());
        }
    }
}

Rendimiento y consideraciones de memoria

Es importante considerar el tamaño del archivo al elegir entre estos métodos:

  • readAllLines() carga cada línea como un objeto String separado en una lista, lo que puede ser más eficiente en memoria para archivos grandes si solo necesitas procesar algunas líneas.
  • readString() carga todo el contenido en una única cadena, lo que es más eficiente para archivos pequeños o medianos que necesitas procesar en su totalidad.

Para archivos extremadamente grandes (cientos de MB o GB), ninguno de estos métodos es adecuado, y deberías considerar alternativas como BufferedReader que permiten procesar el archivo por partes.

Ejemplo práctico: Análisis de un archivo de registro

Veamos un ejemplo más completo donde analizamos un archivo de registro para extraer información relevante:

import java.nio.file.*;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.io.IOException;

public class AnalizadorLogs {
    public static void main(String[] args) {
        Path rutaLog = Paths.get("servidor.log");
        
        try {
            // Leer todas las líneas del archivo de log
            List<String> lineasLog = Files.readAllLines(rutaLog);
            
            // Contar errores por fecha
            Map<String, Integer> erroresPorFecha = new HashMap<>();
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
            String fechaHoy = LocalDate.now().format(formatter);
            
            for (String linea : lineasLog) {
                if (linea.contains("ERROR")) {
                    // Extraer la fecha (asumimos formato: 2023-10-15 14:23:45 ERROR ...)
                    String fecha = linea.substring(0, 10);
                    
                    // Incrementar contador para esta fecha
                    erroresPorFecha.put(fecha, 
                            erroresPorFecha.getOrDefault(fecha, 0) + 1);
                }
            }
            
            // Mostrar resultados
            System.out.println("Análisis de errores en el log:");
            erroresPorFecha.forEach((fecha, cantidad) -> {
                System.out.printf("Fecha: %s - Errores: %d %s%n", 
                        fecha, cantidad, 
                        fecha.equals(fechaHoy) ? "(HOY)" : "");
            });
            
            // Calcular total de errores
            int totalErrores = erroresPorFecha.values().stream()
                    .mapToInt(Integer::intValue)
                    .sum();
            
            System.out.println("Total de errores encontrados: " + totalErrores);
            
        } catch (IOException e) {
            System.err.println("Error al analizar el archivo de log: " + e.getMessage());
        }
    }
}

Este ejemplo muestra cómo readAllLines() puede ser útil para analizar archivos de registro, donde necesitamos procesar cada línea individualmente para extraer información específica.

Files.writeString() y Files.write() (Java 11+)

Así como Java 11 introdujo métodos simplificados para la lectura de archivos, también incorporó métodos concisos para la escritura de datos en archivos. Los métodos Files.writeString() y Files.write() permiten escribir contenido en archivos de texto de manera eficiente y con una sintaxis mucho más limpia que las alternativas tradicionales.

Estos métodos forman parte del paquete java.nio.file y están diseñados para complementar las operaciones de lectura que vimos anteriormente, proporcionando una API coherente para el manejo completo de archivos de texto.

Files.writeString()

El método writeString() permite escribir una cadena de texto completa en un archivo en una sola operación. Este método es ideal cuando tenemos todo el contenido preparado como un único String.

La sintaxis básica es:

Path path = Files.writeString(Path path, CharSequence content, Charset charset, OpenOption... options);

Donde:

  • path es la ruta del archivo donde escribir
  • content es el contenido a escribir (puede ser un String u otro CharSequence)
  • charset es la codificación de caracteres (opcional)
  • options son opciones adicionales para controlar cómo se realiza la escritura

Veamos un ejemplo práctico:

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.charset.StandardCharsets;
import java.nio.file.StandardOpenOption;
import java.io.IOException;

public class EscribirArchivo {
    public static void main(String[] args) {
        // Definir la ruta del archivo
        Path ruta = Paths.get("salida.txt");
        
        // Contenido a escribir
        String contenido = "Este es un ejemplo de texto\n" +
                           "que será escrito en un archivo\n" +
                           "usando Files.writeString().\n";
        
        try {
            // Escribir el contenido en el archivo
            Files.writeString(ruta, contenido, StandardCharsets.UTF_8);
            System.out.println("Archivo escrito correctamente.");
            
            // Añadir más contenido al final del archivo
            String masContenido = "\nEsta línea se añade al final del archivo existente.";
            Files.writeString(ruta, masContenido, StandardCharsets.UTF_8, 
                             StandardOpenOption.APPEND);
            
            System.out.println("Contenido adicional añadido al archivo.");
            
        } catch (IOException e) {
            System.err.println("Error al escribir en el archivo: " + e.getMessage());
        }
    }
}

En este ejemplo, primero escribimos un contenido inicial en el archivo, creándolo si no existe o sobrescribiéndolo si ya existe. Luego, añadimos más contenido al final del archivo existente utilizando la opción StandardOpenOption.APPEND.

Files.write()

El método write() es más versátil que writeString() ya que puede escribir diferentes tipos de datos:

  1. Colecciones de líneas (como List)
  2. Arrays de bytes
  3. Iterables de cualquier tipo que pueda convertirse a String

La sintaxis básica para escribir líneas de texto es:

Path path = Files.write(Path path, Iterable<? extends CharSequence> lines, Charset charset, OpenOption... options);

Veamos algunos ejemplos:

Escribir una lista de líneas

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.charset.StandardCharsets;
import java.io.IOException;
import java.util.List;
import java.util.Arrays;

public class EscribirLineas {
    public static void main(String[] args) {
        Path ruta = Paths.get("listado.txt");
        
        // Lista de líneas a escribir
        List<String> lineas = Arrays.asList(
            "Primera línea del archivo",
            "Segunda línea con datos importantes",
            "Tercera línea con más información",
            "Última línea del archivo"
        );
        
        try {
            // Escribir todas las líneas de una vez
            Files.write(ruta, lineas, StandardCharsets.UTF_8);
            System.out.println("Archivo con líneas escrito correctamente.");
            
        } catch (IOException e) {
            System.err.println("Error al escribir las líneas: " + e.getMessage());
        }
    }
}

Este método es especialmente útil cuando tenemos los datos ya organizados en una colección de líneas, como podría ser el resultado de un procesamiento previo o datos extraídos de otra fuente.

Escribir datos binarios

También podemos usar Files.write() para escribir datos binarios (bytes):

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

public class EscribirBytes {
    public static void main(String[] args) {
        Path ruta = Paths.get("datos.bin");
        
        // Array de bytes a escribir
        byte[] datos = {65, 66, 67, 68, 69, 70}; // Equivale a "ABCDEF" en ASCII
        
        try {
            // Escribir los bytes en el archivo
            Files.write(ruta, datos);
            System.out.println("Archivo binario escrito correctamente.");
            
        } catch (IOException e) {
            System.err.println("Error al escribir los bytes: " + e.getMessage());
        }
    }
}

Opciones de escritura

Tanto writeString() como write() aceptan opciones adicionales que controlan el comportamiento de la operación de escritura. Estas opciones se especifican mediante constantes de la enumeración StandardOpenOption:

  • StandardOpenOption.CREATE: Crea un nuevo archivo si no existe (comportamiento predeterminado)
  • StandardOpenOption.CREATE_NEW: Crea un nuevo archivo, fallando si ya existe
  • StandardOpenOption.APPEND: Añade contenido al final del archivo en lugar de sobrescribirlo
  • StandardOpenOption.TRUNCATE_EXISTING: Elimina el contenido existente antes de escribir (comportamiento predeterminado)
  • StandardOpenOption.WRITE: Abre para escritura (implícito)
  • StandardOpenOption.SYNC: Sincroniza el contenido con el almacenamiento subyacente

Veamos un ejemplo que utiliza varias opciones:

import java.nio.file.*;
import java.nio.charset.StandardCharsets;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.List;

public class OpcionesEscritura {
    public static void main(String[] args) {
        Path logFile = Paths.get("aplicacion.log");
        
        try {
            // Crear el archivo solo si no existe
            if (Files.notExists(logFile)) {
                Files.createFile(logFile);
                System.out.println("Archivo de log creado.");
            }
            
            // Preparar entrada de log
            String timestamp = LocalDateTime.now().toString();
            String entradaLog = String.format("[%s] Aplicación iniciada\n", timestamp);
            
            // Añadir al final del archivo con sincronización
            Files.writeString(logFile, entradaLog, StandardCharsets.UTF_8,
                             StandardOpenOption.APPEND,
                             StandardOpenOption.SYNC);
            
            System.out.println("Entrada añadida al log.");
            
            // Simular algunas operaciones
            System.out.println("Realizando operaciones...");
            Thread.sleep(2000);
            
            // Añadir otra entrada
            timestamp = LocalDateTime.now().toString();
            entradaLog = String.format("[%s] Operaciones completadas\n", timestamp);
            
            Files.writeString(logFile, entradaLog, StandardCharsets.UTF_8,
                             StandardOpenOption.APPEND,
                             StandardOpenOption.SYNC);
            
            System.out.println("Segunda entrada añadida al log.");
            
        } catch (IOException e) {
            System.err.println("Error de E/S: " + e.getMessage());
        } catch (InterruptedException e) {
            System.err.println("Operación interrumpida: " + e.getMessage());
        }
    }
}

Este ejemplo muestra cómo crear un archivo de registro (log) y añadir entradas secuencialmente, asegurando que cada escritura se sincronice con el almacenamiento físico.

Manejo de excepciones y buenas prácticas

Al escribir en archivos, es importante manejar adecuadamente las posibles excepciones que pueden ocurrir:

try {
    Files.writeString(ruta, datos);
} catch (NoSuchFileException e) {
    System.err.println("No se puede encontrar la ruta especificada: " + e.getMessage());
} catch (DirectoryNotEmptyException e) {
    System.err.println("La ruta especifica un directorio no vacío: " + e.getMessage());
} catch (AccessDeniedException e) {
    System.err.println("No tienes permisos para escribir en esta ubicación: " + e.getMessage());
} catch (IOException e) {
    System.err.println("Error de E/S al escribir el archivo: " + e.getMessage());
}

Algunas buenas prácticas al escribir archivos:

  • Verifica que tienes permisos de escritura antes de intentar escribir
  • Considera hacer una copia de seguridad de archivos importantes antes de sobrescribirlos
  • Para archivos críticos, escribe primero en un archivo temporal y luego renómbralo
  • Utiliza StandardOpenOption.SYNC para datos importantes que no deben perderse

Ejemplo práctico: Generador de informes

Veamos un ejemplo más completo que genera un informe en formato CSV a partir de datos procesados:

import java.nio.file.*;
import java.nio.charset.StandardCharsets;
import java.io.IOException;
import java.util.*;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class GeneradorInformes {
    
    static class Venta {
        String producto;
        double importe;
        LocalDate fecha;
        
        Venta(String producto, double importe, LocalDate fecha) {
            this.producto = producto;
            this.importe = importe;
            this.fecha = fecha;
        }
    }
    
    public static void main(String[] args) {
        // Simular algunos datos de ventas
        List<Venta> ventas = Arrays.asList(
            new Venta("Laptop", 899.99, LocalDate.of(2023, 10, 15)),
            new Venta("Monitor", 249.50, LocalDate.of(2023, 10, 15)),
            new Venta("Teclado", 45.99, LocalDate.of(2023, 10, 16)),
            new Venta("Mouse", 22.50, LocalDate.of(2023, 10, 16)),
            new Venta("Laptop", 1299.99, LocalDate.of(2023, 10, 17)),
            new Venta("Impresora", 189.99, LocalDate.of(2023, 10, 17))
        );
        
        // Generar informe CSV
        Path rutaInforme = Paths.get("informe_ventas.csv");
        DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd/MM/yyyy");
        
        try {
            // Preparar encabezado y líneas del informe
            List<String> lineasInforme = new ArrayList<>();
            lineasInforme.add("Producto,Importe,Fecha");
            
            for (Venta venta : ventas) {
                String linea = String.format("%s,%.2f,%s", 
                                            venta.producto,
                                            venta.importe,
                                            venta.fecha.format(fmt));
                lineasInforme.add(linea);
            }
            
            // Escribir el informe
            Files.write(rutaInforme, lineasInforme, StandardCharsets.UTF_8);
            System.out.println("Informe CSV generado en: " + rutaInforme.toAbsolutePath());
            
            // Generar también un resumen
            Path rutaResumen = Paths.get("resumen_ventas.txt");
            
            // Calcular estadísticas
            double totalVentas = ventas.stream()
                                      .mapToDouble(v -> v.importe)
                                      .sum();
            
            double promedioVenta = totalVentas / ventas.size();
            
            String resumen = String.format(
                "RESUMEN DE VENTAS\n" +
                "=================\n" +
                "Período: %s - %s\n" +
                "Total de ventas: %.2f €\n" +
                "Número de transacciones: %d\n" +
                "Importe promedio: %.2f €\n",
                ventas.get(0).fecha.format(fmt),
                ventas.get(ventas.size() - 1).fecha.format(fmt),
                totalVentas,
                ventas.size(),
                promedioVenta
            );
            
            Files.writeString(rutaResumen, resumen, StandardCharsets.UTF_8);
            System.out.println("Resumen de ventas generado en: " + rutaResumen.toAbsolutePath());
            
        } catch (IOException e) {
            System.err.println("Error al generar los informes: " + e.getMessage());
        }
    }
}

Este ejemplo muestra cómo podemos utilizar Files.write() para generar un archivo CSV con múltiples líneas y Files.writeString() para crear un informe de texto formateado, todo a partir de los mismos datos procesados.

Rendimiento y consideraciones

Los métodos writeString() y write() están optimizados para la mayoría de los casos de uso comunes, pero hay algunas consideraciones importantes:

  • Para archivos pequeños o medianos, estos métodos son ideales por su simplicidad y rendimiento
  • Para escrituras frecuentes en el mismo archivo, considera usar un BufferedWriter
  • Para archivos muy grandes o cuando necesitas control preciso sobre el buffer, las clases tradicionales como FileOutputStream pueden ser más apropiadas
  • Si necesitas añadir contenido frecuentemente a un archivo de log, considera abrir un BufferedWriter y mantenerlo abierto durante la ejecución del programa

Trabajar con BufferedReader/BufferedWriter cuando sea necesario

Aunque Java 11+ ofrece métodos simplificados como Files.readString() y Files.writeString(), existen situaciones donde necesitamos un control más granular sobre la lectura y escritura de archivos. Las clases BufferedReader y BufferedWriter son herramientas fundamentales para estos escenarios, proporcionando eficiencia y flexibilidad adicionales.

Estas clases utilizan un buffer interno para minimizar las operaciones de entrada/salida, lo que mejora significativamente el rendimiento cuando trabajamos con archivos grandes o cuando necesitamos procesar datos línea por línea.

¿Cuándo usar BufferedReader?

BufferedReader es especialmente útil en los siguientes escenarios:

  • Cuando necesitas procesar archivos muy grandes que no caben en memoria
  • Cuando requieres leer el archivo línea por línea de forma eficiente
  • Para implementar procesamiento en streaming sin cargar todo el contenido
  • Cuando necesitas control preciso sobre el proceso de lectura

La sintaxis básica para crear un BufferedReader es:

try (BufferedReader reader = new BufferedReader(new FileReader("archivo.txt"))) {
    // Operaciones de lectura
}

Sin embargo, es recomendable utilizar la versión que permite especificar la codificación de caracteres:

try (BufferedReader reader = new BufferedReader(
        new InputStreamReader(new FileInputStream("archivo.txt"), StandardCharsets.UTF_8))) {
    // Operaciones de lectura con codificación específica
}

O mejor aún, utilizando los métodos de la clase Files:

try (BufferedReader reader = Files.newBufferedReader(Paths.get("archivo.txt"), StandardCharsets.UTF_8)) {
    // Operaciones de lectura
}

Lectura eficiente línea por línea

Una de las principales ventajas de BufferedReader es su método readLine(), que permite leer el archivo línea por línea:

import java.io.BufferedReader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.charset.StandardCharsets;
import java.io.IOException;

public class ProcesadorLineas {
    public static void main(String[] args) {
        try (BufferedReader reader = Files.newBufferedReader(
                Paths.get("datos_grandes.csv"), StandardCharsets.UTF_8)) {
            
            String linea;
            long contador = 0;
            
            // Leer y procesar cada línea individualmente
            while ((linea = reader.readLine()) != null) {
                // Procesar solo si la línea contiene datos relevantes
                if (!linea.isEmpty() && !linea.startsWith("#")) {
                    procesarLinea(linea);
                    contador++;
                    
                    // Mostrar progreso cada 10,000 líneas
                    if (contador % 10_000 == 0) {
                        System.out.printf("Procesadas %,d líneas%n", contador);
                    }
                }
            }
            
            System.out.printf("Procesamiento completado. Total: %,d líneas%n", contador);
            
        } catch (IOException e) {
            System.err.println("Error durante la lectura: " + e.getMessage());
        }
    }
    
    private static void procesarLinea(String linea) {
        // Ejemplo: dividir la línea por comas y procesar los campos
        String[] campos = linea.split(",");
        // Aquí iría la lógica específica de procesamiento
    }
}

Este enfoque es mucho más eficiente para archivos grandes que cargar todo el contenido en memoria con Files.readAllLines(), ya que solo mantiene una línea en memoria a la vez.

Procesamiento de archivos con millones de líneas

Para archivos extremadamente grandes, BufferedReader es la opción ideal:

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;

public class AnalizadorLogGigante {
    public static void main(String[] args) {
        Path rutaLog = Paths.get("servidor_gigante.log");
        Map<String, Integer> erroresPorTipo = new HashMap<>();
        
        try (BufferedReader reader = Files.newBufferedReader(rutaLog, StandardCharsets.UTF_8)) {
            String linea;
            long lineasProcesadas = 0;
            long inicio = System.currentTimeMillis();
            
            while ((linea = reader.readLine()) != null) {
                lineasProcesadas++;
                
                // Buscar líneas de error y clasificarlas
                if (linea.contains("ERROR")) {
                    // Extraer el tipo de error (suponemos formato: "ERROR: [TipoError] Mensaje")
                    int inicio1 = linea.indexOf("[");
                    int fin = linea.indexOf("]", inicio1);
                    
                    if (inicio1 != -1 && fin != -1) {
                        String tipoError = linea.substring(inicio1 + 1, fin);
                        erroresPorTipo.put(tipoError, erroresPorTipo.getOrDefault(tipoError, 0) + 1);
                    }
                }
                
                // Mostrar progreso periódicamente
                if (lineasProcesadas % 100_000 == 0) {
                    long tiempoActual = System.currentTimeMillis();
                    double segundos = (tiempoActual - inicio) / 1000.0;
                    System.out.printf("Procesadas %,d líneas (%.2f líneas/seg)%n", 
                            lineasProcesadas, lineasProcesadas / segundos);
                }
            }
            
            // Mostrar resultados
            System.out.println("\nAnálisis completado:");
            System.out.printf("Total líneas procesadas: %,d%n", lineasProcesadas);
            System.out.println("Distribución de errores:");
            
            erroresPorTipo.entrySet().stream()
                    .sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
                    .forEach(entry -> System.out.printf("  %-20s: %,d%n", entry.getKey(), entry.getValue()));
            
        } catch (IOException e) {
            System.err.println("Error al procesar el archivo: " + e.getMessage());
        }
    }
}

Este ejemplo muestra cómo procesar un archivo de registro extremadamente grande, manteniendo estadísticas mientras se lee secuencialmente, sin necesidad de cargar todo el archivo en memoria.

¿Cuándo usar BufferedWriter?

BufferedWriter es la contraparte para escritura y resulta especialmente útil en estos casos:

  • Para escribir grandes volúmenes de datos de manera eficiente
  • Cuando necesitas control preciso sobre el momento de escritura en disco
  • Para escrituras frecuentes donde el buffering mejora el rendimiento
  • Cuando requieres funcionalidades específicas como newLine()

La forma recomendada de crear un BufferedWriter es:

try (BufferedWriter writer = Files.newBufferedWriter(
        Paths.get("salida.txt"), StandardCharsets.UTF_8)) {
    // Operaciones de escritura
}

Escritura eficiente línea por línea

BufferedWriter permite escribir contenido de forma eficiente, especialmente cuando se trata de múltiples líneas:

import java.io.BufferedWriter;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.charset.StandardCharsets;
import java.io.IOException;
import java.util.Random;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class GeneradorDatos {
    public static void main(String[] args) {
        // Generar un millón de registros de datos simulados
        int totalRegistros = 1_000_000;
        Random random = new Random();
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        
        try (BufferedWriter writer = Files.newBufferedWriter(
                Paths.get("datos_generados.csv"), StandardCharsets.UTF_8)) {
            
            // Escribir encabezado
            writer.write("id,timestamp,temperatura,humedad,presion");
            writer.newLine();
            
            // Generar y escribir datos
            for (int i = 1; i <= totalRegistros; i++) {
                // Simular datos de sensores
                LocalDateTime timestamp = LocalDateTime.now().minusSeconds(random.nextInt(86400));
                double temperatura = 15.0 + random.nextDouble() * 20.0;
                int humedad = 30 + random.nextInt(70);
                double presion = 1000.0 + random.nextDouble() * 50.0;
                
                // Formatear y escribir la línea
                String linea = String.format("%d,%s,%.2f,%d,%.2f",
                        i, timestamp.format(formatter), temperatura, humedad, presion);
                
                writer.write(linea);
                writer.newLine();  // Método específico para añadir salto de línea
                
                // Mostrar progreso
                if (i % 100_000 == 0) {
                    System.out.printf("Generados %,d registros (%.1f%%)%n", 
                            i, (i * 100.0 / totalRegistros));
                }
            }
            
            System.out.println("Generación de datos completada.");
            
        } catch (IOException e) {
            System.err.println("Error al generar los datos: " + e.getMessage());
        }
    }
}

Este ejemplo muestra cómo generar un archivo CSV con un millón de registros de forma eficiente. El método newLine() es especialmente útil porque inserta el separador de línea específico de la plataforma.

Combinando BufferedReader y BufferedWriter

Un caso de uso común es procesar un archivo y generar otro, como en este ejemplo de filtrado y transformación:

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;

public class TransformadorArchivos {
    public static void main(String[] args) {
        Path archivoEntrada = Paths.get("datos_crudos.txt");
        Path archivoSalida = Paths.get("datos_procesados.txt");
        
        try (BufferedReader reader = Files.newBufferedReader(archivoEntrada, StandardCharsets.UTF_8);
             BufferedWriter writer = Files.newBufferedWriter(archivoSalida, StandardCharsets.UTF_8)) {
            
            String linea;
            int lineasLeidas = 0;
            int lineasEscritas = 0;
            
            // Procesar el archivo línea por línea
            while ((linea = reader.readLine()) != null) {
                lineasLeidas++;
                
                // Aplicar transformaciones y filtrado
                if (linea.trim().isEmpty() || linea.startsWith("#")) {
                    continue; // Saltar líneas vacías y comentarios
                }
                
                // Transformar la línea (ejemplo: convertir a mayúsculas y añadir timestamp)
                String lineaProcesada = String.format("[%d] %s", 
                        System.currentTimeMillis(), linea.toUpperCase());
                
                // Escribir la línea procesada
                writer.write(lineaProcesada);
                writer.newLine();
                lineasEscritas++;
            }
            
            System.out.printf("Procesamiento completado: %d líneas leídas, %d líneas escritas%n", 
                    lineasLeidas, lineasEscritas);
            
        } catch (IOException e) {
            System.err.println("Error durante el procesamiento: " + e.getMessage());
        }
    }
}

Control del buffer y flush

Una característica importante de BufferedWriter es el control sobre cuándo se vacía el buffer y se escribe físicamente en el disco:

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.time.LocalDateTime;

public class RegistroActividad {
    public static void main(String[] args) {
        Path archivoLog = Paths.get("actividad.log");
        
        try (BufferedWriter logWriter = Files.newBufferedWriter(
                archivoLog, StandardCharsets.UTF_8, StandardOpenOption.CREATE, 
                StandardOpenOption.APPEND)) {
            
            // Simular actividades que generan entradas de log
            for (int i = 1; i <= 10; i++) {
                String mensaje = String.format("[%s] Actividad %d ejecutada", 
                        LocalDateTime.now(), i);
                
                logWriter.write(mensaje);
                logWriter.newLine();
                
                // Forzar la escritura física después de cada entrada
                // Esto garantiza que los datos se escriban inmediatamente
                logWriter.flush();
                
                System.out.println("Registrada: " + mensaje);
                
                // Simular tiempo entre actividades
                try {
                    Thread.sleep(1500);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
            
        } catch (IOException e) {
            System.err.println("Error al escribir en el log: " + e.getMessage());
        }
    }
}

El método flush() fuerza la escritura inmediata del contenido del buffer al archivo físico, lo que es crucial para aplicaciones como registros de actividad donde queremos asegurarnos de que los datos se escriban de inmediato, incluso en caso de fallo del programa.

Rendimiento: BufferedReader/Writer vs. métodos de Files

Para entender mejor cuándo usar cada enfoque, consideremos las diferencias de rendimiento:

  • Files.readAllLines() y Files.readString(): Excelentes para archivos pequeños o medianos (hasta decenas de MB), pero consumen más memoria.
  • BufferedReader: Superior para archivos grandes, procesamiento línea por línea y cuando la memoria es limitada.
  • Files.writeString() y Files.write(): Ideales para escrituras simples y únicas.
  • BufferedWriter: Mejor para escrituras frecuentes, archivos grandes o cuando necesitas control preciso.

Ejemplo práctico: Procesamiento de datos CSV

Veamos un ejemplo completo que muestra cómo procesar un archivo CSV grande, realizar cálculos y generar un informe:

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;

public class ProcesadorVentas {
    
    static class Venta {
        String region;
        String producto;
        double importe;
        
        Venta(String region, String producto, double importe) {
            this.region = region;
            this.producto = producto;
            this.importe = importe;
        }
    }
    
    public static void main(String[] args) {
        Path archivoVentas = Paths.get("ventas_anuales.csv");
        Path archivoInforme = Paths.get("informe_regional.txt");
        
        Map<String, Double> ventasPorRegion = new HashMap<>();
        Set<String> productos = new TreeSet<>();
        
        // Procesar el archivo de ventas
        try (BufferedReader reader = Files.newBufferedReader(archivoVentas, StandardCharsets.UTF_8)) {
            String linea;
            boolean primeraLinea = true;
            
            while ((linea = reader.readLine()) != null) {
                // Saltar la línea de encabezado
                if (primeraLinea) {
                    primeraLinea = false;
                    continue;
                }
                
                // Parsear la línea CSV
                String[] campos = linea.split(",");
                if (campos.length >= 3) {
                    String region = campos[0].trim();
                    String producto = campos[1].trim();
                    double importe = Double.parseDouble(campos[2].trim());
                    
                    // Actualizar estadísticas
                    ventasPorRegion.put(region, 
                            ventasPorRegion.getOrDefault(region, 0.0) + importe);
                    productos.add(producto);
                }
            }
            
            // Generar informe
            try (BufferedWriter writer = Files.newBufferedWriter(archivoInforme, StandardCharsets.UTF_8)) {
                writer.write("INFORME DE VENTAS POR REGIÓN");
                writer.newLine();
                writer.write("==========================");
                writer.newLine();
                writer.newLine();
                
                writer.write(String.format("Total de productos diferentes: %d", productos.size()));
                writer.newLine();
                writer.write(String.format("Total de regiones: %d", ventasPorRegion.size()));
                writer.newLine();
                writer.newLine();
                
                writer.write("VENTAS POR REGIÓN:");
                writer.newLine();
                writer.write("-----------------");
                writer.newLine();
                
                // Ordenar regiones por volumen de ventas (de mayor a menor)
                ventasPorRegion.entrySet().stream()
                        .sorted(Map.Entry.<String, Double>comparingByValue().reversed())
                        .forEach(entry -> {
                            try {
                                writer.write(String.format("%-15s: %,.2f €", 
                                        entry.getKey(), entry.getValue()));
                                writer.newLine();
                            } catch (IOException e) {
                                System.err.println("Error al escribir: " + e.getMessage());
                            }
                        });
                
                System.out.println("Informe generado correctamente en: " + archivoInforme);
            }
            
        } catch (IOException e) {
            System.err.println("Error durante el procesamiento: " + e.getMessage());
        }
    }
}

Este ejemplo demuestra cómo BufferedReader y BufferedWriter pueden trabajar juntos para procesar datos de forma eficiente, incluso con archivos grandes.

Gestión de codificación de caracteres

Un aspecto crucial al trabajar con archivos de texto es la codificación de caracteres. Tanto BufferedReader como BufferedWriter permiten especificar la codificación, lo que es esencial para manejar correctamente caracteres internacionales:

// Lectura con codificación específica
try (BufferedReader reader = Files.newBufferedReader(
        Paths.get("datos_utf8.txt"), StandardCharsets.UTF_8)) {
    // Procesar archivo UTF-8
}

// Escritura con codificación específica
try (BufferedWriter writer = Files.newBufferedReader(
        Paths.get("salida_latin1.txt"), StandardCharsets.ISO_8859_1)) {
    // Escribir con codificación Latin-1
}

Java proporciona varias constantes en StandardCharsets para las codificaciones más comunes:

  • StandardCharsets.UTF_8: La codificación recomendada para la mayoría de los casos
  • StandardCharsets.UTF_16: Para textos que requieren caracteres Unicode de 16 bits
  • StandardCharsets.ISO_8859_1: También conocida como Latin-1, para idiomas de Europa occidental
  • StandardCharsets.US_ASCII: Para texto ASCII básico (7 bits)

En general, UTF-8 es la mejor opción para la mayoría de las aplicaciones modernas, ya que soporta todos los caracteres Unicode de forma eficiente.

Gestión de codificación de caracteres (UTF-8)

La correcta gestión de la codificación de caracteres es fundamental cuando trabajamos con archivos de texto en Java. Una codificación inadecuada puede provocar problemas como la aparición de caracteres extraños (mojibake), pérdida de información o incluso errores en tiempo de ejecución.

En el mundo moderno del desarrollo de software, UTF-8 se ha convertido en el estándar de facto para la codificación de texto. Esta codificación permite representar prácticamente cualquier carácter de cualquier idioma, lo que la hace ideal para aplicaciones internacionales.

¿Por qué es importante la codificación de caracteres?

Cuando trabajamos con archivos de texto, lo que realmente almacenamos son bytes, no caracteres. La codificación determina cómo se mapean estos bytes a caracteres legibles. Diferentes codificaciones utilizan diferentes mapeos, lo que significa que el mismo conjunto de bytes puede interpretarse como caracteres completamente distintos según la codificación utilizada.

Por ejemplo, consideremos el carácter español "ñ":

  • En UTF-8, se representa con los bytes 0xC3 0xB1
  • En ISO-8859-1 (Latin-1), se representa con el byte 0xF1
  • En Windows-1252, también se representa con 0xF1

Si leemos un archivo que contiene "ñ" guardado en ISO-8859-1, pero lo interpretamos como UTF-8, veremos "ñ" en lugar de "ñ".

Especificar la codificación en Java

Java proporciona la clase StandardCharsets (del paquete java.nio.charset) que contiene constantes para las codificaciones más comunes:

import java.nio.charset.StandardCharsets;

// Codificaciones disponibles como constantes
Charset utf8 = StandardCharsets.UTF_8;
Charset utf16 = StandardCharsets.UTF_16;
Charset latin1 = StandardCharsets.ISO_8859_1;
Charset ascii = StandardCharsets.US_ASCII;

También podemos obtener una codificación por su nombre:

import java.nio.charset.Charset;

Charset windows1252 = Charset.forName("windows-1252");
Charset gbk = Charset.forName("GBK"); // Codificación china

Detectar la codificación de un archivo

Java no proporciona una forma nativa de detectar automáticamente la codificación de un archivo. Existen algunas estrategias para intentar determinarla:

  • Marcas BOM (Byte Order Mark): Algunos archivos UTF-8 o UTF-16 comienzan con bytes especiales que indican su codificación.
  • Heurísticas: Analizar patrones de bytes para inferir la codificación probable.
  • Bibliotecas externas: Como juniversalchardet, que implementa el algoritmo de detección de Mozilla.

Un ejemplo simple para detectar BOM en archivos:

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

public class DetectorBOM {
    public static void main(String[] args) throws IOException {
        Path archivo = Paths.get("ejemplo.txt");
        byte[] bytes = Files.readAllBytes(archivo);
        
        String codificacion = "desconocida";
        
        // Verificar marcas BOM
        if (bytes.length >= 3 && 
            bytes[0] == (byte)0xEF && 
            bytes[1] == (byte)0xBB && 
            bytes[2] == (byte)0xBF) {
            codificacion = "UTF-8 con BOM";
        } else if (bytes.length >= 2 && 
                  bytes[0] == (byte)0xFE && 
                  bytes[1] == (byte)0xFF) {
            codificacion = "UTF-16BE";
        } else if (bytes.length >= 2 && 
                  bytes[0] == (byte)0xFF && 
                  bytes[1] == (byte)0xFE) {
            codificacion = "UTF-16LE";
        }
        
        System.out.println("Codificación detectada: " + codificacion);
    }
}

Conversión entre codificaciones

En ocasiones necesitamos convertir texto entre diferentes codificaciones:

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

public class ConversionCodificacion {
    public static void main(String[] args) {
        // Texto original en UTF-8
        String textoOriginal = "Hola, こんにちは, привет, 你好";
        
        // Convertir a bytes usando Latin-1 (perderemos caracteres no latinos)
        byte[] bytesLatin1 = textoOriginal.getBytes(StandardCharsets.ISO_8859_1);
        
        // Convertir de vuelta a String usando Latin-1
        String textoLatin1 = new String(bytesLatin1, StandardCharsets.ISO_8859_1);
        
        // Convertir a bytes usando UTF-8 (preserva todos los caracteres)
        byte[] bytesUtf8 = textoOriginal.getBytes(StandardCharsets.UTF_8);
        
        // Convertir de vuelta a String usando UTF-8
        String textoUtf8 = new String(bytesUtf8, StandardCharsets.UTF_8);
        
        System.out.println("Original: " + textoOriginal);
        System.out.println("Después de Latin-1: " + textoLatin1);
        System.out.println("Después de UTF-8: " + textoUtf8);
        
        // Mostrar diferencia en número de bytes
        System.out.println("Longitud en Latin-1: " + bytesLatin1.length + " bytes");
        System.out.println("Longitud en UTF-8: " + bytesUtf8.length + " bytes");
    }
}

Configuración de la codificación por defecto

Java utiliza la codificación del sistema operativo como predeterminada cuando no se especifica una. Esto puede causar problemas de portabilidad, ya que diferentes sistemas pueden tener diferentes codificaciones predeterminadas.

Podemos verificar y establecer la codificación predeterminada:

import java.nio.charset.Charset;

public class CodificacionPredeterminada {
    public static void main(String[] args) {
        // Obtener la codificación predeterminada del sistema
        Charset predeterminada = Charset.defaultCharset();
        System.out.println("Codificación predeterminada: " + predeterminada);
        
        // También podemos establecerla mediante propiedades del sistema
        // (debe hacerse al inicio de la aplicación)
        System.setProperty("file.encoding", "UTF-8");
        
        // Nota: En Java 9+ esto no siempre funciona debido a cambios en la JVM
        // La mejor práctica es especificar siempre la codificación explícitamente
    }
}

Buenas prácticas para trabajar con UTF-8

Para garantizar una gestión adecuada de la codificación, especialmente con UTF-8, sigue estas recomendaciones:

  • 1. Especifica siempre la codificación explícitamente en todas las operaciones de lectura/escritura:
// Al leer archivos
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
    // Leer con UTF-8 explícito
}

// Al escribir archivos
try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
    // Escribir con UTF-8 explícito
}
  • 2. Utiliza UTF-8 para todos los archivos nuevos que crees:
// Crear un archivo de configuración en UTF-8
Path config = Paths.get("config.json");
String contenido = "{ \"nombre\": \"José Pérez\", \"país\": \"España\" }";
Files.writeString(config, contenido, StandardCharsets.UTF_8);
  • 3. Añade BOM cuando sea necesario para compatibilidad con algunas aplicaciones (especialmente de Microsoft):
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class EscribirConBOM {
    public static void main(String[] args) throws Exception {
        Path archivo = Paths.get("con_bom.txt");
        String texto = "Este archivo tiene BOM UTF-8";
        
        // Escribir BOM UTF-8 seguido del contenido
        try (OutputStream out = Files.newOutputStream(archivo, 
                StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
            // Escribir BOM UTF-8 (EF BB BF)
            out.write(new byte[] { (byte)0xEF, (byte)0xBB, (byte)0xBF });
            
            // Escribir el contenido en UTF-8
            out.write(texto.getBytes(StandardCharsets.UTF_8));
        }
        
        System.out.println("Archivo creado con BOM UTF-8");
    }
}
  • 4. Maneja correctamente los caracteres especiales en URLs y nombres de archivo:
import java.net.URLEncoder;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;

public class CodificacionURL {
    public static void main(String[] args) throws Exception {
        String parametro = "búsqueda=café&país=España";
        
        // Codificar para URL
        String codificado = URLEncoder.encode(parametro, StandardCharsets.UTF_8.toString());
        System.out.println("URL codificada: " + codificado);
        
        // Decodificar desde URL
        String decodificado = URLDecoder.decode(codificado, StandardCharsets.UTF_8.toString());
        System.out.println("URL decodificada: " + decodificado);
    }
}

Manejo de problemas comunes de codificación

Problema 1: Caracteres extraños al leer archivos

Si al leer un archivo aparecen caracteres extraños, probablemente estás usando una codificación incorrecta:

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

public class SolucionarProblemasDecodificacion {
    public static void main(String[] args) throws Exception {
        Path archivo = Paths.get("texto_con_acentos.txt");
        
        // Intentar con diferentes codificaciones
        Charset[] codificaciones = {
            StandardCharsets.UTF_8,
            StandardCharsets.ISO_8859_1,
            Charset.forName("windows-1252")
        };
        
        for (Charset charset : codificaciones) {
            try {
                List<String> lineas = Files.readAllLines(archivo, charset);
                System.out.println("Usando " + charset + ":");
                lineas.forEach(System.out::println);
                System.out.println("-------------------");
            } catch (Exception e) {
                System.out.println("Error con " + charset + ": " + e.getMessage());
            }
        }
    }
}

Problema 2: Pérdida de caracteres al escribir

Si al escribir un archivo se pierden caracteres, asegúrate de usar una codificación que soporte todos los caracteres necesarios:

import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class ComparacionEscritura {
    public static void main(String[] args) throws Exception {
        String texto = "Caracteres especiales: áéíóúñÑ€¥£©®™";
        
        // Escribir con diferentes codificaciones
        Path archivoUtf8 = Paths.get("especiales_utf8.txt");
        Path archivoLatin1 = Paths.get("especiales_latin1.txt");
        
        Files.writeString(archivoUtf8, texto, StandardCharsets.UTF_8);
        Files.writeString(archivoLatin1, texto, StandardCharsets.ISO_8859_1);
        
        // Leer y comparar resultados
        String leido1 = Files.readString(archivoUtf8, StandardCharsets.UTF_8);
        String leido2 = Files.readString(archivoLatin1, StandardCharsets.ISO_8859_1);
        
        System.out.println("Original: " + texto);
        System.out.println("Leído de UTF-8: " + leido1);
        System.out.println("Leído de Latin-1: " + leido2);
        System.out.println("¿UTF-8 preservó todo? " + texto.equals(leido1));
        System.out.println("¿Latin-1 preservó todo? " + texto.equals(leido2));
    }
}

Trabajando con archivos de diferentes codificaciones

En entornos reales, a menudo necesitamos trabajar con archivos que utilizan diferentes codificaciones. Aquí hay un ejemplo de cómo convertir un archivo de una codificación a otra:

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class ConvertidorCodificacion {
    public static void main(String[] args) {
        Path archivoOrigen = Paths.get("origen_windows1252.txt");
        Path archivoDestino = Paths.get("destino_utf8.txt");
        
        // Codificaciones de origen y destino
        Charset codificacionOrigen = Charset.forName("windows-1252");
        Charset codificacionDestino = StandardCharsets.UTF_8;
        
        try (BufferedReader reader = Files.newBufferedReader(archivoOrigen, codificacionOrigen);
             BufferedWriter writer = Files.newBufferedWriter(archivoDestino, codificacionDestino)) {
            
            String linea;
            while ((linea = reader.readLine()) != null) {
                // Aquí podríamos hacer transformaciones adicionales si fuera necesario
                writer.write(linea);
                writer.newLine();
            }
            
            System.out.println("Archivo convertido exitosamente de " + 
                    codificacionOrigen + " a " + codificacionDestino);
            
        } catch (Exception e) {
            System.err.println("Error durante la conversión: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

Consideraciones para aplicaciones internacionales

Si estás desarrollando aplicaciones que se utilizarán en diferentes países o que manejarán texto en múltiples idiomas, considera estas recomendaciones adicionales:

  • Utiliza siempre UTF-8 como codificación predeterminada para todos los archivos de texto.
  • Normaliza las cadenas Unicode cuando sea necesario comparar textos con caracteres especiales.
  • Considera las diferencias culturales en la ordenación y comparación de cadenas.
import java.text.Collator;
import java.text.Normalizer;
import java.util.Arrays;
import java.util.Locale;

public class TextoInternacional {
    public static void main(String[] args) {
        // Normalización de texto Unicode
        String s1 = "café";  // 'é' como un solo carácter
        String s2 = "cafe\u0301"; // 'e' seguido del acento combinante
        
        System.out.println("Sin normalizar:");
        System.out.println("s1: " + s1 + ", longitud: " + s1.length());
        System.out.println("s2: " + s2 + ", longitud: " + s2.length());
        System.out.println("¿Son iguales? " + s1.equals(s2));
        
        // Normalizar a la forma NFC (composición canónica)
        String n1 = Normalizer.normalize(s1, Normalizer.Form.NFC);
        String n2 = Normalizer.normalize(s2, Normalizer.Form.NFC);
        
        System.out.println("\nNormalizados (NFC):");
        System.out.println("n1: " + n1 + ", longitud: " + n1.length());
        System.out.println("n2: " + n2 + ", longitud: " + n2.length());
        System.out.println("¿Son iguales? " + n1.equals(n2));
        
        // Ordenación sensible al idioma
        String[] palabras = {"apple", "época", "zebra", "ñandú", "árbol"};
        
        // Ordenación estándar (basada en valores Unicode)
        Arrays.sort(palabras);
        System.out.println("\nOrdenación estándar:");
        Arrays.stream(palabras).forEach(System.out::println);
        
        // Ordenación específica para español
        Collator collator = Collator.getInstance(new Locale("es", "ES"));
        Arrays.sort(palabras, collator);
        System.out.println("\nOrdenación para español:");
        Arrays.stream(palabras).forEach(System.out::println);
    }
}

Resumen de codificaciones comunes

  • UTF-8: Codificación variable (1-4 bytes por carácter). Compatible con ASCII. Recomendada para casi todos los casos.
  • UTF-16: Codificación variable (2 o 4 bytes por carácter). Usada internamente por Java para String.
  • ISO-8859-1 (Latin-1): Codificación de 1 byte para idiomas de Europa occidental.
  • Windows-1252: Similar a Latin-1 pero con caracteres adicionales en posiciones que Latin-1 no usa.
  • US-ASCII: Codificación de 7 bits para caracteres básicos ingleses.

La elección de UTF-8 como estándar para todos tus archivos de texto simplificará enormemente el manejo de caracteres internacionales y evitará muchos problemas de compatibilidad, especialmente en aplicaciones que se ejecutan en diferentes plataformas o que intercambian datos con otros sistemas.

CONSTRUYE TU CARRERA EN IA Y PROGRAMACIÓN SOFTWARE

Accede a +1000 lecciones y cursos con certificado. Mejora tu portfolio con certificados de superación para tu CV.

30 % DE DESCUENTO

Plan mensual

19.00 /mes

13.30 € /mes

Precio normal mensual: 19 €
63 % DE DESCUENTO

Plan anual

10.00 /mes

7.00 € /mes

Ahorras 144 € al año
Precio normal anual: 120 €
Aprende Java online

Ejercicios de esta lección Leer y escribir archivos

Evalúa tus conocimientos de esta lección Leer y escribir archivos con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

Streams: match

Test

Gestión de errores y excepciones

Código

CRUD en Java de modelo Customer sobre un ArrayList

Proyecto

Clases abstractas

Test

Listas

Código

Métodos de la clase String

Código

Streams: reduce()

Test

API java.nio 2

Puzzle

Polimorfismo

Código

Pattern Matching

Código

Streams: flatMap()

Test

Llamada y sobrecarga de funciones

Puzzle

Métodos referenciados

Test

Métodos de la clase String

Código

Representación de Fecha

Puzzle

Operadores lógicos

Test

Inferencia de tipos con var

Código

Tipos de datos

Código

Estructuras de iteración

Puzzle

Streams: forEach()

Test

Objetos

Puzzle

Funciones lambda

Test

Uso de Scanner

Puzzle

Tipos de variables

Puzzle

Streams: collect()

Puzzle

Operadores aritméticos

Puzzle

Arrays y matrices

Código

Clases y objetos

Código

Interfaz funcional Consumer

Test

CRUD en Java de modelo Customer sobre un HashMap

Proyecto

Interfaces

Código

Enumeraciones Enums

Código

API Optional

Test

Interfaz funcional Function

Test

Encapsulación

Test

Interfaces

Código

Uso de API Optional

Puzzle

Representación de Hora

Test

Herencia básica

Test

Clases y objetos

Código

Interfaz funcional Supplier

Puzzle

HashMap

Puzzle

Sobrecarga de métodos

Test

Polimorfismo de tiempo de ejecución

Puzzle

OOP en Java

Proyecto

Sobrecarga de métodos

Código

CRUD de productos en Java

Proyecto

Clases sealed

Código

Creación de Streams

Test

Records

Código

Encapsulación

Código

Streams: min max

Puzzle

Herencia

Código

Métodos avanzados de la clase String

Puzzle

Funciones

Código

Polimorfismo de tiempo de compilación

Test

Reto sintaxis Java

Proyecto

Conjuntos

Código

Estructuras de control

Código

Recursión

Código

Excepciones

Puzzle

Herencia avanzada

Puzzle

Estructuras de selección

Test

Uso de interfaces

Test

Operadores

Código

Variables

Código

HashSet

Test

Objeto Scanner

Test

Streams: filter()

Puzzle

Operaciones de Streams

Puzzle

Interfaz funcional Predicate

Puzzle

Streams: sorted()

Test

Configuración de entorno

Test

Uso de variables

Test

Clases

Test

Streams: distinct()

Puzzle

Streams: count()

Test

ArrayList

Test

Mapas

Código

Datos de referencia

Test

Interfaces funcionales

Puzzle

Métodos básicos de la clase String

Test

Tipos de datos

Código

Clases abstractas

Código

Instalación

Test

Funciones

Código

Excepciones

Código

Estructuras de control

Código

Herencia de clases

Código

La clase Scanner

Código

Generics

Código

Streams: map()

Puzzle

Funciones y encapsulamiento

Test

Todas las lecciones de Java

Accede a todas las lecciones de Java y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Instalación De Java

Introducción Y Entorno

Configuración De Entorno Java

Introducción Y Entorno

Tipos De Datos

Sintaxis

Variables

Sintaxis

Operadores

Sintaxis

Estructuras De Control

Sintaxis

Funciones

Sintaxis

Recursión

Sintaxis

Arrays Y Matrices

Sintaxis

Excepciones

Programación Orientada A Objetos

Clases Y Objetos

Programación Orientada A Objetos

Encapsulación

Programación Orientada A Objetos

Herencia

Programación Orientada A Objetos

Clases Abstractas

Programación Orientada A Objetos

Interfaces

Programación Orientada A Objetos

Sobrecarga De Métodos

Programación Orientada A Objetos

Polimorfismo

Programación Orientada A Objetos

La Clase Scanner

Programación Orientada A Objetos

Métodos De La Clase String

Programación Orientada A Objetos

Excepciones

Programación Orientada A Objetos

Records

Programación Orientada A Objetos

Pattern Matching

Programación Orientada A Objetos

Inferencia De Tipos Con Var

Programación Orientada A Objetos

Enumeraciones Enums

Programación Orientada A Objetos

Generics

Programación Orientada A Objetos

Clases Sealed

Programación Orientada A Objetos

Listas

Framework Collections

Conjuntos

Framework Collections

Mapas

Framework Collections

Funciones Lambda

Programación Funcional

Interfaz Funcional Consumer

Programación Funcional

Interfaz Funcional Predicate

Programación Funcional

Interfaz Funcional Supplier

Programación Funcional

Interfaz Funcional Function

Programación Funcional

Métodos Referenciados

Programación Funcional

Creación De Streams

Programación Funcional

Operaciones Intermedias Con Streams: Map()

Programación Funcional

Operaciones Intermedias Con Streams: Filter()

Programación Funcional

Operaciones Intermedias Con Streams: Distinct()

Programación Funcional

Operaciones Finales Con Streams: Collect()

Programación Funcional

Operaciones Finales Con Streams: Min Max

Programación Funcional

Operaciones Intermedias Con Streams: Flatmap()

Programación Funcional

Operaciones Intermedias Con Streams: Sorted()

Programación Funcional

Operaciones Finales Con Streams: Reduce()

Programación Funcional

Operaciones Finales Con Streams: Foreach()

Programación Funcional

Operaciones Finales Con Streams: Count()

Programación Funcional

Operaciones Finales Con Streams: Match

Programación Funcional

Api Optional

Programación Funcional

Transformación

Programación Funcional

Reducción Y Acumulación

Programación Funcional

Mapeo

Programación Funcional

Streams Paralelos

Programación Funcional

Agrupación Y Partición

Programación Funcional

Filtrado Y Búsqueda

Programación Funcional

Api Java.nio 2

Entrada Y Salida Io

Fundamentos De Io

Entrada Y Salida Io

Leer Y Escribir Archivos

Entrada Y Salida Io

Httpclient Moderno

Entrada Y Salida Io

Clases De Nio2

Entrada Y Salida Io

Api Java.time

Api Java.time

Localtime

Api Java.time

Localdatetime

Api Java.time

Localdate

Api Java.time

Executorservice

Concurrencia

Virtual Threads (Project Loom)

Concurrencia

Future Y Completablefuture

Concurrencia

Spring Framework

Frameworks Para Java

Micronaut

Frameworks Para Java

Maven

Frameworks Para Java

Gradle

Frameworks Para Java

Lombok Para Java

Frameworks Para Java

Quarkus

Frameworks Para Java

Ecosistema Jakarta Ee De Java

Frameworks Para Java

Introducción A Junit 5

Testing

Accede GRATIS a Java y certifícate

Certificados de superación de Java

Supera todos los ejercicios de programación del curso de Java y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender y utilizar los métodos Files.readAllLines() y Files.readString() para la lectura de archivos.
  • Aprender a escribir archivos con Files.writeString() y Files.write(), incluyendo opciones de apertura y codificación.
  • Conocer el uso eficiente de BufferedReader y BufferedWriter para manejar archivos grandes o con control granular.
  • Entender la importancia de la codificación de caracteres, especialmente UTF-8, y cómo gestionarla en Java.
  • Aplicar buenas prácticas y manejo de excepciones en operaciones de entrada/salida de archivos.