SaveMyCert
Log in
5 of 5 free questions left today·for 30 a day
Performing Input and Output Operations Using the Java I/O API

Java I/O Streams: Console and File Readers, Writers and Streams (1Z0-831)

17 min read1Z0-831 · Performing Input and Output Operations Using the Java I/O APIUpdated

Java I/O streams are the java.io classes that read and write data sequentially — byte streams (InputStream and OutputStream) for raw bytes and character streams (Reader and Writer) for text — and Oracle 1Z0-831 objective 9.1 asks you to use them to read and write console and file data. This lesson covers the two stream families and how to bridge them, what each read method returns at end of stream, how wrapping, buffering, flushing and closing decide what actually reaches a file, truncate versus append, which calls throw IOException, binary data streams, charsets, and console input and output through System.in/out/err, Console and Java 25's java.lang.IO.

What you’ll learn
  • Classify java.io classes as byte or character streams and pick the right family for binary versus text data
  • Predict what read(), read(byte[]), readLine() and the bulk read methods return, including at end of stream
  • Build valid wrapper chains with InputStreamReader, OutputStreamWriter and PrintWriter, and explain what closing a wrapper does
  • Decide whether a write truncates or appends, and when buffered output actually reaches the file (flush, close, PrintWriter autoflush)
  • Identify which I/O calls declare IOException, FileNotFoundException or EOFException, and which report errors through checkError()
  • Read and write binary primitives with DataInputStream and DataOutputStream and choose charsets explicitly when converting text
  • Read and write the console with System.in/out/err, System.console() and java.lang.IO, including redirection and a null console

Byte streams versus character streams

java.io has two stream families: byte streams, which move raw 8-bit bytes and extend InputStream or OutputStream, and character streams, which move 16-bit char values and extend Reader or Writer. The superclass is the reliable test — the class name is not. A character stream always applies a charset when it meets bytes; a byte stream never does.

Byte streamsCharacter streams
Base classesInputStream, OutputStreamReader, Writer
Unit read by read()One byte, returned as an int 0–255One char, returned as an int 0–65535
File classesFileInputStream, FileOutputStreamFileReader, FileWriter
Buffering decoratorsBufferedInputStream, BufferedOutputStreamBufferedReader, BufferedWriter
Formatted printingPrintStream (the type of System.out and System.err)PrintWriter
Typed / otherDataInputStream, DataOutputStream, ObjectInputStream, ObjectOutputStream—
In-memoryByteArrayInputStream, ByteArrayOutputStreamCharArrayReader, CharArrayWriter, StringReader, StringWriter
Use forBinary data: images, archives, any exact copyText

PrintStream is the classic trap: it prints text, but it is an OutputStream subclass that encodes characters into bytes for you. A binary file (a PNG, a ZIP) must be copied with byte streams; passing it through a Reader/Writer pair decodes and re-encodes it, and any byte sequence that is invalid in the charset comes back as a replacement character, so the copy is no longer identical. Object streams belong to serialization (objective 9.2), and reading and writing through Files and Path belongs to objective 9.3.

Reading: read() return values, bulk reads, mark() and reset()

InputStream.read() returns the next byte as an unsigned value from 0 to 255, or -1 at end of stream; Reader.read() does the same with a char (0–65535). The return type is int precisely so that -1 is free to mean "no more data": a stored byte of -1 (0xFF) comes back as 255 and does not stop a != -1 loop. Casting the result to byte before comparing would break that.

read(byte[] b) (and Reader.read(char[])) fills the array from index 0 with up to b.length elements and returns how many it read, or -1 at end of stream. It never clears the rest of the array, so a short final read leaves stale data from the previous call in the tail. Always use the count:

import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException {
        var in = new ByteArrayInputStream("0123456789".getBytes());
        var out = new ByteArrayOutputStream();
        byte[] buf = new byte[4];
        int n;
        while ((n = in.read(buf)) != -1) {
            out.write(buf, 0, n);   // only the n bytes just read
        }
        System.out.println(out);    // 0123456789
    }
}

Any code that uses the whole array rather than its first n elements picks up stale data on the last, short read. The bulk methods signal the end differently from the single-element read():

MethodReadsAt end of stream
read()One byte/charReturns -1
read(byte[]), read(byte[], off, len)Up to the length; returns the countReturns -1
readAllBytes()Everything leftReturns an empty array
readNBytes(int n)Up to n bytes (fewer only if the stream ends)Returns a shorter or empty array
skip(long n)Discards up to n; returns the number skippedUsually returns 0; FileInputStream on a regular file may skip past the end and report the bytes as skipped
transferTo(OutputStream) / Reader.transferTo(Writer)Everything left, written to the target; returns the count as longReturns 0

