# Java I/O and NIO File Handling

> Chapter 11 of the Courseiva ORACLE-OCP-JAVA17 curriculum — https://courseiva.com/learn/oracle-ocp-java17/java-io-and-nio-file-handling

**Official objective:** 7.1 — Read and write data using I/O streams (FileInputStream, FileOutputStream, BufferedReader, etc.) and use NIO.2 Path, Files, and DirectoryStream.

## Introduction

Java I/O (Input/Output) and NIO.2 (New Input/Output, version 2) let your program talk to files on the computer's hard drive. For the 1Z0-829 exam, you must know two different ways to read and write files: the older stream-based approach and the newer Path-based approach, and when to use each.

## The Personal Chef and the Meal Prep Service Analogy

Your kitchen counter, covered in unwashed dishes from last night's dinner party.

You need to get tonight's three-course meal ready, but all your ingredients are scattered across different shops: the butcher across town, the farmers' market on the other side of the river, and the speciality spice shop that only opens on Tuesdays.

Option one is to be a personal chef. You physically go to each shop, one by one. You walk in, pick up the meat, carry it back home in a bag. Then you walk to the farmers' market, grab the vegetables, carry them back. Then you go to the spice shop. This is exactly like Java's I/O streams: you open a direct connection to a single file, read or write data one byte or character at a time, and close the connection. It works, but it's manual and slow. You have to manage every step.

Option two is to use a meal prep delivery service. You tell the service what recipes you want, they coordinate with all the shops, and they deliver everything in one neat box to your doorstep. You can ask for just the ingredient list, or the quantities, or even search for a specific spice across all shops. This is Java's NIO.2: you use the Path object as a simple reference to a file or directory (like the recipe name), and the Files utility class is the delivery service that does all the heavy lifting — checking if the file exists, reading all lines at once, copying files, walking through a whole directory tree. The personal chef (I/O streams) works byte by byte. The meal prep service (NIO.2) gives you higher-level operations on whole files or directories.

## Core explanation

Java I/O (Input/Output) is the system that lets your Java program read data from somewhere (input) and write data to somewhere (output). The 'somewhere' could be a file on your hard drive, data coming from a network connection, or even data typed by a user at the keyboard. NIO.2 (New Input/Output, version 2) is a newer system introduced in Java 7 that provides a more powerful and easier way to work with files and directories.

Let's start with the older I/O streams. A stream is a sequence of data flowing from a source to a destination. Think of a stream like a garden hose: the water flows from the tap (the source) through the hose (the stream) to the sprinkler (the destination). Java has two main types of streams: byte streams and character streams. Byte streams read and write data as raw bytes (8 bits of data). Character streams read and write data as characters, which is better for handling text files because they handle character encoding (the system that maps numbers to letters like 'A' or 'é') automatically.

The most common byte streams are FileInputStream and FileOutputStream. FileInputStream lets you read bytes from a file. FileOutputStream lets you write bytes to a file. Here's a simple example:

FileInputStream fis = new FileInputStream("input.txt");
int data = fis.read();  // reads one byte
while (data != -1) {    // -1 means end of file
    System.out.print((char) data);
    data = fis.read();
}
fis.close();

The read() method returns an int. If there are no more bytes to read, it returns -1. You must always close the stream when you're done, or the file stays locked and other programs cannot access it. This is error-prone, which is why Java 7 introduced try-with-resources, which automatically closes the stream.

For character streams, the most common classes are FileReader and FileWriter, or better yet, BufferedReader and BufferedWriter. A BufferedReader wraps a reader and adds a buffer (a temporary memory area). Instead of reading one character at a time from the hard drive, the BufferedReader reads a whole chunk of characters at once and stores them in the buffer. When your program asks for the next character, the BufferedReader hands it from the buffer, which is much faster than going back to the hard drive for each character. Here's the improved pattern:

try (BufferedReader br = new BufferedReader(new FileReader("input.txt"))) {
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException e) {
    e.printStackTrace();
}

The readLine() method returns one entire line of text (without the line-ending characters) or null when there are no more lines. The try-with-resources block ensures the BufferedReader is closed automatically, even if an exception occurs.

Now let's move to NIO.2, which is in the java.nio.file package. The two most important classes are Path and Files. A Path object is simply a reference to a file or directory location, like an address. It does not represent the actual file data — just where the file lives. You create a Path using the static method Paths.get("filename.txt") or Path.of("filename.txt") in Java 11+.

The Files class is a utility class with many static methods that operate on files. If Path is the address, Files is the handyman who can do all sorts of jobs at that address. For example:

