I/O streams: byte and character

Until now your programs forgot everything when they ended. To save data and read it back, Java uses streams: one-way flows of data between your program and a file, the console or the network. You'll learn the two families, byte streams and character streams, why the difference matters for text like ₹ and हिंदी, and the habits that keep files from getting corrupted.

Module 13 · File Handling and I/O⏱ 22 minIntermediate9 practice problems
1 The story

The receipt that printed “₹”

Riya's canteen app now saves every order to a file so the owner can open it at night. Orders with English names look fine, but every price shows “₹120” instead of “₹120”, and a dish called “पनीर” is complete gibberish.

₹120 characters 226 130 185 49 50 48 bytes in the file ₹120 ₹120 wrong decoder right decoder (UTF-8) myphatshala.in
Riya

Sir, the file has my text. Why does the owner's laptop show rubbish?

Arjun Sir

The file doesn't have your text. It has bytes. ₹ became three bytes when you saved it, and his program turned each byte back into a separate character.

Riya

So text and bytes are different things?

Arjun Sir

Completely. Java even has two separate families of streams for them. Let's open the pipes and look inside.

2 Definition

What is an I/O stream?

Formal definition (write this in exams)

An I/O stream is a sequential flow of data between a program and a source or destination such as a file, the console, memory or a network connection. Input streams read data in; output streams write data out. Java's java.io package has two hierarchies: byte streams, rooted at the abstract classes InputStream and OutputStream, which transfer raw 8-bit bytes; and character streams, rooted at Reader and Writer, which transfer 16-bit Unicode characters and convert to and from bytes using a charset such as UTF-8. InputStreamReader and OutputStreamWriter bridge the two.

In simple words

A stream is a pipe. Byte pipes carry numbers 0–255 and don't care what they mean. Character pipes carry letters, and do the translation to and from bytes for you.

Words you'll meet in this lesson

ByteA number from 0 to 255. Everything stored or sent is bytes.
CharacterA letter or symbol: A, ₹, न.
Charset / encodingThe rule that turns characters into bytes and back. UTF-8 is the standard.
Source / sinkWhere a stream reads from / writes to: file, console, memory, socket.
End of streamread() returns −1 when nothing is left.
ClosingReleases the file handle and flushes pending data. Use try-with-resources.

Byte streams vs character streams

Byte streamsCharacter streams
Base classesInputStream, OutputStreamReader, Writer
Unitbyte (8 bits)char (16 bits)
read() returns0–255, or −1a character code 0–65535, or −1
Charset involved?NoYes (encode/decode)
Use forimages, PDFs, zip, any binarytext: .txt, .csv, .json, .html
File classesFileInputStream, FileOutputStreamFileReader, FileWriter

Exam tip: “Byte streams handle 8-bit binary data (InputStream/OutputStream); character streams handle 16-bit Unicode text (Reader/Writer) and use a charset to convert. read() returns int so that −1 can signal end of stream.”

3 Types and ways to use it

The stream family

BYTE streams                         CHARACTER streams
        InputStream     OutputStream              Reader            Writer
          FileInputStream   FileOutputStream        FileReader        FileWriter
          ByteArrayInputStream ...                  InputStreamReader OutputStreamWriter   (bridges)
          BufferedInputStream BufferedOutputStream  BufferedReader    BufferedWriter
          DataInputStream  DataOutputStream          StringReader      PrintWriter
          ObjectInputStream ObjectOutputStream (13.4)

File streams

Read and write files on disk: FileInputStream, FileReader and their output twins.

source / sink

Bridges

InputStreamReader / OutputStreamWriter turn bytes into characters and back, with a charset.

bytes ⇄ chars

Filters / wrappers

Add features on top of another stream: buffering, primitives (DataInputStream), objects (13.4).

decorators

Standard streams

System.in, System.out, System.err: byte streams already open for you.

console

The streams you've used since day one

