☕ Java

Externalization

Externalization is Java's mechanism for giving a class complete, explicit control over its serialized form. A class that implements java.io.Externalizable takes full responsibility for reading and writing its own state — the JVM provides no default field serialization. Externalizable declares two methods: writeExternal(ObjectOutput out) writes the object's state using the provided ObjectOutput; readExternal(ObjectInput in) reads it back. Unlike Serializable, which uses the JVM's reflection-based automatic field serialization, Externalizable gives developers explicit control over what is written, the byte-level format, the order of fields, and the encoding of each value. Externalizable classes must have a public no-argument constructor, which is called by the deserialization mechanism before readExternal() is invoked. This constructor-calling behavior is a key difference from Serializable's constructor-bypass: Externalizable deserialization does call a constructor, though it may be a no-op constructor. This entry covers the Externalizable contract in full, the two-method API and ObjectOutput/ObjectInput interfaces, the public no-arg constructor requirement, performance characteristics versus Serializable, the identity-preservation mechanism for shared references, version evolution challenges, and when Externalizable is the right choice.

The Externalizable Contract — writeExternal and readExternal

Externalizable extends Serializable and adds two abstract methods. writeExternal(ObjectOutput out) is called when the object is being serialized. The method must write all state needed to reconstruct the object, using the ObjectOutput interface's write methods — writeInt(), writeLong(), writeObject(), writeUTF(), write(byte[]), and so on. These are the same methods as DataOutput, which ObjectOutput extends, plus writeObject() for writing other serializable or externalizable objects. readExternal(ObjectInput in) is called during deserialization and must read the state back in exactly the same order that writeExternal wrote it. The ObjectInput interface extends DataInput, providing all the DataInputStream read methods plus readObject(). The JVM's role in Externalizable is minimal: it writes a class descriptor (class name only — no field information, since fields are entirely the developer's responsibility) and handles object identity tracking (so the same object referenced multiple times in a graph is serialized once and back-referenced correctly). Everything else is up to the class: what fields to write, their encoding, their order, and any additional data. The public no-argument constructor is required by the Externalizable contract and enforced at deserialization time. The JVM calls the no-arg constructor before calling readExternal(). If no public no-arg constructor exists, instantiation throws a RuntimeException during deserialization. This is a hard requirement — there is no workaround within the Externalizable contract (unlike Serializable, where the constructor bypass means no constructor is needed). The no-arg constructor typically does minimal initialization, since readExternal will set all the real field values immediately after. The key behavioral difference from Serializable: with Externalizable, the developer writes every byte of the external representation. The class descriptor written by the JVM does not include field names or types — there is no automatic field-matching on deserialization. The order in which readExternal reads values must match exactly the order in which writeExternal wrote them. Any mismatch — a field added in a new version, a field removed, a type changed — requires explicit version handling code in readExternal.
Java
// ── Basic Externalizable implementation ──────────────────────────────
import java.io.*;

public class Point implements Externalizable {
    // No serialVersionUID needed (Externalizable writes no field metadata)
    // But it's still good practice to include it:
    private static final long serialVersionUID = 1L;

    private int x;
    private int y;

    // REQUIRED: public no-arg constructor — called by deserialization BEFORE readExternal
    public Point() { }   // must be public, must exist

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        // Write ONLY what we need — in any format we choose:
        out.writeInt(x);   // 4 bytes big-endian
        out.writeInt(y);   // 4 bytes big-endian
        // Total: 8 bytes for this object (vs Serializable: 8 bytes for fields
        //        + class descriptor overhead ~50-100 bytes for first instance)
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        // Read in EXACTLY the same order as writeExternal:
        x = in.readInt();
        y = in.readInt();
        // After readExternal: x and y are set — the public() constructor ran first (no-op)
    }

    @Override public String toString() { return "Point(" + x + ", " + y + ")"; }
}

// ── Serialization round-trip ──────────────────────────────────────────
Point original = new Point(10, 20);

// Serialize:
byte[] bytes;
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
     ObjectOutputStream oos = new ObjectOutputStream(baos)) {
    oos.writeObject(original);
    bytes = baos.toByteArray();
}
System.out.println("Serialized size: " + bytes.length + " bytes");

// Deserialize:
// 1. JVM reads class descriptor (class name only)
// 2. JVM calls Point() no-arg constructor  ← constructor IS called
// 3. JVM calls readExternal(in) on the new instance
try (ObjectInputStream ois = new ObjectInputStream(
        new ByteArrayInputStream(bytes))) {
    Point restored = (Point) ois.readObject();
    System.out.println("Restored: " + restored);  // Point(10, 20)
}

// ── Missing public no-arg constructor: runtime failure ────────────────
public class BadExternalizable implements Externalizable {
    private int value;

    // Only private constructor — no public no-arg constructor:
    private BadExternalizable(int v) { this.value = v; }

