AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

18 I/O, files and time / Lesson 4 of 5

Serialization, CSV and the formats you actually use

14 min · +10 XP · 2 exercises · 6 questions

Read the text14 min
Exercises0/2
Quiz6 q

Quick answer

How does serialization work in Java?

Serialization in Java turns an object graph into bytes. A class opts in by implementing the Serializable marker interface; ObjectOutputStream.writeObject then reflectively writes the class name, its serialVersionUID and every non-transient, non-static field, recursively. ObjectInputStream.readObject rebuilds the objects without calling their constructors. JSON or Protobuf with explicit mapping is the recommended alternative today.

Sooner or later an object has to leave the JVM — into a file, across a socket, into a cache — and come back. Java shipped its own answer in 1997: mark a class Serializable and ObjectOutputStream writes the whole object graph. It still works, it is still on interview lists, and the platform's own architects now advise against it. This lesson explains how it works and what its knobs (serialVersionUID, transient, readObject) do, why it is a security problem, and then the formats real systems use — JSON, CSV, Properties — including the CSV parser that everyone gets wrong on the first try.

Java serialization: the mechanism

class Session implements Serializable {
    private static final long serialVersionUID = 1L;
    private final String user;
    private transient String token;          // not written; null after deserialisation
    …
}

try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("s.bin"))) { out.writeObject(session); }
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("s.bin"))) { Session s = (Session) in.readObject(); }

