• Labs icon Lab
  • Core Tech
Labs

Guided: Java SE 21 Developer (Exam 1Z0-830) - File Handling and I/O Operations

This Code Lab will teach you how to handle files and work with I/O operations using BufferedReader, BufferedWriter, and NIO.2. You'll implement file-based storage, manage directories, read and write structured data, and perform operations like searching, deleting, and backing up files. By the end of this lab, you'll have hands-on experience handling persistent data in Java applications.

Labs

Path Info

Level
Clock icon Beginner
Duration
Clock icon 1h 3m
Published
Clock icon Mar 07, 2025

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Table of Contents

  1. Challenge

    Introduction

    Welcome to the lab Guided: Java SE 21 Developer (Exam 1Z0-830) - File Handling and I/O Operations.

    Java provides powerful mechanisms for reading from and writing to files, which are essential for applications that need to store and retrieve information between program executions.

    Consider this scenario: You're developing an application that collects quotes. Without file operations, all information would be lost when the program terminates:

    // Without file persistence
    public class SimpleQuoteApp {
      public static void main(String[] args) {
        List<String> quotes = new ArrayList<>();
        quotes.add("The greatest glory in living lies not in never falling, but in rising every time we fall. - Nelson Mandela");
        
        System.out.println("Stored quotes: " + quotes);
        // When program ends, all quotes are lost!
      }
    }
    

    With proper file handling, you can save this data and retrieve it later:

    // With file persistence
    import java.nio.file.*;
    import java.io.*;
    import java.util.*;
    
    public class PersistentQuoteApp {
      public static void main(String[] args) throws IOException {
        Path filePath = Paths.get("quotes.txt");
        
        // Save a quote
        String quote = "The greatest glory in living lies not in never falling, but in rising every time we fall. - Nelson Mandela";
        Files.writeString(filePath, quote);
        
        // Read it back later
        String savedQuote = Files.readString(filePath);
        System.out.println("Retrieved quote: " + savedQuote);
      }
    }
    

    Java's file handling capabilities have evolved significantly:

    1. Traditional I/O (java.io): The original package providing input and output streams, readers, and writers
    2. New I/O (java.nio): Introduced in Java 1.4 with enhanced features like channels and buffers
    3. NIO.2 (java.nio.file): Added in Java 7, offering a more intuitive API for file operations

    This lab will focus on modern approaches using both buffered I/O and NIO.2's Path API.

    You'll build a Quote Collection Manager that will:

    • Store quotes in text files organized by author
    • Generate unique IDs for each quote
    • Support operations like adding, reading, deleting, and searching quotes
    • Implement backup functionality

    Quotes will be stored in a structured format:

    quotes/                 # Base directory
    ├── quote_counter.txt   # Counter file for generating IDs
    ├── Albert Einstein/    # Author directory
    │   ├── quote_001.txt   # Quote file
    │   └── quote_003.txt
    └── Mark Twain/         # Another author directory
        └── quote_002.txt
    

    Each quote file will contain the following structured data:

    Text: The only source of knowledge is experience.
    Author: Albert Einstein
    Source: Life Experience
    LastModified: 2023-09-25T15:45:30.123Z
    ``` ---
    
    ### Familiarizing with the Program Structure
    
    The application includes the following classes in the `src/main/java` directory:
    
    - `com.pluralsight.quotemanager.model.Quote.java`: A model class representing a quote with text, author, source, and timestamp
    - `com.pluralsight.quotemanager.util.Constants.java`: A class with constants for the names of the base directory and counter file
    - `com.pluralsight.quotemanager.util.FileUtils.java`: A utility class with placeholders for file path management methods
    - `com.pluralsight.quotemanager.service.FileOperationService.java`: A service class with placeholders for file operations
    - `com.pluralsight.quotemanager.Main.java`: The class that runs the command-line interface for user interactions
    
    The `Quote`, `Constants`, and `Main` classes are fully implemented. You'll be working on the `FileUtils` and `FileOperationService` classes, where `TODO` comments indicate the areas to complete. 
    
    You can compile and run the application using the **Run** button located at the bottom-right corner of the **Terminal**. Initially, the application will compile and run successfully, but some options will not function correctly.
    
    Begin by examining the code to understand the program's structure. Once you're ready, proceed with the coding tasks. If you need help, you can find solution files in the `solution` directory, organized by steps (e.g., `step2`, `step3`, etc.). Each solution file follows the naming convention `[filename]-[step]-[task].java` (e.g., `FileUtils-2-1.java` in `step2`).
    
  2. Challenge

    Setting up the File System

    Java NIO.2 provides a comprehensive API for file operations that is more intuitive and feature-rich than the legacy java.io.File class. It is built around two key components: the Path interface and the Files class.

    The Path Interface

    The Path interface represents a file path in the system. The recommended way to obtain a Path object is by using the Path.of method:

    Path path = Path.of("documents", "reports", "quarterly.txt");
    

    The benefits of using Path are:

    • Cross-platform compatibility: It automatically uses the correct file separator for the operating system.
    • Built-in path manipulation methods: Functions like resolve(), relativize(), and normalize(), simplify path handling.
    • Access to path components: Methods such as getFileName(), getParent(), and getRoot() allow easy retrieval of specific parts of the path.

    Now you're ready to start implementing the file system setup for the Quote Collection Manager using the Path interface. ---

    The Files Class

    The Files class provides static utility methods for working with files and directories. For example:

    // Check if a file exists
    boolean exists = Files.exists(path);
    
    // Create directories
    Files.createDirectories(directoryPath);
    
    // Read and write files
    String content = Files.readString(path);
    Files.writeString(path, "New content");
    

    Other operations you can perform with the Files class are:

    • Creating files and directories: createFile(), createDirectory(), and createDirectories()
    • Checking file attributes: exists(), isDirectory(), and isReadable()
    • Reading and writing: readString(), writeString(), newBufferedReader(), and newBufferedWriter()
    • Listing directory contents: list(), walk(), and find()
    • Manipulating files: copy(), move(), delete(), and deleteIfExists()

    For example, creating nested directory structures is straightforward with NIO.2:

    // Creates all necessary parent directories
    Files.createDirectories(Path.of("data", "archive", "2023"));
    

    This is particularly useful when you need to ensure a directory exists before writing files to it. Unlike the older File.mkdirs() method, Files.createDirectories() provides better error handling and returns the created directory path.

    Now you're ready to continue implementing the file system setup for the Quote Collection Manager using the Files class.

  3. Challenge

    Managing File-based Counters

    Java NIO.2 provides straightforward methods for reading and writing simple text files. When you need to read or persist small amounts of data, such as counters or configuration settings, these methods offer a clean alternative to using streams or readers/writers.

    Reading Text Files with Files.readString()

    The Files.readString() method reads the entire contents of a file into a String:

    Path path = Path.of("config.txt");
    try {
      String content = Files.readString(path);
      System.out.println("File content: " + content);
    } catch (NoSuchFileException e) {
      System.out.println("File doesn't exist");
    } catch (IOException e) {
      System.out.println("Error reading file: " + e.getMessage());
    }
    

    Here are some key considerations for Files.readString():

    • It reads the entire file at once, making it suitable for smaller files.
    • If the file doesn't exist, it throws a NoSuchFileException (a subclass of IOException).
    • You can specify a character set as an optional second parameter (defaults to UTF-8).

    However, in many cases, you'll need to handle missing files by using default values. The following example demonstrates how to read a score from a file while providing a default value:

    Path counterFile = Path.of("game_score.txt");
    int score = 0;  // Default value
    
    try {
      String content = Files.readString(counterFile);
      if (!content.isEmpty()) {
        score = Integer.parseInt(content.trim());
      }
    } catch (NoSuchFileException e) {
      // File doesn't exist yet, use the default value
    } catch (IOException e) {
      System.err.println("Error reading counter: " + e.getMessage());
    }
    

    Now you're ready to implement the method that reads the quote IDs from a file. ---

    Writing Text Files with Files.writeString()

    The Files.writeString() method writes a String to a file:

    Path configPath = Path.of("settings.txt");
    String data = "sound=on\ndifficulty=hard";
    
    try {
      Files.writeString(configPath, data);
      System.out.println("Settings saved");
    } catch (IOException e) {
      System.err.println("Error saving settings: " + e.getMessage());
    }
    

    Here are some key considerations for Files.writeString():

    • If the file does not exist, it is created automatically.
    • If the file already exists, its contents are overwritten.
    • You can use options like StandardOpenOption.APPEND to append data instead of overwriting.
    • Similar to readString(), you can specify a character set as an optional parameter.

    Now you're ready to complete the implementation of the file-based counter for unique quote IDs.

  4. Challenge

    Writing and Reading Quotes

    Writing Structured Data with BufferedWriter

    While Files.writeString() is convenient for simple text, BufferedWriter provides greater control when writing structured data to files. It improves performance through buffering (reducing system calls) and offers methods for writing individual lines.

    You can create a BufferedWriter using Files.newBufferedWriter(path):

    Path path = Path.of("data.txt");
    try (BufferedWriter writer = Files.newBufferedWriter(path)) {
      // Use the writer here
    } catch (IOException e) {
      System.err.println("Error writing to file: " + e.getMessage());
    }
    

    The buffering mechanism of BufferedWriter improves efficiency when writing many small pieces of data. If the file doesn't exist, it will be created. If it exists, it will be overwritten by default.

    Also, notice the use of a try-with-resources block to automatically close the writer when the block finishes, even if an exception occurs.

    The BufferedWriter class provides several methods for writing content. Here's an example:

    try (BufferedWriter writer = Files.newBufferedWriter(path)) {
      // Write a string
      writer.write("Customer: John Doe");
      
      // Move to a new line
      writer.newLine();
      
      // Write more data
      writer.write("Account: 12345");
      writer.newLine();
      writer.write("Balance: $1000.00");
    } catch (IOException e) {
      System.err.println("Error writing to file: " + e.getMessage());
    }
    

    The newLine() method adds platform-specific line separators. When writing structured data, organize related information on separate lines to improve readability and facilitate parsing later.

    Now you're ready to implement the method that saves a quote to a file. ---

    Reading Structured Data with BufferedReader

    Just as BufferedWriter facilitates writing structured data, BufferedReader enables efficient, line-by-line reading of text files. Unlike Files.readString(), which reads the entire file at once, BufferedReader is particularly useful for processing files with structured formats where data is organized by lines.

    You can create a BufferedReader using Files.newBufferedReader(path):

    Path path = Path.of("customer.txt");
    try (BufferedReader reader = Files.newBufferedReader(path)) {
      // Use the reader here
    } catch (IOException e) {
      System.err.println("Error reading file: " + e.getMessage());
    }
    

    Files.newBufferedReader() throws NoSuchFileException if the file doesn't exist, and like BufferedWriter, using try-with-resources ensures the reader is properly closed.

    The most common way to use BufferedReader is to read a file line by line:

    try (BufferedReader reader = Files.newBufferedReader(path)) {
      String line;
      while ((line = reader.readLine()) != null) {
        System.out.println("Read line: " + line);
        // Process each line here
      }
    } catch (IOException e) {
      System.err.println("Error reading file: " + e.getMessage());
    }
    

    The readLine() method returns null when it reaches the end of the file.

    When reading structured data, you often need to extract specific information based on line positions or content. Consider a file containing account details in the following format:

    Customer: John Doe
    Account: 123456789
    Balance: $5000.00
    

    The following code reads and extracts the customer name, account number, and balance from the file:

    try (BufferedReader reader = Files.newBufferedReader(path)) {
      String customerLine = reader.readLine();
      String accountLine = reader.readLine();
      String balanceLine = reader.readLine();
      
      // Parse the data
      String customerName = customerLine.substring(customerLine.indexOf(":") + 1).trim();
      String accountNumber = accountLine.substring(accountLine.indexOf(":") + 1).trim();
      String balanceText = balanceLine.substring(balanceLine.indexOf(":") + 1).trim();
      
      System.out.println("Customer: " + customerName);
    } catch (IOException e) {
      System.err.println("Error reading file: " + e.getMessage());
    }
    

    When working with structured data, you can extract specific details using string operations such as substring(), split(), or by checking if lines start with certain prefixes.

    Now you're ready to implement the method that reads a quote from a file.

  5. Challenge

    Deleting and Listing Quotes

    Deleting Files with Files.deleteIfExists()

    The Files.deleteIfExists() method provides a convenient way to delete a file without throwing an exception if the file doesn't exist:

    Path logFile = Path.of("logs", "old_log.txt");
    try {
      boolean deleted = Files.deleteIfExists(logFile);
      if (deleted) {
        System.out.println("File deleted successfully");
      } else {
        System.out.println("File did not exist");
      }
    } catch (IOException e) {
      System.err.println("Error deleting file: " + e.getMessage());
    }
    

    Here are some key considerations for Files.deleteIfExists():

    • Returns true if the file was deleted and false if it didn't exist
    • Throws an IOException if deletion fails due to reasons such as permission issues or the file being in use
    • Often preferred over Files.delete(), which throws a NoSuchFileException if the file does not exist

    Now you're ready to implement the method that will be called when users want to remove a quote from the collection. ---

    Listing Directory Contents with Files.list()

    The Files.list() method returns a Stream<Path> containing the entries in a directory:

    Path directory = Path.of("documents");
    try (Stream<Path> entries = Files.list(directory)) {
      entries.forEach(path -> {
        System.out.println(path.getFileName());
      });
    } catch (IOException e) {
      System.err.println("Error listing directory: " + e.getMessage());
    }
    

    Since Files.list() returns a stream, you can leverage Stream API methods to filter and process files:

    Path documentsDir = Path.of("documents");
    try (Stream<Path> entries = Files.list(documentsDir)) {
      // Get only text files
      List<Path> textFiles = entries
        .filter(Files::isRegularFile)  // Excludes directories
        .filter(path -> path.toString().endsWith(".txt"))  // Keeps only .txt files
        .collect(Collectors.toList());
      
      System.out.println("Found " + textFiles.size() + " text files");
    } catch (IOException e) {
      System.err.println("Error processing directory: " + e.getMessage());
    }
    

    In this example:

    • .filter(Files::isRegularFile): Keeps only regular files, excluding directories
    • .filter(path -> path.toString().endsWith(".txt")): Further filters the stream to include only files with a .txt extension
    • .collect(Collectors.toList()): Collects the filtered paths into a List<Path>

    Since Files.list() returns a stream, you must use a try-with-resources statement to ensure it is properly closed and to avoid resource leaks:

    // Proper resource management with try-with-resources
    try (Stream<Path> stream = Files.list(directory)) {
      // Use the stream
    } catch (IOException e) {
      // Handle exceptions
    }
    

    Finally, note that Files.list() only lists the immediate contents of a directory, it does not perform a recursive listing.

    Now you're ready to implement the method that lists quotes by authors.

  6. Challenge

    Searching and Modifying Quotes

    Searching for Content

    Finding content within a directory structure often requires navigating multiple levels. While the previous step covered listing files in a single directory, searching across all quotes involves:

    1. List all author directories.
    2. For each author directory, process all quote files.
    3. Check each file for the search criteria.

    One approach to navigate a nested directory structure is using nested streams:

    // Outer stream
    try (Stream<Path> directories = Files.list(rootPath)) {
      directories.filter(Files::isDirectory).forEach(directory -> {
        // Inner stream
        try (Stream<Path> files = Files.list(directory)) {
          files.filter(Files::isRegularFile).forEach(file -> {
            // Process each file
            try {
              // Read and analyze file content
            } catch (IOException e) {
              System.err.println("Error processing file: " + e.getMessage());
            }
          });
        } catch (IOException e) {
          System.err.println("Error listing directory: " + e.getMessage());
        }
      });
    }
    

    This snippet recursively processes a nested directory structure using two levels of Files.list() streams:

    • The outer stream iterates over directories inside rootPath.
    • The inner stream lists and processes regular files within each directory.

    Java provides multiple ways to search text within files. Here's a basic example:

    String text = "My favorite colors are brown and blue";
    String searchTerm = "BROWN";
    
    // Case-sensitive search (returns -1 for no match)
    boolean containsExact = text.contains(searchTerm);  // false
    
    // Case-insensitive search
    boolean containsIgnoreCase = text.toLowerCase().contains(searchTerm.toLowerCase());  // true
    

    For a case-insensitive search, both the text and the search term are converted to lowercase. The toLowerCase() method is commonly used for this purpose, though it creates new string objects. For more advanced matching, regular expressions can be used.

    Finally, when gathering results from multiple sources, a common approach is:

    1. Creating a collection to store matching results
    2. Iterating through potential sources
    3. Adding matching items to the collection

    For example:

    List<String> results = new ArrayList<>();
    
    // Process multiple sources
    for (String source : sources) {
      if (source.contains(searchTerm)) {
        results.add(source);
      }
    }
    
    return results;
    

    This pattern of building a result collection is fundamental for search operations across multiple files.

    Now you're ready to implement the search functionality for your Quote Collection Manager. ---

    Moving Files with Files.move()

    One of the final features needed for the application is the ability to move files between directories. Java NIO.2 provides the Files.move() method for this purpose.

    The simplest form of Files.move() takes a source path and a target path:

    Path source = Path.of("documents", "draft.txt");
    Path target = Path.of("documents", "final", "report.txt");
    
    try {
      Files.move(source, target);
      System.out.println("File moved successfully");
    } catch (IOException e) {
      System.err.println("Error moving file: " + e.getMessage());
    }
    

    This snippet moves a file from documents/draft.txt to documents/final/report.txt using Files.move(). If the move is successful, a confirmation message is printed. Otherwise, an IOException is caught and an error message is displayed.

    The Files.move() method allows you to:

    • Move a file to a different directory.
    • Rename a file (when the target is in the same directory but has a different name).
    • Do both simultaneously.

    Additionally, Files.move() supports optional parameters to control its behavior:

    // Move and replace if the target already exists
    Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
    
    // Atomic move (when supported by the file system)
    Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);
    

    The most commonly used option is StandardCopyOption.REPLACE_EXISTING, which overwrites the target file if it already exists. Without this option, an exception is thrown if the target file exists.

    Now you're ready to implement the functionality that lets users change a quote's author by moving its file.

  7. Challenge

    Backing up the Quotes

    Creating a backup system involves copying an entire directory structure, including all subdirectories and files. Java NIO.2 provides powerful tools to accomplish this efficiently.

    Walking Directory Trees with Files.walk()

    Unlike Files.list(), which only processes the immediate contents of a directory, Files.walk() recursively traverses an entire directory tree:

    Path rootDir = Path.of("documents");
    try (Stream<Path> allPaths = Files.walk(rootDir)) {
      allPaths.forEach(path -> {
        System.out.println(path);
      });
    } catch (IOException e) {
      System.err.println("Error walking directory tree: " + e.getMessage());
    }
    

    This code will list all files and directories under documents, including those in subdirectories at any depth.

    When walking a directory tree, you need to distinguish between files and directories. You can use Files.isDirectory() to handle them separately:

    if (Files.isDirectory(path)) {
      // Process directories
    } else {
      // Process files
    }
    

    This distinction allows for different operations, such as recursively traversing directories or reading file contents.

    Working with Relative Paths

    When copying a directory structure, maintaining relative paths between the source and destination is essential. Java NIO.2 provides two key methods for this:

    • relativize(): Creates a relative path between two paths
    • resolve(): Combines a path with a relative path

    For example, given a root directory and a specific file within it:

    Path sourceRoot = Path.of("/home/user/documents");
    Path sourceFile = Path.of("/home/user/documents/project/report.txt");
    

    You can obtain the relative path from the root to the file using relativize():

    Path relativePath = sourceRoot.relativize(sourceFile);  // "project/report.txt"
    

    This relative path can then be applied to a new root directory using resolve():

    Path backupRoot = Path.of("/backup/docs");
    Path backupFile = backupRoot.resolve(relativePath);  // "/backup/docs/project/report.txt"
    

    This approach ensures that directory structures remain intact when copying or backing up files.

    Copying Files with Files.copy()

    Java NIO.2 provides a straightforward way to copy files using Files.copy():

    Path source = Path.of("original.txt");
    Path target = Path.of("backup", "original.txt");
    
    // Basic copy (fails if the target file already exists)
    Files.copy(source, target);
    
    // Copy with options (overwrites the target file if it exists)
    Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
    

    By default, Files.copy() does not overwrite existing files. However, you can specify StandardCopyOption.REPLACE_EXISTING to allow overwriting.

    Now you're ready to implement the backup functionality for your Quote Collection Manager.

  8. Challenge

    Conclusion

    Congratulations on successfully completing this Code Lab!

    You can compile and run the application either by clicking the Run button in the bottom-right corner of the screen or by using the Terminal with the following commands:

    1. Compile and package the application:

      mvn clean package
      
    2. Run the application:

      java -cp target/quote-collection-manager-1.0-SNAPSHOT.jar com.pluralsight.quotemanager.Main
      

    When you start the application, the main menu appears:

    Quote Collection Manager
    1. Add New Quote
    2. Read Quote
    3. Delete Quote
    4. List Quotes by Author
    5. Search Quotes
    6. Change Quote's Author
    7. Backup Collection
    8. Exit
    Choose an option: 
    

    Try adding a few quotes, searching for specific content, and creating a backup to see all your implementation in action! ---

    Extending the Program

    Here are some ideas to further enhance your skills and extend the application's capabilities:

    1. Object Serialization: Modify the program to store quotes as serialized Java objects instead of text files, making the Quote class implement Serializable and use ObjectOutputStream and ObjectInputStream for storage and retrieval.

    2. File Change Monitoring: Automatically refresh the application's data when files are modified externally by using Java's WatchService to monitor the quotes directory for changes.

    3. Memory-Mapped File Access: For very large quote collections, implement memory-mapped file access using FileChannel.map() to improve performance when reading and writing large files.

    By implementing these enhancements, you'll gain a deeper understanding of Java's file-handling capabilities and create a more robust Quote Collection Manager. ---

    Related Courses in Pluralsight's Library

    If you'd like to continue building your Java skills or explore related topics, check out these courses available on Pluralsight:

    These courses cover a wide range of Java programming topics. Explore them to further your learning journey in Java!

Esteban Herrera has more than twelve years of experience in the software development industry. Having worked in many roles and projects, he has found his passion in programming with Java and JavaScript. Nowadays, he spends all his time learning new things, writing articles, teaching programming, and enjoying his kids.

What's a lab?

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Provided environment for hands-on practice

We will provide the credentials and environment necessary for you to practice right within your browser.

Guided walkthrough

Follow along with the author’s guided walkthrough and build something new in your provided environment!

Did you know?

On average, you retain 75% more of your learning if you get time for practice.