All of these continue from the current position: after skip(2) or readNBytes(4), a later transferTo or readAllBytes sees only what is left. transferTo closes neither stream. On the character side, Java 25 adds Reader.readAllAsString() and Reader.readAllLines(); Reader.of(CharSequence), added in Java 24, creates a reader over a string.

mark() and reset()

mark(int readAheadLimit) remembers the current position and reset() returns to it, so the data read after the mark is read again. The argument is how many bytes or characters may be read before the mark may become invalid. Only streams whose markSupported() returns true honour it: BufferedReader, BufferedInputStream, ByteArrayInputStream and StringReader do; FileInputStream and FileReader do not. On a FileReader, mark() itself already throws IOException ("mark() not supported"), and so does reset(); on a FileInputStream, mark() silently does nothing and reset() throws.

Trace it carefully: over "pqrstu", if you read p and q, call mark, read r, s and t, then reset(), the next read() returns r — the first character after the marked position, not the start of the stream.

Wrapping streams: decorators, bridges and close()

java.io streams are built by wrapping: a low-level stream that touches the source (a file, an array, the console) is passed to the constructor of a decorator that adds buffering, line reading, formatting or typed data. Decorators only accept their own family, so crossing from bytes to characters needs an explicit bridge:

ClassConstructor takesNotes
BufferedReaderA ReaderNot an InputStream — that does not compile
BufferedWriterA WriterNot an OutputStream
InputStreamReaderAn InputStream (+ optional charset)Bridge: decodes bytes into chars
OutputStreamWriterAn OutputStream (+ optional charset)Bridge: encodes chars into bytes
PrintWriterA Writer, an OutputStream, a file name or a FileBuilds the bridge itself when given an OutputStream
PrintStream, DataOutputStream, BufferedOutputStreamAn OutputStreamNot a Writer
DataInputStream, BufferedInputStreamAn InputStreamNot a Reader

So the standard text chain over bytes is new BufferedReader(new InputStreamReader(System.in)), and new InputStreamReader(new BufferedReader(...)) fails because the bridge expects bytes, not characters.

Closing a wrapper closes what it wraps. Close only the outermost stream; it flushes and then closes the inner one. For file and console streams such as FileReader, FileWriter or FileInputStream, any later read or write on the inner stream throws IOException ("Stream closed"). The in-memory classes are the exception: closing a ByteArrayInputStream, ByteArrayOutputStream, StringWriter or CharArrayWriter has no effect, so they keep working after their wrapper is closed — which is why toByteArray() or toString() can be called on them afterwards. Calling close() a second time on an already closed java.io stream has no effect. In try-with-resources, resources close in reverse declaration order, so a BufferedWriter declared after its FileWriter is closed first — flushing its buffer into the file writer — before the file writer's own (now harmless) close. When code writes to both a buffering wrapper and the stream beneath it, the destination receives data in the order it was flushed, not the order it was written: writes made directly on the inner stream go through at once, while the wrapper's output waits in its buffer until the wrapper is flushed or closed.

Opening files for writing: truncate or append

Opening a file for writing truncates it by default: new FileWriter(name), new FileOutputStream(name) and new PrintWriter(name) create the file if needed and empty it immediately — even if nothing is ever written. Append mode is chosen only by the constructor that actually opens the file:

ExpressionEffect on an existing file
new FileWriter("f.txt")Truncates
new FileWriter("f.txt", true)Appends
new FileOutputStream("f.txt", true)Appends
new PrintWriter("f.txt")Truncates (no append option)
new PrintWriter(new FileWriter("f.txt", true))Appends — the inner writer opened it in append mode
new PrintWriter(new FileWriter("f.txt"), true)Truncates — here true means autoflush, not append
new BufferedWriter(new FileWriter("f.txt"))Truncates — a wrapper inherits the inner stream's mode

Reading constructors such as new FileReader(name) and new FileInputStream(name) throw FileNotFoundException when the file does not exist. A typical append-then-read program:

import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException {
        try (var w = new PrintWriter(new FileWriter("app.log", true))) {
            w.println("started");
        }
        try (var r = new BufferedReader(new FileReader("app.log"))) {
            String line;
            while ((line = r.readLine()) != null) {
                System.out.println(line.length() + ": " + line);
            }
        }
    }
}

Run twice, it prints 7: started once and then twice, because each run appends a line; readLine returns the text without its line terminator, so the length is 7.

Buffering, flush() and PrintWriter autoflush

