File I/O Part 1

Reading and writing files

Introduction to File I/O

Java provides comprehensive APIs for file and I/O operations through two main packages: the traditional java.io package and the modern java.nio (NIO.2) package introduced in Java 7.

Evolution of File I/O in Java

Key Packages Overview

PackageKey ClassesWhen to UsePerformance
java.ioFile, FileInputStream, BufferedReaderSimple file operations, streamsGood for small files
java.nio.filePath, Files, FileSystemModern file operations, large filesOptimized, memory-efficient
java.nio.channelsFileChannel, ByteBufferHigh-performance, direct I/OFastest for large files

Quick Comparison: Traditional vs NIO.2

Traditional vs Modern Approach

import java.io.*;
import java.nio.file.*;

public class FileIOComparison {
    // Traditional java.io approach
    public static void readFileTraditional(String path) throws IOException {
        BufferedReader reader = new BufferedReader(new FileReader(path));
        String line;
        while ((line = reader.readLine()) != null) {
            System.out.println(line);
        }
        reader.close();
    }

    // Modern NIO.2 approach (Java 7+)
    public static void readFileModern(String path) throws IOException {
        Files.lines(Paths.get(path))
             .forEach(System.out::println);
    }

    // Java 11+ - simplest approach
    public static void readFileSimple(String path) throws IOException {
        String content = Files.readString(Path.of(path));
        System.out.println(content);
    }
}

Traditional I/O (java.io)

The java.io package provides stream-based I/O operations. Understanding these fundamentals is essential even when using modern APIs.

Stream Hierarchy

InputStream / OutputStream (Byte Streams)
  |-- FileInputStream / FileOutputStream
  |-- BufferedInputStream / BufferedOutputStream
  |-- ObjectInputStream / ObjectOutputStream

Reader / Writer (Character Streams)
  |-- FileReader / FileWriter
  |-- BufferedReader / BufferedWriter
  |-- PrintWriter

File Class Operations

File Class Examples

import java.io.*;

public class FileClassDemo {
    public static void main(String[] args) {
        File file = new File("example.txt");

        // File properties
        System.out.println("Exists: " + file.exists());
        System.out.println("Is File: " + file.isFile());
        System.out.println("Is Directory: " + file.isDirectory());
        System.out.println("Can Read: " + file.canRead());
        System.out.println("Can Write: " + file.canWrite());

        // File metadata
        System.out.println("Name: " + file.getName());
        System.out.println("Path: " + file.getPath());
        System.out.println("Absolute Path: " + file.getAbsolutePath());
        System.out.println("Size: " + file.length() + " bytes");

        // Directory operations
        File dir = new File("mydir");
        dir.mkdir();  // Create single directory
        dir.mkdirs(); // Create directory hierarchy

        // List files
        File[] files = dir.listFiles();
        if (files != null) {
            for (File f : files) {
                System.out.println(f.getName());
            }
        }
    }
}

BufferedReader and BufferedWriter

Buffered I/O Operations

import java.io.*;