    @Override public void writeExternal(ObjectOutput out) throws IOException {
        out.writeInt(value);
    }
    @Override public void readExternal(ObjectInput in) throws IOException {
        value = in.readInt();
    }
}

try (ObjectInputStream ois = new ObjectInputStream(...)) {
    BadExternalizable b = (BadExternalizable) ois.readObject();
    // Throws: java.lang.RuntimeException: java.lang.InstantiationException
    //         (no public no-arg constructor)
}

ObjectOutput/ObjectInput, Shared References, and Version Evolution

ObjectOutput and ObjectInput provide a superset of DataOutput and DataInput, adding writeObject(Object) and readObject(). When writeExternal calls out.writeObject(someObject), the JVM serializes someObject using the standard serialization mechanism — if someObject is Externalizable, its writeExternal is called recursively; if it is Serializable, the normal field-based serialization applies; if it is neither, an error is thrown. The JVM's object identity tracking applies to objects written via writeObject — the same object instance written multiple times (within the same stream) is written once and referenced by handle on subsequent writes. The ObjectOutput interface methods available in writeExternal: write(int b), write(byte[]), write(byte[], int, int), writeBoolean(boolean), writeByte(int), writeShort(int), writeChar(int), writeInt(int), writeLong(long), writeFloat(float), writeDouble(double), writeBytes(String), writeChars(String), writeUTF(String), writeObject(Object). All are inherited from DataOutput with the addition of writeObject. Version evolution is the primary weakness of Externalizable compared to Serializable. With Serializable's field-matching mechanism, adding a field is backward-compatible: old serialized data leaves the new field at its default. With Externalizable, the stream is a raw byte sequence with no field name metadata. Adding a field in a new version of readExternal means the old stream, which did not write that field, will fail when readExternal tries to read more bytes than exist. The developer must handle this manually — typically by versioning the stream: write a version number as the first value in writeExternal, and in readExternal, check the version number to determine which fields to read. The performance advantage of Externalizable over Serializable: Externalizable can produce a more compact stream (writing only what is needed, using the most efficient encoding), and avoids the overhead of reflection-based field discovery and the field descriptors written per class in the Serializable stream. For the first instance of a class, the class descriptor overhead dominates. For thousands of instances of the same class in the same stream, the per-instance overhead dominates, and Externalizable's control over field encoding can reduce stream size significantly.
Java
// ── writeObject inside writeExternal: nested serialization ───────────
public class Order implements Externalizable {
    private long   orderId;
    private String customerId;
    private List<OrderLine> lines;   // List<OrderLine> — OrderLine is Serializable

    public Order() { }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeLong(orderId);
        out.writeUTF(customerId);
        out.writeInt(lines.size());
        for (OrderLine line : lines) {
            out.writeObject(line);   // each OrderLine serialized via its own mechanism
        }
    }

    @Override
    public void readExternal(ObjectInput in)
            throws IOException, ClassNotFoundException {
        orderId    = in.readLong();
        customerId = in.readUTF();
        int count  = in.readInt();
        lines      = new ArrayList<>(count);
        for (int i = 0; i < count; i++) {
            lines.add((OrderLine) in.readObject());  // cast required
        }
    }
}

// ── Shared reference preservation ────────────────────────────────────
// Even with Externalizable, the JVM tracks object identity at the writeObject level:
Order o1 = new Order();
Order o2 = new Order();

try (ObjectOutputStream oos = new ObjectOutputStream(new ByteArrayOutputStream())) {
    oos.writeObject(o1);   // O1 written, assigned handle #1
    oos.writeObject(o1);   // O1 again — written as back-reference to handle #1 (not duplicated)
    oos.writeObject(o2);   // O2 written, assigned handle #2
}
// Deserialization: two readObject() calls return the SAME Order instance for o1

// ── Version evolution with explicit version number ─────────────────────
public class VersionedPoint implements Externalizable {
    private static final int CURRENT_VERSION = 2;

    private int    x, y;
    private double z;      // added in version 2

    public VersionedPoint() { }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeInt(CURRENT_VERSION);  // always write version first
        out.writeInt(x);
        out.writeInt(y);
        out.writeDouble(z);   // version 2 addition
    }

    @Override
    public void readExternal(ObjectInput in)
            throws IOException, ClassNotFoundException {
        int version = in.readInt();   // read version first
        x = in.readInt();
        y = in.readInt();
        if (version >= 2) {
            z = in.readDouble();      // only read if stream has version 2 data
        } else {
            z = 0.0;                  // default for v1 streams that didn't write z
        }
        if (version > CURRENT_VERSION) {
            throw new IOException("Unknown version: " + version);
        }
    }
}

