The labs on this site need JavaScript to run. The same content is written out below.
Learn › Java › I/O
I/O streams and buffers
I called write, so why is the file empty?
When you write to a file, calling out.write does not mean that the data goes into the file right away. A buffered stream collects data in memory and writes it out all at once. This lab writes the 9 bytes of the three Korean characters "가나다" one byte at a time, counts how many writes actually reach the underlying stream, and then reads the file back both byte by byte and character by character.
Checked against real execution · The 48 combinations were run on JDK 21 and checked against the results
Concepts in this lab
- Byte streams and character streams
- A stream is a channel through which data flows in sequence from one side to the other, and Java has two kinds: one that handles data in units of bytes and one that handles it in units of characters. The InputStream and OutputStream family are the byte streams, which are used to move content as it is, such as images and compressed files. The Reader and Writer family are the character streams, which convert bytes to characters according to a given encoding. Text in which one character takes several bytes, such as Korean, is garbled if you read it with a byte stream and convert each byte to a character, so use a character stream with a specified encoding to read and write text.
- Buffer and flush
- A buffer is an area of memory that holds data for a while without sending it out right away. BufferedOutputStream and BufferedWriter collect the data they receive through write in a buffer and, when the buffer becomes full, write it to the underlying stream all at once, which reduces the number of writes to a file or the network. flush is a request to write out what has been collected so far even though the buffer is not full, and close performs a flush before it closes the stream. So if neither flush nor close is called, the data that was left in the buffer is not written to the destination.
- Character encoding and UTF-8
- A character encoding is a rule that defines which bytes each character is converted to when it is stored. UTF-8 is the most widely used encoding, and the length of a character varies from 1 byte to 4 bytes depending on the character. English letters and digits take 1 byte, and one Korean character takes 3 bytes. Text is garbled when the encoding used for writing differs from the one used for reading, or when the bytes are cut off in the middle of a character, so it is safer to state the encoding explicitly in code that reads and writes text.
- finally and try-with-resources
- A finally block runs whether the try block ends normally or throws an exception (except when the JVM exits during the try, for example through System.exit()), and it is used to clean up resources. The try-with-resources statement lets you declare a resource that implements AutoCloseable inside the parentheses of try, and close() is then called automatically when the block ends. Compared with closing the resource yourself in finally, it leads to fewer forgotten closes and fewer exception-handling mistakes.
Try these
- Default settings — Wrapped in a buffer and not closed. write was called 9 times, but the file has 0 bytes.
- Closing with close — The 9 bytes collected in the buffer are written out in one write and end up in the file.
- Writing with no buffer — The 9 calls to write reach the underlying stream as 9 writes.
- With a 4-byte buffer — The buffer is written out each time it fills up, and if the stream is not closed, the last 1 byte is left behind.
- An exception before close — close never runs, so the file is empty.
- With try-with-resources — Even when an exception is thrown, the stream is closed automatically and the data is written to the file.
- Reading one byte as one character — The three Korean characters become nine garbled characters.
Running with the default settings
- new FileOutputStream("out.txt"): This opens the file for writing. A CountingOutputStream, which counts write calls, is wrapped around it. On top of that sits a BufferedOutputStream whose buffer is 8192 bytes, so out.write goes through the buffer before it reaches the underlying stream.
- The 3 bytes of "가" (EA B0 80), one at a time with out.write(b): All three writes only store the bytes in the buffer, and nothing reaches the underlying stream. The buffer has collected 3 bytes, and the file has 0 bytes.
- The 3 bytes of "나" (EB 82 98), one at a time with out.write(b): All three writes only store the bytes in the buffer, and nothing reaches the underlying stream. The buffer has collected 6 bytes, and the file has 0 bytes.
- The 3 bytes of "다" (EB 8B A4), one at a time with out.write(b): All three writes only store the bytes in the buffer, and nothing reaches the underlying stream. The buffer has collected 9 bytes, and the file has 0 bytes.
- (no closing code): Execution moves on without a call to flush or close. What is left in the buffer (9 bytes) is only in memory and is not in the file. If the program ends like this, that data is lost. The stream is left open and keeps holding the file.
- counting.calls and Files.size: The number of writes that reached the underlying stream is 0, and the file size is 0 bytes. 9 bytes were written, but the file is missing 9 of them.
- Reading the file back: The file is empty, so in.read() returns -1 (the end) from the very first call. No characters are read.
Result of this run
- BufferedOutputStream collects data in a buffer in memory and writes it out all at once. The data is written out only when the buffer becomes full or when flush or close is called. So if the stream is not closed, the data left in the buffer at the end is not written to the file.
Code
class CountingOutputStream extends OutputStream { // a stream that counts the write calls that reach it
private final OutputStream out;
int calls = 0;
boolean closed = false;
CountingOutputStream(OutputStream out) { this.out = out; }
@Override public void write(int b) throws IOException { calls++; out.write(b); }
@Override public void write(byte[] b, int off, int len) throws IOException { calls++; out.write(b, off, len); }
@Override public void close() throws IOException { closed = true; out.close(); }
}
byte[] data = "가나다".getBytes(StandardCharsets.UTF_8); // 9 bytes
CountingOutputStream counting = new CountingOutputStream(new FileOutputStream("out.txt"));
OutputStream out = new BufferedOutputStream(counting); // wrap it in a buffer
for (byte b : data) out.write(b); // one byte at a time, 9 times
System.out.println(counting.calls); // number of writes that reached the underlying stream
System.out.println(Files.size(Path.of("out.txt"))); // file size
StringBuilder sb = new StringBuilder(); // read the file back
try (Reader in = new InputStreamReader(new FileInputStream("out.txt"), StandardCharsets.UTF_8)) {
int ch;
while ((ch = in.read()) != -1) sb.append((char) ch); // one character at a time
}
System.out.println(sb);
Assumptions of this model
The results of the 48 combinations (the number of writes that reached the underlying stream and the number of bytes in each one, the file size, whether the stream was closed, and the characters read back) were checked against the results of running the code on JDK 21 on this PC. The following were also confirmed by separate runs on the same PC: a BufferedOutputStream created without a size writes its data out after it has collected 8192 bytes, the data left in the buffer is not written to the file even when the program ends normally without closing the stream, and the English letters abc stay the same even when each byte is converted to a character. 8192 is the value seen on this JDK and is not a figure that the Java documentation fixes. "File" in the diagram means the point up to which the program has handed the write over to the operating system. The operating system has buffers of its own, so the data may actually be recorded on the disk later than that, and this is not covered. In the result of reading byte by byte, a character that cannot be shown on the screen is displayed as □. The difference in speed with and without a buffer was not measured. Character streams on the writing side (Writer) and write(byte[]), which writes a large chunk at once, are not covered.
Previous lab: Class loading and initialization · Next lab: Serialization