Java has accumulated several generations of input and output APIs, which is why searching for “read a file in Java” returns five different answers. For new code, use java.nio.file.Files and Path. This guide covers that modern approach first, then the stream-based classes you still need for large files, and the try-with-resources syntax that closes everything for you.
The modern way: Files and Path#
import java.nio.file.Files;
import java.nio.file.Path;
import java.io.IOException;
import java.util.List;
public class ReadDemo {
public static void main(String[] args) throws IOException {
Path path = Path.of("notes.txt");
String all = Files.readString(path);
List<String> lines = Files.readAllLines(path);
System.out.println(lines.size() + " lines");
}
}
Writing is just as short:
import java.nio.file.StandardOpenOption;
Files.writeString(path, "Hello\n");
Files.writeString(path, "Another line\n", StandardOpenOption.APPEND);
Files.write(path, List.of("first", "second"));
Path.of() handles separators for you, so the same code works on Windows and Linux:
Path p = Path.of("data", "2026", "report.csv");
System.out.println(p.getFileName()); // report.csv
System.out.println(p.getParent()); // data/2026
System.out.println(p.toAbsolutePath());
System.out.println(Files.exists(p));
System.out.println(Files.size(p)); // bytes
Large files: read a line at a time#
readAllLines loads everything into memory. For a large log file, stream it:
import java.io.BufferedReader;
try (BufferedReader reader = Files.newBufferedReader(Path.of("big.log"))) {
String line;
while ((line = reader.readLine()) != null) {
if (line.contains("ERROR")) {
System.out.println(line);
}
}
}
Or with the Stream API, which reads lazily as well:
import java.util.stream.Stream;
try (Stream<String> lines = Files.lines(Path.of("big.log"))) {
long errors = lines.filter(l -> l.contains("ERROR")).count();
System.out.println(errors + " errors");
}
The try (...) is essential here. Files.lines keeps the file handle open until the stream is closed, and leaking handles will eventually exhaust the operating system’s limit.
try-with-resources#
The old pattern needed a finally block and a nested try inside it:
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("notes.txt"));
System.out.println(reader.readLine());
} finally {
if (reader != null) {
try { reader.close(); } catch (IOException ignored) { }
}
}
try-with-resources replaces all of that:
try (BufferedReader reader = new BufferedReader(new FileReader("notes.txt"))) {
System.out.println(reader.readLine());
}
Anything declared in the brackets is closed automatically when the block ends, whether it finished normally or threw. You can declare several, separated by semicolons, and they close in reverse order.
Writing with a buffer#
import java.io.BufferedWriter;
try (BufferedWriter writer = Files.newBufferedWriter(Path.of("out.txt"))) {
for (int i = 1; i <= 1000; i++) {
writer.write("Line " + i);
writer.newLine();
}
}
Buffering matters. Writing a thousand lines unbuffered means a thousand trips to the disk; buffered, it is a handful. The difference on large outputs is not subtle.
Reading keyboard input#
import java.util.Scanner;
Scanner scanner = new Scanner(System.in);
System.out.print("What is your name? ");
String name = scanner.nextLine();
System.out.print("How old are you? ");
int age = scanner.nextInt();
System.out.println(name + " will be " + (age + 1) + " next year");
// Sturdier: read as text, convert deliberately
System.out.print("How old are you? ");
int age;
try {
age = Integer.parseInt(scanner.nextLine().trim());
} catch (NumberFormatException e) {
System.out.println("That was not a number.");
return;
}
Directories#
Files.createDirectories(Path.of("output/2026"));
try (Stream<Path> entries = Files.list(Path.of("data"))) {
entries.filter(Files::isRegularFile)
.forEach(System.out::println);
}
// Including subfolders
try (Stream<Path> all = Files.walk(Path.of("data"))) {
all.filter(p -> p.toString().endsWith(".csv"))
.forEach(System.out::println);
}
Files.copy(source, target);
Files.move(source, target);
Files.deleteIfExists(target);
Checked exceptions#
Every I/O operation in Java can throw IOException, and the compiler will not let you ignore it. You have two options: handle it, or declare it.
// Handle it
try {
String text = Files.readString(path);
} catch (NoSuchFileException e) {
System.out.println("That file does not exist: " + e.getFile());
} catch (IOException e) {
System.out.println("Could not read it: " + e.getMessage());
}
// Or declare it and let the caller decide
void load() throws IOException {
String text = Files.readString(path);
}
Catch the specific subclass first when you can do something useful about it. NoSuchFileException and AccessDeniedException have completely different fixes, and telling the user which one occurred is far more helpful than a generic message.
Character encoding#
import java.nio.charset.StandardCharsets;
Files.readString(path, StandardCharsets.UTF_8);
Files.writeString(path, text, StandardCharsets.UTF_8);
Modern Java defaults to UTF-8, but older versions used the platform default, which differs between machines. Being explicit costs one argument and prevents a whole category of “the accented characters are wrong on the server” bugs.
Questions people ask#
Should I use File or Path?
Path, with Files. The older java.io.File class still works and appears in lots of existing code, but its error reporting is poor — many methods return false rather than explaining what went wrong.
What is the difference between a Reader and an InputStream?
Readers handle characters and know about encoding; streams handle raw bytes. Use a Reader for text files and an InputStream for images, archives and anything binary.
Do I still need to call close()?
Not if you use try-with-resources, which is the reason it exists. Outside it, yes — and in a finally block, so it happens even when an exception is thrown.
Why does my file appear empty after writing?
Almost always an unflushed buffer because the writer was never closed. try-with-resources fixes it by flushing and closing on the way out of the block.
Where to go next#
- Java basics — the syntax underneath all of this.
- Object-oriented programming in Java — classes and objects properly.
- Handling errors and exceptions — the ideas behind checked exceptions.