public class Main {
    public static void main(String[] args) {
        System.out.println("System.in  is a " + System.in.getClass().getSimpleName() + " (bytes from the keyboard)");
        System.out.println("System.out is a " + System.out.getClass().getSimpleName() + " (bytes to the console)");
        System.err.println("System.err is for error messages");
    }
}
System.in is a BufferedInputStream (bytes from the keyboard) System.out is a PrintStream (bytes to the console)

The System.err line goes to a separate error stream; consoles often show it in red.

Always name the charset when converting text. "₹".getBytes() or new String(bytes) without a charset uses the platform default, which differs between Windows, Linux and servers. Write getBytes(StandardCharsets.UTF_8) and new String(bytes, StandardCharsets.UTF_8).

4 Step by step

Writing text to a file, layer by layer

  1. Open the stream

    try (Writer w = new OutputStreamWriter(
            new FileOutputStream("note.txt"), StandardCharsets.UTF_8)) {
    FileOutputStream asks the OS to create (or empty) note.txt and gets a file handle. The writer wraps it with a UTF-8 encoder.
  2. Write characters

        w.write("Hi₹");
    The encoder turns 3 characters into 5 bytes: 72 105 226 130 185.
  3. Bytes go down

    The writer hands those bytes to the FileOutputStream, which passes them to the OS. The OS may keep them in its own cache for a moment before the disk.
  4. Close

    }   // try-with-resources calls w.close()
    Closing the writer flushes anything still waiting and closes the inner stream and the file handle. Forgetting to close can leave a file empty or half-written.
  5. Reading goes back up

    FileInputStream returns the 5 bytes; InputStreamReader with UTF-8 decodes them into H, i, ₹. Decode with a different charset and you get different characters.
5 See it happen

Bytes vs characters: both views

Left: what a byte stream and a character stream each see for the text “Hi₹”. Right: the layers your text passes through on its way to the disk.

Bytes vs characters, both views 6 steps
What you see String note = "Hi₹"; out.write(bytes); // OutputStream out.write(bytes); // OutputStream // note.txt on disk reader.read() // Reader in.read() // InputStream H i ₹ 3 characters 3 chars again 72 105 226 130 185 H and i: 1 byte each ₹ needs 3 bytes in UTF-8 the file only stores these 5 numbers a Reader decodes 3 bytes back into ₹ an InputStream hands you 226, then 130… Text: 3 characters. Written as bytes… …₹ becomes 3 bytes. The file: 5 bytes. Character stream: 3 chars. Byte stream: 5 numbers. myphatshala.in
Behind the screen your code write("Hi₹") Writer OutputStreamWriter encoder UTF-8: chars → bytes OutputStream FileOutputStream operating system file descriptor disk note.txt: 5 bytes characters still characters chars → bytes bytes system call stored bytes Your program has text. A Writer accepts chars. The charset encodes them. An OutputStream moves bytes. The OS does the real write. Bytes land on disk. myphatshala.in
6 Real-world use

Streams in real systems

Every upload, download, log file, report export and API call in Java goes through streams.

Watch it with an example: why ₹ turned into “₹” in a bank statement

A bank exports statements as CSV files written in UTF-8. A customer opens one in an old spreadsheet program that assumes the Windows-1252 charset.

The same bytes, two charsets 5 steps
Animated example ₹500 in the bank app E2 82 B9 35 30 30 UTF-8 bytes (hex) statement.csv read as Windows-1252 ₹500 read as UTF-8 ₹500 The amount as text. Encoded to 6 bytes. Saved to a file. Wrong charset: garbage. Right charset: ₹500. myphatshala.in

1. Reports and exports

CSV statements, GST reports and invoices are written with character streams in UTF-8 so ₹ and regional names survive.

transactions→Writer (UTF-8)→statement.csv

2. Uploads and downloads

Profile photos, KYC PDFs and app updates are copied with byte streams; a charset would corrupt them.

photo.jpg→InputStream → OutputStream→storage

3. Logs

Servers append log lines to files all day. Opening in append mode, and rotating files, keeps history safe.

each request→FileWriter(file, true)→app.log

4. Network calls

An HTTP response body is an InputStream of bytes; reading it as JSON text needs the right charset, usually UTF-8.