Buffered output reaches its destination only when the buffer fills, when flush() is called, or when the stream is closed (close flushes first). Until then, another reader of the same file sees nothing: a file just created by new FileWriter exists but is empty, so readLine() returns null. BufferedWriter, PrintWriter and OutputStreamWriter all hold data this way — the last because its encoder keeps an internal byte buffer — so a ByteArrayOutputStream beneath them stays at size 0 until a flush or close.

PrintWriter's autoflush flag (the boolean in PrintWriter(Writer, boolean) or PrintWriter(OutputStream, boolean)) is narrower than its name suggests: it flushes only on println, printf and format. print, write and append never trigger it.

import java.io.*;

public class Main {
    public static void main(String[] args) {
        var buf = new ByteArrayOutputStream();
        var pw = new PrintWriter(buf, true);   // autoflush on
        pw.print("a");
        System.out.println(buf.size());        // 0: print never flushes
        pw.println("b");
        System.out.println(buf.toString().strip()); // ab
        pw.close();
    }
}

The JVM does not flush user-created buffers when main ends. A PrintWriter built over System.out without the boolean has autoflush off and keeps its own buffer, so whatever it holds when the program ends is never printed. System.out itself is different: the JVM creates it as a PrintStream with autoflush on, so its println output appears immediately. Pass true as the second constructor argument, or call flush() or close(), to get a wrapping writer's output out.

Reading lines with BufferedReader and writing them with BufferedWriter

BufferedReader.readLine() returns the next line without its terminator, an empty string "" for a blank line, and null only at end of stream. It recognises all three terminators: \n, \r and \r\n. A terminator at the very end of the data does not create an extra empty line — the next call simply returns null. So "x\r\ny\n\n" is three lines (x, y, ""), and the loop while ((line = br.readLine()) != null) counts blank lines because only null ends it.

On the writing side, BufferedWriter.newLine() writes the platform line separator, and PrintWriter.println does the same after its argument. Writer.write(String) writes exactly the characters given, with no separator.

Which I/O calls throw IOException

Almost every java.io method declares the checked IOException — read, write, readLine, skip, reset, flush and, importantly, close(). The deliberate exceptions are the printing classes: PrintStream and PrintWriter never throw IOException from print, println, printf, format, write(int), write(byte[], int, int), flush() or close(); they set an internal error flag that checkError() reports instead. So a try-with-resources over a PrintWriter or PrintStream alone needs no catch or throws for its implicit close.

CallDeclares IOException?
System.out.println(...), System.out.write(65)No
System.out.writeBytes(byte[]) (Java 14)No
System.out.write(byte[])Yes — PrintStream overrides it with throws IOException to match OutputStream; writeBytes is the non-throwing alternative
System.in.read(), System.in.close()Yes — System.in is a plain InputStream
new FileReader(name), new FileInputStream(name)FileNotFoundException (a subclass of IOException)
reader.readLine(), reader.close()Yes
printWriter.close(), printWriter.flush()No

This matters with try-with-resources: for a reader, writer or file stream, the implicit close() declares IOException, so catching only FileNotFoundException does not compile unless the method also declares or catches IOException (javac reports "unreported exception IOException"). Once a PrintWriter has failed — for example after it was closed — later writes are silently ignored and checkError() returns true:

import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException {
        Reader r = Reader.of("one\ntwo\n");
        System.out.println(r.readAllLines());   // [one, two]
        var pw = new PrintWriter(new ByteArrayOutputStream());
        pw.close();
        pw.print("x");
        System.out.println(pw.checkError());     // true
    }
}

The same program also shows Reader.of (Java 24) and Java 25's readAllLines().

Binary data with DataInputStream and DataOutputStream

DataOutputStream writes Java primitives and strings in a fixed, big-endian binary form, and DataInputStream reads them back — but only correctly if you read the same types in the same order, because the bytes carry no type information. Sizes to know:

MethodBytes written
writeBoolean, writeByte1
writeShort, writeChar2
writeInt, writeFloat4
writeLong, writeDouble8
writeUTF(s)2-byte length prefix + the modified UTF-8 bytes of s (1 byte per ASCII character)

Reading a different type than was written is legal and simply reinterprets the bytes: a single writeInt(1) produces 00 00 00 01, and two readShort() calls split those bytes big-endian into 0 and then 1. The reverse direction works the same way: a readInt() joins four bytes written as smaller values into one big-endian number. Because every value of an int is legal data, the typed reads cannot use -1 as an end marker; readInt, readUTF and the others throw EOFException (a subclass of IOException) when the bytes run out, and code reading an unknown number of records catches it to stop:

import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException {
        var bytes = new ByteArrayOutputStream();
        try (var out = new DataOutputStream(bytes)) {
            out.writeInt(10);
            out.writeInt(20);
            out.writeInt(12);
        }
        int sum = 0;
        try (var in = new DataInputStream(
                new ByteArrayInputStream(bytes.toByteArray()))) {
            while (true) {
                try {
                    sum += in.readInt();
                } catch (EOFException e) {
                    break;                     // normal end of data
                }
            }
        }
        System.out.println(bytes.size() + " " + sum); // 12 42
    }
}

Charsets: where bytes and characters meet

A charset decides how characters become bytes and back, so the byte count of text depends on it. Since Java 18 the default charset is UTF-8, but code that handles files in another encoding should pass a Charset explicitly — FileReader(String, Charset) and FileWriter(String, Charset) (Java 11), InputStreamReader(InputStream, Charset), OutputStreamWriter(OutputStream, Charset) and PrintWriter(OutputStream, boolean, Charset), with constants from java.nio.charset.StandardCharsets.

Charsetaé€Notes
UTF-8123Variable width
UTF-16BE / UTF-16LE222No byte-order mark
UTF-16222Encoding adds a 2-byte byte-order mark at the start
ISO-8859-111—One byte per character; cannot represent €
US-ASCII1——7-bit only

A writer such as OutputStreamWriter replaces a character the charset cannot represent with the charset's replacement byte (?) rather than throwing. Decoding with the wrong charset is equally silent: ñ written in UTF-8 is 2 bytes, and reading those bytes as ISO-8859-1 — which maps every byte to one character, so it never fails — gives 2 characters (ñ). The String.length() of text is a count of char values, never of bytes.

Console I/O: standard streams, System.console() and java.lang.IO

Every Java program has three standard streams: System.in (an InputStream on standard input), and System.out and System.err (PrintStreams on standard output and standard error). They are separate, so shell redirection of one leaves the others alone: java Main.java > log.txt sends System.out to the file while System.err still reaches the terminal, and 2> err.txt does the reverse. Inside a program, System.setOut, System.setErr and System.setIn replace the stream a field refers to — setOut changes only System.out. To read text from standard input, wrap it: new BufferedReader(new InputStreamReader(System.in)).

System.console() returns the JVM's java.io.Console, or null when no console device is available — typically when the program is started by a scheduler or service with its streams redirected and no terminal. Code must null-check it and fall back to the standard streams; calling a method on the null reference throws NullPointerException. Console is character-oriented:

Console methodReturns
readLine(), readLine(String fmt, Object... args)String (prompt is formatted, like printf); null at end of stream
readPassword(), readPassword(String fmt, Object... args)char[], read with echo turned off
writer()PrintWriter
reader()Reader (not a BufferedReader)
printf(...), format(...)The Console itself, for chaining

readPassword returns a char[] so the caller can overwrite the secret after use; an immutable String would linger in memory. Assigning its result to a String does not compile. None of the Console methods declare IOException (failures surface as the unchecked IOError).

import java.io.*;
import java.util.Arrays;

public class Main {
    public static void main(String[] args) throws IOException {
        Console c = System.console();
        if (c != null) {
            String user = c.readLine("User: ");
            char[] pin = c.readPassword("PIN for %s: ", user);
            c.printf("Read %d characters%n", pin.length);
            Arrays.fill(pin, ' ');           // wipe the secret
        } else {
            var in = new BufferedReader(
                new InputStreamReader(System.in));
            System.out.println("No console; read: " + in.readLine());
        }
    }
}

Java 25 adds java.lang.IO (JEP 512), a deliberately small class for beginner and script-style programs such as compact source files; being in java.lang, it needs no import. It has exactly print(Object), println(Object), println(), readln() and readln(String prompt) — no printf or format, so formatted output still goes through System.out.printf or String.format, and %n concatenated into an ordinary string prints literally. readln(prompt) writes the prompt as if by print (no line break), reads a line from standard input and returns null at end of stream; it throws the unchecked IOError, never IOException, so no throws clause is needed.

Tip. Objective 9.1 is examined mainly through short, complete programs that read or write in-memory streams, files or the console, followed by "What is the result?" or a blank to fill. Expect loops over read(), read(byte[]) and readLine() where the end-of-stream signal, an ignored read count or a blank line changes the output; files written twice where truncate versus append decides the content; and writers read back before or after flush(), close() or a PrintWriter autoflush call. Compile-or-not items offer near-identical wrapper chains (BufferedReader over an InputStream, PrintStream over a Writer) or swap Console and System.out method calls whose return types and checked exceptions differ. Other items count bytes produced by DataOutputStream or by a given charset, ask what happens when a closed inner stream is used, compare redirected standard output and standard error, or show a compact source file using java.lang.IO with empty input or a missing printf method.