// ── Externalizable vs Serializable: when to use each ─────────────────
// USE Serializable when:
//   - Simplicity is the priority
//   - The class has few fields and the default format is acceptable
//   - Version evolution via field addition/deletion is the main concern
//   - Security via writeReplace/readResolve/serialization proxy is needed

// USE Externalizable when:
//   - Full control over the byte format is required (e.g., interoperability with non-Java)
//   - Maximum performance and minimum stream size are critical
//   - Custom encoding (variable-length integers, packed bytes) is needed
//   - The class has non-serializable fields that require custom logic anyway

// ── Performance comparison ────────────────────────────────────────────
// Serializable overhead per instance (first class occurrence):
//   Class descriptor: ~60-100 bytes (class name, serialVersionUID, field count, field descriptors)
//   Instance data: actual field values

// Externalizable per instance:
//   Class descriptor: ~30-50 bytes (class name only — no field metadata)
//   Instance data: exactly what writeExternal writes (developer-controlled)

// For a class with 3 int fields (12 bytes of actual data):
// Serializable first instance: ~80 + 12 = 92 bytes total
// Externalizable:              ~40 + 12 = 52 bytes total  (36% smaller)
// For subsequent instances in same stream, class descriptor is referenced not repeated:
// Serializable: ~5 + 12 = 17 bytes (back-reference to class descriptor)
// Externalizable: ~5 + 12 = 17 bytes (same — back-reference)
// So for large arrays: similar size; the difference is in the first instance only

Externalizable vs Serializable — Security and Design Trade-offs

Externalizable has a fundamentally different security posture than Serializable because it calls a public no-arg constructor before readExternal. This constructor call is a key security property: the constructor can initialize internal state (set sentinel values, initialize security checks, establish class invariants) before readExternal reads any untrusted data. The constructor cannot, however, be used to validate the data that readExternal will read, since the data has not been read yet at that point. Conversely, Externalizable is more vulnerable in one specific way: the public no-arg constructor is called for every deserialization, even from untrusted sources. If the no-arg constructor has side effects (acquiring resources, registering with a registry, creating files), those side effects can be triggered by an attacker who sends malicious byte streams. This is less dangerous than the full gadget chain attacks possible with Serializable, but it is a consideration. The absence of automatic field handling in Externalizable means that adding a field is a breaking change to the external format unless version numbers are managed explicitly. This is a significant maintenance burden for classes that evolve frequently. Serializable's field-based approach handles most common evolution patterns (adding fields, removing optional fields) automatically with the same serialVersionUID. A hybrid approach: use Serializable with writeObject/readObject for customization rather than switching to Externalizable. writeObject/readObject gives similar control over what is written while keeping the default field-matching for fields that do not need customization. This hybrid is the preferred approach for most cases — full Externalizable is appropriate for performance-critical scenarios where the stream format must be precisely controlled.
Java
// ── Security: constructor called before readExternal ─────────────────
public class SecureExternalizable implements Externalizable {
    private int    value;
    private String data;
    private boolean initialized = false;

    // Public no-arg constructor: initializes security sentinels
    public SecureExternalizable() {
        // Called BEFORE readExternal — cannot validate stream data yet,
        // but can initialize internal state:
        this.initialized = false;
        this.value = Integer.MIN_VALUE;  // sentinel
        System.out.println("Constructor called (may be from untrusted source)");
    }

    public SecureExternalizable(int value, String data) {
        if (value < 0) throw new IllegalArgumentException("value must be non-negative");
        if (data == null) throw new NullPointerException("data cannot be null");
        this.value = value;
        this.data  = data;
        this.initialized = true;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeInt(value);
        out.writeUTF(data);
    }

    @Override
    public void readExternal(ObjectInput in)
            throws IOException, ClassNotFoundException {
        int    v = in.readInt();
        String d = in.readUTF();

        // Validate deserialized values — similar to readObject in Serializable:
        if (v < 0) throw new IOException("Invalid value: " + v);
        if (d == null) throw new IOException("data cannot be null");

        this.value = v;
        this.data  = d;
        this.initialized = true;
    }

    // Always validate that initialization completed:
    public int getValue() {
        if (!initialized) throw new IllegalStateException("Not properly initialized");
        return value;
    }
}

// ── Hybrid: Serializable + writeObject/readObject (usually better) ────
// Instead of Externalizable, use Serializable with custom writeObject/readObject
// for most cases:
public class HybridCustom implements Serializable {
    private static final long serialVersionUID = 1L;

    private int[]  rawData;             // large array — want compact encoding
    private String name;                // normal field
    private transient int[] decompressed; // derived from rawData

    private void writeObject(ObjectOutputStream oos) throws IOException {
        oos.defaultWriteObject();       // writes rawData, name normally
        // Add extra compressed version for large data:
        byte[] compressed = compress(rawData);
        oos.writeInt(compressed.length);
        oos.write(compressed);
    }

