File Handling in Java
Read and write files in Java using the File API, BufferedReader/Writer, FileInputStream/OutputStream, and the modern NIO.2 Path/Files API.
Java provides two generations of file I/O APIs: the classic java.io package and the modern java.nio.file (NIO.2) package introduced in Java 7. Both are in active use — NIO.2 is preferred for new code because its methods are more expressive, its error messages are clearer, and it handles edge cases like symbolic links and atomic moves more robustly.
The File Class (Legacy)
java.io.File represents a file path as an object. It does not read or write content directly — it is purely about the path itself and the metadata of what exists at that path. You will encounter it in older codebases and APIs that predate NIO.2.
import java.io.File;
File file = new File("data.txt");
File dir = new File("src/main/resources");
// Check existence and type before operating on a file
System.out.println(file.exists()); // true/false
System.out.println(file.isFile()); // true if it's a regular file
System.out.println(file.isDirectory()); // true if it's a directory
// File metadata
System.out.println(file.getName()); // "data.txt"
System.out.println(file.getAbsolutePath()); // full path from filesystem root
System.out.println(file.length()); // size in bytes
System.out.println(file.lastModified()); // timestamp in milliseconds
// Create and delete
file.createNewFile(); // creates empty file if it doesn't exist
file.delete(); // deletes the file
// Directory operations
dir.mkdirs(); // creates directory and all missing parents
String[] entries = dir.list(); // names of contents (strings only)
File[] files = dir.listFiles(); // contents as File objects (useful for filtering)
Reading Files
BufferedReader — Line by Line
Reading character by character is extremely slow. BufferedReader reads a large chunk of the file into an internal buffer and then serves lines from that buffer, making it orders of magnitude faster. Always use try-with-resources to ensure the reader is closed even if an exception occurs mid-read.
import java.io.*;
// try-with-resources: reader is automatically closed when the block exits
try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.err.println("Failed to read file: " + e.getMessage());
}
Reading All Lines at Once (NIO.2)
For smaller files where you want the whole content in memory, NIO.2 provides one-liner methods that are cleaner than managing a reader loop. Files.lines() is the lazy alternative for large files — it streams lines without loading them all at once.
import java.nio.file.*;
import java.util.List;
// Read all lines into a List — simple but loads the entire file into memory
List<String> lines = Files.readAllLines(Path.of("data.txt"));
lines.forEach(System.out::println);
// Read entire file as one String — Java 11+
String content = Files.readString(Path.of("data.txt"));
System.out.println(content);
// Stream lines lazily — good for large files; only processes lines you consume
try (var stream = Files.lines(Path.of("data.txt"))) {
stream.filter(l -> l.contains("error"))
.forEach(System.out::println);
}
FileInputStream — Reading Binary Data
Text methods assume a character encoding. For binary data — images, PDFs, ZIP files — use FileInputStream and work with raw bytes. Reading into a buffer avoids per-byte overhead.
try (FileInputStream fis = new FileInputStream("image.png")) {
byte[] buffer = new byte[4096]; // read 4KB at a time
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
// process buffer[0..bytesRead-1] — bytesRead may be less than buffer.length on the last chunk
System.out.println("Read " + bytesRead + " bytes");
}
}
Writing Files
BufferedWriter
Just as BufferedReader buffers reads, BufferedWriter batches writes into a buffer before flushing to disk, which is significantly faster than writing character by character. Pass true as the second argument to FileWriter to append rather than overwrite.
// Overwrite (or create) the file
try (BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {
writer.write("Line 1");
writer.newLine(); // platform-safe newline (\n on Unix, \r\n on Windows)
writer.write("Line 2");
writer.newLine();
}
// Append to an existing file — pass true to FileWriter
try (BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt", true))) {
writer.write("Line 3 (appended)");
writer.newLine();
}
PrintWriter — Formatted Output
PrintWriter adds printf-style formatting on top of BufferedWriter. It is convenient when you need to write structured text like reports or CSVs.
try (PrintWriter pw = new PrintWriter(new FileWriter("report.txt"))) {
pw.println("=== Report ===");
pw.printf("Total: %d items%n", 42);
pw.printf("Average: %.2f%n", 3.14);
}
Writing All Content at Once (NIO.2)
For simple write operations, NIO.2 one-liners are cleaner than managing a writer. StandardOpenOption flags give fine-grained control over whether to create, overwrite, or append.
import java.nio.file.*;
import java.util.List;
// Write a String — overwrites if the file exists
Files.writeString(Path.of("output.txt"), "Hello, file!");
// Write a list of lines — each element becomes one line
List<String> lines = List.of("Line 1", "Line 2", "Line 3");
Files.write(Path.of("lines.txt"), lines);
// Append — CREATE creates the file if it doesn't exist; APPEND adds to the end
Files.writeString(Path.of("log.txt"), "New entry\n",
StandardOpenOption.APPEND, StandardOpenOption.CREATE);
NIO.2 Path and Files API
Path is the NIO.2 replacement for File. It is more expressive, handles platform path separators transparently, and supports operations like normalize() and resolve() that the old File class lacks. The companion Files class provides static methods for all common file operations.
import java.nio.file.*;
import java.io.IOException;
// Path.of() builds a path safely using the platform separator
Path path = Path.of("data", "users.txt");
// Path information
System.out.println(path.getFileName()); // "users.txt"
System.out.println(path.getParent()); // "data"
System.out.println(path.toAbsolutePath()); // full absolute path
System.out.println(path.normalize()); // resolves . and .. segments
// Existence and type checks
Files.exists(path)
Files.isRegularFile(path)
Files.isDirectory(path)
Files.isReadable(path)
Files.isWritable(path)
// Creating files and directories
Files.createFile(path); // create empty file; throws if exists
Files.createDirectories(Path.of("a/b/c")); // create all missing parent directories
// Copying and moving — REPLACE_EXISTING prevents failure if destination exists
Files.copy(Path.of("src.txt"), Path.of("dst.txt"), StandardCopyOption.REPLACE_EXISTING);
Files.move(Path.of("old.txt"), Path.of("new.txt"), StandardCopyOption.REPLACE_EXISTING);
// Deleting
Files.delete(path); // throws NoSuchFileException if not found
Files.deleteIfExists(path); // silent if not found — safer for cleanup code
// Metadata
long size = Files.size(path);
var lastModified = Files.getLastModifiedTime(path);
Walking Directory Trees
NIO.2 makes recursive directory operations trivial. Files.walk() returns a lazy Stream<Path>, so you can filter and process with standard stream operations rather than writing recursive loops.
// List the immediate contents of a directory
try (var stream = Files.list(Path.of("src"))) {
stream.forEach(System.out::println);
}
// Walk recursively — returns every file and directory under the root
try (var stream = Files.walk(Path.of("src"))) {
stream.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".java"))
.forEach(System.out::println);
}
// Find files matching attributes — more efficient than walk + filter for deep trees
try (var stream = Files.find(Path.of("src"), 10,
(p, attrs) -> attrs.isRegularFile() && p.toString().endsWith(".java"))) {
stream.forEach(System.out::println);
}
Practical Patterns
Reading a CSV File
A reusable CSV reader that maps headers to values, handling variable numbers of columns cleanly.
import java.nio.file.*;
import java.util.*;
public static List<Map<String, String>> readCsv(String filePath) throws IOException {
List<String> lines = Files.readAllLines(Path.of(filePath));
if (lines.isEmpty()) return List.of();
String[] headers = lines.get(0).split(",");
List<Map<String, String>> records = new ArrayList<>();
for (int i = 1; i < lines.size(); i++) {
String[] values = lines.get(i).split(",");
Map<String, String> row = new LinkedHashMap<>(); // LinkedHashMap preserves column order
for (int j = 0; j < headers.length; j++) {
row.put(headers[j].trim(), j < values.length ? values[j].trim() : "");
}
records.add(row);
}
return records;
}
Writing a Log File
A simple append-based file logger using NIO.2. The CREATE option ensures the file is created on first use.
import java.nio.file.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class FileLogger {
private final Path logFile;
private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public FileLogger(String filePath) { this.logFile = Path.of(filePath); }
public void log(String level, String message) throws IOException {
String entry = String.format("[%s] %s %s%n", FMT.format(LocalDateTime.now()), level, message);
// APPEND + CREATE: adds to existing file, or creates it if it doesn't exist
Files.writeString(logFile, entry, StandardOpenOption.APPEND, StandardOpenOption.CREATE);
}
}
Project: Expense Tracker
This project combines NIO.2 file I/O with a CSV persistence layer. The tracker loads existing expenses on startup, appends new ones, and computes totals by category — all using the cleaner NIO.2 API throughout.
import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.time.LocalDate;
public class ExpenseTracker {
record Expense(LocalDate date, String category, String description, double amount) {
// Serialise to a CSV line
String toCsv() { return date + "," + category + "," + description + "," + amount; }
// Deserialise from a CSV line — limit to 4 fields so description can contain commas
static Expense fromCsv(String line) {
String[] p = line.split(",", 4);
return new Expense(LocalDate.parse(p[0]), p[1], p[2], Double.parseDouble(p[3]));
}
}
private final Path dataFile;
private final List<Expense> expenses = new ArrayList<>();
public ExpenseTracker(String dataFilePath) throws IOException {
this.dataFile = Path.of(dataFilePath);
if (Files.exists(dataFile)) {
Files.readAllLines(dataFile).stream()
.skip(1) // skip the CSV header row
.map(Expense::fromCsv)
.forEach(expenses::add);
}
}
public void add(Expense expense) throws IOException {
expenses.add(expense);
save(); // persist after every addition
}
private void save() throws IOException {
List<String> lines = new ArrayList<>();
lines.add("date,category,description,amount"); // header row
expenses.stream().map(Expense::toCsv).forEach(lines::add);
Files.write(dataFile, lines); // overwrites entire file
}
public Map<String, Double> totalByCategory() {
Map<String, Double> totals = new TreeMap<>(); // TreeMap keeps categories sorted
for (Expense e : expenses) {
totals.merge(e.category(), e.amount(), Double::sum);
}
return totals;
}
public static void main(String[] args) throws IOException {
ExpenseTracker tracker = new ExpenseTracker("expenses.csv");
tracker.add(new Expense(LocalDate.now(), "Food", "Lunch", 12.50));
tracker.add(new Expense(LocalDate.now(), "Transport", "Bus", 2.80));
tracker.add(new Expense(LocalDate.now(), "Food", "Coffee", 4.20));
System.out.println("Expenses by category:");
tracker.totalByCategory().forEach((cat, total) ->
System.out.printf(" %-15s $%.2f%n", cat, total));
}
}