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.
Sir, the file has my text. Why does the owner's laptop show rubbish?
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.
So text and bytes are different things?
Completely. Java even has two separate families of streams for them. Let's open the pipes and look inside.
What is an I/O stream?
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.
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
A, ₹, न.read() returns −1 when nothing is left.Byte streams vs character streams
| Byte streams | Character streams | |
|---|---|---|
| Base classes | InputStream, OutputStream | Reader, Writer |
| Unit | byte (8 bits) | char (16 bits) |
read() returns | 0–255, or −1 | a character code 0–65535, or −1 |
| Charset involved? | No | Yes (encode/decode) |
| Use for | images, PDFs, zip, any binary | text: .txt, .csv, .json, .html |
| File classes | FileInputStream, FileOutputStream | FileReader, 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.”
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.
Bridges
InputStreamReader / OutputStreamWriter turn bytes into characters and back, with a charset.
Filters / wrappers
Add features on top of another stream: buffering, primitives (DataInputStream), objects (13.4).
Standard streams
System.in, System.out, System.err: byte streams already open for you.
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");
}
}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).
Writing text to a file, layer by layer
Open the stream
try (Writer w = new OutputStreamWriter( new FileOutputStream("note.txt"), StandardCharsets.UTF_8)) {FileOutputStreamasks the OS to create (or empty) note.txt and gets a file handle. The writer wraps it with a UTF-8 encoder.Write characters
The encoder turns 3 characters into 5 bytes: 72 105 226 130 185.w.write("Hi₹");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.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.} // try-with-resources calls w.close()Reading goes back up
FileInputStreamreturns the 5 bytes;InputStreamReaderwith UTF-8 decodes them into H, i, ₹. Decode with a different charset and you get different characters.
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.
- A Java
Stringholds characters."Hi₹"has 3. - Files, networks and keyboards only move bytes (numbers 0–255).
His 72 andiis 105. - In the UTF-8 encoding,
₹doesn't fit in one byte: it becomes three bytes, 226 130 185. - The file note.txt contains just these 5 bytes. It doesn't “know” they were text.
- A character stream (
Reader) decodes the bytes with a charset and gives you back 3 characters. - A byte stream (
InputStream) gives you the raw bytes one at a time. Perfect for images and PDFs, wrong for text.
- Your program calls
writer.write("Hi₹")with characters. - A
Writeris a character stream.OutputStreamWriteris the bridge from characters to bytes. - Inside the bridge, a charset encoder (here UTF-8) turns each character into one or more bytes.
- The bytes go to a byte stream,
FileOutputStream, which knows nothing about text. - The JVM asks the operating system to write the bytes through a file handle. This is slow compared with memory, which is why buffering matters (Lesson 13.3).
- The disk holds 5 bytes. Reading goes back up the same layers:
FileInputStream→InputStreamReader(decoder) → yourString.
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 statement line shows ₹500: four characters.
- Writing with UTF-8 gives six bytes: three for ₹ (E2 82 B9) and one each for 5, 0, 0.
- The file stores only those bytes; nothing in a plain CSV says which charset was used.
- The spreadsheet decodes each byte as one Windows-1252 character: E2 → â, 82 → ‚, B9 → ¹. The customer sees “₹500” and calls the bank.
- Decoded with UTF-8, the same bytes give ₹500 again. The rule: always name the charset when you turn bytes into text or text into bytes.
1. Reports and exports
CSV statements, GST reports and invoices are written with character streams in UTF-8 so ₹ and regional names survive.
2. Uploads and downloads
Profile photos, KYC PDFs and app updates are copied with byte streams; a charset would corrupt them.
3. Logs
Servers append log lines to files all day. Opening in append mode, and rotating files, keeps history safe.
4. Network calls
An HTTP response body is an InputStream of bytes; reading it as JSON text needs the right charset, usually UTF-8.
Simplified. Modern code often uses the java.nio.file.Files helpers (Lesson 13.2) on top of these same streams.
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));
}
}
}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 forO.r.read(buf)Reads up to 100 characters into the array and returns how many it read.
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.
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.
invoice text: INV-7 Rs 236
copied 12 bytes; sizes 12 and 12
Approach
- Write the bytes of the text to invoice.txt.
- Open a
FileInputStreamon it and aFileOutputStreamon the backup, both in try-with-resources. - Loop:
read()one byte,write()it, untilread()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.
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.
menu.txt: Dosa / Idli / पनीर टिक्का
3 lines, 15 chars, 23 bytes on disk
Approach
- Write the file with a
FileWriter, naming UTF-8. - Read it back with a
FileReader(UTF-8) one character at a time. - 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.
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.
Paid ₹500
bytes: 11 as windows-1252: Paid ₹500 as UTF-8: Paid ₹500
Approach
- Write the text to statement.csv with UTF-8.
- Read all bytes, then decode them twice: once with windows-1252, once with UTF-8.
- 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.
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.
Bytes of a word
Print the UTF-8 byte values (0–255) of Java using a ByteArrayInputStream and read().
Java
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();
}
}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.
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.
Hello file
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);
}
}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.
Characters vs bytes
For the word नमस्ते, print its length in characters and in UTF-8 bytes.
नमस्ते
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");
}
}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.
Count lines in a file
Write three lines to log.txt and count the line breaks by reading characters.
three log lines
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);
}
}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.
Append instead of overwrite
Write A to audit.txt, then open it again in append mode and add B. Print the file's contents.
A then B
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));
}
}
}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.
Handle a missing file
Try to open orders.csv, which doesn't exist. Catch the exception and print a friendly message with its type.
(no file)
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());
}
}
}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.
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.
3 receipt lines
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);
}
}
}How it works: devices and protocols limit bytes, not characters. Measuring with the same charset the printer uses avoids cut-off lines.
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.
prices 1999, 4950, 120 (paise)
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);
}
}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.
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.
10,000-byte file
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");
}
}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.
₹ 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_8to writers, readers,getBytesandnew 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.
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)orFiles.copy.
Avoid: “try a different charset”. No charset is right for non-text data.
“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.
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.