bank API→InputStreamReader(UTF-8)→JSON text

Simplified. Modern code often uses the java.nio.file.Files helpers (Lesson 13.2) on top of these same streams.

7 Run the code

One file, read as bytes and as characters

import java.io.*;
import java.nio.charset.StandardCharsets;

public class Main {
    public static void main(String[] args) throws IOException {
        String note = "Order #77: 2 x Vada Pav";
        try (OutputStream out = new FileOutputStream("note.txt")) {
            out.write(note.getBytes(StandardCharsets.UTF_8));
        }
        try (InputStream in = new FileInputStream("note.txt")) {
            int first = in.read();
            System.out.println("first byte: " + first + " = '" + (char) first + "'");
        }
        try (Reader r = new FileReader("note.txt", StandardCharsets.UTF_8)) {
            char[] buf = new char[100];
            int n = r.read(buf);
            System.out.println("read " + n + " chars: " + new String(buf, 0, n));
        }
    }
}
first byte: 79 = 'O' read 23 chars: Order #77: 2 x Vada Pav
  • try (OutputStream out = …)try-with-resources closes the stream automatically, even if an exception is thrown.
  • note.getBytes(StandardCharsets.UTF_8)Turn text into bytes with a named charset before writing to a byte stream.
  • in.read()One byte, as an int: 79 is the code for O.
  • r.read(buf)Reads up to 100 characters into the array and returns how many it read.
8 Line by line

Five bytes, three characters

Recorded from the real JVM. Watch the byte[] on the heap (Java bytes are signed, so 226 shows as −30) and the two kinds of stream reading the same data.

Worked examples: real problems, solved and traced in memory

Three everyday jobs: copying a file byte by byte, reading text with a character stream, and fixing a charset bug. The programs create their own small files, so you can run them as they are.

Example 1Office · backups

Back up an invoice file byte by byte

Problem statement

An office app saves invoices as files. Before overwriting one, it must make a backup copy. Write the invoice with a FileOutputStream, copy it to invoice.bak with a byte-stream loop, and confirm both files are the same size.

Input
invoice text: INV-7 Rs 236
Expected output
copied 12 bytes; sizes 12 and 12

Approach

  1. Write the bytes of the text to invoice.txt.
  2. Open a FileInputStream on it and a FileOutputStream on the backup, both in try-with-resources.
  3. Loop: read() one byte, write() it, until read() returns −1. Count the bytes.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

Example 2Restaurant · menu

Read a menu with a character stream

Problem statement

A restaurant keeps its menu in menu.txt, one dish per line, including Hindi names. Read it with a Reader, one character at a time, and count the characters and lines.

Input
menu.txt: Dosa / Idli / पनीर टिक्का
Expected output
3 lines, 15 chars, 23 bytes on disk

Approach

  1. Write the file with a FileWriter, naming UTF-8.
  2. Read it back with a FileReader (UTF-8) one character at a time.
  3. Count characters, and count '\n' as line ends.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

Example 3Banking · exports

Find and fix a charset bug

Problem statement

A statement line Paid ₹500 is saved in UTF-8. A legacy tool reads it as Windows-1252 and shows garbage. Reproduce the bug, then read it correctly. Print through a UTF-8 PrintStream so the console shows ₹ properly.

Input
Paid ₹500
Expected output
bytes: 11
as windows-1252: Paid ₹500
as UTF-8:        Paid ₹500

Approach

  1. Write the text to statement.csv with UTF-8.
  2. Read all bytes, then decode them twice: once with windows-1252, once with UTF-8.
  3. Print both through new PrintStream(System.out, true, UTF_8).

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

9 Quiz and revision

Check yourself

Quick revision

Lesson 13.1 in one screen

  • Stream = one-way flow of data. Input reads in, output writes out.
  • Byte streams: InputStream/OutputStream, for binary data. Character streams: Reader/Writer, for text.
  • read() returns an int; −1 means end of stream.
  • Characters ≠ bytes: in UTF-8, ₹ and Hindi letters take 3 bytes each.
  • Bridges: InputStreamReader, OutputStreamWriter. Always name the charset.
  • new FileOutputStream(f) empties the file; (f, true) appends.
  • Close streams with try-with-resources; catch or declare IOException.

