Skip to main content

Ch 15.1 Java I/O Overview

In Java, Input/Output (I/O) refers to transferring data between the computer's internal memory and external devices such as the keyboard, monitor, files, and network. Java's java.io package provides a rich set of classes that handle this data transfer in a consistent, unified manner.

1. Node Streams (Source/Destination Streams)

At the core of Java I/O are node streams, which connect directly to a data source (input) or destination (output) such as a file or network socket.

Depending on the type of data they handle, streams are divided into two families:

Byte-based Streams

Transfer data one byte at a time. Can handle all types of data— images, videos, text, binary data.

  • Top-level abstract classes: InputStream and OutputStream
  • Common implementations:
    • FileInputStream / FileOutputStream — file access
    • ByteArrayInputStream / ByteArrayOutputStream — memory buffers

Character-based Streams

Java's char type is 2 bytes. Using byte streams for text can corrupt multi-byte characters (e.g., Korean, Chinese). Character streams are designed exclusively for text data.

  • Top-level abstract classes: Reader and Writer
  • Common implementations:
    • FileReader / FileWriter — text file access
    • StringReader / StringWriter — string-based I/O

2. The Stream Hierarchy

InputStream (abstract)
├── FileInputStream
├── ByteArrayInputStream
└── FilterInputStream
├── BufferedInputStream
└── DataInputStream

OutputStream (abstract)
├── FileOutputStream
├── ByteArrayOutputStream
└── FilterOutputStream
├── BufferedOutputStream
├── DataOutputStream
└── PrintStream

Reader (abstract)
├── InputStreamReader
│ └── FileReader
├── StringReader
└── BufferedReader

Writer (abstract)
├── OutputStreamWriter
│ └── FileWriter
├── StringWriter
├── BufferedWriter
└── PrintWriter

3. Basic File I/O Example

The most common use of I/O is reading from and writing to files. Resources must always be closed after use with close() to free system handles. Java 7+ introduced try-with-resources to automate this.

import java.io.*;

public class FileIOExample {
public static void main(String[] args) {
// Write a string to a file
try (FileWriter fw = new FileWriter("test.txt")) {
fw.write("Hello, World!\nThis is Java I/O.");
} catch (IOException e) {
e.printStackTrace();
}

// Read the file back character by character
try (FileReader fr = new FileReader("test.txt")) {
int data;
// read() returns -1 when end of file is reached
while ((data = fr.read()) != -1) {
System.out.print((char) data);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
note

Streams are unidirectional— input and output each require a separate stream object. You cannot read and write from the same FileInputStream or FileWriter.

4. The Decorator Pattern in Java I/O

Java I/O uses the Decorator Pattern: you wrap a basic (node) stream with one or more filter (wrapper) streams to add functionality.

// Node stream: connects directly to the file
FileInputStream fis = new FileInputStream("data.txt");

// Decorator 1: add buffering for performance
BufferedInputStream bis = new BufferedInputStream(fis);

// Use the outermost (most decorated) stream
int b = bis.read();

Common decorator chains:

// Text file reading with encoding + buffering
BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream("file.txt"),
java.nio.charset.StandardCharsets.UTF_8
)
);

// Text file writing with buffering + printf support
PrintWriter pw = new PrintWriter(
new BufferedWriter(
new FileWriter("output.txt")
)
);

5. try-with-resources — The Essential Pattern

import java.io.*;

public class TryWithResourcesExample {
public static void main(String[] args) {
// Old style: manual close, error-prone
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("file.txt"));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try { br.close(); } catch (IOException e) { e.printStackTrace(); }
}
}

// Modern style: try-with-resources (Java 7+)
// Any AutoCloseable is automatically closed when the block exits
try (BufferedReader br2 = new BufferedReader(new FileReader("file.txt"))) {
String line;
while ((line = br2.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}

// Multiple resources in one try-with-resources
try (
BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))
) {
String line;
while ((line = reader.readLine()) != null) {
writer.write(line.toUpperCase());
writer.newLine();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

6. Byte vs Character Stream Comparison

import java.io.*;
import java.nio.charset.StandardCharsets;

public class ByteVsCharExample {
public static void main(String[] args) throws IOException {
String text = "Hello, Java I/O!";

// Byte stream: write raw bytes
try (FileOutputStream fos = new FileOutputStream("bytes.bin")) {
fos.write(text.getBytes(StandardCharsets.UTF_8));
}

// Byte stream: read raw bytes
try (FileInputStream fis = new FileInputStream("bytes.bin")) {
byte[] buffer = new byte[1024];
int bytesRead = fis.read(buffer);
String result = new String(buffer, 0, bytesRead, StandardCharsets.UTF_8);
System.out.println(result); // Hello, Java I/O!
}

// Character stream: write text directly
try (FileWriter fw = new FileWriter("chars.txt", StandardCharsets.UTF_8)) {
fw.write(text);
}

// Character stream: read text directly
try (FileReader fr = new FileReader("chars.txt", StandardCharsets.UTF_8)) {
char[] buffer = new char[1024];
int charsRead = fr.read(buffer);
System.out.println(new String(buffer, 0, charsRead)); // Hello, Java I/O!
}
}
}
AspectByte StreamsCharacter Streams
Unit1 byte at a time1-2 chars (Unicode-aware)
Use caseImages, audio, binary dataPlain text files
Base classesInputStream, OutputStreamReader, Writer
EncodingManualAutomatic (charset specified)

7. Buffered I/O for Performance

Unbuffered streams access the disk on every read/write call. Buffered streams collect data in memory first, reducing disk access significantly.

import java.io.*;

public class BufferedPerformanceDemo {
public static void main(String[] args) throws IOException {
// Prepare a 1 MB test file
byte[] data = new byte[1024 * 1024];
try (FileOutputStream fos = new FileOutputStream("test_large.bin")) {
fos.write(data);
}

// Unbuffered read: slow (many disk accesses)
long start1 = System.currentTimeMillis();
try (FileInputStream fis = new FileInputStream("test_large.bin")) {
while (fis.read() != -1) {} // one byte at a time
}
System.out.println("Unbuffered: " + (System.currentTimeMillis() - start1) + "ms");

// Buffered read: fast (reads large chunks at once)
long start2 = System.currentTimeMillis();
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("test_large.bin"))) {
while (bis.read() != -1) {}
}
System.out.println("Buffered: " + (System.currentTimeMillis() - start2) + "ms");
}
}

8. Java NIO.2 — The Modern Alternative

Java 7 introduced java.nio.file (NIO.2) as a more powerful and concise API for file operations. For new code, prefer NIO.2 over traditional I/O.

import java.nio.file.*;
import java.nio.charset.StandardCharsets;
import java.util.List;

public class NioBasicsExample {
public static void main(String[] args) throws Exception {
Path path = Path.of("hello.txt");

// Write a file (one line)
Files.writeString(path, "Hello from NIO.2!\nSecond line.");

// Read entire file as String (small files)
String content = Files.readString(path);
System.out.println(content);

// Read all lines as a List
List<String> lines = Files.readAllLines(path);
lines.forEach(System.out::println);

// File metadata
System.out.println("Size: " + Files.size(path) + " bytes");
System.out.println("Exists: " + Files.exists(path));

// Delete
Files.deleteIfExists(path);
}
}
tip

Pro tip: For modern Java applications:

  • Use java.nio.file.Files and Path for most file operations (simpler, more powerful)
  • Use BufferedReader/Writer when fine-grained control or legacy compatibility is needed
  • Always specify encoding explicitly (StandardCharsets.UTF_8) to avoid platform-dependent behavior
  • Always use try-with-resources — never skip close()