Skip to content
Java io 7 min read

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

ClassPackagePurpose
StreamTokenizerjava.ioBreak a character stream into tokens (numbers, words, symbols)
PipedInputStream / PipedOutputStreamjava.ioPass byte data from one thread to another via a pipe
PipedReader / PipedWriterjava.ioCharacter-level pipe between threads
PushbackInputStreamjava.ioRead a byte and then push it back for re-reading
PushbackReaderjava.ioSame idea, but for characters
LineNumberReaderjava.ioTrack line numbers while reading text
CharArrayReader / CharArrayWriterjava.ioIn-memory char[] treated as a character stream
StringReader / StringWriterjava.ioIn-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

FieldMeaning
ttypeToken type just read (TT_WORD, TT_NUMBER, TT_EOL, TT_EOF, or a char)
svalString value when ttype == TT_WORD
nvalNumeric 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, and st.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.BlockingQueue or 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 after readLine() 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, StreamTokenizer is already on the classpath and needs zero dependencies.


When to Use Which Class

ScenarioRecommended Class
Parse a simple text config or expressionStreamTokenizer
Pass streaming data between two threadsPipedInputStream / PipedOutputStream
Peek ahead in a byte stream without losing dataPushbackInputStream
Track line numbers in a text parserLineNumberReader
In-memory character buffer for testingCharArrayWriter / StringWriter
Feed a Reader-based API from a StringStringReader

  • 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 LineNumberReader extends, and the go-to for efficient text reading
  • Scanner — a higher-level alternative to StreamTokenizer for parsing structured text input
  • Multithreading — essential context for using piped streams correctly across threads
  • Java I/O — the full overview of the java.io package and how all the pieces fit together
Last updated June 13, 2026
Was this helpful?