DEV Community

Cover image for Reading and writing files in Java: Comprehensive 2026 guide
Dev Design Daily
Dev Design Daily

Posted on

Reading and writing files in Java: Comprehensive 2026 guide

Reading and writing files in Java is one of those topics that looks simple at first and then becomes surprisingly important once you start building real applications. You may begin by reading a few lines from a text file, but before long you are dealing with CSV exports, configuration files, logs, binary files, character encodings, large datasets, and error handling.

That is why learning reading and writing files in Java properly matters. Java gives you several APIs for file I/O, including the traditional java.io package and the newer java.nio.file package, and knowing when to use each approach can help you write cleaner, safer, and more efficient code.

In this guide, we will walk through the most useful ways to read and write files in Java, compare the major APIs, look at practical examples, and discuss the mistakes developers commonly make.

Personal note: File I/O was one of those Java topics I initially treated as "just syntax." It became much more important once I started working with applications that needed configuration loading, log processing, data exports, and backend integrations. The APIs themselves are not difficult, but choosing the right one for the size and structure of your data is the skill that matters.

What does reading and writing files in Java mean?

At the simplest level, file I/O means moving information between your Java application and files stored on a filesystem. When your program reads a file, it loads data from disk so that your application can process it, and when it writes a file, it takes data from memory and saves it to disk.

Java provides two major families of APIs for this work. The older java.io package gives you readers, writers, streams, and file abstractions, while the newer java.nio.file package provides the Path and Files APIs that make many common operations easier to express.

Java API Package Best suited for
Traditional I/O java.io Readers, writers, byte streams, legacy applications
New I/O java.nio.file Paths, modern file operations, concise utility methods

For new applications, I generally recommend starting with Path and Files because the APIs are readable and convenient. The older java.io classes still matter, especially when you need stream-based processing or are maintaining an existing codebase.

Understanding Path before working with files

Modern Java file handling usually starts with a Path. A Path represents the location of a file or directory, which is cleaner and safer than passing raw path strings throughout your code.

Here is a simple example:

import java.nio.file.Path;

public class FileExample {
    public static void main(String[] args) {
        Path path = Path.of("data.txt");

        System.out.println(path);
    }
}


You can also create a nested path without manually inserting operating-system-specific separators:

Enter fullscreen mode Exit fullscreen mode

Path path = Path.of("data", "users", "users.txt");


If you are working with older Java code, you may see `Paths.get()` instead:

Enter fullscreen mode Exit fullscreen mode

import java.nio.file.Path;


import java.nio.file.Paths;

Path path = Paths.get("data.txt");

For modern Java versions, `Path.of()` is usually the cleaner option.

> **Tip:** Avoid hardcoding separators such as `data\\users\\users.txt`. Building paths with `Path.of()` makes your code more portable across Windows, Linux, and macOS.


## Reading an entire text file with `Files.readString()`

If you are working with a small or moderately sized text file, `Files.readString()` is one of the simplest ways to read it. The method reads the complete file into a `String`, which makes it useful for configuration files, short templates, JSON documents, or other text content that comfortably fits in memory.

Suppose `message.txt` contains:

Enter fullscreen mode Exit fullscreen mode

Java file handling is easier than it looks.


You can read it like this:

Enter fullscreen mode Exit fullscreen mode

import java.io.IOException;


import java.nio.file.Files;

import java.nio.file.Path;

public class ReadFileExample {

public static void main(String[] args) {

Path path = Path.of("message.txt");

try {

String content = Files.readString(path);

System.out.println(content);

        } catch (IOException e) {

e.printStackTrace();

        }

    }

}

The output will be:

Enter fullscreen mode Exit fullscreen mode

Java file handling is easier than it looks.


This is usually my first choice when the file is small and I simply need its complete contents as text.

> **Personal note:** Convenience methods are great until the data gets large. I have seen developers use methods such as `readString()` for files that should have been streamed line by line, so always think about file size before deciding that the shortest code is automatically the best code.


## Reading a file line by line with `Files.readAllLines()`

