Java File Handling
Reading and writing files is a foundational skill: configurations, logs, data sets, exports. Java has two APIs for files — the classic java.io.File with reader/writer classes, and the modern java.nio.file package added in Java 7. The modern API is cleaner, safer (path validation), and supports try-with-resources for automatic resource cleanup.
This tutorial covers reading and writing text files, byte files, directories, and the convenience methods on Files that make small files a one-liner.
1. Path and Paths
The modern API uses Path instead of File. A Path is an immutable representation of a filesystem path:
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.FileSystems;
class=class="tok-str">"tok-cmt">// relative path - resolved against the working directory
Path p1 = Path.of("data/input.txt");
class=class="tok-str">"tok-cmt">// absolute path
Path p2 = Path.of("/var/log/app.log");
class=class="tok-str">"tok-cmt">// using Paths.get (older, equivalent)
Path p3 = Paths.get("data", "input.txt");
class=class="tok-str">"tok-cmt">// resolve, normalize, relativize
Path base = Path.of("/opt/app");
Path full = base.resolve("data/input.txt"); class=class="tok-str">"tok-cmt">// /opt/app/data/input.txt
Path norm = full.normalize(); class=class="tok-str">"tok-cmt">// collapse .. and .
Path rel = base.relativize(full); class=class="tok-str">"tok-cmt">// data/input.txt
class=class="tok-str">"tok-cmt">// convert to File for legacy APIs
java.io.File f = p1.toFile();
Always prefer Path over File in new code. If you need a File for a legacy API, call path.toFile().
2. Reading a Text File
For small files, Files.readString is a one-liner:
import java.nio.file.Files;
import java.nio.file.Path;
Path file = Path.of("data.txt");
class=class="tok-str">"tok-cmt">// read the whole file as a single string (UTF-class="tok-num">8 by default)
String content = Files.readString(file);
System.out.println(content.length() + " chars");
class=class="tok-str">"tok-cmt">// read all lines as a List<String>
java.util.List<String> lines = Files.readAllLines(file);
System.out.println(lines.size() + " lines");
class=class="tok-str">"tok-cmt">// read all bytes
byte[] data = Files.readAllBytes(file);
For files that may be large, stream them line by line to avoid loading the whole content into memory:
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
Path big = Path.of("large.log");
class=class="tok-str">"tok-cmt">// lazy stream of lines - does not load all into memory
try (Stream<String> lines = Files.lines(big)) {
long count = lines
.filter(line -> line.contains("ERROR"))
.count();
System.out.println(count + " error lines");
} catch (java.io.IOException e) {
e.printStackTrace();
}
class=class="tok-str">"tok-cmt">// Files.lines uses UTF-class="tok-num">8; for other encodings, use
class=class="tok-str">"tok-cmt">// Files.lines(path, Charset.forName("ISO-class="tok-num">8859-class="tok-num">1"))
3. Writing a Text File
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
Path out = Path.of("output.txt");
class=class="tok-str">"tok-cmt">// overwrite or create
Files.writeString(out, "Hello, World!\n");
class=class="tok-str">"tok-cmt">// with options - append to existing
Files.writeString(out, "another line\n",
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
class=class="tok-str">"tok-cmt">// write a list of lines
java.util.List<String> lines = java.util.List.of("a","b","c");
Files.write(Path.of("list.txt"), lines);
class=class="tok-str">"tok-cmt">// specify encoding explicitly
import java.nio.charset.StandardCharsets;
Files.writeString(out, "unicode: alpha-beta-gamma",
StandardCharsets.UTF_8);
StandardOpenOption gives fine control: CREATE, APPEND, TRUNCATE_EXISTING, WRITE, READ, SYNC.
4. Reading and Writing Bytes
Path in = Path.of("input.bin");
Path out = Path.of("output.bin");
class=class="tok-str">"tok-cmt">// read all bytes into memory
byte[] data = Files.readAllBytes(in);
class=class="tok-str">"tok-cmt">// write bytes
Files.write(out, data);
class=class="tok-str">"tok-cmt">// for large files, use InputStream / OutputStream
try (var is = Files.newInputStream(in);
var os = Files.newOutputStream(out)) {
byte[] buf = new byte[class="tok-num">8192];
int n;
while ((n = is.read(buf)) != -class="tok-num">1) {
os.write(buf, class="tok-num">0, n);
}
}
For binary data (images, archives), use the byte methods. For very large files, use InputStream and OutputStream with explicit buffering.
5. Listing and Walking Directories
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
Path dir = Path.of(".");
class=class="tok-str">"tok-cmt">// list immediate children (non-recursive)
try (Stream<Path> children = Files.list(dir)) {
children.forEach(System.out::println);
}
class=class="tok-str">"tok-cmt">// walk recursively
try (Stream<Path> walk = Files.walk(dir)) {
long count = walk.filter(Files::isRegularFile).count();
System.out.println(count + " files");
}
class=class="tok-str">"tok-cmt">// find with a BiPredicate filter
try (Stream<Path> found = Files.find(dir, Integer.MAX_VALUE,
(p, attr) -> p.toString().endsWith(".java") && attr.isRegularFile())) {
found.forEach(System.out::println);
}
Files.walk returns a stream of all paths recursively. For deep directory trees, use Files.walk(path, depth) to limit recursion depth.
6. Creating and Deleting Files
class=class="tok-str">"tok-cmt">// create an empty file (fails if it already exists)
Files.createFile(Path.of("new.txt"));
class=class="tok-str">"tok-cmt">// create directories including missing parents
Files.createDirectories(Path.of("a/b/c"));
class=class="tok-str">"tok-cmt">// create a single directory (parent must exist)
Files.createDirectory(Path.of("a/b/c"));
class=class="tok-str">"tok-cmt">// delete a file or empty directory
Files.delete(Path.of("new.txt"));
class=class="tok-str">"tok-cmt">// delete if it exists - no exception if missing
boolean deleted = Files.deleteIfExists(Path.of("new.txt"));
7. Copying and Moving
Path src = Path.of("input.txt");
Path dst = Path.of("copy.txt");
class=class="tok-str">"tok-cmt">// copy
Files.copy(src, dst);
class=class="tok-str">"tok-cmt">// with options: overwrite if exists
Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING);
class=class="tok-str">"tok-cmt">// move / rename
Files.move(src, dst);
Files.move(src, dst, StandardCopyOption.ATOMIC_MOVE);
class=class="tok-str">"tok-cmt">// copy to a different filesystem (e.g. to a ZIP)
Files.copy(src, Path.of("/mnt/archive/" + src.getFileName()));
8. File Metadata
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
Path p = Path.of("data.txt");
class=class="tok-str">"tok-cmt">// quick checks
boolean exists = Files.exists(p);
boolean isDir = Files.isDirectory(p);
boolean isFile = Files.isRegularFile(p);
boolean readable = Files.isReadable(p);
boolean writable = Files.isWritable(p);
class=class="tok-str">"tok-cmt">// size in bytes
long size = Files.size(p);
class=class="tok-str">"tok-cmt">// full attributes
BasicFileAttributes attrs = Files.readAttributes(p, BasicFileAttributes.class);
FileTime created = attrs.creationTime();
FileTime modified = attrs.lastModifiedTime();
FileTime accessed = attrs.lastAccessTime();
9. The Idiomatic Pattern: try-with-resources
For readers and writers, always use try-with-resources so the underlying file handle is closed even on exception:
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.nio.file.Files;
import java.nio.file.Path;
Path in = Path.of("in.txt");
Path out = Path.of("out.txt");
try (BufferedReader reader = Files.newBufferedReader(in);
BufferedWriter writer = Files.newBufferedWriter(out)) {
String line;
while ((line = reader.readLine()) != null) {
writer.write(line.toUpperCase());
writer.newLine();
}
} class=class="tok-str">"tok-cmt">// both reader and writer closed automatically, even on exception
Unclosed file handles leak operating system resources. On Linux each process has a default limit of 1024 open files; once you hit it, every new file operation throws TooManyOpenFilesException. Try-with-resources eliminates this class of bug.
10. Character Encodings
Always specify the encoding when reading or writing text. UTF-8 is the default on the modern Files methods, which is one of their big advantages over the older FileReader / FileWriter classes — those used the platform default encoding, which differed between Windows and Linux and caused many encoding bugs.
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
class=class="tok-str">"tok-cmt">// modern Files methods default to UTF-class="tok-num">8
String s = Files.readString(Path.of("data.txt")); class=class="tok-str">"tok-cmt">// UTF-class="tok-num">8
class=class="tok-str">"tok-cmt">// explicit charset for the older API
var reader = new java.io.FileReader(
"data.txt", StandardCharsets.UTF_8);
class=class="tok-str">"tok-cmt">// convert between encodings
byte[] utf8 = "Hello".getBytes(StandardCharsets.UTF_8);
byte[] latin1 = "Hello".getBytes(StandardCharsets.ISO_8859_1);
String decoded = new String(utf8, StandardCharsets.UTF_8);
class=class="tok-str">"tok-cmt">// detect the platform default (avoid relying on it)
Charset def = Charset.defaultCharset();
System.out.println("Default charset: " + def); class=class="tok-str">"tok-cmt">// UTF-class="tok-num">8 on Linux, windows-class="tok-num">1252 on Windows
Exercises
- Write a program that reads its own source file and prints the number of lines.
- Append a timestamped log line to a file. Run it several times and verify the lines accumulate.
- List all
.javafiles in a directory tree and count them. - Copy a binary file (an image) byte-for-byte to a new location and confirm the sizes match.
- Read a CSV file, parse each line by splitting on commas, and print the first column of each row.