Java Byte Streams: InputStream and OutputStream
A classic bug when copying a binary file: byte b; while ((b = (byte) in.read()) != -1). It works on a text file, but an image comes out truncated: the first 0xFF byte becomes -1 after the cast to byte, and the loop decides the file is over. To avoid bugs like this, you need to understand how byte streams are built.
Byte streams in Java are classes of the java.io package that read and write data one byte (8 bits) at a time, without converting anything to characters. Every input stream extends the abstract class InputStream, and every output stream extends OutputStream. They work with any binary data: images, archives, network packets, serialized objects. For text, use character streams (Reader and Writer) instead. The overview of all I/O streams is in the lesson Java I/O Streams.
1. The InputStream class
InputStream is the abstract class that every byte input stream in Java extends. Its read() methods return the next byte as an int from 0 to 255, or -1 at the end of the stream. It also has methods for skipping bytes, checking available data and closing the source.
The read() methods block until data is available, the end of the stream is reached, or an exception is thrown. On an I/O error they throw IOException.
InputStream methods
| Method | What it does | Edge case |
|---|---|---|
int read() | Reads one byte and returns it as an int from 0 to 255 | Returns -1 at the end of the stream |
int read(byte[] buffer) | Reads up to buffer.length bytes into the array and returns the number of bytes actually read | May read fewer bytes than the array size; returns -1 at the end of the stream |
int read(byte[] buffer, int offset, int length) | Reads up to length bytes into the array, starting at buffer[offset] | IndexOutOfBoundsException if offset/length go outside the array |
int available() | Returns an estimate of the bytes that can be read without blocking | It is not the file size: for a network stream it is often 0 |
long skip(long n) | Skips up to n bytes | Returns the number of bytes actually skipped (may be less than n) |
byte[] readAllBytes() (Java 9) | Reads all remaining bytes until the end of the stream | For very large streams it can throw OutOfMemoryError |
byte[] readNBytes(int len) (Java 11) | Reads exactly len bytes, or fewer if the stream ends earlier | Never returns -1: at the end of the stream you get an empty array |
long transferTo(OutputStream out) (Java 9) | Copies all remaining bytes to the output stream | Returns the number of bytes copied |
void close() | Closes the source and releases operating system resources | Later attempts to read lead to an IOException |
Since Java 11 there is a factory method InputStream.nullInputStream(): an empty stream that returns -1 immediately. It is handy as a stub in tests.
2. The OutputStream class
OutputStream is the abstract class that every byte output stream in Java extends. It defines write() methods for writing a single byte or an array of bytes, flush() for pushing buffered data out, and close() for closing the destination. On an I/O error the methods throw IOException.
OutputStream methods
| Method | What it does | Edge case |
|---|---|---|
void write(int data) | Writes one byte. The parameter is an int, so you do not have to cast the expression to byte | Only the low 8 bits are written: write(256) writes byte 0, write(-1) writes byte 255 |
void write(byte[] buffer) | Writes the whole array | NullPointerException if the array is null |
void write(byte[] buffer, int offset, int length) | Writes length bytes, starting at buffer[offset] | IndexOutOfBoundsException if the range goes outside the array |
void flush() | Pushes data from buffers to the destination | Does nothing for FileOutputStream; important for BufferedOutputStream |
void close() | Flushes buffers and closes the stream | Later attempts to write lead to an IOException |
Like InputStream, Java 11 added OutputStream.nullOutputStream(): a stream that silently discards every byte written to it.
3. FileInputStream and FileOutputStream
FileInputStream reads bytes from a file, and FileOutputStream writes bytes to a file. They are the main byte streams for working with files: use them to copy and process any file, not only text. If the file to read does not exist, the FileInputStream constructor throws FileNotFoundException.
FileInputStream constructors
InputStream in1 = new FileInputStream("D:/myprogram/java/test.txt");
File file = new File("D:/myprogram/java/test.txt");
InputStream in2 = new FileInputStream(file); The first constructor takes the file name as a string, the second takes a File object (see the lesson Java File Class). Both create an input stream for reading the file.
FileOutputStream constructors
OutputStream out1 = new FileOutputStream("D:/myprogram/java/test.txt");
File file = new File("D:/myprogram/java/test.txt");
OutputStream out2 = new FileOutputStream(file);
// the second parameter true means: append to the end of the file
OutputStream out3 = new FileOutputStream("D:/myprogram/java/log.txt", true); These constructors create an output stream for writing to a file. If the file does not exist, it is created; if the directory that should contain it does not exist, FileNotFoundException is thrown.
Note
The constructor new FileOutputStream("log.txt") without the second parameter wipes an existing file before the first write. To append data to the end, use new FileOutputStream("log.txt", true).
Example 1. Copying a file with FileInputStream and FileOutputStream
The streams are closed automatically by try-with-resources. The data is read in 8 KB blocks instead of one byte at a time, which is many times faster.
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class FileCopy {
public static void main(String[] args) {
try (InputStream in = new FileInputStream("src/io/file.txt");
OutputStream out = new FileOutputStream("src/io/copied_file.txt")) {
byte[] buffer = new byte[8192];
long total = 0;
int count;
while ((count = in.read(buffer)) != -1) {
out.write(buffer, 0, count); // write only the bytes that were read
total += count;
}
System.out.println("Bytes copied: " + total);
} catch (IOException e) {
System.out.println("Error: " + e.getMessage());
}
}
} Output for a file of 1,024 bytes: Bytes copied: 1024. If src/io/file.txt does not exist, the constructor throws FileNotFoundException (a subclass of IOException), and the program prints Error: src/io/file.txt (...) with the system-specific reason.
Since Java 9 the whole loop can be replaced with one line:
long total = in.transferTo(out); Example 2. Writing and reading bytes
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class FileInputOutputStreamExample {
public static void main(String[] args) {
char[] symbols = {'a', 'b', 'c'};
try (OutputStream output = new FileOutputStream("a.txt")) {
for (char symbol : symbols) {
output.write(symbol); // write the character code as one byte
}
} catch (IOException e) {
System.out.println("Write error: " + e.getMessage());
}
try (InputStream input = new FileInputStream("a.txt")) {
int data;
while ((data = input.read()) != -1) { // read until the end of the file
System.out.print((char) data + " ");
}
} catch (IOException e) {
System.out.println("Read error: " + e.getMessage());
}
}
}
// Output: a b c The cast (char) data is correct only for ASCII characters: each of them takes exactly one byte. A character such as é or any Cyrillic letter takes 2 bytes in UTF-8, so reading it byte by byte produces garbled text. For text, use character streams (Reader and Writer) instead.
4. ByteArrayInputStream and ByteArrayOutputStream
ByteArrayInputStream reads bytes from an array in memory, and ByteArrayOutputStream writes bytes to an automatically growing array that you get with toByteArray(). Use them when a method expects an InputStream/OutputStream but your data sits in memory, for example in unit tests.
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
public class ByteArrayStreamsExample {
public static void main(String[] args) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(72); // 'H'
out.writeBytes("i!".getBytes(StandardCharsets.UTF_8)); // Java 11
byte[] bytes = out.toByteArray();
System.out.println(Arrays.toString(bytes));
System.out.println(out.toString(StandardCharsets.UTF_8)); // Java 10
ByteArrayInputStream in = new ByteArrayInputStream(bytes);
int first = in.read();
System.out.println((char) first + ", bytes left: " + in.available());
}
}
// Output:
// [72, 105, 33]
// Hi!
// H, bytes left: 2 There is no try-catch or throws IOException here: in the ByteArray* classes the methods read(), write(int) and writeBytes() are overridden without declaring IOException, because an in-memory stream has no I/O to fail. For the same reason available() in ByteArrayInputStream returns the exact number of remaining bytes.
5. BufferedInputStream and BufferedOutputStream
BufferedInputStream and BufferedOutputStream are wrappers (decorators) around another byte stream. They read and write data in blocks through an internal buffer, 8 KB by default. They speed up byte-by-byte processing: instead of a system call for every read(), the stream goes to the file once per block.
try (InputStream in = new BufferedInputStream(new FileInputStream("photo.jpg"));
OutputStream out = new BufferedOutputStream(new FileOutputStream("photo-copy.jpg"))) {
int b;
while ((b = in.read()) != -1) { // read() takes the byte from the in-memory buffer
out.write(b);
}
} // close() calls flush() itself and writes out the rest of the buffer You can set the buffer size with the second constructor parameter: new BufferedInputStream(in, 64 * 1024). If you already read in large arrays, as in Example 1, a buffered wrapper adds almost nothing.
Important
If you neither close a BufferedOutputStream nor call flush(), the last bytes (up to 8 KB) stay in the buffer and never reach the file. Closing the outer wrapper is enough — it also closes the nested FileOutputStream.
6. Common mistakes
The most common mistakes with byte streams are casting the result of read() to byte before checking for -1, using available() as the file size, and ignoring the number of bytes returned by read(byte[]). All three compile without warnings and show up only on certain data.
What does this code print?
byte[] data = {10, (byte) 0xFF, 20};
InputStream in = new ByteArrayInputStream(data);
int count = 0;
byte b;
while ((b = (byte) in.read()) != -1) {
count++;
}
System.out.println(count); The answer is 1, not 3. For the second byte read() returned 255, but after the cast (byte) 255 became -1, and the loop stopped although the stream was not over. That is exactly why read() returns an int: the values 0–255 are data, and -1 is the end-of-stream signal, so the two can never be confused. The fix is to keep the result in an int and cast to byte only after the check:
int value;
while ((value = in.read()) != -1) {
count++;
}
// Output: 3 Other typical slips
- A loop over
available(). The codefor (int i = 0; i < in.available(); i++)works with a local file, but for a network streamavailable()often returns 0 and nothing is read. Read until-1. out.write(buffer)instead ofout.write(buffer, 0, count). The last block is usually only partly filled, so the copy gets leftovers from the previous block and the file becomes larger than the original.- Closing in
finallywithout anullcheck, or not closing at all. Use try-with-resources: the stream is closed even if an exception is thrown.
7. Table of byte stream classes
The java.io package has about twenty byte streams. Most of them are either sources and destinations of data (a file, an array, a pipe) or decorators that extend FilterInputStream/FilterOutputStream and add features to another stream: buffering, reading primitives, pushing a byte back.
| Class | Extends | Purpose |
|---|---|---|
| InputStream | Object | Abstract class describing a byte input stream |
| OutputStream | Object | Abstract class describing a byte output stream |
| FileInputStream | InputStream | Input stream that reads from a file |
| FileOutputStream | OutputStream | Output stream that writes to a file |
| ByteArrayInputStream | InputStream | Input stream that reads from a byte array |
| ByteArrayOutputStream | OutputStream | Output stream that writes to a byte array |
| FilterInputStream | InputStream | Base class for input stream decorators |
| FilterOutputStream | OutputStream | Base class for output stream decorators |
| BufferedInputStream | FilterInputStream | Buffered input stream |
| BufferedOutputStream | FilterOutputStream | Buffered output stream |
| DataInputStream | FilterInputStream | Input stream with methods for reading Java primitive types (readInt(), readDouble(), readUTF()) |
| DataOutputStream | FilterOutputStream | Output stream with methods for writing Java primitive types |
| PushbackInputStream | FilterInputStream | Input stream that lets you push a byte back (the pushback operation) |
| PrintStream | FilterOutputStream | Output stream with print(), println() and printf(); System.out is of this type |
| ObjectInputStream | InputStream | Input stream for reading serialized objects |
| ObjectOutputStream | OutputStream | Output stream for serializing objects |
| PipedInputStream | InputStream | Input pipe for passing data between threads |
| PipedOutputStream | OutputStream | Output pipe connected to a PipedInputStream |
| SequenceInputStream | InputStream | Input stream that reads two or more input streams one after another |
| RandomAccessFile | Object | Not a stream: lets you move around a file (seek()) and both read from it and write to it |
The bridge between byte and character streams is the pair of classes InputStreamReader and OutputStreamWriter: they turn bytes into characters and back using a charset.
Frequently asked questions
How do I read a whole InputStream into a byte array or a string?
Since Java 9, call in.readAllBytes(). Get a string from the bytes like this: new String(in.readAllBytes(), StandardCharsets.UTF_8). Always give the charset explicitly, otherwise the default charset is used. This approach does not suit gigabyte-sized files — read in blocks or use transferTo().
Why does InputStream.read() return an int and not a byte?
A byte does not have enough values: all 256 of its values can be data, so there is no room left for an end-of-stream marker. That is why read() returns data as 0–255 and the end of the stream as -1. See the example in the section Common mistakes.
Does Java have CountingInputStream and CountingOutputStream?
Not in the standard JDK. Ready-made implementations exist in Apache Commons IO (org.apache.commons.io.input.CountingInputStream, org.apache.commons.io.output.CountingOutputStream) and in Guava (com.google.common.io.CountingInputStream and CountingOutputStream). You can also write your own byte counter by extending FilterInputStream and overriding the read() methods.
Do I need to close a ByteArrayOutputStream?
Technically no: in ByteArrayInputStream and ByteArrayOutputStream the close() method does nothing, and the other methods keep working after it without throwing IOException. You must close streams tied to operating system resources: files, sockets, and the wrappers around them.
Comments