Other I/O Classes
Java’s java.io package contains far more than just FileInputStream and BufferedReader. Once you are comfortable with the core streams, a handful of specialist classes can save you a surprising amount of manual work — from parsing text tokens to piping data between threads. This page tours the most useful ones you have not met yet.
Quick Reference: What’s Covered
| Class | Package | Purpose |
|---|---|---|
StreamTokenizer | java.io | Break a character stream into tokens (numbers, words, symbols) |
PipedInputStream / PipedOutputStream | java.io | Pass byte data from one thread to another via a pipe |
PipedReader / PipedWriter | java.io | Character-level pipe between threads |
PushbackInputStream | java.io | Read a byte and then push it back for re-reading |
PushbackReader | java.io | Same idea, but for characters |
LineNumberReader | java.io | Track line numbers while reading text |
CharArrayReader / CharArrayWriter | java.io | In-memory char[] treated as a character stream |
StringReader / StringWriter | java.io | In-memory String / StringBuffer treated as a stream |
StreamTokenizer
StreamTokenizer turns any Reader into a simple lexer. It scans the stream and categorises each chunk as a word, a number, a quoted string, a comment, or an ordinary character. It is not a full parser, but it handles most configuration-file, expression, or DSL-style input without writing character-by-character loops.
Key Fields
| Field | Meaning |
|---|---|
ttype | Token type just read (TT_WORD, TT_NUMBER, TT_EOL, TT_EOF, or a char) |
sval | String value when ttype == TT_WORD |
nval | Numeric value when ttype == TT_NUMBER |
Example: Parse a Simple Config File
import java.io.*;
public class TokenizerDemo {
public static void main(String[] args) throws IOException {
String config = "width 800\nheight 600\ntitle \"My App\"";
StreamTokenizer st = new StreamTokenizer(new StringReader(config));
while (st.nextToken() != StreamTokenizer.TT_EOF) {
if (st.ttype == StreamTokenizer.TT_WORD) {
System.out.print("WORD: " + st.sval + " ");
} else if (st.ttype == StreamTokenizer.TT_NUMBER) {
System.out.print("NUM: " + (int) st.nval + " ");
} else if (st.ttype == '"') {
System.out.print("STR: " + st.sval + " ");
}
}
}
}
Output:
WORD: width NUM: 800 WORD: height NUM: 600 WORD: title STR: My App
Tip: Call
st.eolIsSignificant(true)if you need to detect line endings, andst.slashSlashComments(true)to auto-skip//comments.
PipedInputStream and PipedOutputStream
A pipe connects exactly two threads: one writes to PipedOutputStream, the other reads from PipedInputStream. The pipe has an internal circular buffer (default 1 024 bytes; configurable in the constructor). The writer blocks when the buffer is full; the reader blocks when it is empty.
Warning: Never use both ends of a pipe on the same thread — the thread will deadlock waiting on itself.
Example: Producer / Consumer Pipe
import java.io.*;
public class PipeDemo {
public static void main(String[] args) throws Exception {
PipedOutputStream out = new PipedOutputStream();
PipedInputStream in = new PipedInputStream(out); // connect them
Thread producer = new Thread(() -> {
try (PrintStream ps = new PrintStream(out)) {
ps.println("Hello from producer!");
} catch (Exception e) { e.printStackTrace(); }
});
Thread consumer = new Thread(() -> {
try (BufferedReader br = new BufferedReader(new InputStreamReader(in))) {
System.out.println("Consumer got: " + br.readLine());
} catch (Exception e) { e.printStackTrace(); }
});
consumer.start();
producer.start();
producer.join();
consumer.join();
}
}
Output:
Consumer got: Hello from producer!
PipedReader and PipedWriter work identically but for character streams.
Note: For high-throughput inter-thread data transfer, prefer a
java.util.concurrent.BlockingQueueor Java 21 Virtual Threads with a channel. Pipes shine when you must interoperate with a stream-based API.
PushbackInputStream and PushbackReader
Sometimes you need to look one step ahead — read a byte to decide how to process it, and then “unread” it so the next read gets it back. PushbackInputStream wraps any InputStream and adds a small internal pushback buffer (default 1 byte).
Example: Detect a Two-Byte Prefix
import java.io.*;
public class PushbackDemo {
public static void main(String[] args) throws IOException {
byte[] data = { 0x1F, 0x8B, 65, 66, 67 }; // fake gzip magic + "ABC"
PushbackInputStream pb = new PushbackInputStream(
new ByteArrayInputStream(data), 2);
byte b1 = (byte) pb.read();
byte b2 = (byte) pb.read();
if (b1 == 0x1F && b2 == (byte) 0x8B) {
System.out.println("Detected gzip header — re-pushing bytes");
pb.unread(b2);
pb.unread(b1);
}
// Both bytes are back; next read sees them again
System.out.println("First byte again: 0x" + Integer.toHexString(pb.read() & 0xFF));
}
}
Output:
Detected gzip header — re-pushing bytes
First byte again: 0x1f
PushbackReader offers the same capability for character streams and also accepts a multi-character pushback buffer size.
LineNumberReader
LineNumberReader extends BufferedReader and tracks the current line number as you read. This is invaluable for error messages in parsers and compilers. Call getLineNumber() at any time; call setLineNumber(n) to reset the counter.
Example: Read With Line Numbers
import java.io.*;
public class LineNumberDemo {
public static void main(String[] args) throws IOException {
String text = "line one\nline two\nline three";
LineNumberReader lnr = new LineNumberReader(new StringReader(text));
String line;
while ((line = lnr.readLine()) != null) {
System.out.println(lnr.getLineNumber() + ": " + line);
}
}
}
Output:
1: line one
2: line two
3: line three
Note:
getLineNumber()is incremented afterreadLine()returns, so the number you read is the number of the line you just consumed.
CharArrayReader and CharArrayWriter
These mirror ByteArray Streams but for characters. They let you use a char[] as a character stream — perfect for testing, in-memory transformations, or capturing output from a Writer-based API.
Example: Capture Writer Output Into a char[]
import java.io.*;
public class CharArrayDemo {
public static void main(String[] args) throws IOException {
CharArrayWriter caw = new CharArrayWriter();
PrintWriter pw = new PrintWriter(caw);
pw.printf("Pi is approximately %.4f%n", Math.PI);
pw.flush();
char[] chars = caw.toCharArray();
System.out.println("Captured: " + new String(chars));
// Now read it back as a stream
CharArrayReader car = new CharArrayReader(chars);
int c;
StringBuilder sb = new StringBuilder();
while ((c = car.read()) != -1) sb.append((char) c);
System.out.println("Re-read: " + sb);
}
}
Output:
Captured: Pi is approximately 3.1416
Re-read: Pi is approximately 3.1416
StringReader and StringWriter
StringReader wraps a String as a character Reader. StringWriter collects characters into an internal StringBuffer which you retrieve with toString(). These are extremely handy for unit-testing any code that expects a Reader or Writer.
import java.io.*;
public class StringStreamsDemo {
public static void main(String[] args) throws IOException {
// StringReader — feed a String to a Reader-based API
StringReader sr = new StringReader("Hello, StringReader!");
int ch;
StringBuilder result = new StringBuilder();
while ((ch = sr.read()) != -1) result.append((char) ch);
System.out.println(result);
// StringWriter — capture Writer output as a String
StringWriter sw = new StringWriter();
sw.write("Captured by StringWriter.");
System.out.println(sw.toString());
}
}
Output:
Hello, StringReader!
Captured by StringWriter.
How It Works
Stream Decorator Pattern
Almost all of these classes are implementations of the Decorator pattern: they wrap an existing stream and add behaviour (line counting, pushback, etc.) without changing the underlying stream’s contract. Because they all implement the standard InputStream/OutputStream/Reader/Writer interfaces, you can chain them freely — for example, wrapping a PushbackReader around a LineNumberReader around a BufferedReader around a FileReader.
Pipe Buffer Internals
PipedInputStream maintains a byte[] of size 1 024 by default. Write operations call notify() on the stream object to wake a waiting reader; read operations call wait() when the buffer is empty. The synchronisation is done with synchronized blocks on the stream object itself. This is why using both ends on the same thread deadlocks: the thread blocks on wait() and can never call notify().
StreamTokenizer State Machine
Internally StreamTokenizer maintains a 256-entry attribute table where each character code maps to a category (whitespace, alphabetic, numeric, quote, comment, ordinary). You can customise these categories via methods like wordChars(int low, int hi), whitespaceChars(), and ordinaryChar() — giving you a lightweight DSL-parsing engine with almost no boilerplate.
Tip: For anything more complex than basic tokenising, consider a proper parser library. But for quick config files,
StreamTokenizeris already on the classpath and needs zero dependencies.
When to Use Which Class
| Scenario | Recommended Class |
|---|---|
| Parse a simple text config or expression | StreamTokenizer |
| Pass streaming data between two threads | PipedInputStream / PipedOutputStream |
| Peek ahead in a byte stream without losing data | PushbackInputStream |
| Track line numbers in a text parser | LineNumberReader |
| In-memory character buffer for testing | CharArrayWriter / StringWriter |
Feed a Reader-based API from a String | StringReader |
Related Topics
- Byte vs Character Streams — understand the fundamental split between byte-oriented and character-oriented I/O before choosing a class
- ByteArray Streams — the byte-level counterparts to
CharArrayReader/CharArrayWriter - BufferedReader — the base class that
LineNumberReaderextends, and the go-to for efficient text reading - Scanner — a higher-level alternative to
StreamTokenizerfor parsing structured text input - Multithreading — essential context for using piped streams correctly across threads
- Java I/O — the full overview of the
java.iopackage and how all the pieces fit together