Sometimes you want each line as a separate element instead of receiving one large string. In that case, `Files.readAllLines()` returns the file contents as a `List<String>`.

Enter fullscreen mode Exit fullscreen mode

import java.io.IOException;


import java.nio.file.Files;

import java.nio.file.Path;

import java.util.List;

public class ReadLinesExample {

public static void main(String[] args) {

Path path = Path.of("languages.txt");

try {

List\<String> lines = Files.readAllLines(path);

for (String line : lines) {

System.out.println(line);

            }

        } catch (IOException e) {

e.printStackTrace();

        }

    }

}

If `languages.txt` contains:

Enter fullscreen mode Exit fullscreen mode

Java


Python

Go

Rust

The program prints each line individually.

This approach is especially convenient when you need to inspect, transform, or filter a small set of lines. However, it still loads the entire file into memory, so it is not ideal for very large files.


## Reading large files with `Files.lines()`

When a file becomes large, loading the entire thing into memory may be inefficient. Java's `Files.lines()` method lets you process the file as a `Stream<String>`, which is often a better fit for logs, datasets, or large text exports.

Enter fullscreen mode Exit fullscreen mode

import java.io.IOException;


import java.nio.file.Files;

import java.nio.file.Path;

import java.util.stream.Stream;

public class StreamFileExample {

public static void main(String[] args) {

Path path = Path.of("large-file.txt");

try (Stream\<String> lines = Files.lines(path)) {

lines.forEach(System.out::println);

        } catch (IOException e) {

e.printStackTrace();

        }

    }

}

The important part is the `try-with-resources` statement. The stream is associated with an open file resource, so you want Java to close it automatically after processing is complete.

You can also combine `Files.lines()` with the Streams API:

Enter fullscreen mode Exit fullscreen mode

try (Stream lines = Files.lines(Path.of("users.txt"))) {


long activeUsers = lines

.filter(line -> line.contains("ACTIVE"))

.count();

System.out.println(activeUsers);

}

This approach is useful because you can process the data while it is being read rather than loading everything first and then performing a second pass.


## Reading files with `BufferedReader`

`BufferedReader` has been one of the standard ways to read text files in Java for years. It remains useful today, particularly when you want direct control over line-by-line processing.

Enter fullscreen mode Exit fullscreen mode

import java.io.BufferedReader;


import java.io.FileReader;

import java.io.IOException;

public class BufferedReaderExample {

public static void main(String[] args) {

try (BufferedReader reader =

new BufferedReader(new FileReader("data.txt"))) {

String line;

while ((line = reader.readLine()) != null) {

System.out.println(line);

            }

        } catch (IOException e) {

e.printStackTrace();

        }

    }

}

The call to `reader.readLine()` returns one line at a time. When there are no more lines, it returns `null`, which makes a `while` loop a natural way to process the file.

`BufferedReader` is a strong choice when your processing logic contains state, multiple conditions, counters, or other logic that feels awkward inside a stream pipeline.


## `Files.lines()` vs `BufferedReader`

Both approaches are useful for reading files without loading everything into memory, but they support slightly different coding styles.

| ApproachBest when      |                                                                     |
| ---------------------- | ------------------------------------------------------------------- |
| `Files.lines()`        | You want to use Java Streams for filtering, mapping, or aggregation |
| `BufferedReader`       | You want explicit control over each line                            |
| `Files.readString()`   | You need the complete contents of a small text file                 |
| `Files.readAllLines()` | You need all lines as a `List<String>`                              |

I tend to use `Files.lines()` when the problem naturally fits a stream pipeline and `BufferedReader` when the logic is easier to understand as a traditional loop. Neither is universally better, so the best choice depends on the work your program needs to perform.


## Writing text files with `Files.writeString()`

Reading files is only half of **reading and writing files in Java**. Writing is just as important, and modern Java makes simple text output very straightforward with `Files.writeString()`.

Enter fullscreen mode Exit fullscreen mode

import java.io.IOException;


import java.nio.file.Files;

import java.nio.file.Path;

