Why File Handling Matters
Programs that can only work with data in memory lose everything when they stop running. File handling gives your programs persistence — the ability to save data to disk and read it back later. Common use cases include:
- Configuration files — reading settings when your application starts
- Log files — recording what your program did (covered in detail in the Java Logging tutorial)
- Data import/export — reading CSV files from spreadsheets, generating reports
- Processing text — analyzing logs, transforming data files, batch processing
Java has two file I/O APIs:
-
java.io— The original API (Java 1.0). Classes likeFile,FileReader,BufferedReader. You'll encounter these in older code. -
java.nio.file— The modern API (Java 7+). Classes likePath,Files. Cleaner, more capable, and the one you should use in new code.
About This Tutorial
This tutorial focuses on the modern
java.nio.file
API for new code. We briefly show the classic
java.io
approach where it helps you understand what's happening underneath, but every recommendation points to the newer API. We cover
text files
only — binary file handling (images, audio, serialized objects) requires different classes and is a separate topic.
Path vs. File: Understanding the Difference
Before reading or writing anything, you need to represent the file's location. Java has two classes for this:
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
public class PathVsFile {
public static void main(String[] args) {
// OLD way (java.io.File)
File file = new File("data/employees.txt");
// NEW way (java.nio.file.Path)
Path path = Paths.get("data", "employees.txt");
// Java 11+ shortcut — equivalent to Paths.get()
Path path2 = Path.of("data", "employees.txt");
System.out.println("File: " + file.getAbsolutePath());
System.out.println("Path: " + path.toAbsolutePath());
System.out.println("Path2: " + path2.toAbsolutePath());
// You can convert between them when needed
Path fromFile = file.toPath(); // File → Path
File fromPath = path.toFile(); // Path → File
}
}
Key differences:
-
PathusesPaths.get("data", "employees.txt")with multiple string arguments — it joins them with the correct separator (/on Linux/Mac,\on Windows). WithFile, you'd have to manually write"data" + File.separator + "employees.txt". -
Pathis an interface (implemented by the JVM).Fileis a class with methods that both represent a path and perform operations.Pathseparates these concerns — the path itself is just a path, and operations go through theFilesutility class. -
Pathhas useful methods likegetParent(),getFileName(),resolve(), andnormalize()that make path manipulation cleaner.
Use Forward Slashes in Java Strings
Even on Windows, you can use forward slashes (
/
) in Java path strings:
"data/employees.txt"
works on every operating system. Windows Java automatically converts them. Avoid backslashes in string literals because
\
is an escape character in Java — you'd need to write
"data\\employees.txt"
, which is harder to read.
The File Class: Checking Files and Directories
Before trying to read or write a file, you often need to check whether it exists, whether it's a file or a directory, or list the contents of a directory. The
java.io.File
class (or the newer
Files
methods) handles this:
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class FileChecks {
public static void main(String[] args) {
Path path = Path.of("data/employees.txt");
// Modern way — static methods on the Files class
System.out.println("Exists: " + Files.exists(path));
System.out.println("Is file: " + Files.isRegularFile(path));
System.out.println("Is dir: " + Files.isDirectory(path));
System.out.println("Readable: " + Files.isReadable(path));
System.out.println("Writable: " + Files.isWritable(path));
try {
System.out.println("Size: " + Files.size(path) + " bytes");
} catch (IOException e) {
System.out.println("Size: (cannot read)");
}
// Classic way — instance methods on the File class
// You'll see this in older codebases:
File file = path.toFile();
System.out.println("\n--- Classic File class ---");
System.out.println("Exists: " + file.exists());
System.out.println("Is file: " + file.isFile());
System.out.println("Is dir: " + file.isDirectory());
System.out.println("Length: " + file.length() + " bytes");
}
}
For new code, prefer
Files.exists()
,
Files.isRegularFile()
, etc. They throw checked exceptions for error cases (like a broken symlink) instead of silently returning
false
like
File.exists()
does. This means you're less likely to mistake a permission error for "file doesn't exist."
Reading Text Files
Java provides several ways to read text files, ranging from the classic approach you'll see in legacy code to concise one-liners in modern Java. We cover all of them so you can recognize each style when you encounter it.
Reading All Lines at Once (Java 7+)
For small to medium files (up to a few megabytes), the simplest approach is
Files.readAllLines()
, which loads the entire file into a
List<String>
:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class ReadAllLines {
public static void main(String[] args) {
// Create a sample file for this example
Path path = Path.of("hello.txt");
try {
// readAllLines loads the ENTIRE file into memory as a List
List<String> lines = Files.readAllLines(path);
System.out.println("File has " + lines.size() + " lines:");
for (String line : lines) {
System.out.println(" " + line);
}
} catch (IOException e) {
System.err.println("Could not read file: " + e.getMessage());
}
}
}
Assuming
hello.txt
contains:
The output is:
Don't Use readAllLines() on Large Files
readAllLines()
loads the entire file into memory at once. A 1 GB file would consume roughly 2 GB of heap memory (the file content plus the
String
object overhead). For large files, use
Files.lines()
(shown next) or
BufferedReader
— both process one line at a time.
Reading with Streams (Java 8+)
Files.lines()
returns a
Stream<String>
that reads the file lazily — one line at a time. This is ideal for large files and pairs naturally with Java Streams operations:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
public class ReadWithStream {
public static void main(String[] args) {
Path path = Path.of("hello.txt");
// Files.lines() returns a Stream — must be closed when done
try (Stream<String> stream = Files.lines(path)) {
// You can use any Stream operations
long lineCount = stream
.filter(line -> !line.isBlank()) // skip empty lines
.peek(System.out::println) // print each line
.count(); // count non-empty lines
System.out.println("Non-empty lines: " + lineCount);
} catch (IOException e) {
System.err.println("Could not read file: " + e.getMessage());
}
}
}
This approach uses constant memory regardless of file size — only one line is in memory at a time. The try-with-resources is important because the stream holds an open file handle.
Reading Line by Line with BufferedReader (Classic)
This is the traditional approach you'll see in codebases written before Java 8. It's still perfectly valid and gives you the most control over the reading process:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class ReadWithBufferedReader {
public static void main(String[] args) {
// Classic pattern: FileReader wrapped in BufferedReader
// BufferedReader provides readLine() — FileReader alone does not
try (BufferedReader reader = new BufferedReader(
new FileReader("hello.txt"))) {
String line;
int lineNumber = 0;
// readLine() returns null when end of file is reached
while ((line = reader.readLine()) != null) {
lineNumber++;
System.out.println(lineNumber + ": " + line);
}
} catch (IOException e) {
System.err.println("Could not read file: " + e.getMessage());
}
}
}
Why BufferedReader?
FileReader
reads one character at a time from the disk, which is slow.
BufferedReader
wraps around it and reads a large chunk (typically 8 KB) into memory at once, then serves characters from that buffer. The difference is dramatic — reading a file character-by-character through
FileReader
might take 100x longer than reading through
BufferedReader
. Always wrap file readers in
BufferedReader
if you're using the classic API.
Reading an Entire File as a Single String (Java 11+)
If you need the entire file content as one string (common for small config files or templates), Java 11 added
Files.readString()
:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class ReadAsString {
public static void main(String[] args) {
try {
String content = Files.readString(Path.of("hello.txt"));
System.out.println("File content (" + content.length() + " chars):");
System.out.println(content);
} catch (IOException e) {
System.err.println("Could not read file: " + e.getMessage());
}
}
}
Same Size Warning
Files.readString()
loads the entire file into memory as a single
String
. Use it only for files you're confident are small (a few KB at most). For anything larger, use
Files.lines()
or
BufferedReader
.
Writing Text Files
Writing follows a similar pattern: you have classic and modern options, and you must always close the writer when done.
Writing with Files.write() (Java 7+)
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class WriteWithFiles {
public static void main(String[] args) throws IOException {
Path path = Path.of("output.txt");
List<String> lines = List.of(
"First line",
"Second line",
"Third line"
);
// Write all lines — creates the file if it doesn't exist,
// OVERWRITES it if it does exist
Files.write(path, lines);
System.out.println("Wrote " + lines.size() + " lines to " + path);
// To APPEND instead of overwrite, use StandardOpenOption:
import java.nio.file.StandardOpenOption;
Files.write(path, List.of("Appended line"),
StandardOpenOption.APPEND);
}
}
Writing with Files.writeString() (Java 11+)
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class WriteString {
public static void main(String[] args) throws IOException {
Path path = Path.of("greeting.txt");
// Write a single string (overwrite)
Files.writeString(path, "Hello, world!\nThis is a file.\n");
// Append more text
Files.writeString(path, "Appended content.\n",
StandardOpenOption.APPEND);
System.out.println("File written successfully.");
}
}
Writing with BufferedWriter (Classic)
When you need to build the file content incrementally (inside loops, conditional logic, etc.),
BufferedWriter
gives you line-by-line control:
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
public class WriteWithBufferedWriter {
public static void main(String[] args) {
Path path = Path.of("numbers.txt");
// FileWriter wrapped in BufferedWriter for performance
// The 'true' argument means APPEND mode; 'false' or omitted means OVERWRITE
try (BufferedWriter writer = new BufferedWriter(
new FileWriter(path.toFile()))) {
for (int i = 1; i <= 10; i++) {
writer.write("Line " + i + ": " + (i * i));
writer.newLine(); // writes the platform-specific line separator
}
System.out.println("Wrote 10 lines to " + path);
} catch (IOException e) {
System.err.println("Could not write file: " + e.getMessage());
}
}
}
Use newLine() Instead of "\n"
writer.newLine()
writes the correct line separator for the current operating system (
\n
on Linux/Mac,
\r\n
on Windows). If you hardcode
"\n"
, files created on Linux will look wrong when opened in some Windows editors (all on one line). For files that stay on one machine this rarely matters, but
newLine()
is the correct practice for portable code.
Character Encoding: Why It Matters
Text files are stored as bytes. The character encoding (charset) defines how those bytes map to characters. If you read a file with the wrong encoding, you get garbled text (mojibake). The most common encodings:
- UTF-8 — The modern standard. Supports every character in every language. This is what you should use for virtually everything.
- ISO-8859-1 (Latin-1) — Old Western European encoding. Cannot represent characters outside Western European languages.
- Windows-1252 — Microsoft's extension of Latin-1. Still common in older Windows systems.
-
UTF-16
— Used internally by Java for
charandString. Rarely used for files.
The problem:
FileReader
and
FileWriter
use the
platform default encoding
, which varies by operating system. On most modern systems it's UTF-8, but on some Windows configurations it's Windows-1252. This means the same code can produce different results on different machines.
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class EncodingDemo {
public static void main(String[] args) throws IOException {
Path path = Path.of("greeting.txt");
// BAD — uses platform default encoding (unpredictable)
String content1 = Files.readString(path);
// GOOD — explicitly specifies UTF-8 (predictable, works everywhere)
String content2 = Files.readString(path, StandardCharsets.UTF_8);
// Writing with explicit encoding
Files.writeString(
Path.of("output-utf8.txt"),
"Héllo, wörld! 你好世界! ",
StandardCharsets.UTF_8
);
// Reading from a file that's in Windows-1252 encoding
String windowsContent = Files.readString(
Path.of("legacy-data.txt"),
java.nio.charset.Charset.forName("Windows-1252")
);
// The modern Files methods accept a Charset parameter
Files.write(
Path.of("output.txt"),
List.of("Line 1", "Line 2"),
StandardCharsets.UTF_8
);
}
}
Rule: Always Specify the Encoding
When reading or writing text files,
always pass an explicit charset
— typically
StandardCharsets.UTF_8
. Never rely on the platform default. This is especially important for files that might be transferred between systems (Linux servers, Windows workstations, cloud storage). A file written with UTF-8 on Linux and read with Windows-1252 on Windows will corrupt any non-ASCII characters.
Working with CSV Files
CSV (Comma-Separated Values) is one of the most common file formats for data exchange. Despite its simplicity, there are edge cases you need to handle: fields containing commas, fields containing quotes, and empty fields.
Here's a robust CSV reader that handles quoted fields correctly, followed by a writer:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
/**
* A simple CSV parser that handles:
* - Quoted fields: "Smith, John" → Smith, John
* - Embedded quotes: "He said ""hello""" → He said "hello"
* - Empty fields: a,,b → ["a", "", "b"]
*/
public class CsvReader {
public static List<String[]> read(Path path) throws IOException {
List<String[]> rows = new ArrayList<>();
try (var reader = Files.newBufferedReader(path)) {
String line;
while ((line = reader.readLine()) != null) {
rows.add(parseLine(line));
}
}
return rows;
}
private static String[] parseLine(String line) {
List<String> fields = new ArrayList<>();
StringBuilder field = new StringBuilder();
boolean inQuotes = false;
for (int i = 0; i < line.length(); i++) {
char c = line.charAt(i);
if (inQuotes) {
if (c == '"') {
// Two consecutive quotes = one literal quote inside a field
if (i + 1 < line.length() && line.charAt(i + 1) == '"') {
field.append('"');
i++; // skip the second quote
} else {
inQuotes = false; // closing quote
}
} else {
field.append(c);
}
} else {
if (c == '"') {
inQuotes = true;
} else if (c == ',') {
fields.add(field.toString());
field = new StringBuilder();
} else {
field.append(c);
}
}
}
fields.add(field.toString()); // don't forget the last field
return fields.toArray(new String[0]);
}
}
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.charset.StandardCharsets;
import java.util.List;
public class CsvWriter {
/**
* Writes a list of rows to a CSV file.
* Fields containing commas or quotes are automatically quoted.
*/
public static void write(Path path, List<String[]> rows) throws IOException {
try (BufferedWriter writer = Files.newBufferedWriter(
path, StandardCharsets.UTF_8)) {
for (String[] row : rows) {
for (int i = 0; i < row.length; i++) {
if (i > 0) {
writer.write(',');
}
writer.write(escapeField(row[i]));
}
writer.newLine();
}
}
}
/**
* If the field contains a comma, quote, or newline, wrap it in quotes
* and double any internal quotes.
*/
private static String escapeField(String field) {
if (field.contains(",") || field.contains("\"") || field.contains("\n")) {
return "\"" + field.replace("\"", "\"\"") + "\"";
}
return field;
}
}
Let's test them together:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
public class CsvDemo {
public static void main(String[] args) throws IOException {
Path csvPath = Path.of("employees.csv");
// Write a CSV with tricky fields
List<String[]> data = List.of(
new String[]{"Name", "Department", "Notes"},
new String[]{"Alice Johnson", "Engineering", "Senior developer"},
new String[]{"Smith, Bob", "Marketing", "Name has a comma"},
new String[]{"Carol Davis", "Sales", "Said \"hello world\" at the meeting"}
);
CsvWriter.write(csvPath, data);
System.out.println("Wrote CSV file. Raw content:");
System.out.println(Files.readString(csvPath));
// Read it back
List<String[]> rows = CsvReader.read(csvPath);
System.out.println("Parsed back:");
for (String[] row : rows) {
System.out.println(" " + Arrays.toString(row));
}
}
}
Notice how the raw CSV file correctly quotes
"Smith, Bob"
and escapes the internal quotes in
"Said ""hello world"" at the meeting"
. The parser reads them back exactly as the original strings. This is the correct CSV behavior defined by
RFC 4180
.
When to Use a Library Instead
The CSV reader/writer above handles the most common cases. For production applications with complex requirements (different delimiters, null handling, type conversion, very large files), consider using a library like OpenCSV or Apache Commons CSV . They handle edge cases we haven't covered here (like multiline quoted fields) and provide a cleaner API for mapping CSV rows directly to Java objects.
Working with Properties Files
Java has built-in support for
.properties
files — simple key-value configuration files widely used in Java applications. You've already seen them if you followed the
Java Logging
tutorial.
# Application Configuration
# Lines starting with # are comments
app.name=My Application
app.version=2.1.0
db.host=localhost
db.port=3306
db.name=company_db
max.connections=10
debug.mode=false
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Properties;
public class PropertiesDemo {
public static void main(String[] args) {
Properties props = new Properties();
// --- READING ---
try (FileInputStream input = new FileInputStream("config.properties")) {
props.load(input);
} catch (IOException e) {
System.err.println("Could not load config: " + e.getMessage());
return;
}
// Retrieve values — all values are Strings
String appName = props.getProperty("app.name");
String dbHost = props.getProperty("db.host");
String dbPort = props.getProperty("db.port", "3306"); // default value
String missing = props.getProperty("nonexistent", "default");
System.out.println("App: " + appName);
System.out.println("Database: " + dbHost + ":" + dbPort);
System.out.println("Missing key: " + missing);
// Type conversion — Properties only stores Strings
int maxConn = Integer.parseInt(props.getProperty("max.connections"));
boolean debug = Boolean.parseBoolean(props.getProperty("debug.mode"));
System.out.println("Max connections: " + maxConn);
System.out.println("Debug mode: " + debug);
// --- WRITING ---
// Modify a value
props.setProperty("app.version", "2.2.0");
props.setProperty("last.updated", java.time.LocalDate.now().toString());
try (FileOutputStream output = new FileOutputStream("config.properties")) {
props.store(output, "Updated by PropertiesDemo");
System.out.println("\nConfig file updated.");
} catch (IOException e) {
System.err.println("Could not save config: " + e.getMessage());
}
}
}
Properties Files Are String-Only
The
Properties
class stores everything as
String
values. Even if you write
max.connections=10
, calling
props.get("max.connections")
returns the
String
"10"
, not the
Integer
10
. You must convert types yourself with
Integer.parseInt()
,
Boolean.parseBoolean()
, etc. The
getProperty()
method returns
String
directly;
get()
returns
Object
and requires casting.
Creating and Traversing Directories
Creating Directories
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class DirectoryOps {
public static void main(String[] args) throws IOException {
// createDirectory() — creates ONE level. Fails if parent doesn't exist.
Path single = Path.of("output");
if (!Files.exists(single)) {
Files.createDirectory(single);
System.out.println("Created: " + single);
}
// createDirectories() — creates ALL missing parent directories.
// This is the one you'll use most often.
Path nested = Path.of("output/reports/2025/january");
Files.createDirectories(nested);
System.out.println("Created: " + nested);
// If the directory already exists, createDirectories() does nothing — no error.
Files.createDirectories(nested); // safe to call again
System.out.println("Called createDirectories again — no error.");
}
}
Always Use createDirectories()
createDirectory()
fails with
NoSuchFileException
if the parent directory doesn't exist.
createDirectories()
creates the entire path and is idempotent (safe to call multiple times). In practice, you should almost always use
createDirectories()
.
Listing Directory Contents
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.stream.Stream;
public class ListDirectory {
public static void main(String[] args) {
Path dir = Path.of("."); // current directory
// List IMMEDIATE children only (non-recursive)
try (Stream<Path> stream = Files.list(dir)) {
stream.forEach(path -> {
try {
if (Files.isDirectory(path)) {
System.out.printf(" [DIR] %s%n", path.getFileName());
} else {
long size = Files.size(path);
System.out.printf(" [FILE] %-25s %8d bytes%n",
path.getFileName(), size);
}
} catch (IOException e) {
System.out.printf(" [????] %s (error: %s)%n",
path.getFileName(), e.getMessage());
}
});
} catch (IOException e) {
System.err.println("Could not list directory: " + e.getMessage());
}
}
}
Walking a Directory Tree (Recursive)
Files.walk()
traverses a directory and all its subdirectories recursively. This is useful for searching, counting, or processing files across an entire project:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
public class WalkDirectory {
public static void main(String[] args) {
Path start = Path.of("src");
// walk() visits every file and directory recursively
// The maxDepth parameter (optional) limits how deep to go
try (Stream<Path> stream = Files.walk(start)) {
// Find all .java files and print their sizes
long javaCount = stream
.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".java"))
.peek(p -> {
try {
System.out.printf(" %s (%d bytes)%n",
start.relativize(p), Files.size(p));
} catch (IOException e) {
System.out.println(" " + start.relativize(p) + " (error)");
}
})
.count();
System.out.println("\nTotal .java files: " + javaCount);
} catch (IOException e) {
System.err.println("Error walking directory: " + e.getMessage());
}
}
}
Symbolic Link Loops
Files.walk()
follows symbolic links by default. If a directory contains a symlink that points to a parent directory (a cycle),
walk()
will loop forever. Use
Files.walk(start, FileVisitOption.FOLLOW_LINKS)
only when you specifically need it, and prefer the default behavior (no link following) for safety. Alternatively, use
Files.walkFileTree()
with a custom
FileVisitor
for more control.
Copying, Moving, and Deleting Files
The
Files
class provides one-line methods for common file operations:
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.StandardCopyOption;
public class FileOperations {
public static void main(String[] args) throws IOException {
// Ensure source file exists
Files.writeString(Path.of("original.txt"), "This is the original file.\n");
// --- COPY ---
Path copy = Path.of("backup/original-copy.txt");
Files.createDirectories(copy.getParent()); // ensure parent exists
// REPLACE_EXISTING overwrites if the target already exists
Files.copy(Path.of("original.txt"), copy,
StandardCopyOption.REPLACE_EXISTING);
System.out.println("Copied to: " + copy);
// --- MOVE (rename) ---
Path moved = Path.of("backup/original-moved.txt");
Files.move(copy, moved, StandardCopyOption.REPLACE_EXISTING);
System.out.println("Moved to: " + moved);
// The original copy no longer exists
System.out.println("Copy still exists? " + Files.exists(copy));
// --- DELETE ---
boolean deleted = Files.deleteIfExists(moved);
System.out.println("Deleted moved file? " + deleted);
// deleteIfExists() returns false if the file didn't exist (no exception)
// delete() throws NoSuchFileException if the file doesn't exist
// --- DELETE a non-empty directory ---
// Files.delete() CANNOT delete a non-empty directory.
// You need to walk the tree and delete files first:
Path dirToDelete = Path.of("backup");
try (var entries = Files.walk(dirToDelete)) {
entries.sorted(java.util.Comparator.reverseOrder()) // delete files before dirs
.forEach(path -> {
try {
Files.deleteIfExists(path);
} catch (IOException e) {
System.err.println("Could not delete: " + path);
}
});
}
System.out.println("Deleted directory tree.");
}
}
Deleting Non-Empty Directories
Files.delete()
and
Files.deleteIfExists()
throw
DirectoryNotEmptyException
if the target is a directory that contains files. You
must
delete the contents first. The
walk().sorted(Comparator.reverseOrder())
pattern shown above ensures files are deleted before their parent directories. Alternatively, use
Files.walkFileTree()
with a
SimpleFileVisitor
that overrides
postVisitDirectory()
for more robust handling.
Real-World Example: Log File Analyzer
Here's a practical example that combines reading, parsing, and analyzing a text file — a simple log analyzer that counts HTTP status codes from a server log file:
2025-07-15 10:01:23 GET /index.html 200 1024
2025-07-15 10:01:24 GET /style.css 200 4096
2025-07-15 10:01:25 GET /logo.png 200 15360
2025-07-15 10:01:26 GET /nonexistent 404 256
2025-07-15 10:01:27 POST /api/login 401 128
2025-07-15 10:01:28 GET /index.html 200 1024
2025-07-15 10:01:29 GET /api/users 200 8192
2025-07-15 10:01:30 GET /old-page 301 0
2025-07-15 10:01:31 POST /api/upload 500 64
2025-07-15 10:01:32 GET /about.html 200 2048
2025-07-15 10:01:33 GET /favicon.ico 404 128
2025-07-15 10:01:34 GET /api/data 200 32768
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Stream;
public class LogAnalyzer {
public static void main(String[] args) {
Path logFile = Path.of("server.log");
Path reportFile = Path.of("analysis-report.txt");
if (!Files.exists(logFile)) {
System.err.println("Log file not found: " + logFile);
return;
}
// Counters
Map<String, Integer> statusCounts = new TreeMap<>();
Map<String, Integer> endpointCounts = new TreeMap<>();
long totalBytes = 0;
long totalRequests = 0;
// Process the log file line by line (constant memory)
try (Stream<String> lines = Files.lines(logFile)) {
totalRequests = lines
.filter(line -> !line.isBlank())
.peek(line -> {
String[] parts = line.split("\\s+");
if (parts.length >= 5) {
String method = parts[2];
String endpoint = method + " " + parts[3];
String status = parts[4];
// Count status codes
statusCounts.merge(status, 1, Integer::sum);
// Count endpoints
endpointCounts.merge(endpoint, 1, Integer::sum);
// Sum response sizes
try {
totalBytes += Long.parseLong(parts[5]);
} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
// skip malformed lines
}
}
})
.count();
} catch (IOException e) {
System.err.println("Error reading log: " + e.getMessage());
return;
}
// Build and write the report
StringBuilder report = new StringBuilder();
report.append("=== Log Analysis Report ===\n\n");
report.append("Total requests: ").append(totalRequests).append("\n");
report.append("Total bytes: ").append(totalBytes).append("\n\n");
report.append("--- Status Code Distribution ---\n");
statusCounts.forEach((status, count) ->
report.append(String.format(" %-6s %d (%.1f%%)%n",
status, count, 100.0 * count / totalRequests))
);
report.append("\n--- Top Endpoints ---\n");
endpointCounts.entrySet().stream()
.sorted(Map.Entry.comparingByValue().reversed())
.forEach(entry ->
report.append(String.format(" %-30s %d hits%n",
entry.getKey(), entry.getValue()))
);
// Write the report to a file
Files.createDirectories(reportFile.getParent());
Files.writeString(reportFile, report.toString());
// Also print to console
System.out.println(report);
System.out.println("Report written to: " + reportFile.toAbsolutePath());
}
}
This example demonstrates several real-world techniques:
-
Files.lines()for constant-memory processing of a potentially large file -
Defensive parsing with
parts.length >= 5and a try-catch for malformed lines — real log files often have anomalies -
Map.merge()for concise counter incrementing -
Combining stream operations (
filter,peek,count,sorted) in a pipeline - Writing the report to both a file and the console
Best Practices
Resource Management
-
Always close file handles.
Use try-with-resources for
BufferedReader,BufferedWriter,Stream<String>fromFiles.lines(), and any other resource that wraps an open file. An unclosed file handle is a file handle leak — the OS limits how many files a process can have open, and when you hit that limit, your program (and possibly other programs on the same machine) can't open any more files. -
The one-liner methods are safe.
Files.readAllLines(),Files.writeString(),Files.write()open and close the file internally — you don't need try-with-resources for these.
Path Handling
-
Use
Path.of()(Java 11+) orPaths.get()instead of string concatenation withFile.separator. -
Use
path.resolve(other)to join paths instead of string concatenation:baseDir.resolve("data").resolve("file.txt"). This handles trailing slashes and absolute paths correctly. -
Use
path.getParent()to get a directory from a file path, and always callFiles.createDirectories(parent)before writing — don't assume the directory exists.
Encoding
- Always specify UTF-8 explicitly when reading or writing text files. Never rely on the platform default encoding.
Error Handling
-
Check
Files.exists()before reading if a missing file is an expected condition (like a config file that might not exist yet). If it's unexpected, let theIOExceptionpropagate — don't swallow it. -
Don't silently ignore
IOException. At minimum, log it. At best, propagate it to the caller so they can decide what to do. -
Validate file paths from user input.
A malicious path like
../../etc/passwdcould read sensitive files. Usepath.normalize()and check that the resolved path stays within an allowed directory.
Common Errors and Fixes
| Exception | Cause | Fix |
|---|---|---|
FileNotFoundException
|
File doesn't exist at the given path |
Check the path; print
path.toAbsolutePath()
to see where Java is actually looking
|
NoSuchFileException
|
Same as above, but from the NIO API | Same fix; also check that the parent directory exists |
AccessDeniedException
|
No read/write permission for the file |
Check file permissions; on Linux,
chmod 644 filename
|
DirectoryNotEmptyException
|
Tried to delete a non-empty directory | Delete contents first (see the delete example above) |
FileAlreadyExistsException
|
Tried to create a file/directory that already exists |
Use
createDirectories()
(idempotent), or add
REPLACE_EXISTING
to copy/move
|
UnsupportedEncodingException
|
Used a charset name the JVM doesn't recognize |
Use
StandardCharsets.UTF_8
instead of string names like
"utf-8"
|
Debugging "File Not Found" Errors
The most common cause of "file not found" is a
wrong working directory
. When you run
java MyClass
, the working directory is wherever you ran the command from — not necessarily where your
.java
file is. Always print
Path.of("myfile.txt").toAbsolutePath()
to see the full path Java is using. In an IDE, the working directory is usually the project root, but it depends on your run configuration.
Exercise: Word Frequency Counter
Build a program that:
-
Reads a text file specified as the first command-line argument (
args[0]). -
Splits each line into words (split on non-letter characters — use
line.split("[^a-zA-Z]+")). - Converts all words to lowercase.
-
Counts how many times each word appears using a
HashMap<String, Integer>. - Sorts the words by frequency (highest first), then alphabetically for ties.
-
Writes the results to a file called
word-frequencies.txtin the format:
the: 42
java: 28
file: 15 - Also prints the top 10 most frequent words to the console.
Requirements:
-
Use
Files.lines()for constant-memory reading. - Use try-with-resources for the stream.
- Specify UTF-8 encoding when writing the output.
- Handle the case where the input file doesn't exist gracefully (print an error message and exit).
Solution
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class WordFrequencyCounter {
public static void main(String[] args) {
if (args.length < 1) {
System.err.println("Usage: java WordFrequencyCounter <input-file>");
return;
}
Path inputFile = Path.of(args[0]);
Path outputFile = Path.of("word-frequencies.txt";
if (!Files.exists(inputFile)) {
System.err.println("File not found: " + inputFile.toAbsolutePath());
return;
}
// Count word frequencies
Map<String, Integer> frequencies = new HashMap<>();
try (Stream<String> lines = Files.lines(inputFile, StandardCharsets.UTF_8)) {
lines.flatMap(line -> Stream.of(line.split("[^a-zA-Z]+")))
.map(String::toLowerCase)
.filter(word -> !word.isEmpty())
.forEach(word -> frequencies.merge(word, 1, Integer::sum));
} catch (IOException e) {
System.err.println("Error reading file: " + e.getMessage());
return;
}
// Sort by frequency (desc), then alphabetically (asc) for ties
List<Map.Entry<String, Integer>> sorted = frequencies.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed()
.thenComparing(Map.Entry::getKey))
.collect(Collectors.toList());
// Print top 10 to console
System.out.println("Top 10 most frequent words:");
sorted.stream()
.limit(10)
.forEach(e -> System.out.printf(" %-20s %d%n", e.getKey() + ":", e.getValue()));
// Write all frequencies to file
String report = sorted.stream()
.map(e -> e.getKey() + ": " + e.getValue())
.collect(Collectors.joining("\n"));
try {
Files.writeString(outputFile, report, StandardCharsets.UTF_8);
System.out.println("\nFull report written to: " + outputFile.toAbsolutePath());
System.out.println("Total unique words: " + sorted.size());
} catch (IOException e) {
System.err.println("Error writing report: " + e.getMessage());
}
}
}
Summary
-
Use
PathandFiles(fromjava.nio.file) for all new code. They're cleaner, more capable, and have better error handling than the legacyjava.io.Fileclass. -
For small files:
Files.readAllLines(),Files.readString(),Files.write(),Files.writeString()— concise one-liners. -
For large files:
Files.lines()(returns a Stream) orFiles.newBufferedReader()— process one line at a time. -
For line-by-line control:
BufferedWriterwithFiles.newBufferedWriter(). -
Always specify UTF-8
encoding with
StandardCharsets.UTF_8. - Always close resources with try-with-resources — file handle leaks are a real production problem.
-
Use
Files.createDirectories()(notcreateDirectory()) — it's idempotent and creates parent directories. - CSV parsing requires handling quoted fields and escaped quotes. Use a library for production CSV work.
- Properties files are Java's built-in format for key-value configuration. All values are Strings — convert types manually.
Next Steps
- Java JDBC — Use file I/O skills to read SQL scripts from files, write query results to CSV reports, and manage database configuration via properties files.
- Java Logging — Understand how the logging framework writes to files, and how to configure file handlers with rotation.
- Exception Handling — Deepen your understanding of try-with-resources and exception chaining, which are essential for robust file handling code.