Interview and viva questions

What is the difference between byte streams and character streams?

Byte streams (InputStream/OutputStream) read and write raw 8-bit bytes and are used for binary data. Character streams (Reader/Writer) read and write 16-bit characters and use a charset to encode and decode, so they are used for text. Character streams are usually built on top of byte streams.

Why does read() return int and not byte?

So it can return every possible byte value (0–255) and still have a separate value, −1, for end of stream. A Java byte only holds −128 to 127, with no spare value.

What are InputStreamReader and OutputStreamWriter?

Bridge classes. InputStreamReader wraps an InputStream and decodes its bytes into characters using a charset; OutputStreamWriter wraps an OutputStream and encodes characters into bytes. They let you read text from any byte source (files, sockets, System.in).

What happens if you don't close a stream?

The file handle stays open (the OS has a limit, so a long-running server eventually fails with “too many open files”), and data in buffers may never be written. Use try-with-resources so close() always runs.

What is a charset, and why does it matter?

A charset maps characters to bytes. The same bytes decoded with different charsets give different text, which causes garbled output like “₹”. UTF-8 can represent every Unicode character and is the standard for files and the web; specify it explicitly.

Practice problems

Work with byte streams and character streams, in memory and on files, and always close what you open. Each program makes its own files, so the output is the same every run. The last three are business tasks.

P1Easy

Bytes of a word

Print the UTF-8 byte values (0–255) of Java using a ByteArrayInputStream and read().

Input
Java
Expected output
74 97 118 97 
Show solution and explanation
import java.io.*;
import java.nio.charset.StandardCharsets;

public class Main {
    public static void main(String[] args) throws IOException {
        InputStream in = new ByteArrayInputStream("Java".getBytes(StandardCharsets.UTF_8));
        int b;
        while ((b = in.read()) != -1) {
            System.out.print(b + " ");
        }
        System.out.println();
    }
}
74 97 118 97

How it works: read() returns the next byte as an int 0–255, or −1 at the end. Using an int (not a byte) is what lets −1 mean “end” without clashing with any real byte value.

P2Easy

Write and read a file

Write Hello file to greet.txt with a FileWriter, then read it back character by character with a FileReader and print it.

Input
Hello file
Expected output
Hello file
Show solution and explanation
import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException {
        try (FileWriter w = new FileWriter("greet.txt")) {
            w.write("Hello file");
        }
        StringBuilder sb = new StringBuilder();
        try (FileReader r = new FileReader("greet.txt")) {
            int c;
            while ((c = r.read()) != -1) sb.append((char) c);
        }
        System.out.println(sb);
    }
}
Hello file

How it works: the int from read() is cast back to char. Since Java 18 FileWriter and FileReader use UTF-8 by default; naming the charset is still clearer.

P3Easy

Characters vs bytes

For the word नमस्ते, print its length in characters and in UTF-8 bytes.

Input
नमस्ते
Expected output
6 chars, 18 bytes
Show solution and explanation
import java.nio.charset.StandardCharsets;

public class Main {
    public static void main(String[] args) {
        String word = "\u0928\u092E\u0938\u094D\u0924\u0947";
        System.out.println(word.length() + " chars, " + word.getBytes(StandardCharsets.UTF_8).length + " bytes");
    }
}
6 chars, 18 bytes

How it works: each Devanagari code point here takes 3 bytes in UTF-8, so 6 characters become 18 bytes. Limits like “max 160 bytes” in SMS or database columns are about bytes, not characters.

P4Medium

Count lines in a file

Write three lines to log.txt and count the line breaks by reading characters.