public class WriteFileExample {

public static void main(String[] args) {

Path path = Path.of("output.txt");

try {

Files.writeString(path, "Hello from Java!");

        } catch (IOException e) {

e.printStackTrace();

        }

    }

}

If the file does not exist, Java creates it under the default options. If it already exists, the existing content is normally replaced.

This method works well for small reports, generated text, configuration output, and simple files where you already have the content available as a `String`.


## Writing multiple lines with `Files.write()`

When your application already stores content as a collection of strings, `Files.write()` can be more convenient than manually joining everything together.

Enter fullscreen mode Exit fullscreen mode

import java.io.IOException;


import java.nio.file.Files;

import java.nio.file.Path;

import java.util.List;

public class WriteLinesExample {

public static void main(String[] args) {

List\<String> languages = List.of(

"Java",

"Python",

"Go",

"Rust"

        );

try {

Files.write(Path.of("languages.txt"), languages);

        } catch (IOException e) {

e.printStackTrace();

        }

    }

}

The resulting file contains:

Enter fullscreen mode Exit fullscreen mode

Java


Python

Go

Rust

This is especially useful for exports, reports, generated configuration files, or any other situation where your program naturally produces a list of lines.


## Appending data instead of overwriting a file

One common beginner mistake is accidentally replacing a file when the intention was to add new content to it. Java allows you to control this behavior through `StandardOpenOption`.

For example, suppose you are creating a simple application log:

Enter fullscreen mode Exit fullscreen mode

import java.io.IOException;


import java.nio.file.Files;

import java.nio.file.Path;

import java.nio.file.StandardOpenOption;

public class AppendFileExample {

public static void main(String[] args) {

Path path = Path.of("app.log");

try {

Files.writeString(

path,

"Application started" + System.lineSeparator(),

StandardOpenOption.CREATE,

StandardOpenOption.APPEND

            );

        } catch (IOException e) {

e.printStackTrace();

        }

    }

}

`CREATE` tells Java to create the file if it does not already exist, while `APPEND` tells it to add content to the end rather than replacing what is already there.

> **Tip:** Before writing any file, decide whether the correct behavior is to replace the file, append to it, or fail if it already exists. Making that decision explicitly helps prevent accidental data loss.


## Writing files with `BufferedWriter`

`BufferedWriter` is useful when you need to perform repeated text writes. Rather than sending every small write operation directly to the underlying destination, buffering lets Java collect content before writing larger chunks.

Enter fullscreen mode Exit fullscreen mode

import java.io.BufferedWriter;


import java.io.FileWriter;

import java.io.IOException;

public class BufferedWriterExample {

public static void main(String[] args) {

try (BufferedWriter writer =

new BufferedWriter(new FileWriter("output.txt"))) {

writer.write("First line");

writer.newLine();

writer.write("Second line");

writer.newLine();

writer.write("Third line");

        } catch (IOException e) {

e.printStackTrace();

        }

    }

}

The `newLine()` method is useful because it uses the appropriate line separator for the current platform. That makes it a better choice than manually assuming a particular newline character when portability matters.


## Why buffering matters

Buffering is primarily about efficiency. If your program performs many tiny read or write operations, repeatedly communicating with the underlying filesystem can create unnecessary overhead.

Without buffering, the flow conceptually looks like this:

Enter fullscreen mode Exit fullscreen mode

Java program


    ↓

small write

    ↓

disk

Java program

    ↓

small write

    ↓

disk

With buffering, multiple small operations can be collected before a larger write takes place:

Enter fullscreen mode Exit fullscreen mode

Java program


    ↓

buffer

    ↓

buffer fills

    ↓

larger disk write

For a tiny text file, you probably will not notice much difference. For repeated operations, larger workloads, and long-running applications, buffering can become much more important.


## Understanding try-with-resources

One of the most important habits when **reading and writing files in Java** is properly closing resources. Older Java code often contains verbose cleanup logic, but modern Java gives you a much cleaner approach through try-with-resources.

Instead of writing manual cleanup code, you can do this:

Enter fullscreen mode Exit fullscreen mode

