Leer archivos en Java
Lee archivos de texto y binarios en Java con FileReader, BufferedReader, Scanner, Files.readString y streams.
Hay cinco formas comunes de leer un archivo de texto en Java, y la elección correcta depende casi por completo del tamaño del archivo y de lo que quieras hacer con el contenido. Este capítulo recorre las cinco desde la más sencilla hasta la más flexible:
Files.readString(path)— archivo completo como unString.Files.readAllLines(path)— archivo completo como unaList<String>.Files.readAllBytes(path)— archivo completo como unbyte[].Files.lines(path)— archivo como unStream<String>, perezoso.BufferedReader/Scanner— decoradores clásicos, control total.
Elige la herramienta más pequeña que se ajuste a tus necesidades. Leer un log de 4 GB con Files.readString es un OutOfMemoryError; leer una configuración de 12 líneas con BufferedReader y un bucle while son seis líneas de código donde una bastaría.
Files.readString(path) — archivo completo, una sola llamada
String text = Files.readString(Path.of("config.json"), StandardCharsets.UTF_8);Añadido en Java 11. Devuelve el archivo completo como un String. Usa UTF-8 por defecto desde Java 18 (aún se recomienda fijar el Charset explícitamente, incluso con el nuevo valor predeterminado). Lanza IOException si el archivo no existe o no puede leerse; lanza OutOfMemoryError si el archivo es más grande que el heap.
Úsalo cuando: el archivo sea "suficientemente pequeño" — archivos de configuración, payloads JSON, capítulos MDX, cualquier cosa que estés dispuesto a leer en una sola ventana del editor. La regla informal clásica es menos de unos pocos megabytes.
Files.readAllLines(path) — lista de líneas
List<String> lines = Files.readAllLines(Path.of("hosts.txt"), StandardCharsets.UTF_8);Devuelve una List<String> inmutable de las líneas del archivo, con los terminadores de línea eliminados. Mismo perfil de memoria que readString más la sobrecarga de la List — también mantiene el archivo completo en memoria.
Úsalo cuando: quieras indexar por número de línea, ordenar el archivo o alimentar líneas en un bucle for (String line : lines) sin configurar streams.
Files.readAllBytes(path) — bytes en bruto
byte[] raw = Files.readAllBytes(Path.of("photo.png"));El equivalente en bytes. Sin Charset porque no se produce ninguna decodificación. Úsalo para archivos binarios (imágenes, archivos comprimidos, ejecutables) o cuando necesites calcular un hash o canalizar bytes hacia un ByteArrayInputStream.
Files.lines(path) — stream perezoso
try (Stream<String> lines = Files.lines(Path.of("app.log"), StandardCharsets.UTF_8)) {
long errors = lines.filter(l -> l.contains("ERROR")).count();
}Este es el único lector integrado que escala a archivos de tamaño arbitrariamente grande. El Stream<String> es perezoso — las líneas se leen bajo demanda, no todas a la vez — y se conecta directamente al vocabulario de pipelines de stream (filter, map, count, toList).
Dos puntos no negociables:
try-with-resources es obligatorio. El stream posee un manejador de archivo abierto; sintry-with-resources, el archivo permanece abierto hasta que el GC actúe, y agotarás los descriptores de archivo en un servidor con mucha carga.- No reutilices el stream después de una operación terminal. Los streams son de un solo uso.
Úsalo cuando: el archivo sea demasiado grande para readAllLines, o quieras que la transformación línea a línea se componga con el resto de tu pipeline de stream.
BufferedReader.readLine() — el clásico
BufferedReader es el caballo de batalla que envuelven los helpers modernos. Almacena en búfer las lecturas subyacentes en un fragmento de memoria de tamaño fijo para que readLine() no emita una llamada al sistema por cada carácter.
try (BufferedReader in = Files.newBufferedReader(Path.of("hosts.txt"), StandardCharsets.UTF_8)) {
String line;
while ((line = in.readLine()) != null) {
System.out.println(line);
}
}Files.newBufferedReader(path) es la fábrica moderna; la versión clásica es new BufferedReader(new FileReader("hosts.txt")) (que usa el charset de la plataforma en JDKs anteriores a la versión 18 — fíjalo a UTF-8 con la sobrecarga de tres argumentos). El contrato de readLine() es:
- Devuelve la siguiente línea sin su terminador (
\n,\r, o\r\n). - Devuelve
nullal final del archivo. La condición del bucle(line = readLine()) != nulles el idioma establecido.
BufferedReader también es un productor de Stream<String>: reader.lines() devuelve un Stream<String> respaldado por el reader. Así es como se implementa Files.lines internamente.
Scanner — análisis token a token
Scanner lee texto por tokens — palabras, enteros, doubles, líneas, incluso coincidencias de regex — y es la herramienta adecuada para leer entradas estructuradas donde las unidades no son líneas completas.
try (Scanner sc = new Scanner(Files.newBufferedReader(Path.of("nums.txt")))) {
while (sc.hasNextInt()) {
int n = sc.nextInt();
System.out.println(n * n);
}
}Scanner es más lento que BufferedReader porque analiza; asigna cadenas cortas y ejecuta regex. Para el procesamiento línea a línea, prefiere BufferedReader. Para tokens tipados de un archivo pequeño (números, palabras, entrada similar a CSV), Scanner ahorra la capa de análisis.
Hay un capítulo completo sobre Scanner más adelante en esta parte — este es el sabor de leer un archivo.
FileReader — el lector de caracteres en bruto
try (FileReader in = new FileReader("notes.txt", StandardCharsets.UTF_8)) {
int c;
while ((c = in.read()) != -1) {
System.out.print((char) c);
}
}FileReader lee caracteres directamente del archivo — sin búfer, sin conciencia de líneas, sin elecciones de decodificación hechas por ti (pasas el Charset, o aceptas el predeterminado de la plataforma en JDKs anteriores a la versión 18). Es la capa sobre la que se asientan los demás. Casi nunca lo usas directamente en código de aplicación; lo envuelves en un BufferedReader.
Aún es útil cuando quieres leer unos pocos cientos de caracteres y detenerte — búsquedas pequeñas donde el costo de la configuración del búfer queda eclipsado por el costo de la llamada.
Cuál usar
| Escenario | Elige |
|---|---|
Archivo pequeño que quieres como un único String | Files.readString |
Archivo pequeño que quieres como una List<String> | Files.readAllLines |
| Archivo binario (imagen, archivo comprimido) | Files.readAllBytes |
| Cualquier archivo con una transformación de estilo stream | Files.lines (dentro de try-with-resources) |
| Bucle línea a línea, control total | Files.newBufferedReader + readLine |
| Tokens tipados (enteros, palabras, coincidencias de regex) | Scanner |
| Un carácter a la vez, archivo pequeño | FileReader |
El valor predeterminado correcto para el caso "solo quiero cargar este pequeño archivo de texto" es Files.readString. El valor predeterminado correcto para "procesar este log gigante sin agotar la memoria" es Files.lines.
Un ejemplo práctico: el mismo archivo, cinco lectores
El programa a continuación escribe un pequeño archivo de texto y luego lo lee de cinco maneras diferentes — readString, readAllLines, Files.lines filtrado a través de un Predicate<String> del vocabulario de la Parte 12, BufferedReader.readLine, y Scanner para enteros tokenizados. Cada bloque imprime lo que obtuvo para que puedas ver las formas una al lado de la otra.
Lo que debes extraer de la ejecución:
Files.readStringdevolvió el archivo completo como un únicoString— fácil y exactamente lo que quieres para configuraciones y plantillas pequeñas. Para un log de 4 GB habría lanzadoOutOfMemoryError.Files.readAllLinesdevolvió unaList<String>indexable con los terminadores eliminados.lines.get(0)funcionó porque la lista está materializada en memoria; no podrías hacer eso con un stream.Files.lines(file)se abrió dentro detry-with-resources porque el stream posee el manejador de archivo. El pipeline.filter(isError).count()tiene la misma forma que cualquier cosa de la Parte 12 — solo cambió la fuente.BufferedReader.readLine()devolviónullal final del archivo. El bucleforaquí se detuvo en tres a propósito, pero el idioma de producción eswhile ((line = in.readLine()) != null).Scannersaltó las líneas que no comenzaban con un entero, luego leyó tokens connextInt()hasta que se agotaron. El mismoScannerpodría haber leído doubles (nextDouble), coincidencias de regex (findInLine) oBigIntegers — por eso cuesta más por token queBufferedReaderpor línea.
Qué viene a continuación
El siguiente capítulo, Escribir archivos en Java, cubre el lado de escritura de las mismas APIs — Files.writeString, Files.write, BufferedWriter, PrintWriter y las banderas StandardOpenOption (APPEND, CREATE_NEW, TRUNCATE_EXISTING) que deciden cómo se maneja un archivo existente.