15.1 I/O Streams and the Persistence Boundary
After learning about exceptions, collections, generics, and resource cleanup, allocate about two hours for this article and run two temporary directory experiments using Java 17. Upon completion, you'll be able to choose the right API for binary versus text data, process large inputs in a streaming fashion, and clearly understand who is responsible for closing resources.
Process Memory Is Not Persistent
An object that exists only in process memory disappears when the process exits. If the next run must restore it, the program has to write the data to an external destination in a format it can read again.
I/O means reading from a source or writing to a destination. These may be files, network connections, process pipes, or memory buffers; an in-memory stream does not itself provide persistence. This page uses files as an example because files are easy to verify repeatedly.
Reading Route
- First determine whether the data is bytes or text;
- Understand that a single
readmight only return a portion of the data; - Use buffering and streaming to manage memory and control call costs;
- Use try-with-resources to clearly define resource ownership;
- Distinguish
flush, close, and durable persistence; - Understand the compatibility and security boundaries of native Java serialization;
- Complete a log analyzer that stops when it encounters
FATAL.
1. A stream is a directional data channel
Source → InputStream / Reader → Program
Program → OutputStream / Writer → DestinationBytes are transmitted by InputStream and OutputStream. Reader and Writer transfer UTF-16 code units. Bridges such as InputStreamReader and OutputStreamWriter decode or encode bytes using a charset. An in-memory character stream such as StringReader needs no byte conversion.
| Data | Inbound/Outbound | Common Types |
|---|---|---|
| Images, ZIP files, audio, protocol frames | byte stream | InputStream, OutputStream |
| UTF-8 logs, CSV, configuration text | character stream | Reader, Writer |
| Known small files | One-time convenient method | Files.readString, readAllBytes |
| Large files or sizes determined externally | Streaming loop | BufferedReader, buffered byte array |
If you're unsure about a file's type, treat it as raw bytes. Arbitrarily decoding and re-encoding will alter binary content that can't be represented by that encoding.
2. Byte Streams: Copying a binary image
try (InputStream input = Files.newInputStream(source);
OutputStream output = Files.newOutputStream(target)) {
byte[] buffer = new byte[8192];
int read;
while ((read = input.read(buffer)) != -1) {
output.write(buffer, 0, read);
}
}Source and target must be distinct files, including through hard links or symbolic links; otherwise opening the output can truncate the input.
read(buffer) returns the actual number of bytes written to the array. The last read often falls short of the buffer length, so writing requires using output.write(buffer, 0, read). Writing the entire array will carry over the residual bytes from the previous read to the file end.
-1 indicates the end of input. A single read is not obligated to fill the array; network streams often return shorter fragments. A buffer of length 0 is a useless edge case and should be avoided.
For ordinary OutputStream, write(buffer, offset, length) either writes the specified range or throws an exception; an exception may occur after a partial write. FileChannel, socket channels, and other NIO channels might process only part of the buffer; when using them, check ByteBuffer.hasRemaining().
Convenient Copy
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);The standard library works well for routine file copying, as well as for streaming to or from files. Still, writing your own loops is worth mastering, because checksums, rate limiting, progress tracking, and protocol parsing all require logic inserted between data blocks.
3. Character Streams: UTF-8 text report
Text files are still made up of bytes. Character encoding specifies how bytes correspond to characters.
try (BufferedReader reader = Files.newBufferedReader(
Path.of("dispatch.txt"), StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}readLine() returns a line of content with trailing \n, \r, or \r\n removed. If the original line endings are to be preserved, using only readLine() is insufficient; character or byte-level preservation of the delimiter is required.
When writing text, encode it clearly:
try (BufferedWriter writer = Files.newBufferedWriter(
Path.of("dispatch-copy.txt"), StandardCharsets.UTF_8)) {
writer.write("a developer lends out BK-15");
writer.newLine();
}newLine() uses the current platform line separator. When the protocol requires a fixed \n, the protocol-specified character should be written directly.
Which APIs default to UTF-8
These methods default to UTF-8. readString/writeString arrived in Java 11; the buffered factories without charset parameters arrived in Java 8:
Files.readString,Files.writeString;Files.newBufferedReader,Files.newBufferedWriter.
In Java 17, FileReader/FileWriter constructors without a charset use the default charset. Java 11+ provides a constructor with Charset, but new code finds the Files factory easier for expressing paths, encodings, and open options.
new FileReader("dispatch.txt", StandardCharsets.UTF_8); // Java 11+The value of UTF-8 isn't just in compatibility with old APIs, it puts file format contracts directly in the code.
What happens when the input contains invalid bytes?
new String(bytes, UTF_8) replaces erroneous input, while Files.newBufferedReader(path, UTF_8) strictly decodes and reports errors. This example configures a CharsetDecoder explicitly. Save it as StrictDecodeDemo.java to run it:
import java.nio.ByteBuffer;
import java.nio.charset.*;
public class StrictDecodeDemo {
public static void main(String[] args) throws CharacterCodingException {
CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT);
try {
decoder.decode(ByteBuffer.wrap(new byte[]{(byte) 0xC3, 0x28}));
throw new AssertionError("Invalid UTF-8 was accepted");
} catch (MalformedInputException expected) {
System.out.println("Invalid UTF-8 rejected");
}
}
}The bytes C3 28 are invalid UTF-8. The program catches MalformedInputException and prints Invalid UTF-8 rejected.
4. Complete Experiment: Binary Copy and Text Reading
The program creates temporary directories on its own, without relying on manually prepared map.jpg or dispatch.txt. TemporaryFiles participates in try-with-resources and attempts cleanup, preserving cleanup failures as primary or suppressed exceptions. Forced process termination can still leave temporary files.
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
public class StreamDemo {
public static void main(String[] args) throws IOException {
Path root = Files.createTempDirectory("stream-demo-");
Path binary = root.resolve("map.bin");
Path copy = root.resolve("map-copy.bin");
Path dispatch = root.resolve("dispatch.txt");
TemporaryFiles cleanup = new TemporaryFiles(copy, dispatch, binary, root);
try (cleanup) {
byte[] payload = {0, 1, 2, 127, (byte) 128, (byte) 255};
try (OutputStream output = Files.newOutputStream(binary)) {
output.write(payload);
}
try (InputStream input = Files.newInputStream(binary);
OutputStream output = Files.newOutputStream(copy)) {
byte[] buffer = new byte[4];
int read;
while ((read = input.read(buffer)) != -1) {
output.write(buffer, 0, read);
}
}
List<String> expectedLines = List.of("a developer lends out BK-15", "Inventory: 1");
try (BufferedWriter writer = Files.newBufferedWriter(
dispatch, StandardCharsets.UTF_8)) {
for (String line : expectedLines) {
writer.write(line);
writer.newLine();
}
}
List<String> actualLines = new ArrayList<>();
try (BufferedReader reader = Files.newBufferedReader(
dispatch, StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
actualLines.add(line);
}
}
boolean identical = Files.mismatch(binary, copy) == -1;
check(actualLines.equals(expectedLines), "Text content inconsistency");
check(identical, "Binary copy inconsistency");
System.out.println("Binary bytes: " + Files.size(binary));
System.out.println("Text lines: " + actualLines);
System.out.println("Copy consistent: " + identical);
System.out.println("Stream check passed");
}
}
private record TemporaryFiles(Path... paths) implements AutoCloseable {
@Override
public void close() throws IOException {
IOException failure = null;
for (Path path : paths) {
try {
Files.deleteIfExists(path);
} catch (IOException error) {
if (failure == null) failure = error;
else failure.addSuppressed(error);
}
}
if (failure != null) throw failure;
}
}
private static void check(boolean condition, String message) {
if (!condition) {
throw new AssertionError(message);
}
}
}Run:
chapter15_stream_root=$(mktemp -d)
mkdir -p "$chapter15_stream_root/out"
# Save the above code as "$chapter15_stream_root/StreamDemo.java"
javac --release 17 -Xlint:all -Werror \
-d "$chapter15_stream_root/out" \
"$chapter15_stream_root/StreamDemo.java"
java -ea -cp "$chapter15_stream_root/out" StreamDemo
rm -r -- "$chapter15_stream_root"Output:
Binary bytes: 6
Text lines: [a developer lends out BK-15, Inventory: 1]
Copy consistent: true
Stream check passedFiles.mismatch(a, b) returns the position of the first differing byte; if the contents are identical, it returns -1. It has been available since Java 12, and this chapter is based on Java 17.
5. Buffer the call granularity, without fixed multiples
try (BufferedInputStream input = new BufferedInputStream(
Files.newInputStream(path))) {
// Read
}A buffer loads larger amounts of data into memory and then satisfies multiple small reads or writes. This typically reduces the number of calls to the underlying I/O stack, but it can't guarantee "a dozen times" or "reducing 10,000 system calls." The outcome depends on these factors:
- Storage media and filesystem caching;
- Sequential access or random access;
- Work performed per item;
- Is buffering already underway inside the library?
- Buffer size and concurrent load.
If you've already batch-read with read(byte[]), adding another layer of BufferedInputStream might not yield measurable benefits. First, write correct streaming code, then measure it using JMH or production-like end-to-end tests. Don't draw conclusions from a single System.currentTimeMillis() run; warm-up, caching, and background load will contaminate your data.
6. Small files can be read entirely, but large files must have a size limit
String config = Files.readString(path, StandardCharsets.UTF_8);
byte[] icon = Files.readAllBytes(path);These methods are simple but load the entire content into memory. Use them only when file size is constrained by the program or a trusted configuration.
For log files ranging from 10 KB to 2 GB, process them line by line:
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
consume(line);
}
}Files.lines(path, charset) also returns a lazy Stream<String>:
try (Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8)) {
long errors = lines.filter(line -> line.startsWith("ERROR ")).count();
}A terminal operation does not automatically close the underlying file resource. Place Files.lines, Files.list, Files.walk, and Files.find inside a try-with-resources.
readLine() still loads one whole line into memory. Without a line-length bound, a huge file containing no newline can exhaust memory. Enforce hard limits while reading character or byte chunks; checking line.length() afterward is too late.
"Streaming" only controls the read mode. If you collect all rows into List, memory still grows with the file. When performing aggregate counting, only the counter is retained; when outputting error rows, rows are read and written as they go.
7. try-with-resources is an ownership declaration
try (InputStream input = Files.newInputStream(source);
OutputStream output = Files.newOutputStream(target)) {
// Using resources
}Resources are closed in reverse order of declaration: close output first, then close input. Java 9+ allows referencing variables that are already declared and final or effectively final (not reassigned):
InputStream input = Files.newInputStream(source);
try (input) {
// Use input
}A component creates resources and typically also closes them. If a method receives a stream provided by the caller, the contract must specify whether it takes ownership of the closure. A common convention in the standard library is "who opens, who closes"; exceptions include decorated streams: closing the outermost BufferedWriter also closes the writer it wraps.
Primary Exception and Suppressed Exceptions
If the try block and close() both throw exceptions, Java retains the exception from the block and attaches the closure failure to getSuppressed():
catch (IOException e) {
for (Throwable suppressed : e.getSuppressed()) {
System.err.println("Failed to close resources:" + suppressed.getMessage());
}
throw e;
}Hand-writing finally easily causes cleanup exceptions to mask actual failures and leads to empty catch (IOException e) {}. try-with-resources handles this priority correctly.
8. flush(), close(), and Durable Storage
BufferedWriter.flush() passes the Java buffer content to the lower target. close() first flushes then closes. Neither guarantees that data will persist after power loss. The operating system, file system, disk controller, and hardware cache may still retain the data.
For a stronger persistence request:
byte[] content = "report complete\n".getBytes(StandardCharsets.UTF_8);
try (FileChannel channel = FileChannel.open(path,
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING,
StandardOpenOption.WRITE)) {
java.nio.ByteBuffer buffer = java.nio.ByteBuffer.wrap(content);
while (buffer.hasRemaining()) {
channel.write(buffer);
}
channel.force(true);
}force(true) requests writing the content and associated metadata to a storage device. The example writes through the same FileChannel before forcing it. The API guarantees these updates for local devices, but provides no equivalent guarantee for non-local devices. Reopening a channel is not a portable guarantee that arbitrary earlier writes will be synchronized. Systems with high requirements (such as databases and transaction logs) must also design write ordering, checksums, directory synchronization, and crash recovery, and cannot treat a single force as a complete transaction.
Ordinary text export often needs only reliable resource closure. Choose the durability level based on the cost of data loss.
9. Open the options to decide whether to overwrite or append
Files.newOutputStream(path); // CREATE + TRUNCATE_EXISTING + WRITE
Files.newOutputStream(path,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND);Default writing creates a file or overwrites existing content. Appending works well for some logs, but atomic boundaries during concurrent append operations by multiple processes or threads depend on the API, provider, and write size. For strict boundary enforcement, use a single writer, file locking protocols, or a dedicated logging system.
Common options:
| Option | Behavior |
|---|---|
CREATE | Create if it doesn't exist; continue if it already exists |
CREATE_NEW | Atomic requires that it did not previously exist; otherwise, throw FileAlreadyExistsException |
TRUNCATE_EXISTING | Truncates an existing file to 0 bytes when opening for writing |
APPEND | Each time written to the end of the file, combination limits vary by provider |
SYNC / DSYNC | Requests stronger synchronization semantics; cost and support depend on the file system |
10. Fail I/O with operation context
IOException is a family:
| Exception | Common Meaning |
|---|---|
NoSuchFileException | Path does not exist or intermediate directories are missing |
FileAlreadyExistsException | CREATE_NEW or create operation encounters an existing target |
AccessDeniedException | Permissions, ACLs, read-only mounting, or security policies denied |
FileSystemException | Other path-level failures reported by the file system |
MalformedInputException | Bytes cannot be strictly decoded with the specified charset |
Preserve the cause when translating an exception at an application boundary:
try {
return Files.readString(path, StandardCharsets.UTF_8);
} catch (IOException e) {
throw new IllegalStateException("Failed to read book directory:" + path.getFileName(), e);
}Don't return the absolute path of a user's directory directly to external clients. Internal logs can record the controlled path, operation, error type, and associated request ID.
11. Saving Objects with Native Java Serialization
final class SessionSnapshot implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private String status;
private int count;
private transient String sessionToken;
}Serializable is a marker interface. ObjectOutputStream traverses the reachable object graph; default serialization traverses non-static, non-transient instance fields, and their non-null object references must also be serializable. Custom serialization methods can change the default behavior. serialVersionUID participates in compatibility checks but cannot automatically complete field migrations.
transient merely indicates that the field does not enter the default serialized data. It is not encryption, access control, or memory erasure. The token may still leak if it appears in other fields, logs, or backups.
Safety Boundary
Do not perform native Java deserialization of network uploads, message queues, or user files. Constructing object graphs may trigger dangerous behavior in the classpath, allowing attackers to exploit gadget chains to execute unintended code.
ObjectInputFilter can restrict allowed classes, array length, graph depth, and reference count, making it useful as defense in depth for legacy systems. It does not transform unknown inputs into trusted inputs. For long-term data and cross-service protocols, use explicit, versioned formats, validate field ranges, and construct only permitted domain objects.
A closed binary format can be used with DataOutputStream/DataInputStream, but you must define:
- Magic number and version;
- Field order, integer width, and byte order;
- String byte encoding and maximum length;
- Failure modes of unknown versions and truncated input.
writeUTF uses modified UTF-8, which is not equivalent to an ordinary UTF-8 text field. A cross-language protocol should write the UTF-8 byte length and content explicitly or use an established format.
12. Standard Input, Output, and Error
System.in // InputStream
System.out // PrintStream
System.err // PrintStreamCommand-line programs should write normal machine-readable output to out and diagnostics to err. The shell can redirect both streams separately.
Don't arbitrarily close System.in/out/err in library code, as other components of the process may still rely on them. If you wrap BufferedReader around System.in, it's typically best to manage the lifecycle uniformly at the application's outermost layer.
Some writing methods on PrintWriter and PrintStream record an error state instead of throwing IOException. When reliable detection is needed, check checkError(), or use the one that throws exceptions, BufferedWriter.
13. Final Challenge: A Streaming Log Analyzer
An application’s server.log might be 10 KB or 2 GB. The program must:
- Read line by line in UTF-8;
- Count INFO, WARN, ERROR, and DEBUG;
- Write unrecognized lines to
error.log; - On encountering FATAL, stop processing and exclude subsequent lines from counts;
- Write the small summary into
report.txt.
The complete program creates a set of fixed inputs and verifies the summary, error lines, and stop positions. A command-line version can accept paths as arguments, but input, report, and error log must be distinct files. Before opening outputs, the example checks normalized paths and existing file identities. It assumes program-controlled directories and does not prevent later link replacement by another process.
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
public class LogAnalyzerDemo {
private enum Level {
INFO, WARN, ERROR, DEBUG
}
private record Result(Map<Level, Long> counts, boolean fatal, long invalid) { }
public static void main(String[] args) throws IOException {
Path root = Files.createTempDirectory("log-demo-");
Path input = root.resolve("server.log");
Path report = root.resolve("report.txt");
Path errors = root.resolve("error.log");
TemporaryFiles cleanup = new TemporaryFiles(errors, report, input, root);
try (cleanup) {
Files.write(input, List.of(
"INFO server started",
"DEBUG catalog loaded",
"BROKEN",
"WARN inventory low",
"ERROR receipt failed",
"INFO retry accepted",
"FATAL database unavailable",
"INFO ignored"), StandardCharsets.UTF_8);
Result result = analyze(input, report, errors);
List<String> reportLines = Files.readAllLines(report, StandardCharsets.UTF_8);
List<String> errorLines = Files.readAllLines(errors, StandardCharsets.UTF_8);
check(result.counts().equals(Map.of(
Level.INFO, 2L,
Level.WARN, 1L,
Level.ERROR, 1L,
Level.DEBUG, 1L)), "Count errors");
check(result.fatal(), "Did not stop at FATAL");
check(result.invalid() == 1, "Invalid rows count error");
check(reportLines.equals(List.of(
"INFO=2", "WARN=1", "ERROR=1", "DEBUG=1", "FATAL=true")), "Report error");
check(errorLines.equals(List.of("line 3: BROKEN")), "Inconsistent error logs");
System.out.println("Stats: " + result.counts());
System.out.println("Encountered FATAL: " + result.fatal());
System.out.println("Invalid line: " + result.invalid());
System.out.println("Report: " + report.getFileName());
System.out.println("Log check passed");
}
}
private static Result analyze(Path input, Path report, Path errors) throws IOException {
requireDistinct(input, report, errors);
EnumMap<Level, Long> counts = new EnumMap<>(Level.class);
for (Level level : Level.values()) {
counts.put(level, 0L);
}
boolean fatal = false;
long invalid = 0;
long lineNumber = 0;
try (BufferedReader reader = Files.newBufferedReader(input, StandardCharsets.UTF_8);
BufferedWriter errorWriter = Files.newBufferedWriter(errors, StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
lineNumber++;
if (line.startsWith("FATAL ") || line.equals("FATAL")) {
fatal = true;
break;
}
int separator = line.indexOf(' ');
String token = separator < 0 ? line : line.substring(0, separator);
try {
Level level = Level.valueOf(token);
counts.merge(level, 1L, Math::addExact);
} catch (IllegalArgumentException e) {
invalid++;
errorWriter.write("line " + lineNumber + ": " + line);
errorWriter.newLine();
}
}
}
try (BufferedWriter writer = Files.newBufferedWriter(report, StandardCharsets.UTF_8)) {
for (Level level : Level.values()) {
writer.write(level + "=" + counts.get(level));
writer.newLine();
}
writer.write("FATAL=" + fatal);
writer.newLine();
}
Map<Level, Long> snapshot = Collections.unmodifiableMap(new EnumMap<>(counts));
return new Result(snapshot, fatal, invalid);
}
private record TemporaryFiles(Path... paths) implements AutoCloseable {
@Override
public void close() throws IOException {
IOException failure = null;
for (Path path : paths) {
try {
Files.deleteIfExists(path);
} catch (IOException error) {
if (failure == null) failure = error;
else failure.addSuppressed(error);
}
}
if (failure != null) throw failure;
}
}
private static void requireDistinct(Path... paths) throws IOException {
for (int i = 0; i < paths.length; i++) {
for (int j = i + 1; j < paths.length; j++) {
boolean sameName = paths[i].toAbsolutePath().normalize()
.equals(paths[j].toAbsolutePath().normalize());
boolean sameFile = Files.exists(paths[i]) && Files.exists(paths[j])
&& Files.isSameFile(paths[i], paths[j]);
if (sameName || sameFile) {
throw new IllegalArgumentException("Input and outputs must be distinct files");
}
}
}
}
private static void check(boolean condition, String message) {
if (!condition) {
throw new AssertionError(message);
}
}
}Output:
Stats: {INFO=2, WARN=1, ERROR=1, DEBUG=1}
Encountered FATAL: true
Invalid line: 1
Report: report.txt
Log check passedLevel names are case-sensitive and space-delimited; INFO alone is valid, while a tab does not act as a delimiter. The program reads only small reports back for testing and processes input line by line with long counters. BufferedReader may read ahead, so FATAL stops counting, not necessarily the underlying byte reads. The eighth line INFO ignored occurs after FATAL, so it is not counted.
Production version requirements still need to be determined: whether input files might grow or rotate during analysis, the maximum line length, error log capacity, whether report replacements require atomicity, and how old reports are preserved in the event of interruptions or disk full conditions. The next page covers these file system-level considerations.
14. Common Fault Diagnosis
| Phenomenon | First check |
|---|---|
| Chinese characters display as garbled text | Does writing and reading use the same explicit charset |
| Extra old bytes at file end | Whether the entire buffer was written, rather than the valid range returned by read |
Large files cause OutOfMemoryError | Whether to use readAllBytes, readString, or collect all lines |
| After the program runs for a while, the file cannot be opened | The file stream or Files.lines may not have been closed |
| Write succeeded but data lost after power failure | flush/close does not equal durability; reevaluate durability requirements |
| Interleaved logs | Do multiple writers lack clear record boundaries and coordination mechanisms |
| Cannot see the primary exception | A cleanup failure in finally may mask the primary failure; inspect the exception chain |
| Serialization triggers unexpected class behavior | Don't accept untrusted native object streams; use an allowlist and explicit formats |
15. Practice
- Change the buffer size of
StreamDemoto 1, 4, and 64 bytes. The results must be identical; do not use a single timing measurement to compare performance. - Add a maximum line length limit to the log analyzer. When exceeded, record an error and continue; clearly specify whether the length is measured in characters or bytes.
- Rewrite the version that only counts ERROR using
Files.lines, ensure the try-with-resources remains present, and do not invoketoList(). - Design a binary record format with a magic number, version, UTF-8 name length, name bytes, status, and count. Validate the magic number, length limit, and version before reading fields.
- Artificially cause both the try block and
close()to fail, print the main exception andgetSuppressed(), and observe the shutdown order.
Run the same data with three different boundary conditions
First, change the binary copy buffer sequentially to 1, 4, and 64 bytes, ensuring each copy matches the original file byte by byte. Then use strict CharsetDecoder to read invalid UTF-8 sequences C3 28 and verify that it throws MalformedInputException instead of silently replacing characters.
Feed the log analyzer input vastly larger than the memory-resident fixture set, yet the processing retains only four counters, the current line, and the error output stream. If an inline call to toList() occurs, memory will still grow with the file. Lines after FATAL are not to be counted, and the error file must have capacity or a rotation policy.
Finally, let both the try block and the two close() fail simultaneously, verifying that the main exception remains the primary exception, resources are closed in reverse declaration order, and if closure fails, it is recorded in suppressed exceptions, only then are resource ownership, access modes, and failure chains truly connected.
Check the official contract for Java 17
InputStreamandReader: partial reads, EOF, and character boundaries.Files: Text encoding, bulk reading, streaming line reading, and file copying.- JLS 14.20.3: try-with-resources: Rules for resource closure order and suppressed exceptions.
ObjectInputFilter: The filtering boundary for the old object stream; it cannot automatically trust unknown inputs.
Next Step: Deciding Where to Store the Data
The stream API handles the transfer: the binary image is copied byte by byte, and the text report is decoded using UTF-8. The next article enters the example directory, where the file system API determines how paths are composed, how directories are traversed, and how reports are replaced.