Serializable is a marker interface — no methods; it grants permission. writeObject walks the object graph reflectively, writing the class name, the serialVersionUID, and every non-transient, non-static field, recursively; shared references are written once and restored as shared. Every class in the graph must be Serializable, or NotSerializableException names the one that is not. readObject recreates the objects without calling any constructor of the serialisable classes (the first non-serialisable superclass's no-arg constructor does run) — invariants a constructor enforced are simply not re-checked.

  • serialVersionUID is the version stamp. If you omit it the compiler computes one from the class's shape, so adding a method changes it and old data fails with InvalidClassException. Declare it explicitly; bump it when the format really changes.
  • transient excludes a field: caches, derived values, secrets, anything not serialisable (Thread, a socket). It comes back as its default (null, 0, false), so restore it in readObject.
  • private void writeObject(ObjectOutputStream) / readObject(ObjectInputStream) hooks customise the format and are where you validate and rebuild transient state — call defaultWriteObject()/defaultReadObject() first.
  • readResolve() replaces the deserialised object with another — the way a serialisable singleton stays single.
  • Records serialise by their components and are reconstructed through the canonical constructor, so their invariants are re-checked — one of several reasons records are safer here.
  • Externalizable is the fully manual variant: you write and read every byte yourself and a public no-arg constructor is called.

Why "do not use it" is the current advice

Deserialising untrusted bytes lets the sender choose which classes are instantiated and how their fields are set. Chains of ordinary library classes ("gadget chains") have been found that execute arbitrary code during readObject; several major breaches rode on this. Mitigations exist — ObjectInputFilter (Java 9) whitelists classes; never deserialise from the network without one — but the structural problem remains: the format couples wire data to class internals, breaks on refactors, is Java-only, and is slow and verbose. Brian Goetz's summary is the one to quote: serialization was a mistake; use a data format you control. For interviews: know the mechanism, know the knobs, and know that the recommendation is JSON/Protobuf with explicit mapping.

JSON, the default today

Use a library — Jackson (the de facto standard), Gson, or JSON-B — never a hand-written parser. The mapping is by property name, so fields can be renamed with annotations, unknown fields ignored, and versions evolve gracefully. Records map cleanly since Jackson 2.12. Two habits: make DTOs (the classes you serialise) separate from domain classes, and configure the mapper once (FAIL_ON_UNKNOWN_PROPERTIES off, dates as ISO-8601 strings). Binary alternatives when size and speed matter: Protocol Buffers, Avro, CBOR — all schema-driven and cross-language.

CSV: deceptively simple

line.split(",") is a CSV parser for exactly as long as no field contains a comma. The real rules (RFC 4180): fields containing commas, quotes or line breaks are wrapped in double quotes; a quote inside a quoted field is doubled ("He said ""hi"""). A correct field splitter is a small state machine:

static List<String> parseCsvLine(String line) {
    List<String> fields = new ArrayList<>();
    StringBuilder cur = new StringBuilder();
    boolean quoted = false;
    for (int i = 0; i < line.length(); i++) {
        char c = line.charAt(i);
        if (quoted) {
            if (c == '"') {
                if (i + 1 < line.length() && line.charAt(i + 1) == '"') { cur.append('"'); i++; }   // escaped quote
                else quoted = false;                                                            // closing quote
            } else cur.append(c);
        } else if (c == '"') quoted = true;
        else if (c == ',') { fields.add(cur.toString()); cur.setLength(0); }
        else cur.append(c);
    }
    fields.add(cur.toString());
    return fields;
}

Fields spanning lines need the reader to keep consuming until the quotes balance. Writing is the mirror: quote a field if it contains a comma, quote, or newline, doubling internal quotes. For anything serious use Apache Commons CSV or OpenCSV — but be able to write the loop above, because "parse this CSV" is a common interview exercise and split(",") is the wrong answer they are waiting for.

Properties and other text formats

java.util.Properties reads key=value files (props.load(reader), props.getProperty("port", "8080")) — configuration, resource bundles, System.getProperties(). It is a Hashtable<Object,Object>, ancient and thread-safe by accident; fine for what it does. YAML and TOML need libraries (SnakeYAML, tomlj). XML has JAXB (removed from the JDK in 11; add the dependency) and the built-in DOM/SAX/StAX parsers — StAX for large documents.

Choosing a format

NeedUse
Interop, APIs, configJSON (Jackson)
Compact, fast, schema-evolvedProtobuf / Avro
Spreadsheets, bulk exportsCSV (library)
Java-only, same version both ends, trusted source, short-livedJava serialization, with an ObjectInputFilter — and still think twice
Key/value configProperties

Interview angle

  • "How does Java serialization work?" Serializable marker + ObjectOutputStream writes the graph reflectively; readObject rebuilds it without constructors.
  • "What is serialVersionUID?" The class's format version; mismatch → InvalidClassException; declare it explicitly.
  • "What does transient do?" Skips the field; it is null/zero after deserialisation.
  • "Why is deserialising untrusted data dangerous?" The sender chooses classes and field values; gadget chains give remote code execution. Filter, or use JSON.
  • "How do you parse CSV with quoted commas?" A state machine tracking whether you are inside quotes; doubled quotes are escapes; not split.

Key takeaways

  • Marker interface, reflective graph write, no constructors on read; serialVersionUID, transient, readObject/writeObject, readResolve are the knobs.
  • Untrusted deserialisation is a remote-code-execution risk; filter with ObjectInputFilter or, better, do not.
  • JSON with Jackson for almost everything; Protobuf/Avro for compact schema-driven data.
  • CSV needs a quote-aware state machine; split(",") is wrong.
  • Records serialise through their constructor and keep their invariants.

Common questions

What is serialVersionUID in Java?

It is the version stamp of a serialisable class's format. If you omit it the compiler computes one from the class's shape, so adding a method changes it and old data fails with InvalidClassException. Declare it explicitly and change it only when the format really changes.

What does the transient keyword do in Java?

transient excludes a field from serialization: it is not written, and after deserialisation it holds its default — null, 0 or false. Use it for caches, derived values, secrets and fields that cannot be serialised, and rebuild them in readObject.

Why is Java deserialization dangerous?

Deserialising untrusted bytes lets the sender choose which classes are instantiated and how their fields are set, and gadget chains of ordinary library classes can execute arbitrary code during readObject. Filter classes with ObjectInputFilter, or better, use a data format such as JSON.

Is the constructor called during deserialization in Java?

Not for serialisable classes: readObject recreates them without running their constructors, so invariants a constructor enforced are not re-checked; only the first non-serialisable superclass's no-arg constructor runs. Records are the exception — they are rebuilt through their canonical constructor.

How do you parse CSV with quoted commas in Java?

Not with split(","). Walk the line character by character with a flag for whether you are inside quotes: a comma outside quotes ends a field, and a doubled quote inside a quoted field is an escaped quote. In production, use Apache Commons CSV or OpenCSV.

Mark as readSign in to keep your place.

Java Serialization Explained: serialVersionUID and transient

How Java serialization writes an object graph with ObjectOutputStream, what serialVersionUID and transient do, why untrusted data is dangerous, and CSV parsing.

  • Course: Java study plan
  • Module: I/O, files and time
  • Kind: Lesson
  • Reading time: 14 min
  • Runtime: OpenJDK 18

How does serialization work in Java?

Serialization in Java turns an object graph into bytes. A class opts in by implementing the Serializable marker interface; ObjectOutputStream.writeObject then reflectively writes the class name, its serialVersionUID and every non-transient, non-static field, recursively. ObjectInputStream.readObject rebuilds the objects without calling their constructors. JSON or Protobuf with explicit mapping is the recommended alternative today.

Lesson

Sooner or later an object has to leave the JVM — into a file, across a socket, into a cache — and come back. Java shipped its own answer in 1997: mark a class Serializable and ObjectOutputStream writes the whole object graph. It still works, it is still on interview lists, and the platform's own architects now advise against it. This lesson explains how it works and what its knobs (serialVersionUID, transient, readObject) do, why it is a security problem, and then the formats real systems use — JSON, CSV, Properties — including the CSV parser that everyone gets wrong on the first try.

Java serialization: the mechanism

class Session implements Serializable {
    private static final long serialVersionUID = 1L;
    private final String user;
    private transient String token;          // not written; null after deserialisation
    …
}

try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("s.bin"))) { out.writeObject(session); }
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("s.bin"))) { Session s = (Session) in.readObject(); }