try (BufferedReader reader =


new BufferedReader(new FileReader("data.txt"))) {

String line;

while ((line = reader.readLine()) != null) {

System.out.println(line);

    }

}

Java automatically closes the resource when execution leaves the `try` block.

This works with resources that implement `AutoCloseable`, including many Java I/O classes.

> **Personal note:** Resource cleanup feels unimportant when you are experimenting with five-line examples because the program exits almost immediately. In long-running applications, leaking file handles can become a real operational problem, so try-with-resources is a habit worth building from the beginning.


## Reading files with a specific character encoding

Character encoding is easy to ignore until you encounter corrupted text. Modern Java methods often use UTF-8 by default, but you can also specify the charset explicitly when you need predictable behavior.

Enter fullscreen mode Exit fullscreen mode

import java.nio.charset.StandardCharsets;


import java.nio.file.Files;

import java.nio.file.Path;

String content = Files.readString(

Path.of("data.txt"),

StandardCharsets.UTF\_8

);

You can use the same approach when writing:

Enter fullscreen mode Exit fullscreen mode

Files.writeString(


Path.of("output.txt"),

"Hello, Java!",

StandardCharsets.UTF\_8

);

This becomes particularly important when your application processes international text, communicates with external systems, or reads files created by applications using a different encoding.

When encoding assumptions do not match, characters may be displayed incorrectly or corrupted entirely.


## Reading binary files in Java

Not every file contains text. Images, PDFs, ZIP archives, audio files, and many custom formats contain binary data, which means you should work with bytes rather than characters.

For a small binary file, you can use `Files.readAllBytes()`:

Enter fullscreen mode Exit fullscreen mode

import java.io.IOException;


import java.nio.file.Files;

import java.nio.file.Path;

public class BinaryReadExample {

public static void main(String[] args) {

try {

byte[] data = Files.readAllBytes(Path.of("image.png"));

System.out.println("Bytes read: " + data.length);

        } catch (IOException e) {

e.printStackTrace();

        }

    }

}

You can write binary data similarly:

Enter fullscreen mode Exit fullscreen mode

byte[] data = {10, 20, 30, 40};


Files.write(Path.of("data.bin"), data);

This is convenient for small files, but larger binary files are usually better handled with streams so that you do not need to load the entire file into memory.


## Reading binary files with `InputStream`

For larger binary files, `InputStream` allows you to process data in chunks.

Enter fullscreen mode Exit fullscreen mode

import java.io.FileInputStream;


import java.io.IOException;

import java.io.InputStream;

public class InputStreamExample {

public static void main(String[] args) {

try (InputStream input =

new FileInputStream("image.png")) {

byte[] buffer = new byte[4096];

int bytesRead;

while ((bytesRead = input.read(buffer)) != -1) {

System.out.println("Read " + bytesRead + " bytes");

            }

        } catch (IOException e) {

e.printStackTrace();

        }

    }

}

Instead of holding the complete file in memory, this program reads chunks of up to 4,096 bytes at a time.

This pattern is common when handling large uploads, downloads, media files, archives, and network-based data transfer.


## Text streams vs byte streams

A useful distinction in Java file handling is the difference between character-based and byte-based APIs.

| Data typeCommon Java classesTypical use |                                                                              |                                     |
| --------------------------------------- | ---------------------------------------------------------------------------- | ----------------------------------- |
| Characters                              | `Reader`, `Writer`, `BufferedReader`, `BufferedWriter`                       | Text files                          |
| Bytes                                   | `InputStream`, `OutputStream`, `BufferedInputStream`, `BufferedOutputStream` | Images, PDFs, archives, binary data |

If the data represents readable text, character-based APIs are usually easier to work with. If the file contains arbitrary binary data, byte streams are the appropriate choice.

> **Tip:** Do not choose an API based only on the file extension. Think about the actual data representation inside the file and whether your program should interpret it as characters or raw bytes.


## Checking whether a file exists

Before reading or modifying a file, you may want to check whether it exists.

Enter fullscreen mode Exit fullscreen mode

import java.nio.file.Files;


import java.nio.file.Path;

Path path = Path.of("config.txt");