Input
three log lines
Expected output
lines: 3
Show solution and explanation
import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException {
        try (Writer w = new FileWriter("log.txt")) {
            w.write("login ok\npayment ok\nlogout\n");
        }
        int lines = 0;
        try (Reader r = new FileReader("log.txt")) {
            int c;
            while ((c = r.read()) != -1) if (c == '\n') lines++;
        }
        System.out.println("lines: " + lines);
    }
}
lines: 3

How it works: a text file is just characters; lines are separated by '\n'. Lesson 13.3's BufferedReader.readLine() does this for you, much faster.

P5Medium

Append instead of overwrite

Write A to audit.txt, then open it again in append mode and add B. Print the file's contents.

Input
A then B
Expected output
AB
Show solution and explanation
import java.io.*;
import java.nio.charset.StandardCharsets;

public class Main {
    public static void main(String[] args) throws IOException {
        try (FileOutputStream f = new FileOutputStream("audit.txt")) {
            f.write('A');
        }
        try (FileOutputStream f = new FileOutputStream("audit.txt", true)) {
            f.write('B');
        }
        try (FileInputStream in = new FileInputStream("audit.txt")) {
            System.out.println(new String(in.readAllBytes(), StandardCharsets.UTF_8));
        }
    }
}
AB

How it works: by default a FileOutputStream starts the file empty. The second constructor argument true means append. Forgetting it is a classic way to lose a log file.

P6Medium

Handle a missing file

Try to open orders.csv, which doesn't exist. Catch the exception and print a friendly message with its type.

Input
(no file)
Expected output
No orders file yet (FileNotFoundException)
Show solution and explanation
import java.io.*;

public class Main {
    public static void main(String[] args) {
        try (FileInputStream in = new FileInputStream("orders.csv")) {
            System.out.println("first byte: " + in.read());
        } catch (FileNotFoundException e) {
            System.out.println("No orders file yet (" + e.getClass().getSimpleName() + ")");
        } catch (IOException e) {
            System.out.println("Could not read orders: " + e.getMessage());
        }
    }
}
No orders file yet (FileNotFoundException)

How it works: FileNotFoundException is a subclass of IOException, so it must be caught first. Every I/O method can throw IOException because disks and networks can fail.

P7Business problem

Receipt size check for a printer

A thermal printer accepts at most 32 bytes per line. Check each receipt line's UTF-8 size and mark lines that are too long.

Input
3 receipt lines
Expected output
OK   23 bytes: Masala Dosa x2   Rs 180
OK   22 bytes: Filter Coffee x3 Rs 90
LONG 39 bytes: Paneer Butter Masala Family Pack Rs 520
Show solution and explanation
import java.nio.charset.StandardCharsets;

public class Main {
    public static void main(String[] args) {
        String[] lines = {"Masala Dosa x2   Rs 180", "Filter Coffee x3 Rs 90", "Paneer Butter Masala Family Pack Rs 520"};
        for (String l : lines) {
            int size = l.getBytes(StandardCharsets.UTF_8).length;
            System.out.println((size <= 32 ? "OK   " : "LONG ") + size + " bytes: " + l);
        }
    }
}
OK 23 bytes: Masala Dosa x2 Rs 180 OK 22 bytes: Filter Coffee x3 Rs 90 LONG 39 bytes: Paneer Butter Masala Family Pack Rs 520

How it works: devices and protocols limit bytes, not characters. Measuring with the same charset the printer uses avoids cut-off lines.

P8Business problem

Save a binary price table

Save three prices (in paise) as raw bytes using DataOutputStream, then read them back with DataInputStream and print the total in rupees.

Input
prices 1999, 4950, 120 (paise)
Expected output
file size 12 bytes, total Rs 70.69
Show solution and explanation
import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException {
        int[] paise = {1999, 4950, 120};
        try (DataOutputStream out = new DataOutputStream(new FileOutputStream("prices.bin"))) {
            for (int p : paise) out.writeInt(p);
        }
        int total = 0;
        try (DataInputStream in = new DataInputStream(new FileInputStream("prices.bin"))) {
            for (int i = 0; i < paise.length; i++) total += in.readInt();
        }
        System.out.println("file size " + new File("prices.bin").length() + " bytes, total Rs " + total / 100 + "." + total % 100);
    }
}
file size 12 bytes, total Rs 70.69