Path path = Path.of("input.txt");
boolean exists = Files.exists(path);  // true if the file exists
boolean isDir = Files.isDirectory(path);  // true if it's a directory
String content = Files.readString(path);  // reads entire file into a String
List<String> lines = Files.readAllLines(path);  // reads all lines into a List
Files.writeString(path, "Hello world");  // writes a String to a file

The Files.readString() method is new in Java 11 and is very convenient for small files. For larger files, you should still use a BufferedReader or the Files.newBufferedReader() method.

A DirectoryStream is used to iterate over the entries in a directory (like listing all files in a folder). Here's how:

Path dir = Path.of("/home/user/docs");
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
    for (Path entry : stream) {
        System.out.println(entry.getFileName());
    }
} catch (IOException e) {
    e.printStackTrace();
}

The DirectoryStream does NOT include subdirectories recursively. If you need to walk through a whole directory tree (including subdirectories), you would use Files.walk() or Files.find().

Why does NIO.2 exist? The old I/O stream classes were low-level and required a lot of boilerplate code. NIO.2 simplifies file operations with one-liners, supports file attributes (like permissions and owner), and can handle symbolic links (shortcuts in the file system). For the exam, you need to know both systems and when each is appropriate.

## Real-world context

Imagine you work for a bank that processes mortgage applications. Every day, a third-party credit bureau sends a CSV file (comma-separated values, like a simple spreadsheet) containing credit scores for all applicants. The file arrives via a secure upload at 2 AM. Your job is to write a Java program that reads this file, validates the data, and updates the company's internal database.

Here is the step-by-step reality:

First, you need to locate the file. You use NIO.2's Path and Files classes to check if the file exists and is readable. You cannot assume the file always arrives on time. If it is missing, you need to log an error and send an alert.

Path incomingFile = Path.of("/data/credit_scores_2025-03-30.csv");
if (Files.exists(incomingFile) && Files.isReadable(incomingFile)) {
    // proceed
} else {
    log.error("Missing credit score file");
}

Second, you need to read the file line by line. The file could be huge — tens of thousands of lines. You would never use Files.readAllLines() for a large file because that loads the entire file into memory, which could crash the application. Instead, you use a BufferedReader wrapped around a FileReader.

try (BufferedReader reader = Files.newBufferedReader(incomingFile)) {
    reader.readLine();  // skip header line
    String line;
    while ((line = reader.readLine()) != null) {
        // parse each line into applicant data
        String[] fields = line.split(",");
        String applicantId = fields[0];
        int creditScore = Integer.parseInt(fields[1]);
        // validate and update database
    }
} catch (IOException e) {
    log.error("Error reading file", e);
}

Third, after processing, you must archive the file. You do not want to delete it immediately in case you need to re-run the process. You use Files.move() to move the file to an archive folder. If the archive folder does not exist, you create it with Files.createDirectories().

Path archiveDir = Path.of("/data/archive/");
Files.createDirectories(archiveDir);
Path archivedFile = archiveDir.resolve("processed_" + incomingFile.getFileName());
Files.move(incomingFile, archivedFile, StandardCopyOption.REPLACE_EXISTING);

Finally, you need to log the outcome. You write a summary line to a log file, again using Files.writeString() or a BufferedWriter.

In this scenario, you used both NIO.2 (for checking, moving, creating directories) and the older I/O streams (for efficient line-by-line reading with BufferedReader). The exam tests your ability to pick the right tool for each job.

## Exam focus

The 1Z0-829 exam tests Java I/O and NIO.2 File Handling in several specific ways. Here is exactly what you need to know.

First, memorise the key classes and their inheritance hierarchies. For I/O streams, you must know:

- InputStream is the abstract parent for byte input streams. FileInputStream is a concrete subclass.
- OutputStream is the abstract parent for byte output streams. FileOutputStream is a concrete subclass.
- Reader is the abstract parent for character input streams. FileReader, BufferedReader are subclasses.
- Writer is the abstract parent for character output streams. FileWriter, BufferedWriter are subclasses.

Common exam traps about I/O streams:
- A FileInputStream constructor throws a FileNotFoundException if the file does not exist, but FileOutputStream does NOT throw that exception — it creates an empty file instead.
- Not closing resources is a classic trap. The exam loves asking about try-with-resources. If you forget to close a stream, the file stays locked.
- The read() method returns an int, not a byte. It returns -1 at end of file. Many beginners mistakenly check for null or 0.
- Buffered streams do not call the underlying stream's read/write methods until the buffer is full (or flushed). You must call flush() or close() to ensure data is actually written to the file.

For NIO.2, the exam focuses on:

- Path objects are immutable. Methods like resolve(), relativize(), normalize() return a new Path and do not modify the original.
- The Files class has static methods. You do not create an instance.
- Files.exists() returns true even if the file is a symbolic link that points to a non-existent file — you must use exists(link, LinkOption.NOFOLLOW_LINKS) to check the link itself.
- Files.walk() and Files.find() traverse directories recursively. DirectoryStream does NOT.
- Files.readAllLines() and Files.readString() are convenient but load the entire file into memory. They will cause OutOfMemoryError for huge files.
- The StandardCopyOption enum includes REPLACE_EXISTING, COPY_ATTRIBUTES, and ATOMIC_MOVE. The exam asks about ATOMIC_MOVE particularly — it guarantees that a file move either completes fully or fails without leaving a partial copy.

Specific question patterns appear:

- 'Which code correctly reads a text file line by line?' Answer: a try-with-resources block with BufferedReader and a loop calling readLine() until null.
- 'What does this code output?' They show you code that uses read() in a while loop and ask what will be printed. Watch out for off-by-one errors and the fact that read() returns an int.
- 'Which NIO.2 method would you use to list only the files in a directory without going into subdirectories?' Answer: Files.newDirectoryStream().
- 'What happens if you use Files.readString() on a binary file that is 2 GB?' Answer: It throws an OutOfMemoryError or is extremely slow.
- 'Which exception is thrown when reading a non-existent file with FileInputStream?' Answer: FileNotFoundException (subclass of IOException).

## Step by step

1. **Choose the right approach: I/O streams vs NIO.2** — For reading/writing text efficiently, especially large files, use BufferedReader/BufferedWriter (I/O streams). For checking file existence, moving, copying, deleting, or listing directories, use NIO.2's Path and Files classes. The exam expects you to pick the correct approach for each scenario.
2. **Create a Path object referencing the file or directory** — Use Path.of("filename.txt") or Paths.get("filename.txt") to create a Path. This does not touch the hard drive — it just represents the file's location. You can then pass this Path to the Files class.
3. **Check file existence and accessibility** — Before reading, call Files.exists(path) and Files.isReadable(path). Before writing, check that the parent directory exists using Files.isDirectory(path.getParent()). This prevents confusing exceptions later.
4. **Open a BufferedReader using NIO.2 or the classic approach** — Use Files.newBufferedReader(path) to get a BufferedReader directly from a Path. This is cleaner than wrapping a FileReader in a BufferedReader. The BufferedReader reads a chunk of data at once, reducing expensive hard drive access.
5. **Read data in a loop and process it** — Call readLine() in a while loop until it returns null. Each line is a String. Process each line (e.g., split by comma, parse numbers). Always handle IOException with try-catch or declare it in the method signature.
6. **Close resources automatically with try-with-resources** — Declare the BufferedReader in the parentheses of a try block: try (BufferedReader br = Files.newBufferedReader(path)) { ... }. The JVM automatically calls br.close() when the block ends, even if an exception occurs. This is guaranteed on the exam.

## Comparisons

### FileInputStream vs FileReader

**FileInputStream:**
- Reads raw bytes (suitable for images, audio, binary data)
- read() returns int (0-255) or -1 for end of file
- Does not handle character encoding

**FileReader:**
- Reads characters (suitable for text files)
- read() returns char as int (0-65535) or -1
- Uses default character encoding to convert bytes to characters

### BufferedReader vs Scanner

**BufferedReader:**
- Efficient for reading large text files line by line
- Only reads data from the underlying Reader
- Cannot parse tokens like integers or doubles directly

**Scanner:**
- Can parse tokens (nextInt(), nextDouble()) directly
- Slower for large files due to parsing overhead
- Has a larger API surface and more memory overhead

### Files.readAllLines() vs BufferedReader.readLine()

**Files.readAllLines():**
- Loads entire file into memory as List<String>
- Risks OutOfMemoryError for large files
- Simple one-line call for small files

**BufferedReader.readLine():**
- Reads one line at a time, memory efficient
- Safe for arbitrarily large files
- Requires manual loop and null check

### Files.newDirectoryStream() vs Files.walk()

**Files.newDirectoryStream():**
- Lists only direct children of a directory
- Returns a DirectoryStream (iterable)
- Does not follow symbolic links by default

**Files.walk():**
- Traverses directory tree recursively
- Returns a Stream<Path> (supports filter, map, etc.)
- Can follow symbolic links with FOLLOW_LINKS option

## Diagram

_Decision tree for choosing between I/O streams and NIO.2 methods based on the type of file operation._

