Lesson 20 — Reading and Writing Files

File Handling

Almost every real program reads from or writes to files — configuration, logs, data export, report generation. Java provides several layers of file IO from low-level streams to high-level convenience classes.

The Java IO Hierarchy

Java's file IO is built from composable classes. Understanding the layer is important:

  • Byte streams (FileInputStream, FileOutputStream) — read/write raw bytes. Used for binary files (images, PDFs).
  • Character streams (FileReader, FileWriter) — handle character encoding automatically. Used for text files.
  • Buffered streams (BufferedReader, BufferedWriter) — wrap character streams for efficient line-at-a-time IO. Most commonly used for text file processing.
  • NIO.2 (java.nio.file) — modern API (Java 7+). Files and Path provide concise, safe operations.

Writing Files — FileWriter + BufferedWriter

JAVA — Writing files
import java.io.*;

// Writing with try-with-resources (auto-closes on exit or exception)
String filename = "attendance.txt";

try (FileWriter fw = new FileWriter(filename);
     BufferedWriter bw = new BufferedWriter(fw)) {

    bw.write("Your IT Tutor Attendance Register");
    bw.newLine();
    bw.write("=".repeat(30));
    bw.newLine();
    bw.write("Monday:    28 learners present");
    bw.newLine();
    bw.write("Tuesday:   31 learners present");
    bw.newLine();

    System.out.println("File written successfully.");

} catch (IOException e) {
    System.out.println("Write failed: " + e.getMessage());
}

// Appending to an existing file (second param true = append mode)
try (BufferedWriter bw = new BufferedWriter(new FileWriter(filename, true))) {
    bw.write("Wednesday: 27 learners present");
    bw.newLine();
} catch (IOException e) {
    e.printStackTrace();
}

Reading Files — BufferedReader

JAVA — Reading files
try (BufferedReader reader = new BufferedReader(new FileReader("attendance.txt"))) {

    String line;
    int lineNumber = 0;

    while ((line = reader.readLine()) != null) {
        lineNumber++;
        System.out.printf("%3d: %s%n", lineNumber, line);
    }

} catch (FileNotFoundException e) {
    System.out.println("File not found: " + e.getMessage());
} catch (IOException e) {
    System.out.println("Read error: " + e.getMessage());
}

Modern NIO.2 API (Recommended)

The java.nio.file package (Java 7+) provides a cleaner, more concise API for common file operations. Use it for new code.

JAVA — NIO.2 API
import java.nio.file.*;
import java.util.List;

Path filePath = Path.of("learners.txt");

// Write all lines at once
List<String> lines = List.of(
    "Thandi Mokoena - Java - 74.5%",
    "Sipho Dlamini  - Python - 81.0%",
    "Lerato Khumalo - SQL - 68.0%"
);
Files.write(filePath, lines); // overwrites

// Append
Files.writeString(filePath, "Bongani Nkosi  - C# - 88.5%\n",
    StandardOpenOption.APPEND);

// Read all lines
List<String> read = Files.readAllLines(filePath);
for (String line : read) System.out.println(line);

// Read as a single string
String content = Files.readString(filePath);
System.out.println("Total chars: " + content.length());

// Check existence
System.out.println("Exists: " + Files.exists(filePath));
System.out.println("Size:   " + Files.size(filePath) + " bytes");

// Delete
// Files.delete(filePath); // throws if not found
Files.deleteIfExists(filePath); // safe

Reading CSV Data

CSV (Comma-Separated Values) is the most common data interchange format. Parsing it with Java IO is a fundamental skill.

JAVA — Reading CSV
// Assuming marks.csv:
// name,course,mark
// Thandi,Java,74.5
// Sipho,Python,81.0

ArrayList<Learner> fromFile = new ArrayList<>();

try (BufferedReader reader = new BufferedReader(new FileReader("marks.csv"))) {

    String line;
    reader.readLine(); // skip header line

    while ((line = reader.readLine()) != null) {
        String[] parts = line.split(",");
        if (parts.length == 3) {
            String name  = parts[0].trim();
            String course = parts[1].trim();
            double mark  = Double.parseDouble(parts[2].trim());
            fromFile.add(new Learner(name, 0, course, mark));
        }
    }

} catch (IOException | NumberFormatException e) {
    System.out.println("Error reading CSV: " + e.getMessage());
}

System.out.println("Loaded " + fromFile.size() + " learners from file.");
fromFile.forEach(l -> System.out.println("  " + l));

Practice Task

Your Turn

Write a LearnerRegistry program: (1) write a method saveToFile(ArrayList<Learner> learners, String filename) that writes each learner as a CSV line (name,course,mark), (2) write a method loadFromFile(String filename) that reads the file back and returns an ArrayList<Learner>, (3) in main: create 5 learners, save to registry.csv, clear the list, reload from file, and print each loaded learner. Handle all possible exceptions with meaningful messages.

Common Mistakes

  • Not closing resources — without try-with-resources, an exception before close() leaves files open. Always use try-with-resources.
  • Assuming the file exists — always catch FileNotFoundException separately and provide a clear message.
  • Writing without flushing — BufferedWriter buffers output. It is flushed when closed. Do not rely on the buffer being written without closing.
  • Platform line endings — use System.lineSeparator() or bw.newLine() instead of \n for cross-platform compatibility.
  • Not handling NumberFormatException when parsing numeric fields from a text file — user-generated data is rarely perfectly formatted.

Professional Tip

File paths are relative to the working directory — which is usually the project root when run from an IDE, and the directory where you run java from the terminal. Use absolute paths or System.getProperty("user.dir") to understand where files are being written.

Mini Quiz

What does try-with-resources guarantee in Java IO?