By WebNest Studio Engineering Team
Java - Core Tutorial
Java I/O Streams Fundamentals
Java's I/O (input/output) system is built around the idea of a "stream" — a sequential flow of data moving from a source (a file, the network, memory, the keyboard) to a destination, one piece at a time. The java.io package organizes these streams into two parallel class hierarchies: one for raw bytes and one for characters, and understanding why both exist — and when to use each — is fundamental to reading and writing data correctly in Java.
Why it matters
Reading files, handling uploads, calling other services and writing logs all go through streams. The whole of java.io follows from two ideas covered here: bytes versus characters, and wrapping one stream in another to add a feature such as buffering. Closing streams correctly is also what prevents resource leaks in long-running programs.
Byte Streams: InputStream and OutputStream
InputStream and OutputStream are the abstract root classes for reading and writing raw 8-bit bytes. They are used for any data that isn't inherently text: images, audio, compiled class files, serialized objects, or network protocols. Common concrete subclasses include FileInputStream/FileOutputStream (files), ByteArrayInputStream/ByteArrayOutputStream (in-memory byte arrays), and BufferedInputStream/BufferedOutputStream, which wrap another stream to reduce the number of expensive underlying I/O calls by reading and writing in larger chunks.
Character Streams: Reader and Writer
Reader and Writer are the abstract root classes for reading and writing 16-bit Unicode characters rather than raw bytes. Character streams exist because text is not just bytes — it is bytes interpreted according to a specific character encoding (UTF-8, UTF-16, ISO-8859-1, and so on), and if you read text data through a raw byte stream without applying the correct encoding, multi-byte characters (accented letters, non-Latin scripts, emoji) can be corrupted. Reader/Writer subclasses such as FileReader/FileWriter and InputStreamReader/OutputStreamWriter handle that byte-to-character (and character-to-byte) conversion internally, using either a specified charset or the platform default. BufferedReader/BufferedWriter add buffering on top, and BufferedReader additionally provides the convenient readLine() method.
Try-With-Resources for Automatic Closing
Every stream, reader, and writer implements Closeable, and holds onto an underlying operating system resource (a file handle, a socket) that must be released when you are done, or it will leak. Prior to Java 7, this required a verbose try/finally block to guarantee closing even if an exception occurred mid-operation. The try-with-resources statement, try (Resource r = ...) { ... }, automatically calls close() on every resource declared in its parentheses when the block exits — whether normally or via an exception — making resource leaks far less likely and the code noticeably shorter.
Examples
Writing and reading raw bytes with a byte stream
import java.io.*;
public class ByteStreamDemo {
public static void main(String[] args) throws IOException {
try (OutputStream out = new FileOutputStream("data.bin")) {
out.write("Hello".getBytes());
}
try (InputStream in = new FileInputStream("data.bin")) {
byte[] buffer = new byte[5];
int bytesRead = in.read(buffer);
System.out.println("Bytes read: " + bytesRead);
System.out.println("Content: " + new String(buffer, 0, bytesRead));
}
}
}
Bytes read: 5
Content: Hello
Writing and reading text with a character stream
import java.io.*;
public class CharacterStreamDemo {
public static void main(String[] args) throws IOException {
try (Writer writer = new FileWriter("notes.txt")) {
writer.write("Character streams handle text encoding correctly.");
}
try (BufferedReader reader = new BufferedReader(new FileReader("notes.txt"))) {
String line = reader.readLine();
System.out.println(line);
}
}
}
Character streams handle text encoding correctly.
Common Mistakes
- Using a byte stream (FileInputStream/FileOutputStream) to read or write plain text files instead of a character stream, risking corrupted output for non-ASCII characters.
- Forgetting to close streams explicitly in older-style code, or relying on garbage collection to close them eventually — always use try-with-resources instead.
- Wrapping a stream in a buffered variant but never actually benefiting from it, because a non-buffered stream is read/written one byte or character at a time in a loop.
- Assuming FileReader always uses UTF-8 — it actually uses the JVM's platform default charset unless you use InputStreamReader with an explicit Charset argument.
Exercise
Copy every byte from the input stream to the output stream using a small buffer, then print the copied text. Both streams are in memory, so no file is needed. Use try-with-resources.
Expected output: hello streams
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
public class CopyBytes {
public static void main(String[] args) throws IOException {
byte[] source = "hello streams".getBytes(StandardCharsets.UTF_8);
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (InputStream in = new ByteArrayInputStream(source)) {
byte[] buffer = new byte[4];
// TODO: read into the buffer until read() returns -1,
// writing only the bytes that were actually read
}
System.out.println(new String(out.toByteArray(), StandardCharsets.UTF_8));
}
}
Show hints
in.read(buffer)returns how many bytes it placed in the buffer, or-1at the end.- Write with
out.write(buffer, 0, count). Writing the whole buffer would repeat old bytes on the last, shorter read.
Show solution
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
public class CopyBytes {
public static void main(String[] args) throws IOException {
byte[] source = "hello streams".getBytes(StandardCharsets.UTF_8);
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (InputStream in = new ByteArrayInputStream(source)) {
byte[] buffer = new byte[4];
int count;
while ((count = in.read(buffer)) != -1) {
out.write(buffer, 0, count); // only the bytes just read
}
}
System.out.println(new String(out.toByteArray(), StandardCharsets.UTF_8)); // hello streams
}
}
Quick quiz
1. What does read() return when the end of a stream is reached?
- A. -1
- B. 0
- C. null
- D. It throws EOFException
Show answer
A. The value -1 marks the end, which is why read loops test for it.
2. Which kind of stream should be used to read a text file?
- A. A character stream (Reader), which decodes bytes into characters
- B. A byte stream, always
- C. Either; there is no difference
- D. An object stream
Show answer
A. A character in an encoding such as UTF-8 may take several bytes. A Reader handles the decoding.
3. What does try-with-resources guarantee?
- A. The code never throws
- B. The file is deleted afterwards
- C. Each resource is closed when the block ends, including when an exception is thrown
- D. The stream is buffered
Show answer
C. close() is called automatically, in the reverse of the order in which the resources were declared.
Interview questions
What is the difference between byte streams and character streams?
Show answer
Byte streams, the InputStream and OutputStream classes, read and write raw 8-bit bytes and are used for binary data such as images and archives. Character streams, the Reader and Writer classes, read and write characters and convert between bytes and characters using a character encoding, so they are used for text.
Why wrap a stream in a buffered stream?
Show answer
Without a buffer, each read() or write() call may go to the operating system, which is slow. A buffered stream reads or writes a large block at a time and serves individual calls from memory, which greatly reduces the number of system calls. A buffered output stream must be flushed or closed for the last block to be written.
Key Points to Remember
- Byte streams (InputStream/OutputStream) handle raw binary data; character streams (Reader/Writer) handle text and manage character encoding.
- Character streams exist specifically to avoid corrupting multi-byte text when converting between bytes and characters.
- Buffered variants (BufferedInputStream, BufferedReader, etc.) wrap another stream to reduce costly underlying I/O calls.
- Try-with-resources should be the default way to open any Closeable stream, guaranteeing it is closed even when an exception occurs.
Practice in the playground
Write and run your own code for this topic, or start from the exercise or an example. Runs plain Java 17. Frameworks such as Spring, and databases, are not available here. Your code for each topic is kept in this browser.
Run the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Written by
WebNest Studio Engineering Team
Practical lessons from developers working with Java, Python, React, databases and production systems.
Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering