Externalizable vs Serializable in Java: Differences & Example

Externalizable vs Serializable in Java with a runnable example, constructor rules, read order, real size numbers, versioning, records and security.

Two serialization paths in Java, the JVM writing field descriptors and values for a Serializable class, and writeExternal and readExternal writing only chosen values for an Externalizable class that is rebuilt through its public no-arg constructor

The key difference in Externalizable vs Serializable is who writes the bytes. A Serializable class lets the JVM write and read every non-transient field for us, whereas an Externalizable class writes and reads its own state in writeExternal() and readExternal(). The interface Externalizable extends Serializable, so both kinds of objects go through the same ObjectOutputStream and ObjectInputStream.

We use Serializable for most classes that must be cached, sent to another JVM or stored in an HTTP session. We pick Externalizable when we want full control over the byte format, for example to skip derived fields, to write a compact binary layout or to read several old formats by hand.

The following example declares the same music track class both ways. The two helper methods toBytes() and fromBytes() come from section 2.

class Track implements Serializable {
    String title;
    int seconds;

    Track(String title, int seconds) {
        this.title = title;
        this.seconds = seconds;
    }
}

class ExtTrack implements Externalizable {
    String title;
    int seconds;

    public ExtTrack() { }                      // required by Externalizable

    ExtTrack(String title, int seconds) {
        this.title = title;
        this.seconds = seconds;
    }

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

    @Override
    public void readExternal(ObjectInput in) throws IOException {
        title = in.readUTF();
        seconds = in.readInt();
    }
}
byte[] plain = toBytes(new Track("Intro", 95));
byte[] custom = toBytes(new ExtTrack("Intro", 95));
int plainSize = plain.length;                     // 83, includes field names and types
int customSize = custom.length;                   // 49, only our two values after the class name

ExtTrack copy = fromBytes(custom);
String title = copy.title;                        // "Intro"
int seconds = copy.seconds;                       // 95

Notice that the Externalizable version needs a public no-arg constructor and two methods, while the Serializable version needs nothing. We compare the two interfaces first, look at what each one does during a round trip, build a complete Externalizable class and finish with size, versioning, security and a choice table.

1. Externalizable vs Serializable at a Glance

Both interfaces live in java.io and both objects are written with ObjectOutputStream.writeObject(). The stream checks the class first. If the class implements Externalizable, the stream calls writeExternal() and ignores the default field writing, as well as any private writeObject() method. Otherwise it writes the fields of each Serializable class in the hierarchy.

Two serialization paths in Java, the JVM writing field descriptors and values for a Serializable class, and writeExternal and readExternal writing only chosen values for an Externalizable class that is rebuilt through its public no-arg constructor
With Serializable the JVM writes field names, types and values; with Externalizable the class writes only what it chooses and is rebuilt with its public no-arg constructor

The rows that cause most bugs are the constructor rule and the superclass rule, so the table states them precisely.

PointSerializableExternalizable
Methods to implementNone, it is a marker interfacewriteExternal(ObjectOutput) and readExternal(ObjectInput)
Who writes the fieldsThe JVM, by reflectionOur code, field by field
Constructor on readThe class constructor does not run; the no-arg constructor of the first non-serializable superclass runsThe public no-arg constructor runs, followed by readExternal()
Missing constructorWorks without a no-arg constructor in the classInvalidClassException: no valid constructor
Superclass fieldsWritten for every Serializable superclassOur code must write them
transient keywordSkips the fieldHas no effect, we decide what to write
Read orderNot our concernMust match the write order and types
final fieldsRestoredCannot be assigned in readExternal()
RecordsSupported, rebuilt through the canonical constructorThe two methods are ignored for records
writeReplace() and readResolve()HonoredHonored

The record row surprises many developers. A record is always rebuilt through its canonical constructor, and class-specific methods such as writeExternal() and readExternal() of a record are ignored.

2. What Serializable Does for Us

A class that implements Serializable needs no code at all. On write, ObjectOutputStream records a class descriptor with the class name, the serialVersionUID and the name and type of every field, followed by the field values. On read, ObjectInputStream creates the object without calling its constructor and puts the values back by reflection.

The following helper methods do one round trip in memory. We use them in every later example, and the Java serialization guide covers the background.

static byte[] toBytes(Object value) throws IOException {
    ByteArrayOutputStream buffer = new ByteArrayOutputStream();
    try (ObjectOutputStream out = new ObjectOutputStream(buffer)) {
        out.writeObject(value);
    }
    return buffer.toByteArray();
}
@SuppressWarnings("unchecked")
static <T> T fromBytes(byte[] bytes) throws IOException, ClassNotFoundException {
    try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
        return (T) in.readObject();
    }
}

The constructor rule matters as soon as a class extends a class that is not serializable. The JVM runs the no-arg constructor of the first non-serializable superclass and leaves that superclass’s fields at whatever the constructor sets. In the next example, Media is not serializable, so its genre field comes back with the constructor default and the transient field comes back as null.

class Media {
    String genre = "unknown";
    Media() { }
}

class Song extends Media implements Serializable {
    String title;
    transient String lyricsCache;

    Song(String title, String genre) {
        this.title = title;
        this.genre = genre;
        this.lyricsCache = "la la la";
    }
}
Song restored = fromBytes(toBytes(new Song("Intro", "rock")));
String songTitle = restored.title;              // "Intro"
String genre = restored.genre;                  // "unknown", Media is not Serializable
String lyrics = restored.lyricsCache;           // null, transient

If we want to keep the default behavior but adjust it, for example to encrypt one field, we add private writeObject() and readObject() methods to the Serializable class. The post on custom serialization with readObject and writeObject shows that middle ground, which covers most needs without switching interfaces.

3. Writing an Externalizable Class

An Externalizable class takes over the whole job. The stream still writes the class name and the serialVersionUID, and after that it hands an ObjectOutput to writeExternal(). On read, the stream calls the public no-arg constructor and hands an ObjectInput to readExternal().

A music app keeps user settings in a local file, with the volume, the last played track and the shuffle flag. The app also holds a decoded album cover in memory, which takes several megabytes and can be rebuilt at any time, so it must never reach the file.

class PlayerSettings implements Externalizable {
    @Serial
    private static final long serialVersionUID = 1L;

    private int volume;
    private String lastTrack;                 // may be null
    private boolean shuffle;
    private byte[] coverCache;                // never stored

    public PlayerSettings() { }

    PlayerSettings(int volume, String lastTrack, boolean shuffle) {
        this.volume = volume;
        this.lastTrack = lastTrack;
        this.shuffle = shuffle;
        this.coverCache = new byte[1024];
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeInt(volume);
        out.writeBoolean(lastTrack != null);
        if (lastTrack != null) {
            out.writeUTF(lastTrack);
        }
        out.writeBoolean(shuffle);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException {
        volume = in.readInt();
        lastTrack = in.readBoolean() ? in.readUTF() : null;
        shuffle = in.readBoolean();
    }

    int volume() { return volume; }
    String lastTrack() { return lastTrack; }
    boolean shuffle() { return shuffle; }
    byte[] coverCache() { return coverCache; }
}
PlayerSettings saved = new PlayerSettings(70, "Intro", true);
PlayerSettings loaded = fromBytes(toBytes(saved));
int volume = loaded.volume();                  // 70
String lastTrack = loaded.lastTrack();         // "Intro"
boolean shuffle = loaded.shuffle();            // true
byte[] cover = loaded.coverCache();            // null, never written

PlayerSettings empty = fromBytes(toBytes(new PlayerSettings(30, null, false)));
String noTrack = empty.lastTrack();            // null

We can see that the field coverCache needed no transient keyword, because writeExternal() never writes it. The rest of this section looks at the rules that the compiler does not check for us.

3.1. Read the Values in the Same Order and Type

The stream does not store field names for an Externalizable class, only raw values. The method readExternal() must read the values in the same order and with the same types that writeExternal() wrote them. A mismatch either throws an exception or, worse, fills fields with wrong values without any error.

In the following class, readExternal() reads the two int values in swapped order. Both reads succeed, so the bug shows up only in the data.

class SwappedTrack implements Externalizable {
    int seconds;
    int plays;

    public SwappedTrack() { }

    SwappedTrack(int seconds, int plays) {
        this.seconds = seconds;
        this.plays = plays;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeInt(seconds);
        out.writeInt(plays);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException {
        plays = in.readInt();                 // wrong order
        seconds = in.readInt();
    }
}
SwappedTrack swapped = fromBytes(toBytes(new SwappedTrack(95, 3)));
int swappedSeconds = swapped.seconds;          // 3, should be 95
int swappedPlays = swapped.plays;              // 95, should be 3

3.2. Provide a Public No-Arg Constructor

The stream creates an Externalizable object with its public no-arg constructor before it calls readExternal(). A class without one can be written, but reading it back throws an InvalidClassException with the message no valid constructor.

class NoDefaultCtor implements Externalizable {
    int volume;

    NoDefaultCtor(int volume) {
        this.volume = volume;
    }

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

    @Override
    public void readExternal(ObjectInput in) throws IOException {
        volume = in.readInt();
    }
}
byte[] written = toBytes(new NoDefaultCtor(50));
NoDefaultCtor broken = fromBytes(written);      // InvalidClassException: no valid constructor

An Externalizable class also cannot restore final fields, because readExternal() is a normal method and cannot assign a final field. Classes that must stay immutable either stay Serializable or use the serialization proxy pattern with writeReplace().

3.3. Handle null Values and Superclass Fields

The method writeUTF() throws a NullPointerException for a null string, which is why PlayerSettings writes a boolean flag before the optional value. Another option is writeObject(), which accepts null but adds an object header to the stream.

ExtTrack noTitle = new ExtTrack(null, 60);
byte[] failed = toBytes(noTitle);               // NullPointerException: Cannot invoke "String.length()"

Superclass fields follow the same idea. The stream does not write any superclass state for an Externalizable class, so writeExternal() writes the inherited fields too, or calls a protected helper in the superclass that does it.

4. Size and Speed in Practice

The usual claim is that Externalizable is faster and smaller because it skips reflection and field descriptors. That holds for a single small object, but the picture changes for collections. The stream writes the class descriptor only once per stream, while every Externalizable object gets its own block-data header and end marker.

List<Track> plainList = new ArrayList<>();
List<ExtTrack> customList = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
    plainList.add(new Track("Track " + i, i));
    customList.add(new ExtTrack("Track " + i, i));
}
int plainListSize = toBytes(plainList).length;      // 22009
int customListSize = toBytes(customList).length;    // 23973

We can see that for 1,000 tracks the Externalizable version is about 9% larger. The exact numbers depend on the class names, which are part of the stream.

DataSerializableExternalizable
One track83 bytes49 bytes
List of 1,000 tracks22,009 bytes23,973 bytes

Speed follows the same pattern, as Externalizable saves the reflective field access but still pays for the stream, the class lookup and object creation. Measure with a benchmark tool such as JMH on real data before switching. If raw speed and size matter more than Java compatibility, a format such as Protocol Buffers or JSON with a custom Jackson serializer gains more than Externalizable does.

5. Versioning an Externalizable Class

Both interfaces check the serialVersionUID, so an old stream and a class with a different value fail with an InvalidClassException. Beyond that check, the two behave differently when fields change. A Serializable class tolerates added and removed fields, because the stream carries field names. An Externalizable class has only the raw values, so it must handle old formats itself.

A common way is to write a format version as the first value and branch on it in readExternal(). Version 2 of VersionedSettings adds an equalizer preset and still reads files written by version 1.

class VersionedSettings implements Externalizable {
    private static final int FORMAT = 2;

    int volume;
    String preset = "flat";