```mermaid
flowchart TD
    A[Application Code] --> B{Data Source?}
    B -->|Text file, small| C[Files.readString]
    B -->|Text file, large| D[BufferedReader]
    B -->|Binary file| E[FileInputStream]
    B -->|Need to list directory| F[Files.newDirectoryStream]
    B -->|Need to move/copy/delete| G[Files.move / Files.copy / Files.delete]
    C --> H[String in memory]
    D --> I[while loop readLine]
    E --> J[while loop read()]
    F --> K[Iterate Path objects]
    G --> L[File operation completed]
```

## Common misconceptions

- **Misconception:** FileInputStream.read() returns a byte, so I should store the result in a byte variable. **Reality:** FileInputStream.read() returns an int. It returns -1 to signal end of file, which is outside the range of a byte (-128 to 127). Always store the result in an int. (Beginners see 'byte stream' and assume the method returns a byte. The -1 sentinel value forces the API to use int.)
- **Misconception:** BufferedReader.readLine() returns an empty string at the end of the file. **Reality:** BufferedReader.readLine() returns null at the end of the file. An empty string means the file contained a blank line, not that the file ended. (Null pointer exceptions are common when beginners forget to check for null. They test for empty string instead.)
- **Misconception:** The Files.exists() method checks if the file exists and is readable. **Reality:** Files.exists() only checks if the file exists. It does not check permissions. Use Files.isReadable() to check if the file can be read. (The name 'exists' sounds like it checks availability, but it only checks presence in the file system.)
- **Misconception:** You can use Files.newDirectoryStream() to walk through all files in a directory tree recursively. **Reality:** Files.newDirectoryStream() only lists the direct children of a directory. To traverse recursively, use Files.walk() or Files.find(). (The word 'DirectoryStream' sounds comprehensive, but the API documentation clearly states it is not recursive.)
- **Misconception:** FileOutputStream throws FileNotFoundException if the file does not exist. **Reality:** FileOutputStream creates a new empty file if the file does not exist (unless the file system is read-only or the path is invalid). It throws FileNotFoundException if the file is a directory or cannot be created. (Beginners often confuse FileInputStream and FileOutputStream constructors. FileInputStream needs the file to exist; FileOutputStream creates it.)

## Key takeaways

- Always close I/O streams using try-with-resources to prevent resource leaks and file locks.
- BufferedReader.readLine() returns null at end of file, not an empty string.
- FileInputStream.read() returns an int, not a byte, and returns -1 when there is no more data.
- Path objects are immutable — methods like resolve() and normalize() return a new Path without changing the original.
- Files.newDirectoryStream() lists only direct children of a directory and does not go into subdirectories.
- Files.readString() and Files.readAllLines() load the entire file into memory — never use them on large files.
- FileOutputStream creates a new file if it does not exist, while FileInputStream throws FileNotFoundException.

## FAQ

**What is the difference between FileInputStream and FileReader in Java?**

FileInputStream reads raw bytes (for binary files like images or PDFs). FileReader reads characters (for text files) and automatically converts bytes to characters using the default character encoding.

**Why do I need BufferedReader if I can just use FileReader?**

FileReader reads one character at a time from the hard drive, which is slow. BufferedReader adds an internal buffer that reads a large block of characters at once, dramatically improving performance. Always wrap FileReader in BufferedReader for text files.

**What does Files.readString() do and when should I not use it?**

Files.readString() reads the entire content of a file into a single String. Do not use it for large files because the file must fit entirely in memory, which can cause OutOfMemoryError.

**How do I list all files in a directory in Java?**

Use Files.newDirectoryStream(Path) inside a try-with-resources block. It returns a DirectoryStream<Path> that you can iterate over. It lists only the direct contents (not subdirectories).

**What does the -1 mean from FileInputStream.read()?**

It means the end of the stream has been reached. There are no more bytes to read. You always check for -1 in your loop condition.

**Do I need to close a FileOutputStream if the file was created successfully?**

Yes. Always close any stream, including FileOutputStream, or the file may remain locked and data may be lost. Use try-with-resources to automate this.

## Check your understanding

1. **What does FileInputStream.read() return when the end of the file is reached?**

   Answer: It returns -1 to signal end of file.

2. **Which NIO.2 method should you use to list only the direct files in a directory without traversing subdirectories?**

   Answer: Files.newDirectoryStream()

3. **What is the purpose of the try-with-resources statement when working with I/O streams?**

   Answer: It automatically closes the stream at the end of the try block, even if an exception occurs.

4. **Can you use Files.readString() on a 5 GB log file?**

   Answer: No, it would likely throw an OutOfMemoryError because it loads the entire file into memory.

5. **What does BufferedReader.readLine() return when there are no more lines to read?**

   Answer: It returns null.

---

Interactive version with quiz and diagrams: https://courseiva.com/learn/oracle-ocp-java17/java-io-and-nio-file-handling
