Skip to content
Happy Programming Guide
Start learning
Java

Java Input and Output (I/O)

Reading and writing files in Java: the modern Files and Path API, buffered streams for large files, reading keyboard input, and try-with-resources.

A person working on a laptop

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#

JAVA
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:

JAVA
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:

JAVA
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:

JAVA
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:

JAVA
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:

JAVA
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:

JAVA
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#

JAVA
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#

JAVA
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");
JAVA
// 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#

JAVA
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.

JAVA
// 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#

JAVA
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, from your first class to collectionsRead next

Keep reading

Java

What’s New in Java 25 LTS

Java 25 is the current long-term release. Flexible constructors, compact source files, module imports and scoped values are final; here is what…

4 min read

Keep going — pick your next guide

The fastest way to improve is to read one guide, then build the thing it describes. Start with the basics, or jump straight to a project.

Ask a question or share what worked

Your email address will not be published. Required fields are marked *