Key takeaways
  • Reader/Writer subclasses are character streams; InputStream/OutputStream subclasses are byte streams — PrintStream prints text but is a byte stream.
  • read() returns 0–255 (or a char) and -1 only at end of stream; read(buf) returns a count and leaves the rest of the array untouched.
  • readLine() drops the terminator, returns "" for a blank line and null at end of stream; a trailing terminator adds no empty line.
  • Decorators wrap only their own family; InputStreamReader and OutputStreamWriter (or PrintWriter over an OutputStream) bridge bytes and characters.
  • Closing a wrapper flushes and closes the inner stream; a closed file or console stream then throws IOException on use, in-memory streams ignore close(), and a second close() is harmless.
  • Opening a file for writing truncates it unless the opening constructor is given append = true; PrintWriter's boolean is autoflush, not append.
  • Buffered output reaches its destination only on flush(), close() or a full buffer; PrintWriter autoflush covers println, printf and format only.
  • PrintStream and PrintWriter swallow IOException (check checkError()), including in flush() and close(), except PrintStream.write(byte[]); most other java.io methods, close() included, declare it.
  • DataInputStream reads big-endian bytes back in the order written and throws EOFException at the end; bulk reads return empty arrays instead.
  • System.console() can be null; Console.readPassword returns char[], and java.lang.IO offers only print, println and readln.

Frequently asked questions

What is the difference between byte streams and character streams in Java?

Byte streams extend InputStream or OutputStream and move raw 8-bit bytes, so they suit binary data such as images and exact file copies. Character streams extend Reader or Writer and move 16-bit chars, applying a charset whenever they convert to or from bytes, so they suit text. InputStreamReader and OutputStreamWriter bridge the two families.

Why does InputStream.read() return an int instead of a byte?

InputStream.read() returns each byte as an unsigned value from 0 to 255 so that -1 can be reserved to mean end of stream. A stored byte of -1 (0xFF) is therefore returned as 255, and a loop that compares the result with -1 keeps reading it as data. Storing the result in a byte before the comparison would make that byte look like end of stream.

Does new FileWriter("file.txt") overwrite or append?

new FileWriter("file.txt") overwrites: it creates the file if needed and truncates it to zero length as soon as it is opened. To append, use new FileWriter("file.txt", true) or new FileOutputStream("file.txt", true). Wrapping a writer in PrintWriter or BufferedWriter never changes the mode, and the boolean in new PrintWriter(writer, true) turns on autoflush, not append.

Why is nothing written to my file when I use BufferedWriter or PrintWriter?

BufferedWriter and PrintWriter keep output in a memory buffer, and the data reaches the file only when the buffer fills, flush() is called or the writer is closed. The JVM does not flush these buffers when the program ends, so output that is never flushed or closed is lost. Closing the outermost writer, ideally with try-with-resources, flushes it and closes the streams it wraps.

What does PrintWriter autoflush actually flush?

With autoflush enabled through PrintWriter(Writer, true) or PrintWriter(OutputStream, true), a PrintWriter flushes after println, printf and format calls only. Output written with print, write or append stays in the buffer until an explicit flush() or close(), even though autoflush is on.

Can System.console() return null?

Yes. System.console() returns null when no console device is available, for example when a program is launched by a scheduler or service with its input and output redirected and no terminal attached. Programs should check for null and fall back to System.in and System.out; calling a Console method on the null result throws NullPointerException.

Why does Console.readPassword return a char array instead of a String?

Console.readPassword reads a line with echo turned off and returns it as a char[] so that the program can overwrite the password, for example with Arrays.fill, as soon as it is no longer needed. A String is immutable and could stay in memory until garbage collection. Because the return type is char[], assigning the result to a String variable does not compile.

What is java.lang.IO in Java 25?

java.lang.IO is a small console class made standard in Java 25 (JEP 512) for simple programs such as compact source files. It provides print(Object), println(Object), println(), readln() and readln(String prompt); it has no printf or format method. readln prints the prompt without a line break, returns null at end of standard input and throws the unchecked IOError rather than IOException.

Source

This lesson covers the "Performing Input and Output Operations Using the Java I/O API" domain of the official 1Z0-831 exam guide. Vendors revise their guides — check the source for the current version.

Sign up free to mark lessons complete, bookmark topics and track your exam readiness.

Spotted a mistake in this lesson?