    private void readObject(ObjectInputStream ois)
            throws IOException, ClassNotFoundException {
        ois.defaultReadObject();        // restores rawData, name
        int len = ois.readInt();
        byte[] compressed = new byte[len];
        ois.readFully(compressed);
        this.decompressed = decompress(compressed);
    }

    private byte[] compress(int[] data) { return new byte[0]; }   // placeholder
    private int[]  decompress(byte[] b) { return new int[0]; }    // placeholder
}

// ── Summary: choosing between Externalizable and Serializable ─────────
//
//                    Serializable          Externalizable
// ──────────────────────────────────────────────────────────────────────
// Control over format   Low (automatic)       Complete
// Version evolution     Easy (field matching) Manual (version numbers)
// Constructor behavior  Bypassed              Called (public no-arg required)
// Security hooks        writeReplace/Resolve  readExternal validation
// Maintenance burden    Low                   High
// Performance           Good (JVM-optimized)  Better (hand-tuned possible)
// Best for              Most cases            Performance-critical, format-specific

Related Topics in Java I/O

FileWriter
FileWriter is a convenience class for writing characters to a file, extending OutputStreamWriter with a FileOutputStream underneath. It encodes Java characters into bytes using the platform's default charset (or an explicit charset since Java 11) and writes them to a named file or File object. FileWriter supports two modes: overwrite (the default, which truncates the file to zero length on opening) and append (which positions the write pointer at the end of the existing file content). Like FileReader, FileWriter is unbuffered — each write() call propagates immediately to the underlying FileOutputStream, triggering system calls. In practice, FileWriter is almost always wrapped in a BufferedWriter to batch writes into efficient OS calls. The charset trap is identical to FileReader: pre-Java-11 constructors use the platform default charset silently, which causes portability problems; Java 11 constructors accept an explicit Charset. This entry covers all constructor variants with their charset and append semantics, the write methods and their character vs string behavior, newLine() in BufferedWriter, the flush/close contract, and the preferred modern alternatives.
BufferedReader
BufferedReader wraps any Reader with an in-memory character buffer, dramatically reducing system calls for character-by-character or line-by-line reading. Its defining method is readLine(), which reads a complete line of text terminated by \n, \r, or \r\n and returns it without the terminator, or returns null at end-of-file. Beyond buffering, BufferedReader also exposes a lines() method (Java 8+) that returns a lazy Stream<String> of lines, enabling the full Stream API for file processing without loading the entire file into memory. BufferedReader supports mark/reset with a caller-specified readAheadLimit. It is obtained either by wrapping a Reader (new BufferedReader(new FileReader(...))) or directly from Files.newBufferedReader(path, charset), which is the preferred idiom in modern Java. This entry covers construction and buffer sizing, all read methods and their contracts, readLine() edge cases (empty lines, last line without terminator), the lines() stream and its relationship to reader lifecycle, mark/reset semantics with readAheadLimit, and the use of BufferedReader as a base for protocol parsing.
BufferedWriter
BufferedWriter wraps any Writer with an in-memory character buffer, reducing system calls by accumulating characters until the buffer fills, flush() is called, or close() is called. It adds two capabilities not present in Writer: newLine(), which writes the platform-specific line separator, and an optimized write(String, int, int) that avoids creating a char[] copy by writing directly from the String. BufferedWriter is the standard output partner to BufferedReader — together they provide efficient line-by-line text file processing. It is constructed either by wrapping a Writer (new BufferedWriter(new FileWriter(...))) or via Files.newBufferedWriter(path, charset, options), the modern idiomatic alternative. Like all buffered streams, correct usage requires try-with-resources to guarantee that buffered data is flushed and the file is closed even when exceptions occur. This entry covers construction and buffer sizing, all write methods and their interaction with the buffer, newLine() and its platform behavior, flush semantics including when explicit flush is necessary, the difference between close() and flush(), and performance patterns for high-throughput text writing.
PrintWriter
PrintWriter is a character-based output class that wraps any Writer or OutputStream and adds convenience methods for printing formatted representations of all Java primitive types, strings, and objects. Its defining characteristic is that none of its print(), println(), and printf() methods throw checked IOException — errors are silently swallowed and can only be detected after the fact by calling checkError(). This makes PrintWriter easy to use interactively and in situations where I/O failure is genuinely unrecoverable (writing to System.out, generating diagnostic output), but makes it dangerous for critical data writing where exceptions must be caught and handled. PrintWriter can auto-flush on println(), printf(), and format() calls when constructed with autoFlush=true, which is useful for interactive console output and network protocol streams. Its printf() and format() methods delegate to java.util.Formatter, enabling C-style formatted output with full locale awareness. This entry covers all constructor variants and their autoFlush and buffering behavior, every print/println/printf method, the checkError() error detection model, the difference between PrintWriter and PrintStream, charset handling, and when PrintWriter is the right choice versus BufferedWriter.