Java File Handling

INTERMEDIATE ~8 min read Tutorial

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:

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

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

java
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

java
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

java
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

java
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

java
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

java
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

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

java
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
Always close file handles

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.

java
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

  1. Write a program that reads its own source file and prints the number of lines.
  2. Append a timestamped log line to a file. Run it several times and verify the lines accumulate.
  3. List all .java files in a directory tree and count them.
  4. Copy a binary file (an image) byte-for-byte to a new location and confirm the sizes match.
  5. Read a CSV file, parse each line by splitting on commas, and print the first column of each row.