Java · lesson 31 of 66
Beginner
17- 1 Introduction to Java
- 2 Setting Up Java
- 3 Data Types in Java
- 4 Operators in Java
- 5 Variables and Operators in Java
- 6 Control Flow in Java
- 7 Arrays in Java
- 8 Functions (Methods) in Java
- 9 Strings in Java
- 10 Methods in Java
- 11 Java OOP — Complete Guide
- 12 Introduction to Object-Oriented Programming in Java
- 13 Encapsulation in Java
- 14 Inheritance in Java
- 15 Polymorphism in Java
- 17 Abstraction in Java
- 18 Error Types in Java
Intermediate
38- 16 Regular Expressions in Java
- 19 Interfaces in Java — A Deep Dive
- 20 Abstract Classes in Java
- 21 Method Overloading in Java
- 22 Composition vs Inheritance in Java
- 23 Generics in Java
- 27 Exception Handling in Java
- 28 Java Streams API
- 29 Collections Framework in Java
- 30 Optional in Java
- 31 File Handling in Java
- 32 Records in Java
- 33 Java 8 Features
- 34 Sealed Classes and Interfaces in Java
- 35 Multithreading in Java
- 37 JDBC — Database Access in Java
- 39 Maven — Java Build and Dependency Management
- 42 Gradle — Modern Java Build Tool
- 44 Unit Testing with JUnit 5 and Mockito
- 45 Logging in Java with SLF4J and Logback
- 49 Spring Boot — Introduction
- 50 Dependency Injection in Spring Boot
- 51 Java Project Ideas
- 52 Building REST APIs with Spring Boot
- 53 Validation in Spring Boot
- 54 Spring Data JPA — Database Access
- 55 Exception Handling in Spring Boot
- 56 Spring Security — Authentication and Authorization
- 57 JWT Authentication in Spring Boot
- 58 Dockerizing Spring Boot Applications
- 59 Java Enums: Beyond Simple Constants
- 60 Inner and Anonymous Classes in Java
- 61 Java Generics: Bounded Types, Wildcards, and Generic Methods
- 62 Java Date/Time API: LocalDate, LocalDateTime, and Duration
- 63 Java NIO.2: Paths, Files, and WatchService
- 64 Java Annotations: Custom Types and Meta-Information
- 65 Java Reflection: Dynamic Type Inspection and Method Invocation
- 66 Java Localization: ResourceBundle and Locale Support
Advanced
11File 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.
By SkillByExample · Updated
Java has two file APIs: the classic java.io streams and readers, and the modern java.nio.file
(NIO.2) Path and Files classes from Java 7. Prefer NIO.2 for new code; you will still meet
java.io everywhere. Each example is a single file run with the JDK 21 source launcher
(java File.java), creates the files it needs in the current directory, and some runs are
followed by shell commands that show what ended up on disk.
Writing and reading a whole file
Files.writeString and Files.readString handle small files in one call each.
import java.io.IOException;
import java.nio.file.*;
import java.util.List;
public class WholeFile {
public static void main(String[] args) throws IOException {
Path notes = Path.of("notes.txt");
Files.writeString(notes, "first line\nsecond line\n");
Files.writeString(notes, "third line\n", StandardOpenOption.APPEND);
String text = Files.readString(notes);
List<String> lines = Files.readAllLines(notes);
System.out.print(text);
System.out.println(lines.size() + " lines, " + Files.size(notes) + " bytes");
}
}
$ java WholeFile.java
first line
second line
third line
3 lines, 34 bytes
$ cat notes.txt
first line
second line
third line
Without APPEND, the second write would have replaced the file. readAllLines strips the line
terminators, so the list holds three strings. Both methods read the entire file into memory — fine
for config files, wrong for gigabyte logs.
Missing files fail loudly
import java.io.IOException;
import java.nio.file.*;
public class Missing {
public static void main(String[] args) {
try {
Files.readString(Path.of("nope.txt"));
} catch (NoSuchFileException e) {
System.out.println("no such file: " + e.getFile());
} catch (IOException e) {
System.out.println("other I/O problem: " + e);
}
System.out.println("exists? " + Files.exists(Path.of("nope.txt")));
}
}
$ java Missing.java
no such file: nope.txt
exists? false
NIO.2 throws specific subclasses of IOException — NoSuchFileException,
FileAlreadyExistsException, AccessDeniedException — so you can react to each. The old
java.io.File.delete() simply returns false and leaves you guessing why.
Line by line with BufferedReader
For large files, read one line at a time so memory use stays constant.
import java.io.*;
import java.nio.file.*;
public class LineByLine {
public static void main(String[] args) throws IOException {
Files.writeString(Path.of("app.log"), "INFO start\nERROR disk full\nINFO retry\nERROR timeout\n");
int lineNo = 0;
try (BufferedReader reader = Files.newBufferedReader(Path.of("app.log"))) {
String line;
while ((line = reader.readLine()) != null) {
lineNo++;
if (line.startsWith("ERROR")) System.out.println(lineNo + ": " + line);
}
}
System.out.println("read " + lineNo + " lines");
}
}
$ java LineByLine.java
2: ERROR disk full
4: ERROR timeout
read 4 lines
readLine() returns null at end of file, which ends the loop. Files.newBufferedReader is the
NIO.2 equivalent of new BufferedReader(new FileReader(...)), and try-with-resources closes it.
Lazy streams with Files.lines
Files.lines returns a Stream<String> that reads as you consume it. It holds an open file
handle, so it belongs in try-with-resources too.
import java.io.IOException;
import java.nio.file.*;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class LogStats {
public static void main(String[] args) throws IOException {
Files.writeString(Path.of("app.log"), "INFO start\nERROR disk full\nINFO retry\nWARN slow\nERROR timeout\n");
try (Stream<String> lines = Files.lines(Path.of("app.log"))) {
Map<String, Long> byLevel = lines
.map(l -> l.split(" ", 2)[0])
.collect(Collectors.groupingBy(level -> level, TreeMap::new, Collectors.counting()));
System.out.println(byLevel);
}
}
}
$ java LogStats.java
{ERROR=2, INFO=2, WARN=1}
Only one line is in memory at a time; the counting happens as lines stream past.
Writing with BufferedWriter and PrintWriter
BufferedWriter batches writes; PrintWriter adds printf. Data reaches the file when the writer
is flushed or closed.
import java.io.*;
import java.nio.file.*;
public class Writers {
public static void main(String[] args) throws IOException {
try (PrintWriter out = new PrintWriter(Files.newBufferedWriter(Path.of("report.txt")))) {
out.println("=== Report ===");
out.printf("%-8s %6.2f%n", "coffee", 4.2);
out.printf("%-8s %6.2f%n", "lunch", 12.5);
}
PrintWriter leaky = new PrintWriter(new FileWriter("unflushed.txt"));
leaky.println("this never reaches the disk");
System.out.println("unflushed.txt size: " + Files.size(Path.of("unflushed.txt")));
}
}
$ java Writers.java
unflushed.txt size: 0
$ cat report.txt
=== Report ===
coffee 4.20
lunch 12.50
report.txt is complete because try-with-resources closed (and flushed) the writer. leaky was
never closed, so its text sat in a buffer and the file is still empty — a classic “my file is
blank” bug.
Binary data with streams
Bytes, not characters: images, archives, anything that isn’t text. Read into a buffer rather than one byte at a time.
import java.io.*;
import java.nio.file.*;
public class Binary {
public static void main(String[] args) throws IOException {
byte[] data = new byte[10_000];
for (int i = 0; i < data.length; i++) data[i] = (byte) i;
Files.write(Path.of("blob.bin"), data);
long total = 0;
int chunks = 0;
try (InputStream in = new FileInputStream("blob.bin")) {
byte[] buffer = new byte[4096];
int n;
while ((n = in.read(buffer)) != -1) {
chunks++;
total += n;
System.out.println("chunk " + chunks + ": " + n + " bytes");
}
}
System.out.println("total " + total + ", first bytes " + data[0] + " " + data[255] + " " + data[256]);
}
}
$ java Binary.java
chunk 1: 4096 bytes
chunk 2: 4096 bytes
chunk 3: 1808 bytes
total 10000, first bytes 0 -1 0
read(buffer) returns how many bytes it filled — the last chunk is short — and -1 at the end.
Java’s byte is signed, so 255 is stored as -1 and 256 wraps to 0.
Paths, directories, copy, move, delete
import java.io.IOException;
import java.nio.file.*;
public class PathOps {
public static void main(String[] args) throws IOException {
Path p = Path.of("data", "2026", "..", "users.txt");
System.out.println(p + " -> " + p.normalize() + ", file name " + p.getFileName());
Files.createDirectories(Path.of("data/archive"));
Files.writeString(Path.of("data/users.txt"), "alice\nbob\n");
Files.copy(Path.of("data/users.txt"), Path.of("data/archive/users.txt"), StandardCopyOption.REPLACE_EXISTING);
Files.move(Path.of("data/users.txt"), Path.of("data/users.old"), StandardCopyOption.REPLACE_EXISTING);
try (var entries = Files.walk(Path.of("data"))) {
entries.sorted().forEach(e -> System.out.println((Files.isDirectory(e) ? "dir " : "file ") + e));
}
System.out.println("deleted? " + Files.deleteIfExists(Path.of("data/users.txt")));
}
}
$ java PathOps.java
data/2026/../users.txt -> data/users.txt, file name users.txt
dir data
dir data/archive
file data/archive/users.txt
file data/users.old
deleted? false
normalize() removes the 2026/.. detour. createDirectories makes every missing parent and
doesn’t fail if they exist. Files.walk visits the whole tree as a stream; sorting it makes the
listing stable, because directory order is up to the file system. deleteIfExists returned
false because users.txt had already been moved.
Project: expense tracker with CSV persistence
The tracker loads expenses.csv if it exists, adds three expenses, saves, and prints totals.
Running it twice shows that the state really lives on disk.
import java.io.IOException;
import java.nio.file.*;
import java.util.*;
public class ExpenseTracker {
record Expense(String category, String description, double amount) {
String toCsv() { return category + "," + description + "," + amount; }
static Expense fromCsv(String line) {
String[] p = line.split(",");
return new Expense(p[0], p[1], Double.parseDouble(p[2]));
}
}
public static void main(String[] args) throws IOException {
Path file = Path.of("expenses.csv");
List<Expense> expenses = new ArrayList<>();
if (Files.exists(file)) Files.readAllLines(file).stream().skip(1).map(Expense::fromCsv).forEach(expenses::add);
expenses.add(new Expense("Food", "Lunch", 12.50));
expenses.add(new Expense("Transport", "Bus", 2.80));
expenses.add(new Expense("Food", "Coffee", 4.20));
List<String> lines = new ArrayList<>(List.of("category,description,amount"));
expenses.forEach(e -> lines.add(e.toCsv()));
Files.write(file, lines);
Map<String, Double> totals = new TreeMap<>();
expenses.forEach(e -> totals.merge(e.category(), e.amount(), Double::sum));
System.out.println(expenses.size() + " expenses " + totals);
}
}
$ java ExpenseTracker.java
3 expenses {Food=16.7, Transport=2.8}
$ java ExpenseTracker.java
6 expenses {Food=33.4, Transport=5.6}
$ head -3 expenses.csv
category,description,amount
Food,Lunch,12.5
Transport,Bus,2.8
The second run loaded the three rows the first run saved and added three more. Delete
expenses.csv and the first run’s totals come back.
Practice
- A description like
Lunch, with clientbreaksfromCsv. Reproduce the failure, then fix it so that only the description may contain commas — and say whysplit(",", 3)alone isn’t enough.
Solution
public class CsvComma {
public static void main(String[] args) {
String line = "Food,Lunch, with client,12.5";
String[] naive = line.split(",");
System.out.println(naive.length + " fields: " + String.join(" | ", naive));
try {
Double.parseDouble(naive[2]);
} catch (NumberFormatException e) {
System.out.println("naive parse fails: " + e.getMessage());
}
int last = line.lastIndexOf(',');
String[] head = line.substring(0, last).split(",", 2);
System.out.println(head[0] + " | " + head[1] + " | " + Double.parseDouble(line.substring(last + 1)));
}
}
$ java CsvComma.java
4 fields: Food | Lunch | with client | 12.5
naive parse fails: For input string: "with client"
Food | Lunch, with client | 12.5
split(",", 3) alone would put " with client,12.5" in the third field because the comma is in
the middle column. Taking the amount from the last comma and limiting the rest to 2 fields
works as long as only the description contains commas; real CSV needs quoting and a CSV library.
- Make the tracker append only the new rows with
StandardOpenOption.APPENDinstead of rewriting the file, writing the header only when the file is new.
Solution
import java.io.IOException;
import java.nio.file.*;
import java.util.List;
public class AppendTracker {
public static void main(String[] args) throws IOException {
Path file = Path.of("expenses2.csv");
if (Files.notExists(file)) Files.writeString(file, "category,description,amount\n");
Files.write(file, List.of("Food,Lunch,12.5", "Transport,Bus,2.8"), StandardOpenOption.APPEND);
System.out.println(Files.readAllLines(file).size() - 1 + " rows");
}
}
$ java AppendTracker.java
2 rows
$ java AppendTracker.java
4 rows
Appending writes only the new bytes, so the cost no longer grows with the file. The trade-off: you can no longer edit or delete earlier rows without rewriting.
- Count the
.javafiles under a directory tree withFiles.walk. Create a few first so the answer is predictable.
Solution
import java.io.IOException;
import java.nio.file.*;
import java.util.stream.Stream;
public class CountJava {
public static void main(String[] args) throws IOException {
Files.createDirectories(Path.of("proj/src/util"));
for (String f : new String[] {"proj/src/App.java", "proj/src/util/Strings.java", "proj/README.md"}) {
Files.writeString(Path.of(f), "");
}
try (Stream<Path> paths = Files.walk(Path.of("proj"))) {
System.out.println(paths.filter(p -> p.toString().endsWith(".java")).count() + " .java files");
}
}
}
$ java CountJava.java
2 .java files
Files.walk descends into util automatically; README.md is filtered out.
- Copy
blob.bin-style binary data withInputStream.transferToand verify the copy is byte-for-byte identical withFiles.mismatch.
Solution
import java.io.*;
import java.nio.file.*;
public class CopyBytes {
public static void main(String[] args) throws IOException {
byte[] data = new byte[5000];
new java.util.Random(1).nextBytes(data);
Files.write(Path.of("in.bin"), data);
try (InputStream in = new FileInputStream("in.bin"); OutputStream out = new FileOutputStream("out.bin")) {
System.out.println("copied " + in.transferTo(out) + " bytes");
}
System.out.println("first difference at: " + Files.mismatch(Path.of("in.bin"), Path.of("out.bin")));
}
}
$ java CopyBytes.java
copied 5000 bytes
first difference at: -1
transferTo does the buffer loop for you, and Files.mismatch returns -1 when two files are identical.