Java File I/O: Byte Streams, Character Streams & the File Class

Input/Output (I/O) in Java allows your application to interact with external sources: files on disk, network sockets, keyboard inputs, and printer streams. In Java, all data transport is modeled through Streams — an ordered sequence of data flowing from a producer (source) to a consumer (destination).

Universal Stream Rule

In Java I/O: Input means reading data INTO your JVM memory from an external source. Output means writing data OUT from your JVM memory to an external file or destination.

The Stream Hierarchy in java.io

Java divides I/O streams into two fundamentally distinct hierarchies based on data granularity:

Java I/O Stream Hierarchy: Byte Streams vs Character Streams
FeatureByte Streams (1 Byte / 8-bit)Character Streams (2 Bytes / 16-bit)
Root Abstract ClassesInputStream / OutputStreamReader / Writer
Primary Data TypeRaw binary data: byte, images, PDFs, MP3sUnicode text: char, Strings, text logs, CSV
Encoding AwarenessNone (raw bytes passed as-is)Translates bytes to Unicode chars (UTF-8, UTF-16, ASCII)
Common Disk ImplementationsFileInputStream, FileOutputStreamFileReader, FileWriter
Buffered DecoratorsBufferedInputStream, BufferedOutputStreamBufferedReader, BufferedWriter

1. The java.io.File Class

The File class represents an abstract pathname of a file or directory on the file system. It does not read or write data inside the file; it inspects and manages file metadata (existence, length, permissions, deletion, directory creation).

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

public class FileDemo {
    public static void main(String[] args) {
        File file = new File("veewom-notes.txt");
        
        try {
            if (file.createNewFile()) {
                System.out.println("File created: " + file.getName());
            } else {
                System.out.println("File already exists on disk.");
            }
            
            System.out.println("Absolute Path: " + file.getAbsolutePath());
            System.out.println("Can Read: " + file.canRead());
            System.out.println("Can Write: " + file.canWrite());
            System.out.println("File Size (bytes): " + file.length());
            System.out.println("Is Directory: " + file.isDirectory());
        } catch (IOException e) {
            System.err.println("I/O Error: " + e.getMessage());
        }
    }
}

2. Byte Streams: FileInputStream & FileOutputStream

Byte streams read and write raw 8-bit bytes. Use byte streams whenever you transfer non-text binary assets (e.g., JPEG images, ZIP archives) or need raw binary compatibility.

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

public class ByteStreamCopy {
    public static void main(String[] args) {
        // Copying an image byte-by-byte with try-with-resources
        try (FileInputStream in = new FileInputStream("input.png");
             FileOutputStream out = new FileOutputStream("copy.png")) {
            
            byte[] buffer = new byte[4096]; // 4 KB chunk buffer
            int bytesRead;
            
            while ((bytesRead = in.read(buffer)) != -1) {
                out.write(buffer, 0, bytesRead);
            }
            System.out.println("Binary file copied successfully!");
        } catch (IOException e) {
            System.err.println("File copy failed: " + e.getMessage());
        }
    }
}

3. Character Streams: FileReader & FileWriter

Character streams read and write 16-bit Unicode characters. In modern Java (Java 11+), you can explicitly specify the StandardCharsets.UTF_8 encoding to avoid platform-dependent encoding bugs (e.g., Windows CP-1252 vs Linux UTF-8).

import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class CharStreamDemo {
    public static void main(String[] args) {
        String data = "Welcome to Veewom Java Tutorials!\nLearn Java 21 LTS.";
        
        // Writing text using FileWriter
        try (FileWriter writer = new FileWriter("greeting.txt", StandardCharsets.UTF_8)) {
            writer.write(data);
            System.out.println("Data successfully written to greeting.txt");
        } catch (IOException e) {
            e.printStackTrace();
        }

        // Reading text using FileReader
        try (FileReader reader = new FileReader("greeting.txt", StandardCharsets.UTF_8)) {
            int ch;
            while ((ch = reader.read()) != -1) {
                System.out.print((char) ch); // cast int to character
            }
            System.out.println();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Dry-Run & Memory Execution Trace

Step-by-Step Trace: File Stream read() Loop
StepExpressionVariable (ch / bytesRead)Action / Condition
1in.read(buffer)4096Reads first 4096 bytes from disk; condition != -1 is TRUE. Writes chunk.
2in.read(buffer)1420Reads remaining 1420 bytes of file; condition != -1 is TRUE. Writes slice.
3in.read(buffer)-1 (EOF)End-of-File reached. While loop terminates immediately.
4try-with-resources exitin.close(), out.close()Native OS file handles released in reverse order of declaration.
BCA / University Exam Corner
  • Viva Question: Why does read() method return an int instead of a byte or char?
    Answer: A byte can hold values from -128 to 127 (or 0 to 255 unsigned). To signal the End-of-File (EOF) condition, Java requires a unique sentinel value, which is -1. An int has 32 bits, allowing 0 to 255 for valid byte values and -1 for EOF without collision.
  • Exam Distinction: Always write out the differences between InputStream and Reader when asked in theoretical papers.

Frequently Asked Questions (FAQ)

Q1. What is the primary difference between Byte Streams and Character Streams in Java?
Byte Streams (InputStream and OutputStream) process raw binary data 8 bits (1 byte) at a time, making them ideal for images, audio, video, and PDF files. Character Streams (Reader and Writer) process Unicode characters 16 bits (2 bytes) at a time with automatic character-set translation (such as UTF-8 or UTF-16), making them suitable for plain text, CSV, and XML files.
Q2. Why should we always close file streams or use try-with-resources?
File streams hold operating system file descriptors (native resources). If streams are not closed, it causes resource leaks, locks files on disk preventing modification by other processes, and exhausts the OS file descriptor table. The try-with-resources statement ensures AutoCloseable streams are guaranteed to close even when exceptions occur.
Q3. Does the java.io.File class create a physical file on disk when instantiated?
No. Creating a new File("data.txt") object only creates an abstract pathname representation in JVM memory. It does not create a physical file on the disk until you invoke methods like file.createNewFile() or wrap it in a file output stream.

Explore Related Java Tutorials

Accelerate your Java mastery by studying these tightly connected core tutorials:

BufferedReader & Scanner →

High-speed buffered reading and token parsing.

Java NIO.2 Files & Paths →

Modern non-blocking and high-performance file APIs.

Try-with-Resources →

Safe automatic closing of file streams.

Java Object Serialization →

Saving and restoring Java objects to file streams.

Share Share on Facebook Share on Twitter Share on LinkedIn Pin on Pinterest Share on Stumbleupon Share on Tumblr Share on Reddit Share on Diggit

You may also like this!