if (Files.exists(path)) {

System.out.println("File exists");

} else {

System.out.println("File not found");

}

You can also determine whether the path points to a regular file:

Enter fullscreen mode Exit fullscreen mode

if (Files.isRegularFile(path)) {


System.out.println("This is a regular file");

}

Or whether it represents a directory:

Enter fullscreen mode Exit fullscreen mode

if (Files.isDirectory(path)) {


System.out.println("This is a directory");

}

These checks become useful when your program accepts paths from users or interacts with files that may be created and removed outside your application's control.


## Creating directories before writing files

Suppose your application needs to write a report here:

Enter fullscreen mode Exit fullscreen mode

reports/2026/summary.txt


If the parent directories do not exist, the write operation may fail. You can create the entire directory hierarchy with `Files.createDirectories()`.

Enter fullscreen mode Exit fullscreen mode

Path directory = Path.of("reports", "2026");


Files.createDirectories(directory);

You can then build the file path using `resolve()`:

Enter fullscreen mode Exit fullscreen mode

Path file = directory.resolve("summary.txt");


Files.writeString(file, "Annual report");

Using `resolve()` keeps path construction readable and avoids manually concatenating strings.


## Handling `IOException` properly

Many file operations can throw `IOException`, and that is not just a compiler inconvenience. File operations genuinely fail for many reasons, so your application should decide what to do when that happens.

For small learning examples, you often see:

Enter fullscreen mode Exit fullscreen mode

catch (IOException e) {


e.printStackTrace();

}

In a real application, you may want something more intentional:

Enter fullscreen mode Exit fullscreen mode

try {


String content = Files.readString(Path.of("config.txt"));

} catch (IOException e) {

System.err.println("Unable to read configuration file.");

}

Some common reasons file operations fail include:

-  The file does not exist. 
-  Your application does not have permission to access it. 
-  The filesystem is unavailable. 
-  The disk is full. 
-  The path points to a directory rather than a file. 
-  Another process is interfering with access. 
-  The file uses an unexpected encoding. 
-  A parent directory does not exist. 

Good file-handling code should treat these situations as part of normal error handling rather than as impossible edge cases.


## A practical example: Reading and updating a simple task file

Let's combine several of the ideas from this guide into a small task manager.

Suppose `tasks.txt` contains:

Enter fullscreen mode Exit fullscreen mode

Learn Java streams


Practice file handling

Build a small project

We want to read the existing tasks and then append another one.

Enter fullscreen mode Exit fullscreen mode

import java.io.IOException;


import java.nio.file.Files;

import java.nio.file.Path;

import java.nio.file.StandardOpenOption;

import java.util.List;

public class TaskManager {

public static void main(String[] args) {

Path taskFile = Path.of("tasks.txt");

try {

if (Files.exists(taskFile)) {

List\<String> tasks = Files.readAllLines(taskFile);

System.out.println("Current tasks:");

for (String task : tasks) {

System.out.println("- " + task);

                }

            }

Files.writeString(

taskFile,

System.lineSeparator() + "Review Java NIO",

StandardOpenOption.CREATE,

StandardOpenOption.APPEND

            );

        } catch (IOException e) {

System.err.println("Unable to process task file.");

e.printStackTrace();

        }

    }

}

This small example checks whether a file exists, reads the current content, displays each line, and then appends another task. The same concepts appear in larger systems, even if the surrounding architecture becomes significantly more complex.

---

## Common mistakes when reading and writing files in Java

File I/O is straightforward once you understand the APIs, but a few mistakes appear repeatedly in beginner and production code.

One common issue is loading huge files entirely into memory because a convenience method was easier to write. Methods such as `Files.readString()` and `Files.readAllLines()` are excellent when the file is small, but large datasets and logs should usually be processed incrementally.

Another common mistake is forgetting to close resources. Try-with-resources solves most of this problem cleanly, so you should use it whenever you are working directly with closable streams, readers, or writers.

Encoding assumptions also cause bugs that can be difficult to notice at first. If your application writes text using one encoding and another program reads it using a different one, characters may become corrupted.

