Entrada/Salida avanzada: archivos y streams
Introducción
El manejo de entrada/salida (E/S o I/O) es una parte fundamental de cualquier lenguaje de programación, ya que permite a nuestras aplicaciones interactuar con el mundo exterior. En Java, el sistema de E/S se basa en el concepto de streams (flujos), que son secuencias ordenadas de datos que fluyen desde un origen hacia un destino.
En artículos anteriores, hemos visto cómo trabajar con entrada y salida básica por consola. Ahora, daremos un paso adelante y exploraremos el sistema de E/S avanzado de Java, centrándonos especialmente en la manipulación de archivos y diferentes tipos de streams. Este conocimiento es esencial para desarrollar aplicaciones que necesiten persistir datos, procesar archivos o comunicarse con recursos externos.
En este artículo, aprenderemos las diferencias entre streams basados en bytes y caracteres, cómo trabajar con archivos, y exploraremos las clases más importantes del paquete java.io
y java.nio
.
Fundamentos de la E/S en Java
Jerarquía de clases para E/S
Java organiza sus capacidades de E/S en dos paquetes principales:
java.io
: El paquete clásico, presente desde las primeras versiones de Java.java.nio
: Introducido en Java 1.4, proporciona capacidades de E/S no bloqueante y otras mejoras de rendimiento.
Tipos de streams
En Java, existen dos tipos fundamentales de streams:
- Streams de bytes (
InputStream
yOutputStream
): Trabajan con datos binarios, byte a byte. - Streams de caracteres (
Reader
yWriter
): Trabajan con datos de texto, utilizando el sistema de codificación de caracteres de Java.
Esta distinción es importante porque:
- Los streams de bytes son adecuados para datos binarios (imágenes, archivos comprimidos, etc.).
- Los streams de caracteres son más apropiados para texto (documentos, configuraciones, etc.).
Trabajando con archivos
La clase File
La clase File
representa una ruta a un archivo o directorio en el sistema de archivos. No contiene métodos para leer o escribir datos, pero es útil para obtener información sobre archivos y directorios:
import java.io.File;
import java.io.IOException;
import java.util.Date;
public class ManipulacionArchivos {
public static void main(String[] args) {
// Crear un objeto File
File archivo = new File("datos.txt");
// Comprobar si el archivo existe
if (archivo.exists()) {
System.out.println("El archivo existe");
System.out.println("Nombre: " + archivo.getName());
System.out.println("Ruta absoluta: " + archivo.getAbsolutePath());
System.out.println("Tamaño: " + archivo.length() + " bytes");
System.out.println("Última modificación: " + new Date(archivo.lastModified()));
System.out.println("¿Es directorio? " + archivo.isDirectory());
System.out.println("¿Es archivo? " + archivo.isFile());
System.out.println("¿Se puede leer? " + archivo.canRead());
System.out.println("¿Se puede escribir? " + archivo.canWrite());
} else {
System.out.println("El archivo no existe, intentando crearlo...");
try {
if (archivo.createNewFile()) {
System.out.println("Archivo creado correctamente");
} else {
System.out.println("No se pudo crear el archivo");
}
} catch (IOException e) {
System.out.println("Error al crear el archivo: " + e.getMessage());
}
}
}
}
Trabajando con directorios
Para trabajar con directorios, también usamos la clase File
:
import java.io.File;
public class ManipulacionDirectorios {
public static void main(String[] args) {
// Crear un objeto File para un directorio
File directorio = new File("documentos");
// Comprobar si existe y crear si no
if (!directorio.exists()) {
if (directorio.mkdir()) {
System.out.println("Directorio creado");
} else {
System.out.println("No se pudo crear el directorio");
return;
}
}
// Listar contenido del directorio
File[] archivos = directorio.listFiles();
if (archivos != null) {
System.out.println("Contenido del directorio:");
for (File archivo : archivos) {
String tipo = archivo.isDirectory() ? "Directorio" : "Archivo";
System.out.println(tipo + ": " + archivo.getName());
}
}
}
}
Streams de bytes
InputStream y OutputStream
Las clases abstractas InputStream
y OutputStream
son la base para todas las operaciones de E/S basadas en bytes:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class CopiaArchivo {
public static void main(String[] args) {
// Rutas de los archivos
String archivoOrigen = "origen.dat";
String archivoDestino = "destino.dat";
try (
// Streams de bytes para lectura y escritura
FileInputStream entrada = new FileInputStream(archivoOrigen);
FileOutputStream salida = new FileOutputStream(archivoDestino)
) {
int bytesLeidos;
byte[] buffer = new byte[1024]; // Buffer de 1KB
// Leer del origen y escribir en el destino
while ((bytesLeidos = entrada.read(buffer)) != -1) {
salida.write(buffer, 0, bytesLeidos);
}
System.out.println("Archivo copiado correctamente");
} catch (IOException e) {
System.out.println("Error de E/S: " + e.getMessage());
}
}
}
Este ejemplo utiliza try-with-resources
(introducido en Java 7), que asegura que los streams se cierren automáticamente al finalizar el bloque try
, incluso si ocurre una excepción.
Streams de bytes filtrados
Java proporciona streams filtrados que añaden funcionalidades adicionales:
- BufferedInputStream/BufferedOutputStream: Añaden un buffer para mejorar el rendimiento.
- DataInputStream/DataOutputStream: Permiten leer/escribir tipos primitivos de Java.
- ObjectInputStream/ObjectOutputStream: Permiten leer/escribir objetos serializados.
Ejemplo con DataInputStream
y DataOutputStream
:
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class DatosEstudiante {
public static void main(String[] args) {
// Escribir datos
try (
DataOutputStream salida = new DataOutputStream(
new FileOutputStream("estudiante.dat"))
) {
// Escribir datos de un estudiante
salida.writeUTF("Ana García"); // Nombre (String)
salida.writeInt(25); // Edad (int)
salida.writeDouble(8.75); // Nota media (double)
salida.writeBoolean(true); // ¿Beca? (boolean)
System.out.println("Datos escritos correctamente");
} catch (IOException e) {
System.out.println("Error al escribir: " + e.getMessage());
}
// Leer datos
try (
DataInputStream entrada = new DataInputStream(
new FileInputStream("estudiante.dat"))
) {
// Leer datos en el mismo orden
String nombre = entrada.readUTF();
int edad = entrada.readInt();
double notaMedia = entrada.readDouble();
boolean tieneBeca = entrada.readBoolean();
System.out.println("Datos del estudiante:");
System.out.println("Nombre: " + nombre);
System.out.println("Edad: " + edad);
System.out.println("Nota media: " + notaMedia);
System.out.println("Tiene beca: " + tieneBeca);
} catch (IOException e) {
System.out.println("Error al leer: " + e.getMessage());
}
}
}
Streams de caracteres
Reader y Writer
Para trabajar con texto, Java proporciona las clases abstractas Reader
y Writer
:
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class CopiaTexto {
public static void main(String[] args) {
String archivoOrigen = "origen.txt";
String archivoDestino = "destino.txt";
try (
FileReader entrada = new FileReader(archivoOrigen);
FileWriter salida = new FileWriter(archivoDestino)
) {
int caracterLeido;
// Leer carácter a carácter
while ((caracterLeido = entrada.read()) != -1) {
salida.write(caracterLeido);
}
System.out.println("Archivo de texto copiado correctamente");
} catch (IOException e) {
System.out.println("Error de E/S: " + e.getMessage());
}
}
}
Streams de caracteres filtrados
Similar a los streams de bytes, existen streams filtrados para caracteres:
- BufferedReader/BufferedWriter: Añaden un buffer para mejorar el rendimiento.
- PrintWriter: Proporciona métodos para imprimir representaciones formateadas de objetos.
Ejemplo de lectura de líneas con BufferedReader
:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class LeerLineas {
public static void main(String[] args) {
String archivo = "poema.txt";
try (
BufferedReader br = new BufferedReader(new FileReader(archivo))
) {
String linea;
int numLinea = 1;
// Leer línea a línea
while ((linea = br.readLine()) != null) {
System.out.println(numLinea + ": " + linea);
numLinea++;
}
} catch (IOException e) {
System.out.println("Error de lectura: " + e.getMessage());
}
}
}
Ejemplo de escritura con PrintWriter
:
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
public class EscribirFormateado {
public static void main(String[] args) {
String archivo = "informe.txt";
try (
PrintWriter pw = new PrintWriter(new FileWriter(archivo))
) {
// Escribir texto formateado
pw.println("Informe de ventas");
pw.println("================");
pw.println();
String[] productos = {"Portátil", "Móvil", "Tablet"};
double[] precios = {899.99, 349.50, 199.75};
int[] cantidades = {5, 12, 8};
pw.printf("%-10s %10s %10s %10s%n", "Producto", "Precio", "Cantidad", "Total");
pw.println("-------------------------------------------");
double total = 0;
for (int i = 0; i < productos.length; i++) {
double subtotal = precios[i] * cantidades[i];
total += subtotal;
pw.printf("%-10s %10.2f€ %10d %10.2f€%n",
productos[i], precios[i], cantidades[i], subtotal);
}
pw.println("-------------------------------------------");
pw.printf("TOTAL: %33.2f€%n", total);
System.out.println("Informe generado correctamente");
} catch (IOException e) {
System.out.println("Error de escritura: " + e.getMessage());
}
}
}
Serialización de objetos
La serialización permite convertir objetos Java en secuencias de bytes para almacenarlos o transmitirlos, y después reconstruirlos:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
// Clase serializable
class Persona implements Serializable {
private static final long serialVersionUID = 1L;
private String nombre;
private int edad;
private transient String contrasena; // transient: no se serializa
public Persona(String nombre, int edad, String contrasena) {
this.nombre = nombre;
this.edad = edad;
this.contrasena = contrasena;
}
@Override
public String toString() {
return "Persona [nombre=" + nombre + ", edad=" + edad +
", contraseña=" + (contrasena != null ? "****" : "null") + "]";
}
}
public class SerializacionObjetos {
public static void main(String[] args) {
String archivo = "personas.dat";
// Crear y serializar objetos
try (
ObjectOutputStream salida = new ObjectOutputStream(
new FileOutputStream(archivo))
) {
// Crear lista de personas
List<Persona> personas = new ArrayList<>();
personas.add(new Persona("Carlos", 30, "clave123"));
personas.add(new Persona("Laura", 28, "segura456"));
personas.add(new Persona("Miguel", 35, "contraseña789"));
// Serializar la lista completa
salida.writeObject(personas);
System.out.println("Objetos serializados correctamente");
} catch (IOException e) {
System.out.println("Error al serializar: " + e.getMessage());
}
// Deserializar objetos
try (
ObjectInputStream entrada = new ObjectInputStream(
new FileInputStream(archivo))
) {
// Leer la lista completa
@SuppressWarnings("unchecked")
List<Persona> personasLeidas = (List<Persona>) entrada.readObject();
System.out.println("\nPersonas deserializadas:");
for (Persona p : personasLeidas) {
System.out.println(p);
}
} catch (IOException | ClassNotFoundException e) {
System.out.println("Error al deserializar: " + e.getMessage());
}
}
}
Puntos clave sobre serialización:
- Las clases deben implementar la interfaz
Serializable
. - Se recomienda definir un
serialVersionUID
para control de versiones. - Los campos marcados como
transient
no se serializan (útil para datos sensibles o temporales). - Las relaciones entre objetos se mantienen en la serialización.
El paquete java.nio
Java NIO (New I/O) proporciona una API alternativa para operaciones de E/S con mejor rendimiento y características adicionales:
Canales y buffers
En NIO, los datos se leen desde canales a buffers y se escriben desde buffers a canales:
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class EjemploNIOCanales {
public static void main(String[] args) {
// Definir rutas
Path rutaOrigen = Paths.get("origen.dat");
Path rutaDestino = Paths.get("destino.dat");
try (
// Abrir canales para lectura y escritura
FileChannel canalOrigen = FileChannel.open(rutaOrigen, StandardOpenOption.READ);
FileChannel canalDestino = FileChannel.open(rutaDestino,
StandardOpenOption.CREATE, StandardOpenOption.WRITE)
) {
// Crear buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
// Leer datos del origen y escribirlos en el destino
while (canalOrigen.read(buffer) > 0) {
// Preparar buffer para lectura
buffer.flip();
// Escribir contenido del buffer al canal de destino
canalDestino.write(buffer);
// Limpiar buffer para siguiente lectura
buffer.clear();
}
System.out.println("Archivo copiado correctamente con NIO");
} catch (IOException e) {
System.out.println("Error de E/S: " + e.getMessage());
}
}
}
En este ejemplo:
- Abrimos canales para el archivo de origen (solo lectura) y destino (creación y escritura).
- Creamos un buffer de bytes con capacidad de 1KB.
- Leemos datos del canal de origen al buffer.
- Usamos
flip()
para cambiar el buffer del modo escritura al modo lectura. - Escribimos datos del buffer al canal de destino.
- Usamos
clear()
para preparar el buffer para la siguiente operación de lectura.
Path y Files
El paquete java.nio.file
proporciona las clases Path
y Files
que simplifican muchas operaciones:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.List;
public class EjemploNIOFiles {
public static void main(String[] args) {
// Crear objeto Path
Path archivo = Paths.get("ejemplo.txt");
try {
// Escribir líneas de texto a un archivo
List<String> lineas = List.of(
"Primera línea",
"Segunda línea",
"Tercera línea"
);
Files.write(archivo, lineas);
System.out.println("Archivo escrito correctamente");
// Leer todas las líneas de un archivo
List<String> lineasLeidas = Files.readAllLines(archivo);
System.out.println("\nContenido del archivo:");
for (String linea : lineasLeidas) {
System.out.println(linea);
}
// Copiar un archivo
Path copia = Paths.get("copia_ejemplo.txt");
Files.copy(archivo, copia, StandardCopyOption.REPLACE_EXISTING);
System.out.println("\nArchivo copiado correctamente");
// Obtener información del archivo
System.out.println("\nInformación del archivo:");
System.out.println("Tamaño: " + Files.size(archivo) + " bytes");
System.out.println("Última modificación: " + Files.getLastModifiedTime(archivo));
System.out.println("Es directorio: " + Files.isDirectory(archivo));
System.out.println("Es archivo regular: " + Files.isRegularFile(archivo));
System.out.println("Es ejecutable: " + Files.isExecutable(archivo));
} catch (IOException e) {
System.out.println("Error de E/S: " + e.getMessage());
}
}
}
La API de Files
proporciona métodos estáticos para operaciones comunes como:
- Lectura y escritura completa de archivos
- Copia, movimiento y eliminación de archivos
- Obtención de atributos
- Creación de directorios y archivos temporales
- Listado de contenidos de directorios
Walking the File Tree
Para recorrer una estructura de directorios recursivamente:
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
public class RecorridoDirectorios {
public static void main(String[] args) {
Path inicio = Paths.get("."); // Directorio actual
try {
Files.walkFileTree(inicio, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path archivo, BasicFileAttributes attrs) {
System.out.println("Archivo: " + archivo);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
System.out.println("Directorio: " + dir);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path archivo, IOException exc) {
System.err.println("Error al acceder a: " + archivo);
return FileVisitResult.CONTINUE;
}
});
} catch (IOException e) {
System.out.println("Error al recorrer directorios: " + e.getMessage());
}
}
}
Streams en Java moderno
Con Java 8 se introdujeron nuevas características en el manejo de archivos utilizando Streams:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class StreamsArchivos {
public static void main(String[] args) {
Path directorio = Paths.get(".");
try {
// Listar archivos en el directorio actual
System.out.println("Archivos en el directorio actual:");
try (Stream<Path> stream = Files.list(directorio)) {
stream.forEach(System.out::println);
}
// Contar líneas en un archivo de texto
Path archivo = Paths.get("ejemplo.txt");
if (Files.exists(archivo)) {
long numLineas = Files.lines(archivo).count();
System.out.println("\nNúmero de líneas en ejemplo.txt: " + numLineas);
// Encontrar las 3 palabras más frecuentes
System.out.println("\nPalabras más frecuentes:");
Map<String, Long> frecuenciaPalabras = Files.lines(archivo)
.flatMap(linea -> Stream.of(linea.split("\\s+")))
.filter(palabra -> !palabra.isEmpty())
.map(String::toLowerCase)
.collect(Collectors.groupingBy(
palabra -> palabra,
Collectors.counting()
));
frecuenciaPalabras.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.limit(3)
.forEach(e -> System.out.println(e.getKey() + ": " + e.getValue()));
}
// Buscar archivos recursivamente
System.out.println("\nArchivos .java en directorios y subdirectorios:");
try (Stream<Path> stream = Files.find(directorio,
Integer.MAX_VALUE,
(path, attr) -> path.toString().endsWith(".java"))) {
stream.forEach(System.out::println);
}
} catch (IOException e) {
System.out.println("Error: " + e.getMessage());
}
}
}
Este ejemplo muestra cómo usar:
Files.list()
para listar archivos en un directorioFiles.lines()
para leer un archivo línea por líneaFiles.find()
para buscar archivos recursivamente- Operaciones de Stream para procesar el contenido de los archivos
Trabajando con archivos de propiedades
Los archivos de propiedades son comunes para configuraciones:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Properties;
public class ArchivoPropiedades {
public static void main(String[] args) {
String archivo = "config.properties";
// Crear y guardar propiedades
Properties propiedades = new Properties();
propiedades.setProperty("db.url", "jdbc:mysql://localhost:3306/midb");
propiedades.setProperty("db.usuario", "admin");
propiedades.setProperty("db.password", "secreto");
propiedades.setProperty("app.timeout", "30");
propiedades.setProperty("app.debug", "true");
try (FileOutputStream salida = new FileOutputStream(archivo)) {
propiedades.store(salida, "Configuración de la aplicación");
System.out.println("Archivo de propiedades guardado");
} catch (IOException e) {
System.out.println("Error al guardar propiedades: " + e.getMessage());
}
// Cargar propiedades
Properties propiedadesCargadas = new Properties();
try (FileInputStream entrada = new FileInputStream(archivo)) {
propiedadesCargadas.load(entrada);
System.out.println("\nPropiedades cargadas:");
System.out.println("URL de la base de datos: " +
propiedadesCargadas.getProperty("db.url"));
System.out.println("Timeout de la aplicación: " +
propiedadesCargadas.getProperty("app.timeout") + " segundos");
// Valor por defecto si la propiedad no existe
String puerto = propiedadesCargadas.getProperty("app.puerto", "8080");
System.out.println("Puerto: " + puerto);
} catch (IOException e) {
System.out.println("Error al cargar propiedades: " + e.getMessage());
}
}
}
Buenas prácticas
-
Siempre cerrar recursos: Utiliza
try-with-resources
para garantizar que los recursos se cierren correctamente. -
Buffers para mejor rendimiento: Utiliza streams con buffer (
BufferedInputStream
,BufferedReader
, etc.) para mejorar el rendimiento con archivos grandes. -
Manejo adecuado de excepciones: No ignores las excepciones de E/S; manéjalas o propágalas según corresponda.
-
Especificar codificación de caracteres: Al trabajar con archivos de texto, especifica siempre la codificación.
Files.write(path, lines, StandardCharsets.UTF_8); BufferedReader reader = new BufferedReader( new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8) );
-
Usa NIO para operaciones avanzadas: Considera usar
java.nio
para operaciones no bloqueantes, manipulación de grandes volúmenes de datos o cuando necesites características avanzadas. -
Liberar recursos en el orden correcto: Al cerrar recursos manualmente, ciérralos en orden inverso a como fueron abiertos.
Ejemplo práctico completo: Gestor de notas
Vamos a crear un ejemplo completo que utilice diversas técnicas de E/S para gestionar un sistema simple de notas:
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
class Nota implements Serializable {
private static final long serialVersionUID = 1L;
private String titulo;
private String contenido;
private LocalDateTime fechaCreacion;
public Nota(String titulo, String contenido) {
this.titulo = titulo;
this.contenido = contenido;
this.fechaCreacion = LocalDateTime.now();
}
public String getTitulo() {
return titulo;
}
public String getContenido() {
return contenido;
}
public LocalDateTime getFechaCreacion() {
return fechaCreacion;
}
@Override
public String toString() {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm");
return "Título: " + titulo +
"\nFecha: " + fechaCreacion.format(formatter) +
"\n--------------------\n" + contenido +
"\n====================\n";
}
}
public class GestorNotas {
private static final String DIRECTORIO_NOTAS = "notas";
private static final String ARCHIVO_INDICE = DIRECTORIO_NOTAS + "/indice.dat";
private static final DateTimeFormatter FORMATO_ARCHIVO =
DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
public static void main(String[] args) {
// Crear directorio de notas si no existe
try {
Files.createDirectories(Paths.get(DIRECTORIO_NOTAS));
} catch (IOException e) {
System.err.println("Error al crear directorio: " + e.getMessage());
return;
}
// Cargar índice existente o crear uno nuevo
Map<String, String> indice = cargarIndice();
// Menú principal
Scanner scanner = new Scanner(System.in);
int opcion;
do {
System.out.println("\nGESTOR DE NOTAS");
System.out.println("1. Crear nota");
System.out.println("2. Listar notas");
System.out.println("3. Ver nota");
System.out.println("4. Eliminar nota");
System.out.println("0. Salir");
System.out.print("Seleccione opción: ");
try {
opcion = Integer.parseInt(scanner.nextLine());
} catch (NumberFormatException e) {
opcion = -1;
}
switch (opcion) {
case 1:
crearNota(scanner, indice);
break;
case 2:
listarNotas(indice);
break;
case 3:
verNota(scanner, indice);
break;
case 4:
eliminarNota(scanner, indice);
break;
case 0:
guardarIndice(indice);
System.out.println("¡Hasta pronto!");
break;
default:
System.out.println("Opción no válida");
}
} while (opcion != 0);
scanner.close();
}
private static Map<String, String> cargarIndice() {
Map<String, String> indice = new HashMap<>();
Path archivoIndice = Paths.get(ARCHIVO_INDICE);
if (Files.exists(archivoIndice)) {
try (ObjectInputStream entrada = new ObjectInputStream(
new FileInputStream(ARCHIVO_INDICE))) {
@SuppressWarnings("unchecked")
Map<String, String> indiceLeido = (Map<String, String>) entrada.readObject();
indice = indiceLeido;
System.out.println("Índice cargado correctamente");
} catch (IOException | ClassNotFoundException e) {
System.err.println("Error al cargar índice: " + e.getMessage());
}
}
return indice;
}
private static void guardarIndice(Map<String, String> indice) {
try (ObjectOutputStream salida = new ObjectOutputStream(
new FileOutputStream(ARCHIVO_INDICE))) {
salida.writeObject(indice);
System.out.println("Índice guardado correctamente");
} catch (IOException e) {
System.err.println("Error al guardar índice: " + e.getMessage());
}
}
private static void crearNota(Scanner scanner, Map<String, String> indice) {
System.out.println("\nCREAR NOTA");
System.out.print("Título: ");
String titulo = scanner.nextLine().trim();
if (titulo.isEmpty()) {
System.out.println("El título no puede estar vacío");
return;
}
System.out.println("Contenido (termina con una línea que contenga solo '---'):");
StringBuilder contenido = new StringBuilder();
String linea;
while (!(linea = scanner.nextLine()).equals("---")) {
contenido.append(linea).append("\n");
}
Nota nota = new Nota(titulo, contenido.toString());
String nombreArchivo = FORMATO_ARCHIVO.format(nota.getFechaCreacion()) + ".txt";
String rutaArchivo = DIRECTORIO_NOTAS + "/" + nombreArchivo;
try {
// Guardar contenido de la nota
Files.write(Paths.get(rutaArchivo),
nota.toString().getBytes(StandardCharsets.UTF_8));
// Actualizar índice
indice.put(titulo, nombreArchivo);
guardarIndice(indice);
System.out.println("Nota creada correctamente");
} catch (IOException e) {
System.err.println("Error al guardar la nota: " + e.getMessage());
}
}
private static void listarNotas(Map<String, String> indice) {
System.out.println("\nLISTA DE NOTAS");
if (indice.isEmpty()) {
System.out.println("No hay notas guardadas");
return;
}
List<String> titulos = new ArrayList<>(indice.keySet());
Collections.sort(titulos);
for (int i = 0; i < titulos.size(); i++) {
System.out.println((i + 1) + ". " + titulos.get(i));
}
}
private static void verNota(Scanner scanner, Map<String, String> indice) {
if (indice.isEmpty()) {
System.out.println("\nNo hay notas guardadas");
return;
}
System.out.println("\nVER NOTA");
System.out.print("Título de la nota: ");
String titulo = scanner.nextLine().trim();
if (!indice.containsKey(titulo)) {
System.out.println("No existe ninguna nota con ese título");
return;
}
String rutaArchivo = DIRECTORIO_NOTAS + "/" + indice.get(titulo);
try {
List<String> lineas = Files.readAllLines(Paths.get(rutaArchivo),
StandardCharsets.UTF_8);
System.out.println("\n======== NOTA ========");
for (String linea : lineas) {
System.out.println(linea);
}
} catch (IOException e) {
System.err.println("Error al leer la nota: " + e.getMessage());
}
}
private static void eliminarNota(Scanner scanner, Map<String, String> indice) {
if (indice.isEmpty()) {
System.out.println("\nNo hay notas guardadas");
return;
}
System.out.println("\nELIMINAR NOTA");
System.out.print("Título de la nota a eliminar: ");
String titulo = scanner.nextLine().trim();
if (!indice.containsKey(titulo)) {
System.out.println("No existe ninguna nota con ese título");
return;
}
String rutaArchivo = DIRECTORIO_NOTAS + "/" + indice.get(titulo);
try {
Files.deleteIfExists(Paths.get(rutaArchivo));
indice.remove(titulo);
guardarIndice(indice);
System.out.println("Nota eliminada correctamente");
} catch (IOException e) {
System.err.println("Error al eliminar la nota: " + e.getMessage());
}
}
}
Este ejemplo completo integra:
- Manejo de archivos y directorios
- Serialización para guardar el índice de notas
- Lectura y escritura de archivos de texto
- Manejo de excepciones
- Uso de clases modernas como
Path
yFiles
Resumen
Java proporciona un sistema de entrada/salida completo y flexible a través de los paquetes java.io
y java.nio
. Los streams de bytes (InputStream
y OutputStream
) nos permiten trabajar con datos binarios, mientras que los streams de caracteres (Reader
y Writer
) son adecuados para texto. Para operaciones avanzadas, el paquete java.nio
ofrece canales, buffers y métodos simplificados a través de las clases Path
y Files
.
Hemos explorado las principales clases y métodos para trabajar con archivos y directorios, visto cómo serializar objetos para persistencia, y aprendido sobre las operaciones más comunes como lectura, escritura, copia y eliminación de archivos. También hemos revisado las buenas prácticas y acabamos de ver un ejemplo práctico completo que demuestra cómo integrar estas técnicas en una aplicación real.
Con estas herramientas, estarás bien equipado para desarrollar aplicaciones Java que necesiten interactuar con el sistema de archivos, procesar datos externos o comunicarse con otros sistemas a través de operaciones de entrada/salida.