Serializable is a marker interface — no methods; it grants permission. writeObject walks the object graph reflectively, writing the class name, the serialVersionUID, and every non-transient, non-static field, recursively; shared references are written once and restored as shared. Every class in the graph must be Serializable, or NotSerializableException names the one that is not. readObject recreates the objects without calling any constructor of the serialisable classes (the first non-serialisable superclass's no-arg constructor does run) — invariants a constructor enforced are simply not re-checked.

  • serialVersionUID is the version stamp. If you omit it the compiler computes one from the class's shape, so adding a method changes it and old data fails with InvalidClassException. Declare it explicitly; bump it when the format really changes.
  • transient excludes a field: caches, derived values, secrets, anything not serialisable (Thread, a socket). It comes back as its default (null, 0, false), so restore it in readObject.
  • private void writeObject(ObjectOutputStream) / readObject(ObjectInputStream) hooks customise the format and are where you validate and rebuild transient state — call defaultWriteObject()/defaultReadObject() first.
  • readResolve() replaces the deserialised object with another — the way a serialisable singleton stays single.
  • Records serialise by their components and are reconstructed through the canonical constructor, so their invariants are re-checked — one of several reasons records are safer here.
  • Externalizable is the fully manual variant: you write and read every byte yourself and a public no-arg constructor is called.

Why "do not use it" is the current advice

Deserialising untrusted bytes lets the sender choose which classes are instantiated and how their fields are set. Chains of ordinary library classes ("gadget chains") have been found that execute arbitrary code during readObject; several major breaches rode on this. Mitigations exist — ObjectInputFilter (Java 9) whitelists classes; never deserialise from the network without one — but the structural problem remains: the format couples wire data to class internals, breaks on refactors, is Java-only, and is slow and verbose. Brian Goetz's summary is the one to quote: serialization was a mistake; use a data format you control. For interviews: know the mechanism, know the knobs, and know that the recommendation is JSON/Protobuf with explicit mapping.

JSON, the default today

Use a library — Jackson (the de facto standard), Gson, or JSON-B — never a hand-written parser. The mapping is by property name, so fields can be renamed with annotations, unknown fields ignored, and versions evolve gracefully. Records map cleanly since Jackson 2.12. Two habits: make DTOs (the classes you serialise) separate from domain classes, and configure the mapper once (FAIL_ON_UNKNOWN_PROPERTIES off, dates as ISO-8601 strings). Binary alternatives when size and speed matter: Protocol Buffers, Avro, CBOR — all schema-driven and cross-language.

CSV: deceptively simple

line.split(",") is a CSV parser for exactly as long as no field contains a comma. The real rules (RFC 4180): fields containing commas, quotes or line breaks are wrapped in double quotes; a quote inside a quoted field is doubled ("He said ""hi"""). A correct field splitter is a small state machine:

static List<String> parseCsvLine(String line) {
    List<String> fields = new ArrayList<>();
    StringBuilder cur = new StringBuilder();
    boolean quoted = false;
    for (int i = 0; i < line.length(); i++) {
        char c = line.charAt(i);
        if (quoted) {
            if (c == '"') {
                if (i + 1 < line.length() && line.charAt(i + 1) == '"') { cur.append('"'); i++; }   // escaped quote
                else quoted = false;                                                            // closing quote
            } else cur.append(c);
        } else if (c == '"') quoted = true;
        else if (c == ',') { fields.add(cur.toString()); cur.setLength(0); }
        else cur.append(c);
    }
    fields.add(cur.toString());
    return fields;
}

Fields spanning lines need the reader to keep consuming until the quotes balance. Writing is the mirror: quote a field if it contains a comma, quote, or newline, doubling internal quotes. For anything serious use Apache Commons CSV or OpenCSV — but be able to write the loop above, because "parse this CSV" is a common interview exercise and split(",") is the wrong answer they are waiting for.

Properties and other text formats

java.util.Properties reads key=value files (props.load(reader), props.getProperty("port", "8080")) — configuration, resource bundles, System.getProperties(). It is a Hashtable<Object,Object>, ancient and thread-safe by accident; fine for what it does. YAML and TOML need libraries (SnakeYAML, tomlj). XML has JAXB (removed from the JDK in 11; add the dependency) and the built-in DOM/SAX/StAX parsers — StAX for large documents.

Choosing a format

NeedUse
Interop, APIs, configJSON (Jackson)
Compact, fast, schema-evolvedProtobuf / Avro
Spreadsheets, bulk exportsCSV (library)
Java-only, same version both ends, trusted source, short-livedJava serialization, with an ObjectInputFilter — and still think twice
Key/value configProperties

Interview angle

  • "How does Java serialization work?" Serializable marker + ObjectOutputStream writes the graph reflectively; readObject rebuilds it without constructors.
  • "What is serialVersionUID?" The class's format version; mismatch → InvalidClassException; declare it explicitly.
  • "What does transient do?" Skips the field; it is null/zero after deserialisation.
  • "Why is deserialising untrusted data dangerous?" The sender chooses classes and field values; gadget chains give remote code execution. Filter, or use JSON.
  • "How do you parse CSV with quoted commas?" A state machine tracking whether you are inside quotes; doubled quotes are escapes; not split.

Key takeaways

  • Marker interface, reflective graph write, no constructors on read; serialVersionUID, transient, readObject/writeObject, readResolve are the knobs.
  • Untrusted deserialisation is a remote-code-execution risk; filter with ObjectInputFilter or, better, do not.
  • JSON with Jackson for almost everything; Protobuf/Avro for compact schema-driven data.
  • CSV needs a quote-aware state machine; split(",") is wrong.
  • Records serialise through their constructor and keep their invariants.

Common questions

What is serialVersionUID in Java?

It is the version stamp of a serialisable class's format. If you omit it the compiler computes one from the class's shape, so adding a method changes it and old data fails with InvalidClassException. Declare it explicitly and change it only when the format really changes.

What does the transient keyword do in Java?

transient excludes a field from serialization: it is not written, and after deserialisation it holds its default — null, 0 or false. Use it for caches, derived values, secrets and fields that cannot be serialised, and rebuild them in readObject.

Why is Java deserialization dangerous?

Deserialising untrusted bytes lets the sender choose which classes are instantiated and how their fields are set, and gadget chains of ordinary library classes can execute arbitrary code during readObject. Filter classes with ObjectInputFilter, or better, use a data format such as JSON.

Is the constructor called during deserialization in Java?

Not for serialisable classes: readObject recreates them without running their constructors, so invariants a constructor enforced are not re-checked; only the first non-serialisable superclass's no-arg constructor runs. Records are the exception — they are rebuilt through their canonical constructor.

How do you parse CSV with quoted commas in Java?

Not with split(","). Walk the line character by character with a flag for whether you are inside quotes: a comma outside quotes ends a field, and a doubled quote inside a quoted field is an escaped quote. In production, use Apache Commons CSV or OpenCSV.

Exercises

Serialize in memory

Read user token x y. Define class Session implements Serializable with a String user and a transient String token, and record Point(int x, int y) implements Serializable. Write both objects to an ObjectOutputStream over a ByteArrayOutputStream, read them back through an ObjectInputStream, and print session=<user>/<token> (the token comes back as null), point=<the record's toString>, equalPoint=<restored.equals(original)> and samePoint=<restored == original>.

Example: ann secret 3 4 →

session=ann/null
point=Point[x=3, y=4]
equalPoint=true
samePoint=false

CSV fields, with quotes

Read CSV lines until end of input and split each into fields by the RFC 4180 rules: fields are separated by commas; a field wrapped in double quotes may contain commas; a doubled quote inside a quoted field is one literal quote. Print each line as <field count>: [<fields joined by |>]. Do not use split.

Example input

a,b,c
"x, y",z
"He said ""hi""",1
a,,b

→

3: [a|b|c]
2: [x, y|z]
2: [He said "hi"|1]
3: [a||b]

In this module: I/O, files and time

← Files and paths — java.nio.file · java.time — dates, times, zones and durations →