Core and Web-based Java
Exceptions, Resource Management, I/O and Serialization
PGCP-AC
Real programs encounter missing files, malformed input, unavailable networks and violated business rules. Java represents these failures with exceptions and provides structured propagation and recovery. Its I/O APIs then separate raw bytes, decoded characters, buffered operations, filesystem paths and object graphs. Correct programs connect these subjects through one principle: preserve useful failure information while releasing every acquired resource predictably.
1. The exception hierarchy
Throwable is the root of values that Java can throw and catch. Its main branches are Error and Exception.
Errordescribes serious JVM or environment failures, such asOutOfMemoryError. Application code usually does not recover from them.- Checked exceptions are
Exceptionsubclasses outsideRuntimeException, such asIOException. The compiler generally requires them to be caught or declared. - Unchecked exceptions are
RuntimeExceptionand its subclasses, such asNullPointerException,IllegalArgumentExceptionandNumberFormatException. The compiler does not require catch-or-declare handling.
Checked exceptions commonly represent anticipated external failures from which a caller might recover. Unchecked exceptions commonly represent invalid arguments, invalid state or programming defects. This is a design convention supported by compiler rules, not a claim that every unchecked failure is unrecoverable.
2. Throwing and declaring
throw transfers control using one exception object:
if (amount <= 0) {
throw new IllegalArgumentException("amount must be positive");
}
throws appears in a method declaration and states that a failure may propagate:
String load(Path path) throws IOException {
return Files.readString(path);
}
A method can declare several exception types. Declaring throws Exception is usually too broad for an application API because callers cannot tell which failures are expected. A clear exception type and message should describe the failed operation and relevant safe context.
3. Stack unwinding and propagation
When an exception is thrown, normal execution stops. The JVM searches for a matching handler in the current method. If none exists, that method's frame is unwound and the search continues at its caller. This continues until a handler is found or the exception reaches the thread's uncaught-exception mechanism.
The stack trace records the path through method calls and usually identifies the throw location. Catch an exception only where code can recover, add useful context, translate it at an abstraction boundary or perform required reporting. Catching and ignoring it loses evidence while allowing potentially invalid execution to continue.
4. Try and catch matching
try {
int count = Integer.parseInt(input);
process(count);
} catch (NumberFormatException ex) {
System.out.println("Enter a whole number");
} catch (IllegalArgumentException ex) {
System.out.println("Value is outside the accepted range");
}
The first compatible catch executes. Therefore, catch clauses must go from specific types to general types. Placing a superclass first makes a later subclass catch unreachable and causes compilation to fail.
A multi-catch handles alternatives identically:
try {
loadConfiguration();
} catch (IOException | SecurityException ex) {
report(ex);
}
Alternatives cannot have a subclass relationship because the narrower type would be redundant. The multi-catch variable is implicitly final.
5. Finally and control flow
A finally block normally runs as execution leaves its associated try or catch, whether by completion, return, break, continue or exception:
try {
return calculate();
} finally {
audit();
}
It is not an absolute guarantee: abrupt JVM termination, a system crash or a process kill can prevent it. Never put a return in finally; it can replace a pending return value or suppress an exception. Manual resource closing in finally is also verbose and error-prone, so use try-with-resources for closable resources.
6. Custom exceptions and causes
A domain-specific exception gives a failure meaningful vocabulary:
class OrderImportException extends Exception {
OrderImportException(String message, Throwable cause) {
super(message, cause);
}
}
void importOrders(Path path) throws OrderImportException {
try {
parse(Files.readString(path));
} catch (IOException ex) {
throw new OrderImportException(
"Could not read order file: " + path.getFileName(), ex);
}
}
The wrapper describes the higher-level operation, while the cause retains the original type, message and stack trace. This is exception translation, not information loss. Choose checked or unchecked inheritance based on whether callers are expected and able to recover.
7. Try-with-resources
Try-with-resources manages objects implementing AutoCloseable:
try (BufferedReader reader = Files.newBufferedReader(
Path.of("notes.txt"), StandardCharsets.UTF_8)) {
System.out.println(reader.readLine());
}
close() runs automatically after the body, including when the body throws. Resources are closed in reverse declaration order:
try (InputStream in = Files.newInputStream(source);
OutputStream out = Files.newOutputStream(target)) {
in.transferTo(out);
}
Here out closes before in. Java 9 and later can also use an already-declared final or effectively final resource variable in the resource header.
8. Suppressed exceptions
Both the try body and close() can fail. Try-with-resources preserves the body's exception as primary and attaches the close failure as a suppressed exception:
catch (IOException ex) {
for (Throwable suppressed : ex.getSuppressed()) {
logger.error("Cleanup also failed", suppressed);
}
}
This prevents cleanup failure from erasing the more relevant operational failure. In contrast, careless manual closing in a finally block can replace the original exception.
9. Byte streams
InputStream and OutputStream are abstract bases for binary I/O. Use them for images, archives, audio, protocol messages and any data whose meaning is bytes.
try (InputStream in = new BufferedInputStream(
Files.newInputStream(Path.of("photo.jpg")));
OutputStream out = new BufferedOutputStream(
Files.newOutputStream(Path.of("photo-copy.jpg")))) {
in.transferTo(out);
}
read() returns an int so it can represent byte values 0–255 and -1 for end of stream. Bulk reads may return fewer bytes than requested, so code must process the actual count. Never convert arbitrary binary data through a character encoding.
10. Character streams and encodings
Reader and Writer operate on characters. Encoding translates between characters and bytes. The same encoding must be understood at both ends:
Path path = Path.of("message.txt");
try (BufferedWriter writer =
Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
writer.write("नमस्ते Java");
}
InputStreamReader decodes bytes into characters, while OutputStreamWriter encodes characters into bytes. Avoid platform-default encoding when a file or protocol needs portable behaviour. UTF-8 is a common explicit choice.
BufferedReader.readLine() returns a line without its line terminator or null at end of input. PrintWriter offers convenient formatted text output but some methods record errors rather than throwing them, so check its contract when error detection matters.
11. Buffering and stream wrapping
Physical I/O calls can be expensive. A buffered wrapper accumulates data so many small application operations require fewer underlying operations. Wrappers form a pipeline:
try (DataInputStream input = new DataInputStream(
new BufferedInputStream(
Files.newInputStream(path)))) {
int id = input.readInt();
double amount = input.readDouble();
}
Close the outermost wrapper; its close operation normally closes the wrapped resource according to its contract. A larger buffer is not automatically faster and buffering does not change the logical data format.
12. Data streams and object streams
DataOutputStream writes primitive values in a defined binary representation and DataInputStream reads them. Writer and reader must use the same field order and types.
ObjectOutputStream and ObjectInputStream encode and restore supported Java object graphs. They are different from data streams: object streams include type and graph information and preserve repeated-reference relationships within the stream.
Do not treat Java object serialization as a general long-term interchange format. It couples data to Java types and version details. JSON, a schema-based binary format or explicit records are often better for external boundaries.
13. Path and Files
Path represents a filesystem path, while Files performs operations:
Path source = Path.of("data", "input.txt");
Path target = Path.of("backup", "input.txt");
Files.createDirectories(target.getParent());
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
Useful operations include exists, isDirectory, size, readString, writeString, copy, move, deleteIfExists, list and walk. Streams returned by methods such as Files.list and Files.walk hold resources and should be used in try-with-resources.
File is the older pathname abstraction. Constructing either a File or Path object does not create a filesystem entry. Relative paths are resolved against the process's current working directory.
14. NIO channels and buffers
Channels transfer data to or from buffers. A ByteBuffer has a capacity, position and limit. A common read cycle is:
- read bytes from a channel into the buffer;
- call
flip()to set the limit to the written position and position to zero; - consume bytes while
hasRemaining(); - call
clear()orcompact()before the next read.
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (channel.read(buffer) != -1) {
buffer.flip();
while (buffer.hasRemaining()) {
consume(buffer.get());
}
buffer.clear();
}
clear() resets indexes but does not erase bytes. Channels support features such as seeking, file locking, memory mapping and non-blocking network operations depending on the channel type.
15. Default Java serialization
A class opts into ordinary serialization by implementing the marker interface Serializable:
class SessionData implements Serializable {
private static final long serialVersionUID = 1L;
private String userName;
private transient String temporaryToken;
}
Default serialization records eligible non-static, non-transient instance state through the serializable object graph. Static fields belong to the class rather than an individual object and are not ordinary serialized state. A transient field receives its default value after deserialization unless custom logic reconstructs it.
serialVersionUID declares a version identifier used during compatibility checking. Declaring it avoids relying on a compiler-generated value that can change when the class changes.
16. Serialization construction and customization
Deserialization does not create a serializable class by invoking its ordinary constructor. The first non-serializable superclass must have an accessible no-argument constructor and its portion is initialized through that constructor. Serializable subclass fields are restored from the stream.
Private writeObject and readObject methods can customize default serialization, validate restored state or reconstruct transient data. Such code must preserve class invariants. Deserialization callbacks and object creation make untrusted native serialization dangerous; accept only controlled data with strict filters or use a safer format.
17. Shallow and deep copying
A shallow copy creates a new outer object but shares referenced nested objects:
original Employee ──► Address
copied Employee ──► same Address
A deep copy duplicates selected nested state:
original Employee ──► Address A
copied Employee ──► Address B
The business model must decide what to share. Immutable values may safely remain shared; mutable owned state may require duplication; entities with independent identity should not be copied blindly. Serialization round-tripping is not a universal deep-copy definition and introduces failure, performance, security and versioning concerns. Prefer explicit copy constructors or factory methods that state the policy.
18. Reliable I/O design
Keep parsing separate from transport. One method can obtain text, another can validate and convert it and domain code can operate on typed values. This division makes errors clearer and tests easier.
At system boundaries:
- validate paths and input sizes;
- choose and document the character encoding;
- avoid logging secrets or entire sensitive files;
- write important files atomically when partial output would be harmful;
- wrap low-level exceptions only when the new exception adds meaningful context;
- retain the original cause.
Practical considerations
| Mistake | Correct approach |
|---|---|
Catching Exception everywhere | Catch only failures the layer can handle |
| Swallowing an exception | Recover, translate with cause or propagate |
| Returning from finally | Let the original result or failure survive |
| Manually closing several resources | Use try-with-resources |
| Reading text as raw bytes without a charset | Decode using an explicit encoding |
| Assuming one read fills a buffer | Use the returned byte count |
| Treating File construction as file creation | Call an actual Files creation operation |
| Serializing secrets or untrusted graphs | Use controlled explicit formats |
| Calling serialization a universal deep copy | Define copy ownership explicitly |
Worked exception trace
static int example() {
try {
throw new IllegalStateException("body");
} finally {
System.out.println("cleanup");
}
}
The exception begins to propagate from the try block. Before the method exits, finally prints cleanup. Because finally completes normally, the original IllegalStateException continues to the caller. If finally threw another exception or returned, it could replace the original outcome, which is why finally should contain careful cleanup rather than outcome-changing control flow.
Continue learning
Related notes
Put this topic into timed practice
Open mock tests when you want full-exam pacing, or keep drilling in practice mode.