Introducción a Java I/O
Resumen de Java I/O: flujos de bytes y caracteres, I/O con buffer, java.io vs. java.nio.file.
La Parte 12 terminó con un vocabulario que puedes llevar directamente a esta parte: lambdas, Consumer<T> y Supplier<T> como las formas detrás de "dame una línea" y "haz algo con esta línea," try-with-resources para cualquier cosa que necesite limpieza determinista, y el pipeline de Stream para datos orientados a líneas. Las APIs de I/O de Java fueron diseñadas exactamente en torno a esas formas — mucho antes de que existieran las palabras "interfaz funcional", los objetos subyacentes ya tenían un método cada uno, y la fachada posterior a Java 8 trajo el resto del camino.
Esta parte cubre cuatro conjuntos de herramientas superpuestos:
- Flujos de
java.io— la API original de Java 1.0:InputStream/OutputStreampara bytes,Reader/Writerpara caracteres, y los decoradoresBuffered*,Data*,Print*que los envuelven. java.io.File— la clase legada "esta cadena es una ruta". Sigue siendo omnipresente en código antiguo; reemplazada porjava.nio.file.Pathpara el trabajo nuevo.java.nio.file— la API moderna (Java 7+):Path,Files, y los métodos estáticos auxiliares (Files.readString,Files.writeString,Files.lines,Files.walk) que convierten la mayoría de las operaciones con archivos en una sola línea.- Serialización — convertir grafos de objetos en bytes y viceversa con
ObjectOutputStream/ObjectInputStream.
Los primeros seis capítulos trabajan las operaciones de archivo de alto nivel (abrir, crear, leer, escribir, eliminar) usando tanto java.io como java.nio.file para que puedas ver la misma tarea de dos maneras. Los capítulos intermedios amplían las propias clases de flujo — bytes vs. caracteres, buffer, datos, print. Los últimos capítulos cubren la serialización y la API de rutas y recorrido de directorios.
La división byte/carácter
Cada API de I/O en java.io tiene una de estas dos formas:
InputStream / OutputStream — byte-oriented (raw bytes: int read() returns 0..255 or -1)
Reader / Writer — character-oriented (decoded text: int read() returns a char or -1)La división no es cosmética. Los bytes son lo que almacenan los discos y los sockets; los caracteres son lo que leen los humanos. Un archivo .png son bytes; un .txt también son bytes en disco pero normalmente quieres decodificarlo como caracteres usando un charset. Mezclar los dos sin un charset es la fuente más común de errores de "caracteres extraños" en el Java legado.
Las clases puente — InputStreamReader y OutputStreamWriter — convierten entre ambos y aceptan un argumento Charset. Usa StandardCharsets.UTF_8 a menos que tengas una razón documentada para usar otra cosa; las formas sin argumento usan el charset predeterminado de la plataforma, que varía entre sistemas operativos y es la causa clásica de los errores de "funciona en mi Mac, falla en el servidor Linux".
El patrón decorador
java.io está construido sobre el patrón decorador: un pequeño conjunto de flujos crudos (FileInputStream, FileOutputStream, FileReader, FileWriter) envueltos en funcionalidad en capas (buffer, texto línea a línea, tipos primitivos, salida formateada). Compones lo que necesitas en el punto de llamada:
// Read a UTF-8 text file line by line:
try (BufferedReader in = new BufferedReader(
new InputStreamReader(new FileInputStream("a.txt"), StandardCharsets.UTF_8))) {
String line;
while ((line = in.readLine()) != null) {
System.out.println(line);
}
}Tres capas, de abajo hacia arriba: FileInputStream lee bytes crudos; InputStreamReader los decodifica como caracteres UTF-8; BufferedReader añade un buffer en memoria y el método readLine(). Cada capa es una clase separada con una sola responsabilidad. Java 11 simplificó exactamente este patrón a una línea — Files.newBufferedReader(path) — pero la decoración sigue siendo lo que ocurre internamente.
try-with-resources es la regla
Cada flujo, reader, writer y canal en java.io y java.nio implementa AutoCloseable. Cerrar importa: un FileOutputStream sin cerrar puede perder su buffer final; un socket sin cerrar pierde un descriptor de archivo; un reader sin cerrar en Windows mantiene un bloqueo que el sistema operativo no liberará. La construcción try-with-resources (Java 7+) garantiza que close() se llama en cada ruta exitosa y excepcional:
try (BufferedReader in = Files.newBufferedReader(path)) {
return in.readLine();
} // close() runs here, even if readLine() throwsPuedes declarar más de un recurso en el mismo try; se cierran en orden inverso. El código antiguo con try/finally que llama a close() manualmente casi siempre es incorrecto — la excepción interna oculta la excepción de cierre, o el cierre se olvida en la ruta de error. Usa try-with-resources para cualquier cosa que abra un manejador.
java.io versus java.nio.file
java.io.File (1996) modeló una ruta como un String y ofreció un puñado de operaciones (exists, isFile, delete, listFiles). La clase sigue siendo omnipresente en código legado, y muchas APIs aún devuelven o aceptan File. Pero tiene limitaciones que el JDK ya no finge ignorar:
- No hay forma de preguntar por qué falló una operación —
file.delete()devuelvefalsetanto para "el archivo no existe," "permiso denegado" como "el archivo está abierto." No puedes distinguir cuál. - Sin soporte para enlaces simbólicos, atributos de archivo, permisos u operaciones atómicas.
- No hay forma de recorrer un árbol de directorios sin escribir la recursión tú mismo.
java.nio.file (Java 7) lo reemplaza. Path es el nuevo tipo "esto es una ruta", y Files es una clase de utilidades static con alrededor de 80 métodos para todo lo que querrías hacer con una ruta:
Path p = Path.of("data", "users.txt"); // platform-independent path
String text = Files.readString(p, StandardCharsets.UTF_8); // whole file, one call
List<String> lines = Files.readAllLines(p, StandardCharsets.UTF_8);
Files.writeString(p, "hello\n", StandardCharsets.UTF_8);
try (Stream<String> s = Files.lines(p, StandardCharsets.UTF_8)) {
s.filter(l -> !l.isBlank()).forEach(System.out::println);
}Dos cosas a notar. Primero, Files.lines(path) devuelve un Stream<String> — el pipeline de stream que aprendiste en la Parte 12 lee archivos directamente. Segundo, el stream posee un manejador de archivo abierto, por lo que el wrapper try-with-resources es obligatorio — sin él, el archivo permanece abierto hasta el próximo GC.
A lo largo de la Parte 13 mostraremos ambas APIs lado a lado. El código nuevo debería recurrir primero a java.nio.file; los capítulos legados existen porque encontrarás las formas más antiguas en cualquier base de código anterior a Java 11.
A dónde va esta parte
- El próximo capítulo, Java File Class, recorre la API legada
java.io.File— sus métodos de consulta, listado, y las limitaciones que motivaronjava.nio.file. - Los cuatro capítulos siguientes (Creating Files, Reading Files, Writing Files, Deleting Files) cubren las operaciones de alto nivel "hacer una cosa con un archivo" usando ambas APIs.
- Los capítulos sobre flujos de bytes, caracteres y con buffer amplían la pila decoradora subyacente de
java.io. - La serialización, luego
Path,Files, y la API de recorrido de directorios cierran la parte.
Un ejemplo trabajado: la misma tarea, cuatro formas
El programa a continuación escribe un breve archivo de texto de cuatro maneras — una vez con el moderno Files.writeString, una vez con el clásico FileWriter + try-with-resources, una vez decorado con BufferedWriter, y una vez con PrintWriter para salida formateada. Luego lee el archivo de vuelta de dos maneras — una con Files.readString (archivo completo, una llamada) y otra con Files.lines como Stream<String> filtrado con un Predicate<String>. El ejemplo usa un archivo temporal del sistema para funcionar en cualquier entorno.
Lo que se puede extraer de la ejecución:
- El mismo archivo se escribió de cuatro formas diferentes.
Files.writeStringes el camino más corto para "colocar esta cadena en este archivo";FileWriteres el writer crudo clásico;BufferedWriterañade un buffer en memoria (barato cuando escribes muchos fragmentos pequeños);PrintWriterañadeprintf. Cada uno sobreescribió el contenido anterior porque el modo de apertura predeterminado es "truncar y luego escribir" — debes pasarStandardOpenOption.APPEND(cubierto en el capítulo de escritura) para añadir a un archivo. - Cada writer se ejecutó dentro de
try-with-resources. Omitir eso en un writer con buffer es el error en el que los últimos caracteres nunca llegan al disco —close()es lo que vacía el buffer final. Files.readStringdevolvió el archivo completo como un soloString— adecuado para archivos pequeños, la elección incorrecta para un log de 4 GB.Files.linesdevolvió unStream<String>que puedes encadenar confilter,mapycountsin mantener todo el archivo en memoria. Eltry-with-resources requerido en el stream se debe a que el stream posee un manejador de archivo abierto.- La línea
Predicate<String> nameLine = l -> l.startsWith("name")usa el mismo vocabulario de la Parte 12 — un valorPredicate, pasado aStream.filter.Files.lineses donde la API de streams se encuentra con la API de I/O. Files.deleteIfExistses la eliminación sin excepciones: devuelvetruesi eliminó el archivo,falsesi no existía. El legadoFile.delete()devuelve unbooleantanto para "eliminado" como "no se pudo eliminar" —Filesdistingue los dos lanzando una excepción.
Qué sigue
Antes de que la API moderna java.nio.file tome el control del resto de la parte, el próximo capítulo cubre la clase que encontrarás primero en cualquier base de código antigua: Java File Class. Es el tipo legado de ruta y metadatos — limitado, pero omnipresente — y ver lo que no puede hacer es lo que justifica el uso de Path y Files.