public class BufferedIOExample {
    // Reading with BufferedReader (try-with-resources)
    public static void readFile(String filename) {
        try (BufferedReader br = new BufferedReader(new FileReader(filename))) {
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // Writing with BufferedWriter
    public static void writeFile(String filename, String content) {
        try (BufferedWriter bw = new BufferedWriter(new FileWriter(filename))) {
            bw.write(content);
            bw.newLine(); // Platform-independent line separator
            bw.flush();   // Ensure data is written
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // Append to file
    public static void appendToFile(String filename, String content) {
        try (BufferedWriter bw = new BufferedWriter(
                new FileWriter(filename, true))) { // true = append mode
            bw.write(content);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

New I/O (NIO.2 - java.nio)

Java NIO.2 (introduced in Java 7) provides a more modern, efficient, and feature-rich API for file operations with better exception handling, symbolic link support, and platform-independent file attributes.

Why NIO.2 Over Traditional I/O?

Path vs File - Deep Dive

FeatureFile (java.io)Path (java.nio)
PerformanceSlower, more system callsOptimized, fewer system calls
Exception HandlingReturns false on errorThrows specific exceptions
Symbolic LinksLimited supportFull support with LinkOption
File AttributesBasic (size, modified time)Comprehensive (owner, permissions, ACLs)
File System SupportDefault file system onlyMultiple file systems (ZIP, JAR, custom)
Atomic OperationsNo guaranteeAtomic move and copy options
Directory StreamsArray of filesStream-based iteration

Path API - Advanced Examples

Comprehensive Path Operations

import java.nio.file.*;
import java.io.IOException;

public class PathOperationsAdvanced {
    public static void main(String[] args) throws IOException {
        // Creating Paths - Multiple ways
        Path path1 = Path.of("data", "file.txt");          // Java 11+
        Path path2 = Paths.get("data/file.txt");         // Java 7+
        Path path3 = Path.of(URI.create("file:///home/user/file.txt"));

        // Working with home directory
        Path home = Paths.get(System.getProperty("user.home"));
        Path config = home.resolve(".config/myapp/settings.json");

        // Path decomposition
        Path fullPath = Path.of("/home/user/documents/report.pdf");
        System.out.println("File name: " + fullPath.getFileName());
        System.out.println("Parent: " + fullPath.getParent());
        System.out.println("Root: " + fullPath.getRoot());
        System.out.println("Name count: " + fullPath.getNameCount());

        // Iterating path elements
        for (Path element : fullPath) {
            System.out.println("Element: " + element);
        }

        // Path resolution - combining paths
        Path base = Path.of("/home/user");
        Path resolved = base.resolve("documents/file.txt");

        // Resolve sibling - replace file name
        Path file = Path.of("/data/old.txt");
        Path sibling = file.resolveSibling("new.txt");

        // Relativizing - create relative path
        Path path = Path.of("/home/user/documents/file.txt");
        Path base2 = Path.of("/home/user");
        Path relative = base2.relativize(path);

        // Normalize - clean up path
        Path messy = Path.of("./data/../data/./file.txt");
        Path clean = messy.normalize();

        // Convert to absolute path
        Path relative2 = Path.of("data/file.txt");
        Path absolute = relative2.toAbsolutePath();

        // Compare paths
        boolean isSame = Files.isSameFile(path1, path2);

        // StartsWith / Ends With
        boolean isInHome = fullPath.startsWith("/home");
        boolean isPdf = fullPath.toString().endsWith(".pdf");
    }
}

Files Utility Class - Complete Guide

Files Class Advanced Operations

import java.nio.file.*;
import java.nio.file.attribute.*;
import java.io.IOException;
import java.util.*;

public class FilesUtilityComplete {
    public static void main(String[] args) throws IOException {
        Path path = Path.of("example.txt");

        // ===== Existence and Type Checks =====
        boolean exists = Files.exists(path);
        boolean notExists = Files.notExists(path);
        boolean isRegular = Files.isRegularFile(path);
        boolean isDir = Files.isDirectory(path);
        boolean isSymLink = Files.isSymbolicLink(path);
        boolean isHidden = Files.isHidden(path);

        // ===== Permission Checks =====
        boolean isReadable = Files.isReadable(path);
        boolean isWritable = Files.isWritable(path);
        boolean isExecutable = Files.isExecutable(path);

        // ===== File Metadata =====
        long size = Files.size(path);
        FileTime lastModified = Files.getLastModifiedTime(path);
        UserPrincipal owner = Files.getOwner(path);

        // ===== Create Operations =====
        Path newFile = Files.createFile(Path.of("new.txt"));
        Files.createDirectory(Path.of("newdir"));
        Files.createDirectories(Path.of("a/b/c/d"));
        Path tempFile = Files.createTempFile("prefix", ".tmp");
        Path tempDir = Files.createTempDirectory("myapp");

        // ===== Copy Operations =====
        Path source = Path.of("source.txt");
        Path target = Path.of("target.txt");

        Files.copy(source, target);
        Files.copy(source, target,
            StandardCopyOption.REPLACE_EXISTING,
            StandardCopyOption.COPY_ATTRIBUTES,
            LinkOption.NOFOLLOW_LINKS);

        // ===== Move/Rename Operations =====
        Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);
        Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);

        // ===== Delete Operations =====
        Files.delete(path);
        Files.deleteIfExists(path);

        // ===== File Attributes =====
        BasicFileAttributes attrs = Files.readAttributes(
            path, BasicFileAttributes.class);
        System.out.println("Size: " + attrs.size());
        System.out.println("Created: " + attrs.creationTime());
        System.out.println("Modified: " + attrs.lastModifiedTime());

        // ===== File Comparison =====
        long mismatch = Files.mismatch(path, target);
    }
}

Working with File Systems

File System Operations

import java.nio.file.*;
import java.net.URI;

public class FileSystemExample {
    public static void main(String[] args) throws Exception {
        // Get default file system
        FileSystem defaultFS = FileSystems.getDefault();
        System.out.println("File System: " + defaultFS);

        // Get root directories
        for (Path root : defaultFS.getRootDirectories()) {
            System.out.println("Root: " + root);
            FileStore store = Files.getFileStore(root);
            System.out.println("  Total: " + store.getTotalSpace() / (1024 * 1024 * 1024) + " GB");
            System.out.println("  Available: " + store.getUsableSpace() / (1024 * 1024 * 1024) + " GB");
        }

        // Working with ZIP file system
        Map<String, String> env = new HashMap<>();
        env.put("create", "true");

        URI uri = URI.create("jar:file:/path/to/file.zip");
        try (FileSystem zipFS = FileSystems.newFileSystem(uri, env)) {
            Path fileInZip = zipFS.getPath("/data/file.txt");
            Files.writeString(fileInZip, "Content in ZIP");
        }
    }
}

Reading Files Efficiently

Choose the right reading method based on file size, content type, and performance requirements. Modern Java provides multiple approaches, each optimized for different scenarios.

Reading Strategies - Decision Tree

Text File Reading - Complete Examples

Comprehensive Text Reading

import java.nio.file.*;
import java.io.*;
import java.util.*;
import java.util.stream.*;

public class TextFileReading {

    // Method 1: Read entire file as String (Java 11+)
    public static String readAsString(String filename) throws IOException {
        return Files.readString(Path.of(filename), StandardCharsets.UTF_8);
    }

    // Method 2: Read all lines into List
    public static List<String> readAllLines(String filename) throws IOException {
        return Files.readAllLines(Path.of(filename), StandardCharsets.UTF_8);
    }

    // Method 3: Stream-based reading (memory efficient)
    public static void processLargeFile(String filename) throws IOException {
        try (Stream<String> lines = Files.lines(Path.of(filename))) {
            lines.filter(line -> !line.trim().isEmpty())
                 .filter(line -> line.contains("ERROR"))
                 .map(String::toUpperCase)
                 .forEach(System.out::println);
        }
    }

    // Method 4: BufferedReader for custom processing
    public static void readWithBufferedReader(String filename) throws IOException {
        try (BufferedReader reader = Files.newBufferedReader(
                Path.of(filename), StandardCharsets.UTF_8)) {

            String line;
            int lineNumber = 0;

            while ((line = reader.readLine()) != null) {
                lineNumber++;
                processLine(line, lineNumber);
            }
        }
    }

    // Method 5: Scanner for formatted data
    public static void readWithScanner(String filename) throws IOException {
        try (Scanner scanner = new Scanner(Path.of(filename))) {
            while (scanner.hasNextLine()) {
                String line = scanner.nextLine();
                System.out.println(line);
            }

            scanner.useDelimiter(",");
            while (scanner.hasNext()) {
                String token = scanner.next();
                System.out.println(token.trim());
            }
        }
    }

    // Method 6: Read CSV file
    public static List<String[]> readCSV(String filename) throws IOException {
        try (Stream<String> lines = Files.lines(Path.of(filename))) {
            return lines.skip(1)
                        .map(line -> line.split(","))
                        .collect(Collectors.toList());
        }
    }

    // Method 7: Read and count words
    public static Map<String, Long> wordFrequency(String filename)
            throws IOException {
        try (Stream<String> lines = Files.lines(Path.of(filename))) {
            return lines.flatMap(line -> Arrays.stream(line.split("\\s+")))
                        .map(String::toLowerCase)
                        .collect(Collectors.groupingBy(
                            Function.identity(),
                            Collectors.counting()));
        }
    }
}

Binary File Reading - Advanced

Binary File Operations

import java.nio.*;
import java.nio.channels.*;
import java.io.*;

public class BinaryFileReading {

    // Method 1: Read all bytes (small files)
    public static byte[] readAllBytes(String filename) throws IOException {
        return Files.readAllBytes(Path.of(filename));
    }

    // Method 2: Buffered reading for large files
    public static void readLargeBinary(String filename) throws IOException {
        try (InputStream is = Files.newInputStream(Path.of(filename));
             BufferedInputStream bis = new BufferedInputStream(is)) {

            byte[] buffer = new byte[8192];
            int bytesRead;

            while ((bytesRead = bis.read(buffer)) != -1) {
                processBytes(buffer, bytesRead);
            }
        }
    }

    // Method 3: FileChannel for high performance
    public static void readWithChannel(String filename) throws IOException {
        try (FileChannel channel = FileChannel.open(
                Path.of(filename), StandardOpenOption.READ)) {

            ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 64);

            while (channel.read(buffer) > 0) {
                buffer.flip();
                while (buffer.hasRemaining()) {
                    byte b = buffer.get();
                }
                buffer.clear();
            }
        }
    }

    // Method 4: Memory-mapped file (fastest for large files)
    public static void readMemoryMapped(String filename) throws IOException {
        try (FileChannel channel = FileChannel.open(
                Path.of(filename), StandardOpenOption.READ)) {

            long size = channel.size();
            MappedByteBuffer buffer = channel.map(
                FileChannel.MapMode.READ_ONLY, 0, size);

            while (buffer.hasRemaining()) {
                byte b = buffer.get();
            }
        }
    }

    // Method 5: Random access file
    public static void randomAccess(String filename) throws IOException {
        try (RandomAccessFile raf = new RandomAccessFile(filename, "r")) {
            raf.seek(100);
            int value = raf.readInt();
            double d = raf.readDouble();
            String line = raf.readLine();
            long position = raf.getFilePointer();
            raf.seek(raf.length() - 100);
        }
    }
}

Performance Optimization Tips

Reading Performance Best Practices

  • Buffer Size: Use 8KB-64KB buffers for optimal performance
  • Direct Buffers: Use ByteBuffer.allocateDirect() for large files
  • Memory Mapping: Fastest for files >100MB accessed sequentially
  • Stream Processing: Use Files.lines() to avoid loading entire file
  • Charset Specification: Always specify UTF-8 explicitly
  • Resource Management: Always use try-with-resources

Writing Files Efficiently

Modern Java provides multiple ways to write files, each optimized for different scenarios.

Simple File Writing (Java 11+)

Modern File Writing

import java.nio.file.*;
import java.io.IOException;

public class WriteFileModern {
    // Write string to file (Java 11+)
    public static void writeString(String filename, String content)
            throws IOException {
        Files.writeString(Path.of(filename), content);
    }

    // Write lines
    public static void writeLines(String filename, List<String> lines)
            throws IOException {
        Files.write(Path.of(filename), lines);
    }

    // Append to file
    public static void appendContent(String filename, String content)
            throws IOException {
        Files.writeString(Path.of(filename), content,
            StandardOpenOption.APPEND);
    }

    // Write with BufferedWriter
    public static void writeWithBuffer(String filename) throws IOException {
        try (BufferedWriter writer = Files.newBufferedWriter(Path.of(filename))) {
            writer.write("Line 1");
            writer.newLine();
            writer.write("Line 2");
        }
    }
}

Binary File Writing

Writing Binary Files

import java.nio.file.*;
import java.nio.*;

public class WriteBinaryFile {
    // Write bytes directly
    public static void writeBytes(String filename, byte[] data)
            throws IOException {
        Files.write(Path.of(filename), data);
    }

    // Write using OutputStream
    public static void writeWithStream(String filename, byte[] data)
            throws IOException {
        try (OutputStream os = Files.newOutputStream(Path.of(filename))) {
            os.write(data);
            os.flush();
        }
    }

    // High-performance with FileChannel
    public static void writeWithChannel(String filename, byte[] data)
            throws IOException {
        try (FileChannel channel = FileChannel.open(
                Path.of(filename),
                StandardOpenOption.CREATE,
                StandardOpenOption.WRITE)) {

            ByteBuffer buffer = ByteBuffer.wrap(data);
            channel.write(buffer);
        }
    }
}

Performance Comparison

Files.writeString()

Use Case: Small text files

Performance: Fast

BufferedWriter

Use Case: Large text files

Performance: Efficient

FileChannel

Use Case: Binary files

Performance: Fastest

Serialization and Deserialization

Serialization converts objects into byte streams for storage or transmission. Java provides built-in serialization and modern alternatives.

Java Serialization

Object Serialization

import java.io.*;

class Person implements Serializable {
    private static final long serialVersionUID = 1L;

    private String name;
    private transient int age; // Won't be serialized
    private static String company; // Static fields not serialized
}

public class SerializationExample {
    // Serialize object to file
    public static void serialize(Person person, String filename) {
        try (ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream(filename))) {
            oos.writeObject(person);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // Deserialize object from file
    public static Person deserialize(String filename) {
        try (ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream(filename))) {
            return (Person) ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
            return null;
        }
    }
}

Modern Alternatives - JSON Serialization

JSON with Jackson/Gson

import com.google.gson.*;
import java.nio.file.*;

public class JsonSerialization {
    private static final Gson gson = new GsonBuilder()
        .setPrettyPrinting()
        .create();

    // Serialize to JSON
    public static void serializeToJson(Object obj, String filename)
            throws IOException {
        String json = gson.toJson(obj);
        Files.writeString(Path.of(filename), json);
    }

    // Deserialize from JSON
    public static <T> T deserializeFromJson(String filename, Class<T> clazz)
            throws IOException {
        String json = Files.readString(Path.of(filename));
        return gson.fromJson(json, clazz);
    }
}

Advanced File Operations

File Watching Service

Monitor File Changes

import java.nio.file.*;

public class FileWatcher {
    public static void watchDirectory(String dirPath) throws IOException {
        WatchService watchService = FileSystems.getDefault().newWatchService();
        Path path = Path.of(dirPath);

        path.register(watchService,
            StandardWatchEventKinds.ENTRY_CREATE,
            StandardWatchEventKinds.ENTRY_MODIFY,
            StandardWatchEventKinds.ENTRY_DELETE);

        while (true) {
            WatchKey key = watchService.take();
            for (WatchEvent<?> event : key.pollEvents()) {
                System.out.println("Event: " + event.kind() +
                    " - File: " + event.context());
            }
            key.reset();
        }
    }
}

Memory-Mapped Files

High-Performance I/O

import java.nio.*;
import java.nio.channels.*;

public class MemoryMappedFile {
    public static void readMemoryMapped(String filename)
            throws IOException {
        try (RandomAccessFile file = new RandomAccessFile(filename, "r");
             FileChannel channel = file.getChannel()) {

            MappedByteBuffer buffer = channel.map(
                FileChannel.MapMode.READ_ONLY, 0, channel.size());

            while (buffer.hasRemaining()) {
                byte b = buffer.get();
            }
        }
    }
}

Walking File Trees

Traverse Directory Structure

import java.nio.file.*;

public class FileTreeWalker {
    public static void walkFileTree(String startPath) throws IOException {
        Files.walk(Path.of(startPath))
             .filter(Files::isRegularFile)
             .filter(p -> p.toString().endsWith(".java"))
             .forEach(System.out::println);
    }

    // Find files by criteria
    public static void findLargeFiles(String dir) throws IOException {
        Files.find(Path.of(dir),
                    Integer.MAX_VALUE,
                    (path, attrs) -> attrs.isRegularFile() &&
                                     attrs.size() > 1_000_000)
             .forEach(path -> System.out.println(path + ": " +
                     Files.size(path) + " bytes"));
    }

    // Copy directory recursively
    public static void copyDirectory(Path source, Path target)
            throws IOException {
        Files.walk(source)
             .forEach(sourcePath -> {
                 Path targetPath = target.resolve(source.relativize(sourcePath));
                 try {
                     Files.copy(sourcePath, targetPath);
                 } catch (IOException e) {
                     e.printStackTrace();
                 }
             });
    }
}

Best Practices and Performance

Resource Management

Proper Resource Handling

import java.nio.file.*;

public class ResourceManagement {
    // CORRECT: Try-with-resources (Java 7+)
    public static void correctApproach(String filename) throws IOException {
        try (BufferedReader reader = Files.newBufferedReader(Path.of(filename))) {
            reader.lines().forEach(System.out::println);
        } // Automatically closed
    }

    // AVOID: Manual closing
    public static void manualApproach(String filename) {
        BufferedReader reader = null;
        try {
            reader = Files.newBufferedReader(Path.of(filename));
            // Read file
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

Performance Optimization

Buffer Size

  • Default: 8KB - good for most cases
  • Large files: 64KB - 1MB buffers
  • Network files: Smaller buffers (4-16KB)

Reading Strategy

  • Sequential: Use BufferedReader
  • Random access: Use RandomAccessFile
  • Large files: Use streams/channels

Writing Strategy

  • Batch writes when possible
  • Use buffered streams
  • Flush strategically

Common Pitfalls and Solutions

PitfallProblemSolution
Platform-dependent pathsHard-coded ”/” or ”\“Use File.separator or Path API
Not closing resourcesMemory leaksAlways use try-with-resources
Reading large files at onceOutOfMemoryErrorUse streams or BufferedReader
Ignoring character encodingCorrupted textSpecify UTF-8 explicitly

Performance Comparison

Benchmarking Example

import java.nio.file.*;

public class PerformanceBenchmark {
    public static void benchmark(String filename) {
        long start, end;

        // Method 1: Files.readAllLines()
        start = System.nanoTime();
        try {
            List<String> lines = Files.readAllLines(Path.of(filename));
            System.out.println("Lines: " + lines.size());
        } catch (IOException e) { }
        end = System.nanoTime();
        System.out.println("readAllLines: " + (end - start) / 1_000_000 + " ms");

        // Method 2: Files.lines() stream
        start = System.nanoTime();
        try (Stream<String> stream = Files.lines(Path.of(filename))) {
            long count = stream.count();
            System.out.println("Lines: " + count);
        } catch (IOException e) { }
        end = System.nanoTime();
        System.out.println("lines() stream: " + (end - start) / 1_000_000 + " ms");
    }
}

Best Practice Checklist

  • Always use try-with-resources for automatic cleanup
  • Use NIO.2 (Path/Files) for new code
  • Specify character encoding explicitly (UTF-8)
  • Use buffered streams for better performance
  • Handle exceptions appropriately
  • Use Files.lines() for large files
  • Validate file paths and permissions
  • Close streams in finally or use try-with-resources