Finally, developers sometimes catch every `IOException` and ignore it. If your application fails to read an important configuration file or cannot write critical output, continuing silently can create much harder problems later.

> **Tip:** Treat reading and writing as operations that can fail, and decide what the correct recovery behavior should be before you simply add a `catch` block.


## Which Java file API should you use?

If the number of options feels confusing, this practical decision table is a useful place to start.

| SituationRecommended approach    |                                        |
| -------------------------------- | -------------------------------------- |
| Read a small text file           | `Files.readString()`                   |
| Read all lines from a small file | `Files.readAllLines()`                 |
| Process a large text file        | `Files.lines()` or `BufferedReader`    |
| Write a small string             | `Files.writeString()`                  |
| Write a collection of lines      | `Files.write()`                        |
| Perform many text writes         | `BufferedWriter`                       |
| Read a small binary file         | `Files.readAllBytes()`                 |
| Process large binary data        | `InputStream` or buffered byte streams |
| Append to an existing file       | `StandardOpenOption.APPEND`            |

For most modern Java applications, I recommend starting with `Path` and `Files`. When your problem becomes more stream-oriented, memory-sensitive, or specialized, move toward readers, writers, input streams, and output streams.


## A good way to practice reading and writing files in Java

The fastest way to get comfortable with **reading and writing files in Java** is to build something small rather than trying to memorize every method in the standard library.

A command-line notes application is a good starting point because it forces you to create files, append entries, read previous notes, and handle missing files. A CSV analyzer is another useful project because it introduces line-by-line processing, parsing, validation, and data transformation.

A log analyzer is also a great practice exercise. You can read a log file, find lines containing `"ERROR"`, count them, and then write the result to a summary file.

Enter fullscreen mode Exit fullscreen mode

try (var lines = Files.lines(Path.of("application.log"))) {




long errors = lines

.filter(line -> line.contains("ERROR"))

.count();

Files.writeString(

Path.of("summary.txt"),

"Total errors: " + errors

    );

}

This example is small, but it combines file reading, streams, filtering, aggregation, and file writing in one practical workflow.

> **Personal note:** I learn Java concepts much faster when I connect them together. File I/O becomes far more memorable when you combine it with streams, collections, exceptions, and small projects instead of treating each topic as a separate chapter.


## How file I/O connects to real Java applications

It is easy to think of file handling as a beginner topic, but the underlying concepts appear throughout professional Java development.

Backend services may read configuration files at startup. Data pipelines may process exported CSV or JSON files. Build tools read and generate files continuously. Logging systems write application events to files or forward them to other systems. Command-line applications frequently store local state, while migration utilities often read data from one format and write transformed output somewhere else.

The exact API may change depending on the application, but the fundamentals remain the same. You still need to understand paths, character encoding, resource cleanup, streaming, buffering, error handling, and the difference between text and binary data.

That is why **reading and writing files in Java** is worth learning properly even if you expect to spend most of your career building APIs or cloud services.


## Final thoughts

Learning **reading and writing files in Java** is less about memorizing every class in `java.io` and `java.nio` and more about understanding what kind of data you have and how your application should process it.

For small text files, methods such as `Files.readString()` and `Files.writeString()` keep your code concise and readable. For larger text files, `Files.lines()` and `BufferedReader` let you process data incrementally, while byte streams remain essential when you are working with images, archives, PDFs, and other binary formats.

The most important habits are fairly simple. Use try-with-resources when working with closable resources, think deliberately about encoding, avoid loading huge files into memory unnecessarily, choose append versus overwrite behavior carefully, and treat I/O failures as something your program needs to handle intentionally.

Once those habits become natural, file handling stops feeling like a standalone Java topic. It becomes another practical tool you can use when building configuration systems, log processors, command-line applications, backend services, data pipelines, and larger Java projects.

If you want to keep building your skills beyond this guide, Educative's Java courses are a useful next step because they let you practice concepts such as file I/O alongside the broader Java skills that real applications require.
Enter fullscreen mode Exit fullscreen mode

Top comments (0)