How it works: DataOutputStream is a filter on a byte stream that writes each int as exactly 4 bytes, so 3 prices take 12 bytes. Binary formats are compact, but you need the same program logic to read them back.

P9Business problem

Copy a file with a buffer array

Copying one byte per call is slow. Copy a 10,000-byte file using a 1,024-byte array with read(byte[]), and count how many read calls were needed.

Input
10,000-byte file
Expected output
copied 10000 bytes in 10 reads
Show solution and explanation
import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException {
        try (FileOutputStream f = new FileOutputStream("scan.dat")) {
            f.write(new byte[10_000]);
        }
        int calls = 0;
        long copied = 0;
        try (InputStream in = new FileInputStream("scan.dat");
             OutputStream out = new FileOutputStream("scan-copy.dat")) {
            byte[] buf = new byte[1024];
            int n;
            while ((n = in.read(buf)) != -1) {
                out.write(buf, 0, n);
                copied += n;
                calls++;
            }
        }
        System.out.println("copied " + copied + " bytes in " + calls + " reads");
    }
}
copied 10000 bytes in 10 reads

How it works: read(buf) fills up to 1,024 bytes and returns how many it actually read; the last chunk is shorter, so always write n bytes, not the whole array. 10 reads instead of 10,000 (for a local file; a stream may return fewer bytes per call). Java 9+ also has in.transferTo(out).

Real-world scenario questions (asked in company interviews)

I/O bugs reach customers directly: corrupted files, missing data and server outages. These questions check whether you know why.

Fintech · exportsFresher

₹ shows as “?” in exported reports

Statements generated on the Linux server show “?500” instead of “₹500”, while on a developer's laptop they look fine. The code uses new FileWriter(path) and text.getBytes(). Explain.

The interviewer is checking: platform default charsets.

Show a strong answer
  • Without a charset, Java uses a platform default that can differ between machines (older JDKs and some server locales use ASCII or Latin-1, where ₹ doesn't exist, so it's replaced by “?”).
  • Always pass StandardCharsets.UTF_8 to writers, readers, getBytes and new String.
  • For CSVs opened in Excel, a UTF-8 BOM or an explicit import charset may also be needed.

Avoid: replacing ₹ with “Rs” as the fix; the next customer name in Hindi will break too.

Media · uploadsFresher

Uploaded photos are corrupted

A KYC service copies uploaded photos with BufferedReader.readLine() and FileWriter.write(). The saved images won't open. Why?

The interviewer is checking: binary vs text streams.

Show a strong answer
  • Images are binary. Readers decode bytes as characters (invalid sequences become replacement characters) and readLine drops line-ending bytes, so the data changes.
  • Copy with byte streams: InputStream.transferTo(OutputStream) or Files.copy.

Avoid: “try a different charset”. No charset is right for non-text data.

Backend · outageJunior

“Too many open files” after a few days

A report service works for days, then every request fails with IOException: Too many open files. The code opens a FileInputStream per request and calls close() at the end of the method. What's wrong?

The interviewer is checking: resource leaks and try-with-resources.

Show a strong answer
  • When an exception happens before close(), the stream is never closed. Each failure leaks a file handle until the OS limit is reached.
  • Use try-with-resources so the stream is closed on every path.
  • Monitor open file descriptors and add a test that exercises the error path.

Avoid: raising the OS file limit as the fix; it only delays the outage.

Logging · data lossJunior

Yesterday's audit log disappeared

An audit logger opens new FileWriter("audit.log") each time the app starts. After a restart, all old entries are gone. Explain and fix.

The interviewer is checking: append mode.

Show a strong answer
  • The one-argument constructor truncates the file. Use new FileWriter("audit.log", StandardCharsets.UTF_8, true) to append.
  • For real systems, use a logging framework with file rotation and keep audit data in durable storage.

Avoid: reading the whole old file and rewriting it each time; it's slow and risks losing everything on a crash.