Skip to content

Chapter 7 of 8

Exception Handling, I/O, and Serialization

Java's exception model centers on the Throwable hierarchy. Throwables split into Error (serious, unrecoverable conditions such as OutOfMemoryError and StackOverflowError, which applications should not catch) and Exception (recoverable conditions). Exceptions are further divided into checked exceptions (subclasses of Exception other than RuntimeException, which must be either caught or declared in the method signature with throws) and unchecked exceptions (RuntimeException and its subclasses, which do not require explicit handling). Custom exceptions extend Exception or RuntimeException as appropriate and are thrown with throw new MyException("reason"), with the calling method declaring them via throws. When looking at a stack trace, the "Caused by" chain shows the root cause; the last "Caused by" is the original root cause and the first exception is what the application directly saw. The two keywords surrounding exception control are easily confused: throw is the statement that actually raises an exception, while throws is a method-signature clause that declares which exception types a method may propagate. Catching Throwable or Error is a code smell—the JVM is usually in an unstable state and you should let it report and die.

The try-with-resources statement (Java 7+) automatically closes any number of resources that implement AutoCloseable, even when exceptions are thrown during opening or use. Resources are closed in reverse order of declaration in a finally-equivalent block, and exceptions raised while closing one resource are attached to the primary exception via Throwable.addSuppressed() and retrieved with getSuppressed(). The three homonyms final/finally/finalize are similarly easy to mix up: final is the unchangeable modifier, finally is the block that always runs after a try/catch for cleanup (skipped only if the JVM exits via System.exit() or the thread is killed), and finalize is the deprecated legacy method Object.finalize() called by the garbage collector. Functional interfaces add a wrinkle to checked exceptions: because Supplier, Function, Consumer, and Predicate do not declare throws Exception, a lambda body that throws a checked exception (such as IOException from Files.lines) will not compile directly. The standard workarounds are catching and rethrowing as a runtime exception, defining a custom functional interface that declares throws Exception, or using a helper such as Lombok's @SneakyThrows.

Java's I/O libraries have evolved through three generations. BIO (blocking I/O) in java.io is the simple, classic model: one thread per socket, easy to reason about, but limited to a few hundred simultaneous connections. NIO (Java 1.4+) introduced non-blocking channels, buffers, and Selectors that let a single thread multiplex many connections—it is the foundation of high-performance servers like Netty and gRPC. NIO.2 (Java 7+) added true asynchronous I/O (AsynchronousChannel, AsynchronousFileChannel), the modern Path and Files APIs, and the WatchService. The class hierarchy splits into byte streams (InputStream/OutputStream for binary data) and character streams (Reader/Writer for text, with explicit charset control via StandardCharsets.UTF_8 to avoid platform-dependent bugs). BufferedInputStream wraps an InputStream for raw bytes and supports mark/reset; BufferedReader wraps a Reader and adds readLine() for text.

The NIO ByteBuffer is the fundamental primitive of high-throughput I/O, available in two flavors. ByteBuffer.allocate(n) creates a buffer backed by a byte[] on the JVM heap—fast to allocate and freed by GC, but the bytes must be copied when handed to native channels. ByteBuffer.allocateDirect(n) creates an off-heap buffer that can be passed to native channels without copying, ideal for high-throughput networking, at the cost of more expensive allocation and Cleaner-based reclamation. The Selector ties it together: opening a selector, registering non-blocking SelectableChannel instances with interest sets (SelectionKey.OP_READ, OP_WRITE, OP_ACCEPT, OP_CONNECT), looping on selector.select(), and iterating the selected keys is the canonical pattern for a single-threaded, many-connection server. File path handling follows a similar modernization: the old java.io.File is superseded by the immutable java.nio.file.Path and the static utilities in java.nio.file.Files (readAllBytes, write, lines, copy, walk), and Files.walk returns a lazy Stream<Path> while Files.walkFileTree drives a FileVisitor with preVisitDirectory, visitFile, visitFileFailed, and postVisitDirectory for non-trivial work over a tree.

Serialization converts an object to a byte stream for storage or transport. Any class opts in by implementing the Serializable marker interface, after which ObjectOutputStream and ObjectInputStream handle the bytes; on deserialization the JVM bypasses constructors and populates fields reflectively, which is why serialVersionUID should be declared explicitly for version compatibility. Externalizable goes further by giving the class full control: implementing writeExternal and readExternal, with a public no-arg constructor required for reconstruction—you decide exactly which fields to write and in what format, making externalizable output often smaller and more version-tolerant than default serialization. The main pitfall is security: deserialization gadget chains can execute attacker code, mitigated since Java 9 by validating input via ObjectInputFilter. The transient keyword excludes a field from default serialization—useful for derived values, non-serializable state, and sensitive data—while the volatile keyword concerns cross-thread visibility; the two are unrelated, and only the name is similar.

All chapters
  1. 1Java Platform and Memory Model
  2. 2Core Language Features
  3. 3Object-Oriented Programming
  4. 4Generics and the Collections Framework
  5. 5Functional Programming and the Streams API
  6. 6Concurrency and the Java Memory Model
  7. 7Exception Handling, I/O, and Serialization
  8. 8Modern Java, Design Patterns, and Best Practices

Drill it

Reading is not remembering. These come from the Java Programming deck:

Q

What is the JVM?

The Java Virtual Machine (JVM) is an abstract computing machine that executes Java bytecode. It provides platform independence by translating bytecode into nati...

Q

What is the difference between JDK, JRE, and JVM?

JDK (Java Development Kit) includes tools for developing Java programs.JRE (Java Runtime Environment) provides libraries and JVM to run Java applications.JVM (J...

Q

What are the four pillars of OOP in Java?

Encapsulation – bundling data and methods, restricting direct accessInheritance – a class derives from another classPolymorphism – one interface, many implement...

Q

What is encapsulation in Java?

Encapsulation is the practice of keeping fields private and providing access through public getter and setter methods. This protects the internal state of an ob...