Learn Java Io Modern Io Resource Boundaries Part 002 Io Mental Model
title: Learn Java IO, Modern IO, Streams, Buffers, Resources, Serialization & Data Boundaries - Part 002 description: A deep mental model of IO as bytes, characters, records, streams, and boundary contracts in Java systems. series: learn-java-io-modern-io-resource-boundaries seriesTitle: Learn Java IO, Modern IO, Streams, Buffers, Resources, Serialization & Data Boundaries order: 2 partTitle: IO Mental Model: Bytes, Characters, Records, Boundaries tags:
- java
- io
- streams
- encoding
- framing
- boundary
- mental-model
- series date: 2026-06-30
Part 002 — IO Mental Model: Bytes, Characters, Records, Boundaries
1. Tujuan Part Ini
Part ini membangun fondasi mental untuk seluruh Java IO.
Kita akan menjawab:
- apa sebenarnya IO dari sudut pandang sistem;
- mengapa byte, character, record, message, dan object tidak boleh dicampur;
- mengapa stream bukan array;
- mengapa
read()yang mengembalikan sebagian data bukan bug; - bagaimana encoding dan framing membentuk boundary contract;
- bagaimana merancang IO boundary yang tidak ambigu.
Kalau Part 001 adalah peta skill, Part 002 adalah model fisika-nya.
Tanpa mental model ini, API Java IO akan terlihat seperti koleksi class. Dengan mental model ini, API Java IO menjadi alat untuk mengontrol data yang bergerak melintasi boundary.
2. Definisi Praktis IO
Dalam sistem software, IO adalah perpindahan data antara program dan sesuatu di luar CPU/register/memory lokal program.
Contoh source/sink IO:
- file lokal;
- directory;
- socket;
- pipe process;
- console;
- classpath resource;
- memory-mapped file;
- archive entry;
- cloud object stream;
- HTTP request/response body;
- serial device;
- external command stdout/stderr.
Secara mental:
Boundary adalah tempat ketidakpastian masuk.
Di dalam program, operasi object biasa relatif deterministik. Di boundary IO:
- data bisa tidak lengkap;
- data bisa datang lambat;
- source bisa hilang;
- permission bisa ditolak;
- encoding bisa salah;
- disk bisa penuh;
- network bisa putus;
- proses lain bisa mengubah file;
- resource bisa leak;
- operasi bisa meninggalkan state parsial.
Maka prinsip utama:
IO code yang baik adalah code yang memperlakukan boundary sebagai tempat failure normal, bukan kejadian luar biasa yang mustahil.
3. Tiga Level Data: Byte, Character, Record
Sebagian besar bug IO berasal dari mencampur tiga level ini.
3.1 Bytes
Byte adalah unit paling dasar untuk IO.
File, socket, compressed data, encrypted payload, image, video, dan serialized object pada akhirnya adalah byte sequence.
Java API utama:
InputStream;OutputStream;byte[];ByteBuffer;ReadableByteChannel;WritableByteChannel;FileChannel.
Byte tidak tahu apakah dirinya text, gambar, CSV, JSON, ZIP, protobuf, atau serialized object. Makna diberikan oleh layer di atasnya.
3.2 Characters
Character adalah hasil interpretasi byte menggunakan charset.
Contoh:
Bytes: 48 65 6C 6C 6F
Charset: UTF-8
Text: Hello
Java API utama:
Reader;Writer;InputStreamReader;OutputStreamWriter;BufferedReader;Charset;CharsetDecoder;CharsetEncoder.
Character boundary selalu membutuhkan charset. Tanpa charset, “text” tidak lengkap sebagai contract.
3.3 Records
Record adalah unit logis data.
Contoh record:
- satu line di file log;
- satu row CSV;
- satu frame length-prefixed;
- satu archive entry;
- satu object serialized;
- satu message di protocol custom;
- satu event di append-only file.
Record tidak otomatis ada di stream. Record perlu framing.
Contoh framing:
| Framing | Contoh | Kelebihan | Risiko |
|---|---|---|---|
| Delimiter | newline, comma, null byte | sederhana | delimiter bisa muncul di data |
| Length-prefix | 4-byte length + payload | robust untuk binary | length bisa corrupt/malicious |
| Fixed-width | setiap record 128 bytes | random access mudah | rigid, padding, versioning susah |
| Self-describing | JSON object, CBOR, protobuf | evolvable | parser lebih kompleks |
| External index | data file + index file | cepat untuk lookup | konsistensi dua file |
4. Stream Bukan Array
Ini mental model paling penting.
Array:
Known size, random access, in memory, reusable
Stream:
Unknown size, sequential, possibly blocking, maybe one-shot, can fail mid-flow
Perbandingan:
| Aspek | Array | Stream |
|---|---|---|
| Size | diketahui | bisa tidak diketahui |
| Access | random | sequential |
| Location | memory | external/source dependent |
| Re-read | mudah | sering tidak bisa |
| Failure | jarang di tengah access | umum di tengah transfer |
| Latency | lokal | bergantung source/sink |
| Partial result | tidak umum | normal |
| Close needed | tidak | sering ya |
Kesalahan umum:
byte[] data = input.readAllBytes();
Ini mengubah stream menjadi array. Kadang valid, kadang berbahaya.
Valid jika:
- size kecil;
- size dibatasi;
- source trusted;
- memory cukup;
- latency tidak penting;
- data memang perlu utuh di memory.
Berbahaya jika:
- input tidak terbatas;
- file bisa besar;
- request body dari user;
- archive entry tidak dibatasi;
- stream berasal dari network;
- banyak request paralel;
- process output tidak terkontrol.
Rule praktis:
Jangan materialize stream menjadi array/string kecuali kamu tahu batas ukurannya dan memang butuh seluruh data sekaligus.
5. InputStream Contract secara Mental
InputStream merepresentasikan source byte sequential.
Model konseptual:
5.1 read() Single Byte
Konsep:
int b = input.read();
Return value:
0..255berarti satu byte berhasil dibaca;-1berarti EOF;IOExceptionberarti failure.
Kenapa return type int, bukan byte? Karena perlu representasi -1 untuk EOF tanpa bentrok dengan nilai byte valid.
5.2 read(byte[])
Konsep:
byte[] buffer = new byte[8192];
int n = input.read(buffer);
Return value:
n > 0: jumlah byte aktual yang dibaca;n == -1: EOF;ntidak wajib sama denganbuffer.length;- method bisa block sampai data tersedia, EOF, atau error.
Bug klasik:
byte[] buffer = new byte[8192];
input.read(buffer);
output.write(buffer); // wrong
Masalah:
- return value diabaikan;
- buffer mungkin hanya terisi sebagian;
- sisa buffer bisa berisi data lama atau zero;
- output bisa corrupt.
Benar:
byte[] buffer = new byte[8192];
int n;
while ((n = input.read(buffer)) != -1) {
output.write(buffer, 0, n);
}
Invariant:
Jumlah byte valid dalam buffer adalah return value dari
read, bukan ukuran buffer.
6. OutputStream Contract secara Mental
OutputStream merepresentasikan sink byte sequential.
Model konseptual:
6.1 write(int)
Menulis satu byte lower 8 bits dari integer.
6.2 write(byte[], off, len)
Menulis len byte dari array mulai offset off.
Correctness penting:
output.write(buffer, 0, n);
Jangan:
output.write(buffer);
kecuali seluruh buffer memang berisi data valid.
6.3 flush()
flush() mendorong buffered data ke layer bawah. Tetapi artinya bergantung implementasi.
Contoh:
BufferedOutputStream.flush()mendorong buffer ke underlying stream;OutputStreamWriter.flush()mengeluarkan encoded bytes yang tertahan;- socket flush tidak berarti peer sudah memproses data;
- file flush tidak selalu berarti data durable di storage fisik.
Rule:
Flush adalah visibility/progress signal ke layer bawah, bukan universal durability guarantee.
6.4 close()
close() biasanya:
- flushes pending data;
- releases resource;
- membuat operasi berikutnya invalid;
- bisa menutup underlying stream jika wrapper.
Karena close() bisa throw, try-with-resources penting.
7. Reader/Writer: Character Boundary
Reader dan Writer bukan versi “lebih modern” dari stream. Mereka berada di level data berbeda.
7.1 InputStreamReader sebagai Bridge
InputStreamReader menjembatani byte stream ke character stream menggunakan charset.
Contoh:
try (Reader reader = new InputStreamReader(input, StandardCharsets.UTF_8)) {
// read characters
}
Poin penting:
- bytes dibaca dari underlying
InputStream; - bytes didecode menjadi characters;
- decoder bisa read-ahead;
- jumlah byte yang dibaca tidak sama dengan jumlah char yang dikembalikan;
- invalid byte sequence harus punya policy.
7.2 OutputStreamWriter sebagai Bridge
Contoh:
try (Writer writer = new OutputStreamWriter(output, StandardCharsets.UTF_8)) {
writer.write("hello");
}
Poin penting:
- characters di-encode menjadi bytes;
- encoder bisa buffer state;
flush()/close()penting untuk mengeluarkan pending bytes;- tidak semua character representable di semua charset.
8. Charset adalah Bagian dari Contract
Text boundary tanpa charset adalah contract yang belum lengkap.
Buruk:
String s = Files.readString(path); // charset implicit
Lebih jelas:
String s = Files.readString(path, StandardCharsets.UTF_8);
Untuk boundary internal yang sangat controlled, default mungkin cukup. Untuk data eksternal, config, import/export, file exchange, audit file, atau long-lived data, charset harus eksplisit.
8.1 Charset Bugs
| Bug | Penyebab | Gejala |
|---|---|---|
| Mojibake | bytes didecode dengan charset salah | karakter aneh |
| Silent replacement | invalid bytes diganti � | data loss diam-diam |
| Truncated char | multibyte char terpotong | malformed input |
| BOM surprise | BOM dianggap karakter biasa | header field aneh |
| Newline mismatch | CRLF vs LF | parser line salah |
| Platform default drift | default berbeda antar runtime lama/platform | hasil beda antar environment |
8.2 Strict Decoding
Untuk data regulasi, finansial, audit, dan enforcement lifecycle, silent replacement sering tidak bisa diterima. Lebih baik fail fast.
Contoh strict decoder:
CharsetDecoder decoder = StandardCharsets.UTF_8
.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT);
try (Reader reader = new InputStreamReader(input, decoder)) {
// read text strictly
}
Ini akan melempar error jika input tidak valid sesuai charset.
9. Record Boundary: Framing
Stream tidak membawa record boundary secara otomatis.
Contoh stream byte:
48656c6c6f576f726c64
Apakah itu:
- satu string
HelloWorld? - dua string
HellodanWorld? - lima field masing-masing 2 byte?
- compressed payload?
- encrypted payload?
Tidak bisa diketahui tanpa framing contract.
9.1 Delimiter Framing
Contoh newline-delimited records:
record-1\n
record-2\n
record-3\n
Kelebihan:
- mudah dibaca manusia;
- mudah diproses line-by-line;
- cocok untuk log sederhana.
Risiko:
- delimiter bisa muncul di data;
- escaping perlu jelas;
- newline beda platform;
- record terakhir mungkin tanpa newline;
- line bisa sangat panjang.
9.2 Length-Prefix Framing
Contoh:
[4-byte length][payload bytes]
Kelebihan:
- cocok untuk binary;
- payload boleh mengandung delimiter apapun;
- parser tahu jumlah byte yang harus dibaca.
Risiko:
- length corrupt;
- length malicious sangat besar;
- endian harus jelas;
- unexpected EOF harus dideteksi;
- versioning butuh strategi.
Pseudo parser mental:
int length = readIntExactly(input);
if (length < 0 || length > maxFrameSize) {
throw new IOException("Invalid frame length: " + length);
}
byte[] payload = readExactly(input, length);
Invariant:
Length dari input eksternal tidak boleh dipercaya tanpa batas maksimum.
9.3 Fixed-Width Framing
Contoh:
000001JOHN 20260630
000002ALICE 20260701
Kelebihan:
- offset predictable;
- parsing cepat;
- cocok untuk legacy mainframe/interchange format tertentu.
Risiko:
- trimming/padding rules harus jelas;
- charset penting;
- field evolution sulit;
- multibyte charset bisa merusak asumsi width jika width dihitung bytes vs characters.
9.4 Self-Describing Framing
Contoh:
- JSON object per line;
- CBOR;
- protobuf message dengan schema;
- Avro object container file;
- ZIP entry metadata.
Kelebihan:
- lebih evolvable;
- metadata ikut data;
- parser bisa validasi struktur.
Risiko:
- parser kompleks;
- dependency format;
- size bomb;
- schema compatibility.
10. EOF: Normal vs Unexpected
EOF adalah signal bahwa source selesai. Tetapi maknanya bergantung framing.
10.1 EOF Normal
Contoh copy stream:
while ((n = input.read(buffer)) != -1) {
output.write(buffer, 0, n);
}
EOF normal berarti semua bytes yang tersedia sudah dibaca.
10.2 Unexpected EOF
Contoh length-prefixed frame:
length = 1024
payload bytes received = 700
EOF
Ini bukan EOF normal. Ini data truncated.
Maka helper readExactly harus melempar exception jika EOF datang sebelum jumlah byte yang dibutuhkan.
static void readExactly(InputStream in, byte[] target, int off, int len) throws IOException {
int remaining = len;
int position = off;
while (remaining > 0) {
int n = in.read(target, position, remaining);
if (n == -1) {
throw new EOFException("Expected " + remaining + " more bytes");
}
position += n;
remaining -= n;
}
}
Rule:
EOF hanya normal jika protocol mengatakan “data boleh selesai di sini”.
11. Partial Read and Partial Write
11.1 Partial Read
Partial read adalah normal.
Alasan:
- kernel hanya punya sebagian data saat ini;
- network packet boundary berbeda dari application boundary;
- stream implementation memilih return lebih cepat;
- buffer internal habis;
- decoder hanya bisa menghasilkan sebagian char;
- non-blocking channel memang bisa return 0.
Maka code harus loop berdasarkan contract.
11.2 Partial Write
Di classic OutputStream, write(byte[], off, len) umumnya mencoba menulis semua atau throw. Tetapi di NIO WritableByteChannel.write(ByteBuffer) bisa menulis sebagian dan return jumlah byte yang ditulis.
Mental model NIO:
while (buffer.hasRemaining()) {
channel.write(buffer);
}
Tetapi untuk non-blocking channel, loop seperti ini bisa spin jika write return 0. Nanti dibahas di selector/non-blocking part.
Rule:
API yang berbeda punya partial semantics berbeda. Jangan bawa asumsi
OutputStreamsecara buta keChannel.
12. Boundary Dimensions
Setiap boundary perlu dideskripsikan dalam beberapa dimensi.
| Dimensi | Pertanyaan | Contoh Jawaban |
|---|---|---|
| Ownership | siapa menutup resource? | callee consumes and closes |
| Unit | byte, char, line, record? | newline-delimited UTF-8 records |
| Encoding | charset apa? | UTF-8 strict |
| Framing | record dibatasi bagaimana? | 4-byte big-endian length prefix |
| Size | batas maksimum? | max 16 MiB per frame |
| Blocking | operasi bisa block? | yes, caller thread blocks |
| Timeout | bagaimana timeout? | caller controls via socket timeout |
| Seekability | bisa random access? | no, stream-only |
| Replayability | bisa dibaca ulang? | no, one-shot input |
| Idempotency | aman retry? | yes after staging manifest |
| Durability | perlu tahan crash? | final publish requires force + atomic move |
| Trust | input trusted? | no, external upload |
| Error model | exception apa? | IOException for transport, InvalidRecordException for parse |
| Cleanup | artifact parsial? | temp files deleted on failure |
Boundary yang tidak menyatakan dimensi ini akan menyebarkan asumsi tersembunyi.
13. API Design: Contoh Contract yang Jelas
13.1 Buruk: Contract Tersembunyi
Report parse(InputStream input) throws IOException;
Yang tidak jelas:
- apakah input ditutup?
- charset apa?
- format apa?
- max size berapa?
- apakah stream harus support mark/reset?
- apakah method membaca sampai EOF?
- apakah parser boleh read-ahead?
- error parsing dilempar sebagai apa?
13.2 Lebih Baik: Contract Terdokumentasi
/**
* Parses one UTF-8 report document from the given input stream.
*
* Contract:
* - The input stream is consumed but not closed.
* - The stream is decoded as UTF-8 using strict malformed-input handling.
* - The document must be at most maxBytes bytes before decoding.
* - The parser reads until EOF.
* - Syntax errors are reported as ReportParseException.
* - Transport/read failures are reported as IOException.
*/
Report parseReport(InputStream input, long maxBytes) throws IOException, ReportParseException;
Bahkan lebih baik jika kamu butuh ownership jelas:
/**
* Opens, parses, and closes the report file.
*/
Report parseReportFile(Path path, long maxBytes) throws IOException, ReportParseException;
Perhatikan bedanya:
InputStreamAPI cocok jika caller mengontrol source;PathAPI cocok jika method harus mengontrol open/close dan filesystem behavior;Supplier<InputStream>cocok jika method butuh retry/replay;byte[]cocok jika data memang sudah bounded in-memory.
14. Bounded vs Unbounded Input
Salah satu prinsip terpenting:
Semua input eksternal harus dianggap unbounded sampai dibatasi secara eksplisit.
Buruk:
byte[] body = requestBody.readAllBytes();
Lebih aman:
static byte[] readAtMost(InputStream input, int maxBytes) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream(Math.min(maxBytes, 8192));
byte[] buffer = new byte[8192];
int total = 0;
while (true) {
int remainingLimit = maxBytes - total;
if (remainingLimit <= 0) {
if (input.read() != -1) {
throw new IOException("Input exceeds limit of " + maxBytes + " bytes");
}
return output.toByteArray();
}
int n = input.read(buffer, 0, Math.min(buffer.length, remainingLimit));
if (n == -1) {
return output.toByteArray();
}
output.write(buffer, 0, n);
total += n;
}
}
Catatan:
- helper ini masih materialize ke memory;
- ia hanya aman karena ada
maxBytes; - untuk data besar, lebih baik streaming ke file/temp processor;
- batas harus berasal dari requirement, bukan angka asal.
15. Replayability dan Seekability
15.1 Replayability
Replayability berarti input bisa dibaca ulang dari awal.
| Source | Replayable? | Catatan |
|---|---|---|
byte[] | ya | selama array tersedia |
local file Path | biasanya ya | jika file tidak berubah |
InputStream dari socket | tidak | one-shot |
| HTTP request body | biasanya tidak | bergantung framework buffering |
| process stdout | tidak | one-shot pipe |
Supplier<InputStream> | bisa | jika supplier membuka stream baru |
| memory-mapped file | ya/random | selama mapping valid |
Jika operasi butuh retry, jangan menerima InputStream mentah tanpa strategi.
Contoh API:
interface ReplayablePayload {
InputStream openStream() throws IOException;
long size() throws IOException;
}
15.2 Seekability
Seekability berarti bisa membaca posisi tertentu.
API terkait:
RandomAccessFile;FileChannel.position(long);FileChannel.read(ByteBuffer dst, long position);SeekableByteChannel;MappedByteBuffer.
Seekability berguna untuk:
- index file;
- resume transfer;
- partial verification;
- parallel chunk reading;
- binary file parser;
- log segment scanning.
Tetapi seekability tidak tersedia di semua source. Socket stream, pipe, compressed stream, dan many HTTP bodies umumnya sequential.
16. Idempotency dan Retry dalam IO
IO sering gagal setelah sebagian efek terjadi. Maka retry harus dirancang.
16.1 Non-Idempotent Write
append record -> failure -> retry append same record -> duplicate
16.2 Idempotent Publish Pattern
write temp -> verify -> atomic move to final name
Jika failure terjadi sebelum publish, final file belum berubah. Jika failure terjadi setelah publish, final file terlihat sebagai satu artifact lengkap.
Diagram:
Part detail akan dibahas di file operation dan durability sections. Untuk sekarang, pahami prinsipnya:
Retry aman jika setiap attempt memiliki staging state dan publish point yang jelas.
17. Blocking Model
IO bisa blocking, non-blocking, asynchronous, atau memory-mapped.
| Model | Mental Model | Java API |
|---|---|---|
| Blocking stream | thread menunggu progress | InputStream, OutputStream, Reader, Writer |
| Blocking channel | thread menunggu channel operation | FileChannel, blocking SocketChannel |
| Non-blocking readiness | selector memberi hint siap | Selector, SelectableChannel |
| Async completion | operasi selesai nanti | AsynchronousFileChannel, CompletionHandler, Future |
| Memory mapped | page fault saat memory access | MappedByteBuffer, MemorySegment mapped file |
Blocking bukan otomatis buruk. Blocking sederhana dan benar untuk banyak workload. Yang buruk adalah blocking tanpa batas pada thread yang salah, tanpa timeout, tanpa cancellation, atau tanpa backpressure.
18. Source/Sink Pressure
IO pipeline memiliki pressure point.
Jika producer lebih cepat dari consumer:
- memory buffer membesar;
- disk temp membesar;
- queue menumpuk;
- latency naik;
- process/thread blocked;
- request timeout;
- system collapse.
Backpressure adalah cara memberi sinyal bahwa consumer tidak mampu menerima lebih cepat.
Di IO sederhana, backpressure bisa berupa blocking write. Di pipeline kompleks, bisa berupa bounded queue, rate limit, flow control, atau cancellation.
Rule:
Jangan membuat unbounded buffering untuk menyembunyikan consumer lambat. Itu hanya memindahkan failure ke memory/disk.
19. Layering: Dari Bytes ke Domain
Boundary yang sehat memisahkan layer.
Tidak semua pipeline butuh semua layer. Tetapi urutan layer penting.
Contoh salah:
parse domain object dulu, baru cek size
Ini terlambat. Size bomb sudah bisa menghabiskan resource.
Contoh lebih baik:
limit bytes -> decode strict -> parse records -> validate domain
20. Error Taxonomy
IO error sebaiknya dipisahkan secara mental.
| Kategori | Contoh | Biasanya Dilempar Sebagai |
|---|---|---|
| Transport/source failure | read gagal, disk error | IOException |
| Sink failure | disk penuh, broken pipe | IOException |
| Lifecycle failure | resource closed | IOException, ClosedChannelException |
| Encoding failure | malformed UTF-8 | CharacterCodingException, wrapped exception |
| Framing failure | invalid length, unexpected EOF | IOException atau custom protocol exception |
| Syntax failure | CSV/JSON/domain syntax salah | custom parse exception |
| Validation failure | field invalid | validation/domain exception |
| Policy failure | size limit exceeded | custom limit exception / IOException |
| Security/trust failure | path traversal, disallowed class | security/policy exception |
Menggabungkan semuanya menjadi RuntimeException("failed") membuat operasi sulit di-debug dan sulit di-retry.
Rule:
Error model adalah bagian dari boundary contract.
21. Small Code Pattern: Counting and Limiting Stream
Kadang kamu perlu menghitung bytes yang dibaca tanpa mengubah parser.
Contoh wrapper sederhana:
final class CountingInputStream extends FilterInputStream {
private long count;
CountingInputStream(InputStream in) {
super(in);
}
@Override
public int read() throws IOException {
int b = super.read();
if (b != -1) {
count++;
}
return b;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int n = super.read(b, off, len);
if (n > 0) {
count += n;
}
return n;
}
long count() {
return count;
}
}
Limiting stream:
final class LimitedInputStream extends FilterInputStream {
private long remaining;
LimitedInputStream(InputStream in, long limit) {
super(in);
if (limit < 0) {
throw new IllegalArgumentException("limit must be >= 0");
}
this.remaining = limit;
}
@Override
public int read() throws IOException {
if (remaining == 0) {
return -1;
}
int b = super.read();
if (b != -1) {
remaining--;
}
return b;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (remaining == 0) {
return -1;
}
int allowed = (int) Math.min(len, remaining);
int n = super.read(b, off, allowed);
if (n > 0) {
remaining -= n;
}
return n;
}
}
Catatan penting:
- wrapper seperti ini berguna untuk latihan;
- production library sering sudah punya helper sejenis;
- semantics “return EOF after limit” berbeda dari “throw if input exceeds limit”;
- pilih behavior berdasarkan boundary contract.
22. Common Anti-Patterns
22.1 Ignore Return Value
input.read(buffer);
process(buffer);
Bug: jumlah byte valid tidak diketahui.
22.2 Materialize Unknown Input
String payload = new String(input.readAllBytes(), UTF_8);
Bug: input bisa unbounded.
22.3 Implicit Charset
new InputStreamReader(input);
Bug: contract encoding tidak eksplisit.
22.4 Treat EOF as Always Success
read header
read body until EOF
Bug: EOF di tengah body mungkin data truncated.
22.5 Close Someone Else's Stream
void parse(InputStream in) {
try (in) {
// ...
}
}
Bug: ownership tidak jelas.
22.6 Assume One Read Equals One Message
int n = socketInput.read(buffer);
handleMessage(buffer, n);
Bug: stream transport tidak menjamin message boundary.
22.7 Use available() as Total Size
byte[] data = new byte[input.available()];
input.read(data);
Bug: available() bukan total length.
22.8 Unbounded Line Reading
String line = reader.readLine();
Bug: line bisa sangat panjang; readLine tidak otomatis enforce max line length.
22.9 Parse Before Limit
var object = parser.parse(input);
checkSizeAfterwards(object);
Bug: resource sudah habis sebelum policy dicek.
22.10 Mix Bytes and Characters Incorrectly
int byteLength = text.length();
Bug: String.length() menghitung UTF-16 code units, bukan UTF-8 byte length.
23. Practice: Build a Boundary Description
Ambil sebuah file import hipotetis:
regulatory-case-events-2026-06-30.ndjson.gz
Tulis boundary contract.
Contoh jawaban:
## Boundary Contract
Source:
- Local filesystem path in staging directory.
Trust:
- External partner upload; untrusted until validated.
Encoding:
- UTF-8 strict after gzip decompression.
Compression:
- GZIP single stream.
Framing:
- Newline-delimited JSON, one event per line.
Size limits:
- Compressed file max 512 MiB.
- Decompressed bytes max 4 GiB.
- Single line max 1 MiB.
Ownership:
- Ingestion service opens and closes all streams.
Replayability:
- File path is replayable while staging file remains immutable.
Durability:
- Output manifest is written temp + atomic move.
Partial failure:
- Failed ingestion leaves input untouched and output temp cleaned.
Error model:
- IOException: filesystem/compression failure.
- EncodingException: malformed UTF-8.
- RecordParseException: invalid JSON line.
- PolicyViolationException: size or line limit exceeded.
Latihan ini lebih penting dari menulis code. Code yang benar muncul dari contract yang benar.
24. Practice: Identify the Unit
Untuk setiap case, tentukan unit data yang benar.
| Case | Unit Salah | Unit Benar |
|---|---|---|
| Upload PDF | String | byte stream/file |
| CSV import | byte only | text + row records |
| Binary protocol | line | framed bytes |
| Log tailing | full file | line/event stream |
| ZIP extraction | file bytes only | archive entries + paths + bytes |
| Java object cache | JSON string | object serialization or explicit schema |
| Process command output | one string | stdout/stderr byte or line streams |
| Fixed-width bank file | Java char count | byte/char width per spec |
Pertanyaan review:
- Apakah unit data sesuai dengan format?
- Apakah boundary kehilangan informasi penting?
- Apakah parser bisa mendeteksi truncation?
- Apakah ada maximum unit size?
25. Practice: Failure Mode Walkthrough
Ambil pipeline sederhana:
read input file -> parse records -> write output file
Sekarang pecah menjadi failure points:
Untuk setiap edge, jawab:
- exception apa yang terlihat?
- artifact apa yang tertinggal?
- apakah retry aman?
- apakah caller bisa membedakan failure?
- apakah ada data loss?
- apakah ada corruption yang silent?
26. Design Heuristics
26.1 Terima Path Jika Method Membutuhkan File Semantics
Gunakan Path jika method perlu:
- membuka stream sendiri;
- membaca metadata;
- mengontrol symlink;
- membuat temp file;
- atomic move;
- file locking;
- durability policy.
26.2 Terima InputStream Jika Method Hanya Membaca Bytes
Gunakan InputStream jika method tidak peduli source asalnya.
Tetapi documentasikan:
- apakah stream ditutup;
- apakah dibaca sampai EOF;
- apakah blocking;
- apakah size dibatasi;
- format/encoding/framing.
26.3 Terima Reader Jika Boundary Sudah Text
Gunakan Reader jika decoding sudah tanggung jawab caller atau layer sebelumnya.
Tetapi hati-hati: dengan menerima Reader, method tidak tahu byte length asli. Jika kamu perlu enforce byte limit, lakukan sebelum decoding.
26.4 Terima ByteBuffer Jika Ingin NIO-Level Control
Gunakan ByteBuffer jika:
- parsing binary protocol;
- menggunakan channel;
- perlu zero-copy-ish workflow;
- perlu direct buffer;
- perlu stateful incremental parser.
26.5 Terima Supplier<InputStream> Jika Butuh Replay
Contoh:
Report importWithRetry(Supplier<InputStream> streamSupplier) throws IOException
Ini memungkinkan method membuka stream baru untuk retry. Tetapi supplier harus punya contract: setiap pemanggilan menghasilkan stream baru dari payload yang sama.
27. “Small Enough” Bukan Alasan Tanpa Batas
Banyak bug production dimulai dari kalimat:
“File-nya kecil kok.”
Pertanyaannya:
- kecil menurut siapa?
- ada enforce limit?
- apa yang terjadi jika partner salah kirim?
- apa yang terjadi jika file corrupt dan parser tidak menemukan delimiter?
- berapa concurrency maksimum?
- apakah memory cukup saat 100 request paralel?
Jika benar kecil, tulis batasnya.
Contoh:
private static final int MAX_CONFIG_BYTES = 256 * 1024;
Boundary yang “kecil” tapi tidak punya limit sebenarnya adalah boundary tidak terbatas.
28. IO Mental Model Summary Diagram
29. Exit Criteria Part 002
Kamu dianggap selesai dengan Part 002 jika bisa menjawab:
- Mengapa stream bukan array?
- Apa bedanya byte, character, record, dan domain object?
- Mengapa charset adalah bagian dari boundary contract?
- Apa bedanya EOF normal dan unexpected EOF?
- Mengapa
read(byte[])harus dicek return value-nya? - Mengapa one read tidak sama dengan one message?
- Kapan
readAllBytesvalid dan kapan berbahaya? - Apa itu framing?
- Apa saja dimensi boundary contract?
- Mengapa retry IO butuh idempotency atau staging state?
30. Rangkuman
IO adalah tentang data yang bergerak melewati boundary yang tidak sepenuhnya kita kontrol.
Mental model utama:
- semua IO berawal dari bytes;
- text adalah bytes + charset + error policy;
- record adalah stream + framing contract;
- object/domain model adalah hasil parsing dan validasi;
- stream bisa partial, blocking, one-shot, dan gagal di tengah;
- EOF hanya normal jika protocol mengizinkan selesai di titik itu;
- resource ownership harus eksplisit;
- input eksternal harus dibatasi;
- retry butuh idempotency;
- boundary contract lebih penting daripada API convenience.
Part berikutnya akan masuk ke API klasik: InputStream, OutputStream, Reader, Writer, dan contract dasar java.io.
31. Referensi
- Oracle Java SE 25 API —
java.iopackage summary: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/io/package-summary.html - Oracle Java SE 25 API —
InputStreamReader: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/io/InputStreamReader.html - Oracle Java SE 25 API —
java.niopackage summary: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/nio/package-summary.html - Oracle Java SE 25 API —
java.nio.channelspackage summary: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/nio/channels/package-summary.html - Oracle Java SE 25 API —
java.nio.filepackage summary: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/nio/file/package-summary.html
You just completed lesson 02 in start here. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.