    public VersionedSettings() { }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeByte(FORMAT);
        out.writeInt(volume);
        out.writeUTF(preset);                 // added in format 2
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException {
        int format = in.readByte();
        volume = in.readInt();
        if (format >= 2) {
            preset = in.readUTF();
        }
    }
}

Files from format 1 have no preset, so the field keeps its default value “flat” from the field initializer, which runs in the no-arg constructor.

6. Security Is the Same for Both

Implementing Externalizable does not make deserialization safe. ObjectInputStream reads the class name from the stream first and creates that class, so a stream from an untrusted source can name any serializable class on the classpath. Attackers chain such classes into gadget chains whose methods run during deserialization, before our code sees the object.

Since Java 9, an ObjectInputFilter (JEP 290) accepts or rejects classes before they are created. The following filter, built with allowFilter() from Java 17, accepts only our settings class and rejects every other class.

ObjectInputFilter onlySettings = ObjectInputFilter.allowFilter(
        type -> type == PlayerSettings.class, ObjectInputFilter.Status.REJECTED);
byte[] stored = toBytes(new PlayerSettings(70, "Intro", true));
try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(stored))) {
    in.setObjectInputFilter(onlySettings);
    PlayerSettings accepted = (PlayerSettings) in.readObject();
    int acceptedVolume = accepted.volume();     // 70
}

We can also set a pattern such as com.example.PlayerSettings;!* for the whole JVM with the system property jdk.serialFilter. A stream that names any other class fails with an InvalidClassException that reports the filter status REJECTED.

7. Which Interface to Use in Each Situation

Most classes that need Java serialization are best served by Serializable, with transient fields or a private writeObject() and readObject() pair for special cases. Externalizable earns its extra code when the byte layout itself is the requirement.

SituationPick
A DTO, a session attribute or a cache entrySerializable
Immutable class or recordSerializable (records ignore Externalizable)
Skip a few fields, keep the rest automaticSerializable with transient
Encrypt or validate one fieldSerializable with writeObject() and readObject()
Compact layout for one large object, such as a game save or indexExternalizable
Read several historic formats by handExternalizable with a format byte
Exchange data with non-Java systemsJSON, XML or Protocol Buffers, not Java serialization

8. Externalizable and Serializable FAQs

The size claim and the constructor rule are the two points that candidates most often get wrong in interviews.

8.1. Is Externalizable Faster Than Serializable?

Sometimes. It skips reflection and field descriptors, so one small object is faster to write and smaller, as the 49-byte versus 83-byte track in the intro shows. For a collection of many objects the stream can be larger, as section 4 shows, so we measure before switching.

8.2. Why Does Externalizable Need a Public No-Arg Constructor?

The stream has no field values it could set on its own, so it must create an empty object first and pass it to readExternal(). It calls the public no-arg constructor for that, and a missing or non-public one fails with InvalidClassException: no valid constructor.

8.3. Does the transient Keyword Work in an Externalizable Class?

No. The keyword affects only default serialization. In an Externalizable class, a field is stored only if writeExternal() writes it, whether or not it is marked transient.

8.4. Can a Record Implement Externalizable?

A record with these methods compiles, but the two methods are never called. The serialization specification treats every record as a Serializable record and rebuilds it through its canonical constructor, which also runs any validation in a compact constructor.

8.5. Is Serializable a Marker Interface?

Yes. It declares no methods and only marks the class as allowed for serialization. Externalizable is not a marker interface, because it declares writeExternal() and readExternal().

8.6. Does serialVersionUID Matter for Externalizable?

Yes. The stream stores it and compares it on read. We declare it explicitly so that a recompiled class with the same format still reads old data.

9. Conclusion

Serializable hands the work to the JVM, which writes field names, types and values and copes with added or removed fields. Externalizable hands the work to our code, which decides the exact bytes and must read them back in the same order, with a public no-arg constructor and its own versioning.

The size and speed gains of Externalizable are real for single objects and can turn negative for collections, so we keep Serializable as the default and switch only when the byte layout is a requirement. Neither interface protects against untrusted data, which is the job of an ObjectInputFilter.

10. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.