This post is a brief summary (“TL;DR”) of the official Java tutorial lesson on Basic I/O.

At first glance, standard input/output seems so complicated! There are so many options! This post should clear the cloud up.


It all starts from “streams”. There are two types of streams: byte streams and character streams.

  1. Byte streams:
    • InputStream
    • OutputStream
  2. Character streams:
    • Reader
    • Writer
  • The familiar System.out is a PrintStream, which is a subclass of OutputStream, which is discussed far below.
  • The (perhaps not-so-) familiar is a InputStream.

File I/O

These are obviously subclasses of the base stream classes of corresponding names.

  • FileInputStream
  • FileOutputStream
  • FileReader
  • FileWriter

Adapting byte streams into character streams

This can be done through these two classes:

  • InputStreamReader
  • OutputStreamWriter

They are “adaptors” (in the design patterns sense). They:

  • Are subclasses of the character stream base classes; and
  • Take instances of the byte stream classes as a constructor argument.

Obviously, a charset is needed. If not provided, the “default charset” is used.

InputStream i =;              // or any other InputStream
Reader r = new InputStreamReader(i);    // bytes encoded into chars

Decorating streams into buffered streams

There are “buffered” versions of each of the four base stream classes:

  • BufferedInputStream
  • BufferedOutputStream
  • BufferedReader
  • BufferedWriter

These clases are “decorators” (in the design patterns sense). They:

  • Are subclasses of their decorated classes; and
  • Take instances of their decorated classes.

Since they’re “buffers”, obviously:

  • Input buffers fetch when empty;
  • Output buffers flush when full.

The buffered stream classes are subclasses of each of their decoratees.

InputStream i =;
InputStream bi = new BufferedInputStream(i);    // adds buffer

Up to this point, you can already have enough things to fiddle with to get creative, e.g.:

InputStream i =;
Reader r = new InputStreamReader(i);        // encodes bytes into chars
BufferedReader br = new BufferedReader(r);  // adds buffer to char stream

The above is exactly what Mk Yong introduced in his article, which I claimed, at the start of this article, to be “complicated”. Now we should have an idea of what’s happening.

Higher-level stuff: “scanning” and “formatting”

  • “Scanning” means taking character/byte streams and interpreting (raw) data (i.e. chars/bytes) into structured data (e.g. ints).
    • It provides all the hasNextBigInteger, nextLine APIs.
  • “Formatting” means the reverse: taking structured data and displaying it as characters/bytes.
    • It provides all the .format("The int is: %d", 42), .println(42) APIs.

Java classes involved:

  • Scanner, which can “scan” from all sorts of streams;
  • PrintStream, which can “format” to byte streams;
  • PrintWriter, which can “format” to characters streams.

The inheritance status of these 3 classes aren’t as symmetric as the other classes mentioned above.

  • Scanner doesn’t inherit from anything; and
  • PrintStream is an OutputStream; and
  • PrintWriter is a Writer.

It’s worth mentioning that both the Scanner and PrintWriter classes can be constructed from byte streams as well as character streams. However in the case of byte streams, quite obviously, if not specified explicitly, the “default charset” will be used.

That’s all!

I have been using Java every now and then for quite a while, but I seldom needed to do standard I/O - it was more often some OOP in some framework, like Android.

I might be spoilt by Python with regards to I/O - just an input() call and a print() call, and it’s all done. I used C++ a tiny bit for some programming contests, and encountered the idea of “streams” — which made the Java stuff less intimidating. Eventually — e.g. now — I figured the Java stuff out.