Index

Java IO Fundamentals

Java IO and NIO

2.1 Byte Streams vs Character Streams

Java IO provides two primary types of streams to handle input and output operations: byte streams and character streams. Understanding the distinction between these two is crucial for selecting the right tools when reading or writing data in a Java program.

Byte Streams

Definition

Byte streams are designed to handle raw binary data β€” such as images, audio files, or serialized objects. These streams read and write 8-bit bytes, making them suitable for any kind of binary input or output.

Java provides the following abstract base classes for byte streams:

All byte stream classes in Java are derived from these.

Typical Use Cases

Example: Copying a Binary File Using Byte Streams

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class ByteStreamExample {
    public static void main(String[] args) {
        try (
            // Open input and output streams for binary file
            FileInputStream input = new FileInputStream("input.jpg");
            FileOutputStream output = new FileOutputStream("output.jpg");
        ) {
            int data;
            // Read and write one byte at a time
            while ((data = input.read()) != -1) {
                output.write(data);
            }
            System.out.println("File copied successfully using byte streams.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Explanation:

Character Streams

Definition

Character streams are designed to handle text data, dealing with 16-bit Unicode characters. These streams automatically handle character encoding and decoding, making them ideal for reading and writing text files.

Java provides the following abstract base classes for character streams:

These streams are encoding-aware and useful when working with text in any language.

Typical Use Cases

Example: Copying a Text File Using Character Streams

import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class CharacterStreamExample {
    public static void main(String[] args) {
        try (
            // Open character streams for text file
            FileReader reader = new FileReader("input.txt");
            FileWriter writer = new FileWriter("output.txt");
        ) {
            int ch;
            // Read and write one character at a time
            while ((ch = reader.read()) != -1) {
                writer.write(ch);
            }
            System.out.println("File copied successfully using character streams.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Explanation:

Key Differences Between Byte Streams and Character Streams

Feature Byte Streams Character Streams
Base Classes InputStream / OutputStream Reader / Writer
Data Type Bytes (8-bit) Characters (16-bit Unicode)
Suitable For Binary data (images, files) Text data (logs, documents)
Encoding Awareness Not aware of encoding Automatically handles encoding
Performance (text) May corrupt text without encoding Ideal for reading/writing text
Common Subclasses FileInputStream, BufferedInputStream FileReader, BufferedReader

When to Use Which

For example:

Recap

Java’s stream architecture cleanly separates IO into byte streams and character streams, each tailored to specific types of data. Byte streams provide raw access to data, making them perfect for binary files, while character streams add encoding support, making them safer and more efficient for text.

By choosing the appropriate stream type, developers can avoid common bugs such as text corruption and improve the performance and maintainability of their applications. Understanding this distinction is a foundational skill for mastering Java IO.

Index

2.2 InputStream and OutputStream Classes

In Java’s IO system, the InputStream and OutputStream classes form the foundation for all byte stream input and output operations. They enable reading and writing of binary data, such as images, audio files, PDF documents, or raw network data.

These two abstract classes reside in the java.io package and define the core API for handling low-level byte IO. All other byte-based stream classes in Java either extend or decorate these two base classes.

InputStream: Reading Bytes from a Source

InputStream is an abstract class that provides methods to read one byte at a time, or an array of bytes, from a source such as a file, socket, or byte array.

Key Methods

Method Description
int read() Reads one byte of data and returns it as an int (0–255), or -1 if end of stream is reached.
int read(byte[] b) Reads bytes into the provided array.
int read(byte[] b, int off, int len) Reads up to len bytes into array b, starting at offset off.
void close() Closes the stream and releases any resources.
int available() Returns the number of bytes that can be read without blocking.

Common Subclasses

Example: Reading Bytes from a File

import java.io.FileInputStream;
import java.io.IOException;

public class InputStreamExample {
    public static void main(String[] args) {
        try (FileInputStream fis = new FileInputStream("input.dat")) {
            int byteData;
            // Read one byte at a time until end of file
            while ((byteData = fis.read()) != -1) {
                System.out.print((char) byteData); // Print byte as character (for demo)
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Note: This approach works best for small files. For larger files, use buffered streams.

OutputStream: Writing Bytes to a Destination

OutputStream is the abstract superclass for all classes that write raw bytes to an output destination, such as a file, byte array, or network socket.

Key Methods

Method Description
void write(int b) Writes the specified byte (lower 8 bits of int).
void write(byte[] b) Writes all bytes from the given array.
void write(byte[] b, int off, int len) Writes len bytes from the array starting at offset off.
void flush() Forces any buffered output bytes to be written out.
void close() Closes the stream and releases resources.

Common Subclasses

Example: Writing Bytes to a File

import java.io.FileOutputStream;
import java.io.IOException;

public class OutputStreamExample {
    public static void main(String[] args) {
        String message = "Hello, this is a test message!";
        try (FileOutputStream fos = new FileOutputStream("output.dat")) {
            // Convert the string to bytes and write to file
            fos.write(message.getBytes());
            System.out.println("Message written to file.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Explanation: getBytes() converts the string to a byte array, which is then written to the file.

Combining InputStream and OutputStream: File Copy Example

Here’s a practical example that demonstrates using both InputStream and OutputStream to copy the contents of a binary file.

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileCopyExample {
    public static void main(String[] args) {
        try (
            FileInputStream input = new FileInputStream("source.dat");
            FileOutputStream output = new FileOutputStream("copy.dat");
        ) {
            byte[] buffer = new byte[1024]; // 1KB buffer
            int bytesRead;
            // Read from source and write to destination
            while ((bytesRead = input.read(buffer)) != -1) {
                output.write(buffer, 0, bytesRead);
            }
            System.out.println("File copied successfully.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Explanation:

Best Practices When Using Streams

Recap

InputStream and OutputStream are the backbone of Java’s byte stream IO system. They provide a flexible, low-level interface for reading and writing binary data across a variety of sources and destinations. By understanding these classes and their common subclasses, you gain the tools necessary to handle a wide range of IO tasks β€” from simple file operations to complex network data processing. Mastering them sets the foundation for efficient, reliable IO handling in any Java application.

Index

2.3 Reader and Writer Classes

Java provides a distinct set of stream classes for character-based input and output, built around the Reader and Writer abstract classes. These classes were introduced to support Unicode and allow seamless handling of text data in a platform- and encoding-independent manner.

While InputStream and OutputStream are designed for byte-level operations, Reader and Writer operate at the character level, abstracting away byte-to-character conversions and making it easier to work with textual content.

Why Character Streams?

Early Java IO relied solely on byte streams (InputStream and OutputStream). However, byte streams aren’t encoding-aware β€” they deal with raw bytes, so developers had to manually convert bytes to characters, which led to common bugs and encoding issues.

To resolve this, the Java platform introduced Reader and Writer, which:

Reader Class

Reader is an abstract base class for reading character streams. It reads 16-bit Unicode characters from text sources such as files, memory, or network streams.

Key Methods

Method Description
int read() Reads a single character, returns -1 if end of stream.
int read(char[] cbuf) Reads characters into an array.
int read(char[] cbuf, int off, int len) Reads up to len characters into cbuf starting at off.
void close() Closes the stream.
boolean ready() Checks if the stream is ready to be read.

Common Subclasses

Writer Class

Writer is the abstract superclass for writing character streams. It writes characters, arrays, or strings to text destinations.

Key Methods

Method Description
void write(int c) Writes a single character.
void write(char[] cbuf) Writes an array of characters.
void write(String str) Writes a string.
void flush() Flushes the stream (forces buffered data to be written).
void close() Closes the stream.

Common Subclasses

Example 1: Reading Text with FileReader and BufferedReader

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class ReaderExample {
    public static void main(String[] args) {
        try (BufferedReader reader = new BufferedReader(new FileReader("example.txt"))) {
            String line;
            // Read line by line until end of file
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Explanation:

Example 2: Writing Text with FileWriter and BufferedWriter

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;

public class WriterExample {
    public static void main(String[] args) {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {
            writer.write("Hello, world!");
            writer.newLine();
            writer.write("This was written using character streams.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Explanation:

Handling Character Encoding

Character streams like FileReader and FileWriter use the platform's default encoding, which can vary. To specify an encoding (e.g., UTF-8), use bridging streams like InputStreamReader and OutputStreamWriter.

Example: Reading with UTF-8 Encoding

import java.io.*;

public class UTF8ReadExample {
    public static void main(String[] args) {
        try (
            InputStreamReader isr = new InputStreamReader(new FileInputStream("utf8.txt"), "UTF-8");
            BufferedReader reader = new BufferedReader(isr)
        ) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Example: Writing with UTF-8 Encoding

import java.io.*;

public class UTF8WriteExample {
    public static void main(String[] args) {
        try (
            OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("utf8-output.txt"), "UTF-8");
            BufferedWriter writer = new BufferedWriter(osw)
        ) {
            writer.write("γ“γ‚“γ«γ‘γ―γ€δΈ–η•ŒοΌ"); // Japanese: "Hello, World!"
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Note: Always specify character encoding explicitly for portable, correct international text handling.

Key Differences Between Byte and Character Streams

Feature Byte Streams (InputStream/OutputStream) Character Streams (Reader/Writer)
Data Type Bytes (8-bit) Characters (16-bit Unicode)
Encoding Awareness Not aware Handles character encoding/decoding
Best For Binary files (images, audio, PDFs) Text files, source code, logs, CSVs
Example Classes FileInputStream, BufferedOutputStream BufferedReader, FileWriter, PrintWriter

Recap

The Reader and Writer classes bring structure and clarity to handling character data in Java. They solve the encoding challenges present in byte streams and allow developers to work naturally with Unicode and multi-language text. By choosing character streams over byte streams for text-based operations, Java developers can write more robust, maintainable, and internationalization-friendly code.

Index

2.4 File Handling Basics

When working with files and directories in Java, the primary tool is the java.io.File class. Unlike input/output stream classes, File does not handle reading or writing the contents of a fileβ€”it represents the abstract path of a file or directory in the file system and allows you to manipulate metadata and perform file-level operations such as creating, deleting, renaming, or checking properties.

Understanding the File Class

The File class represents both files and directories. It is used to:

To use it, you simply create an instance of File by passing a pathname (as a String or Path):

import java.io.File;

File myFile = new File("example.txt");

This line does not create a physical file. It only creates a representation of the path. The actual file operations require calling methods on the File object.

Creating Files and Directories

To create a new empty file, use the createNewFile() method:

import java.io.File;
import java.io.IOException;

public class CreateFileExample {
    public static void main(String[] args) {
        File file = new File("sample.txt");
        try {
            if (file.createNewFile()) {
                System.out.println("File created: " + file.getName());
            } else {
                System.out.println("File already exists.");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

To create a directory, use mkdir():

File dir = new File("myFolder");
if (dir.mkdir()) {
    System.out.println("Directory created.");
} else {
    System.out.println("Failed to create directory.");
}

To create nested directories, use mkdirs():

File nestedDir = new File("parent/child/grandchild");
nestedDir.mkdirs();  // creates all intermediate directories if needed

Deleting Files and Directories

To delete a file or empty directory, use the delete() method:

File file = new File("sample.txt");
if (file.delete()) {
    System.out.println("Deleted the file: " + file.getName());
} else {
    System.out.println("Failed to delete the file.");
}

Important:

Renaming and Moving Files

To rename or move a file, use renameTo():

File oldFile = new File("sample.txt");
File newFile = new File("renamed.txt");

if (oldFile.renameTo(newFile)) {
    System.out.println("File renamed successfully.");
} else {
    System.out.println("Rename failed.");
}

Note: This method can also move a file to a different directory.

Checking File Properties

The File class provides several methods to inspect the file or directory:

import java.io.File;

public class Test {

    public static void main(String[] argv) throws Exception {
        File file = new File("example.txt");

        System.out.println("File name: " + file.getName());
        System.out.println("Absolute path: " + file.getAbsolutePath());
        System.out.println("Parent: " + file.getParent());

        System.out.println("Exists: " + file.exists());
        System.out.println("Is directory: " + file.isDirectory());
        System.out.println("Is file: " + file.isFile());
        System.out.println("Readable: " + file.canRead());
        System.out.println("Writable: " + file.canWrite());
        System.out.println("Executable: " + file.canExecute());
        System.out.println("File size (bytes): " + file.length());
    }
}

These methods allow you to verify if a file exists, distinguish between files and directories, and check permissions.

Listing Files and Directories

To list files inside a directory, use list() or listFiles():

import java.io.File;

public class Test {

    public static void main(String[] argv) throws Exception {
        File directory = new File("myFolder");

        if (directory.isDirectory()) {
            String[] fileNames = directory.list();
            for (String name : fileNames) {
                System.out.println(name);
            }
        }
    }
}

Or get File objects:

import java.io.File;

public class Test {

    public static void main(String[] argv) throws Exception {
        File directory = new File("myFolder");
        File[] files = directory.listFiles();
        for (File f : files) {
            System.out.println(f.getName() + (f.isDirectory() ? " (dir)" : " (file)"));
        }
    }
}

Working with Absolute and Relative Paths

import java.io.File;

public class Test {

    public static void main(String[] argv) throws Exception {
        File relative = new File("sample.txt");
        File absolute = new File("/home/user/docs/sample.txt");

        System.out.println("Relative path: " + relative.getPath());
        System.out.println("Absolute path: " + absolute.getAbsolutePath());
    }
}

Summary of Common File Methods

Method Purpose
createNewFile() Creates a new file
mkdir() / mkdirs() Creates directories
delete() Deletes a file or empty directory
renameTo(File) Renames or moves a file
exists() Checks existence
isFile() / isDirectory() Checks file type
canRead() / canWrite() / canExecute() Checks permissions
length() Gets file size in bytes
list() / listFiles() Lists files in a directory

Recap

The File class in Java IO provides powerful tools to interact with the file system, enabling developers to create, delete, inspect, and manipulate files and directories. Although it doesn't handle file content, it is essential for managing file paths and metadata. For actual content processing, Reader/Writer or InputStream/OutputStream classes are used in conjunction. Mastering the File class sets the foundation for reliable and efficient file handling in Java applications.

Index

2.5 Buffered Streams and Their Importance

In Java IO, reading and writing data directly to and from a source (like a file or socket) using unbuffered streams (e.g., FileInputStream, FileOutputStream) can be inefficient, especially when data is processed byte-by-byte or character-by-character. Buffered streams were introduced to solve this performance issue by minimizing the number of expensive disk or network access operations through the use of an in-memory buffer.

What is Buffering in IO?

Buffering refers to the technique of using a temporary memory areaβ€”a bufferβ€”to store data before it's read or written. Instead of performing a system-level read/write operation for every byte or character, a buffered stream:

This reduces the number of IO operations, improving performance dramatically.

Buffered Streams in Java

Java provides four main buffered stream classes:

Buffered Stream Class Base Class Type
BufferedInputStream InputStream Byte-based input
BufferedOutputStream OutputStream Byte-based output
BufferedReader Reader Character-based input
BufferedWriter Writer Character-based output

How BufferedInputStream Works

BufferedInputStream wraps an existing InputStream and reads a block of bytes (default 8192 bytes) into an internal buffer. When your program reads from the stream, it accesses data from the buffer, not the file or socket directlyβ€”unless the buffer is empty.

Example: Reading a File Efficiently

import java.io.*;

public class BufferedInputExample {
    public static void main(String[] args) {
        try (
            FileInputStream fis = new FileInputStream("input.txt");
            BufferedInputStream bis = new BufferedInputStream(fis)
        ) {
            int byteData;
            while ((byteData = bis.read()) != -1) {
                System.out.print((char) byteData); // Convert byte to char
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Why it's efficient: Instead of one disk read per byte, a chunk of data is loaded at once, then served byte-by-byte from memory.

How BufferedOutputStream Works

BufferedOutputStream collects bytes in a memory buffer and writes them in chunks. This avoids frequent, slow writes to disk or a network stream.

Example: Writing to a File Efficiently

import java.io.*;

public class BufferedOutputExample {
    public static void main(String[] args) {
        try (
            FileOutputStream fos = new FileOutputStream("output.txt");
            BufferedOutputStream bos = new BufferedOutputStream(fos)
        ) {
            String message = "Buffered output stream example in Java.";
            bos.write(message.getBytes());
            bos.flush(); // Ensure data is written from buffer to file
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Important: Always call flush() before closing to ensure remaining data in the buffer is written.

How BufferedReader Works

BufferedReader reads characters efficiently by wrapping a Reader (e.g., FileReader or InputStreamReader). It also provides convenient methods like readLine(), which are not available in unbuffered readers.

Example: Reading Text Line-by-Line

import java.io.*;

public class BufferedReaderExample {
    public static void main(String[] args) {
        try (
            BufferedReader reader = new BufferedReader(new FileReader("notes.txt"))
        ) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line); // Print each line of the file
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Benefits:

How BufferedWriter Works

BufferedWriter buffers character data and writes it in bulk. It also includes a newLine() method to write platform-specific line separators.

Example: Writing Text with BufferedWriter

import java.io.*;

public class BufferedWriterExample {
    public static void main(String[] args) {
        try (
            BufferedWriter writer = new BufferedWriter(new FileWriter("log.txt"))
        ) {
            writer.write("BufferedWriter is efficient for writing text.");
            writer.newLine();
            writer.write("It reduces the number of write operations.");
            writer.flush(); // Push remaining data to file
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Note: Like BufferedOutputStream, always flush the writer to avoid data loss.

When to Use Buffered Streams

Buffered streams are especially useful when:

Default Buffer Size and Customization

By default, Java uses an 8 KB (8192 bytes) buffer for buffered streams. You can specify a different size:

import java.io.BufferedInputStream;
import java.io.FileInputStream;

public class Test {

    public static void main(String[] argv) throws Exception {
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("data.bin"), 16384); // 16 KB buffer
    }
}

Custom buffer sizes can optimize performance depending on the data volume and system architecture.

Recap

Buffered streams in Java are vital for efficient IO performance. By reading and writing data in blocks rather than byte-by-byte or character-by-character, they reduce the number of costly interactions with the file system or network. Whether you're dealing with binary or text data, buffered streams offer higher speed, lower latency, and improved application responsiveness. As a best practice, always prefer buffered streams unless working with very small data or special cases where buffering is unnecessary.

Index

2.6 Data Streams for Primitive Types

Java’s standard input and output streams (InputStream and OutputStream) operate at the byte level and lack the ability to directly read or write Java primitive data types (like int, float, or boolean). To bridge this gap, Java provides data streams, specifically DataInputStream and DataOutputStream, which enable applications to read and write Java primitives and strings in a platform-independent and efficient way.

Why Use Data Streams?

Data streams offer:

This makes them ideal for saving and restoring structured binary data in a format that can later be decoded accuratelyβ€”without manual byte parsing.

DataOutputStream

DataOutputStream extends FilterOutputStream and provides methods to write Java primitives in a standardized binary format.

Key Methods

Method Description
writeInt(int v) Writes 4 bytes for an int
writeDouble(double v) Writes 8 bytes for a double
writeBoolean(boolean v) Writes 1 byte (0 or 1)
writeUTF(String s) Writes a string in modified UTF-8 format
writeChar(int v) Writes 2 bytes for a char

Constructor

DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.bin"));

DataInputStream

DataInputStream extends FilterInputStream and complements DataOutputStream by reading data in the same format it was written.

Key Methods

Method Description
readInt() Reads 4 bytes and returns an int
readDouble() Reads 8 bytes and returns a double
readBoolean() Reads 1 byte and returns boolean
readUTF() Reads a string in modified UTF-8
readChar() Reads 2 bytes and returns a char

Constructor

DataInputStream dis = new DataInputStream(new FileInputStream("data.bin"));

Important: The order in which you read data must exactly match the order it was written.

Example: Writing Primitive Data Using DataOutputStream

import java.io.*;

public class DataOutputExample {
    public static void main(String[] args) {
        try (DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.bin"))) {
            dos.writeInt(42);             // 4 bytes
            dos.writeDouble(3.14159);     // 8 bytes
            dos.writeBoolean(true);       // 1 byte
            dos.writeUTF("Hello, Java");  // String with length prefix
            dos.writeChar('J');           // 2 bytes
            System.out.println("Data written to file.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Explanation:

Example: Reading Primitive Data Using DataInputStream

import java.io.*;

public class DataInputExample {
    public static void main(String[] args) {
        try (DataInputStream dis = new DataInputStream(new FileInputStream("data.bin"))) {
            int intValue = dis.readInt();
            double doubleValue = dis.readDouble();
            boolean boolValue = dis.readBoolean();
            String strValue = dis.readUTF();
            char charValue = dis.readChar();

            System.out.println("Read values:");
            System.out.println("Int: " + intValue);
            System.out.println("Double: " + doubleValue);
            System.out.println("Boolean: " + boolValue);
            System.out.println("String: " + strValue);
            System.out.println("Char: " + charValue);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Explanation:

Common Use Cases

Handling Exceptions

Data streams can throw several checked exceptions:

Always wrap stream operations in try-with-resources to ensure automatic resource management.

Important Considerations

Recap

DataInputStream and DataOutputStream are powerful tools for binary IO in Java. They eliminate the complexity of manually converting primitives to and from byte arrays, ensuring accurate and portable storage of Java’s basic data types. Whether you're building a low-level file format or communicating over sockets, data streams provide a clean, efficient, and consistent way to serialize and deserialize primitive values.

Index