Java Byte Streams vs Character Streams and Buffered I/O
Introduction
Underneath methods such as
(Note - This tutorial builds on Reading and Writing Files with Path and FilesClick to read tutorial: Reading and Writing Files in Java with Path and Files and try-with-resourcesClick to read tutorial: Java try-with-resources and AutoCloseable Explained, which every stream example here uses.) Two families of streams
Streams are designed to be wrapped inside each other, each layer adding a feature -
OUTPUT of the above code
Character streams - reading and writing text
Files store bytes, not characters. A character encoding - such as UTF-8 - defines how characters are turned into bytes and back. Readers and writers do this conversion for you.
OUTPUT of the above code
Zürich has 6 characters but 7 bytes, and 東京 2 characters but 6 bytes - in UTF-8, characters outside basic English take more than one byte. This is why text must be handled as characters, not bytes: splitting a byte stream at an arbitrary point could cut a character in half.
Why the encoding matters
If text is written in one encoding and read in another, characters outside basic ASCII come out wrong - mojibake. Always state the encoding explicitly when it matters, and use UTF-8 unless you have a reason not to. (Since Java 18, UTF-8 is the default for the file APIs everywhere; before that, the default depended on the operating system.)
OUTPUT of the above code
Buffered I/O - why it matters
Every
OUTPUT of the above code (timings vary from machine to machine)
The buffered version is dramatically faster for the same work. Always buffer file I/O that reads or writes in small pieces - or read and write in large blocks yourself, as the byte copy example did. Remember that a buffered writer holds data in memory until its buffer fills, it is flushed with
OUTPUT of the above code
Files.readString(), Java reads and writes data through streams - sequences of data flowing from a source or to a destination. (These I/O streams are unrelated to the Streams API of Java 8.) Java has two families of them: byte streams for raw binary data, and character streams for text. This tutorial explains byte streams vs character streams, and buffered I/O, in Java. We will start with the four base classes and how they are organized. We will then copy a binary file with byte streams, and read and write text with readers and writers - and see why the character encoding matters. Next we will measure what buffering does for speed. Lastly, we will connect bytes to characters with bridge classes, and choose the right class for a job.(Note - This tutorial builds on Reading and Writing Files with Path and FilesClick to read tutorial: Reading and Writing Files in Java with Path and Files and try-with-resourcesClick to read tutorial: Java try-with-resources and AutoCloseable Explained, which every stream example here uses.) Two families of streams
| Byte streams | Character streams | |
| Unit of data | 8-bit bytes | 16-bit chars (text) |
| Base classes | InputStream, OutputStream | Reader, Writer |
| File classes | FileInputStream, FileOutputStream | FileReader, FileWriter |
| Buffered versions | BufferedInputStream, BufferedOutputStream | BufferedReader, BufferedWriter |
| Use for | Images, audio, ZIP files, network protocols - any binary data | Text: CSV, logs, JSON, configuration files |
Streams are designed to be wrapped inside each other, each layer adding a feature -
new BufferedReader(new FileReader(file)) adds buffering and readLine() to a plain file reader. This is the Decorator design pattern. In modern code, Files.newInputStream(), Files.newBufferedReader() and similar methods create the right stream for a Path in one call.
Byte streams - reading and writing raw bytes
InputStream.read() returns one byte as an int from 0 to 255, or -1 at the end of the stream. Reading into a byte[] buffer with read(buffer), which returns how many bytes it read, is far more efficient. Here we write a small binary file and copy it.
Writing and copying binary data with byte streams
//ByteCopy.java
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
public class ByteCopy {
public static void main(String[] args) throws IOException {
Path source = Path.of("data.bin");
try (OutputStream out = Files.newOutputStream(source)) {
for (int i = 0; i < 1000; i++) {
out.write(i % 256); // write one byte
}
}
Path target = Path.of("data-copy.bin");
try (InputStream in = Files.newInputStream(source);
OutputStream out = Files.newOutputStream(target)) {
byte[] buffer = new byte[256];
int count;
int reads = 0;
while ((count = in.read(buffer)) != -1) { // -1 means end of stream
out.write(buffer, 0, count);
reads++;
}
System.out.println("Copied using " + reads + " reads of up to 256 bytes");
}
byte[] copied = Files.readAllBytes(target);
System.out.println("Size: " + copied.length + ", first bytes: " + Arrays.toString(Arrays.copyOf(copied, 6)));
System.out.println("Identical: " + Arrays.equals(Files.readAllBytes(source), copied));
try (InputStream in = Files.newInputStream(source)) {
System.out.println("transferTo copied " + in.transferTo(OutputStream.nullOutputStream()) + " bytes");
}
Files.delete(source);
Files.delete(target);
}
}
Copied using 4 reads of up to 256 bytes Size: 1000, first bytes: [0, 1, 2, 3, 4, 5] Identical: true transferTo copied 1000 bytes
write(buffer, 0, count)writes only the bytes actually read - the last read usually fills only part of the buffer (1000 bytes needed four reads: 256, 256, 256 and 232).- Since Java 9,
InputStream.transferTo()copies a whole stream in one call, andFiles.copy()does the same for files.
BufferedReader.readLine() reads a line at a time, and BufferedWriter.newLine() writes the platform's line separator.
Writing and reading text with character streams
//TextStreams.java
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class TextStreams {
public static void main(String[] args) throws IOException {
Path file = Path.of("cities.txt");
try (BufferedWriter writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) {
for (String city : new String[] {"London", "Zürich", "Mumbai", "東京"}) {
writer.write(city);
writer.newLine();
}
}
try (BufferedReader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
String line;
int number = 1;
while ((line = reader.readLine()) != null) { // null means end of file
System.out.println(number++ + ": " + line + " (" + line.length() + " chars, "
+ line.getBytes(StandardCharsets.UTF_8).length + " bytes in UTF-8)");
}
}
Files.delete(file);
}
}
1: London (6 chars, 6 bytes in UTF-8) 2: Zürich (6 chars, 7 bytes in UTF-8) 3: Mumbai (6 chars, 6 bytes in UTF-8) 4: 東京 (2 chars, 6 bytes in UTF-8)
Reading UTF-8 text with the wrong encoding
//EncodingMismatch.java
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class EncodingMismatch {
public static void main(String[] args) throws IOException {
Path file = Path.of("menu.txt");
Files.writeString(file, "Crème brûlée - 8€", StandardCharsets.UTF_8);
System.out.println("Read as UTF-8 : " + Files.readString(file, StandardCharsets.UTF_8));
System.out.println("Read as ISO-8859-1: " + Files.readString(file, StandardCharsets.ISO_8859_1));
Files.delete(file);
}
}
Read as UTF-8 : Crème brûlée - 8€ Read as ISO-8859-1: Crème brûlée - 8â¬
read() or write() on an unbuffered file stream can become a call into the operating system, which is slow. A buffered stream keeps an internal buffer - 8 KB by default - and talks to the file in large blocks, while your code reads and writes small pieces from memory. Let us write and read 1,000,000 single bytes, with and without a buffer.
Unbuffered vs buffered streams
//BufferingSpeed.java
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class BufferingSpeed {
static long writeAndRead(String file, boolean buffered) throws IOException {
long start = System.nanoTime();
try (OutputStream out = buffered ? new BufferedOutputStream(new FileOutputStream(file)) : new FileOutputStream(file)) {
for (int i = 0; i < 1_000_000; i++) out.write(i);
}
long sum = 0;
try (InputStream in = buffered ? new BufferedInputStream(new FileInputStream(file)) : new FileInputStream(file)) {
int b;
while ((b = in.read()) != -1) sum += b;
}
new java.io.File(file).delete();
return (System.nanoTime() - start) / 1_000_000;
}
public static void main(String[] args) throws IOException {
System.out.println("Unbuffered: " + writeAndRead("plain.bin", false) + " ms");
System.out.println("Buffered : " + writeAndRead("buffered.bin", true) + " ms");
}
}
Unbuffered: 5899 ms Buffered : 90 ms
flush(), or it is closed - another reason to close streams with try-with-resources.
Bridges between bytes and characters
Sometimes you have a byte stream - from a network socket, a ZIP entry or System.in - and need text. InputStreamReader and OutputStreamWriter are the bridges: they wrap a byte stream and decode or encode characters with a given charset. PrintWriter adds the familiar println() and printf() to any writer.
Converting between byte and character streams
//Bridges.java
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
public class Bridges {
public static void main(String[] args) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream(); // an in-memory byte stream
try (PrintWriter out = new PrintWriter(new OutputStreamWriter(bytes, StandardCharsets.UTF_8))) {
out.printf("%-6s %5.2f%n", "Tea", 2.5);
out.printf("%-6s %5.2f%n", "Coffee", 3.75);
}
System.out.println("Encoded to " + bytes.size() + " bytes");
try (BufferedReader in = new BufferedReader(new InputStreamReader(
new ByteArrayInputStream(bytes.toByteArray()), StandardCharsets.UTF_8))) {
in.lines().forEach(line -> System.out.println("read back: [" + line + "]"));
}
}
}
Encoded to 28 bytes read back: [Tea 2.50] read back: [Coffee 3.75]
ByteArrayInputStream and ByteArrayOutputStream are byte streams over memory instead of a file - useful in tests, and to show that every stream class works the same way whatever the source is.
Choosing the right class
- Whole small text or binary file:
Files.readString(),writeString(),readAllBytes(). - Large text file, line by line:
Files.newBufferedReader()withreadLine(), orFiles.lines(). - Writing lots of text:
Files.newBufferedWriter(), wrapped in aPrintWriterfor formatting. - Binary data:
Files.newInputStream()/newOutputStream()with abyte[]buffer, ortransferTo(). - Text from a byte source:
InputStreamReaderwith an explicit charset, inside aBufferedReader. - Always use try-with-resources, and always specify the charset for text.
InputStream and OutputStream, and character streams based on Reader and Writer - and how they wrap each other. We then copied a binary file with a byte buffer, and read and wrote text with BufferedReader and BufferedWriter, seeing that one character can take several bytes. Next we saw what happens when the wrong encoding is used, and measured how much faster buffered streams are. Lastly, we bridged byte and character streams with InputStreamReader and OutputStreamWriter, and summarized which class to choose. In the next tutorial, we will write whole objects to streams with serializationClick to read tutorial: Java Serialization and Deserialization Basics.Core Java Tutorials
Reading and Writing Files in Java with Path and FilesClick to read tutorial: Reading and Writing Files in Java with Path and Files Java Serialization and Deserialization BasicsClick to read tutorial: Java Serialization and Deserialization Basics Stack vs Heap Memory in Java and Object ReferencesClick to read tutorial: Stack vs Heap Memory in Java and Object References
Reading and Writing Files in Java with Path and FilesClick to read tutorial: Reading and Writing Files in Java with Path and Files Java Serialization and Deserialization BasicsClick to read tutorial: Java Serialization and Deserialization Basics Stack vs Heap Memory in Java and Object ReferencesClick to read tutorial: Stack vs Heap Memory in Java and Object References