Skip to content
ES EN
Theme: automatic

Solved exercise: Java serialization with transient fields

Serialize a warehouse to a working file with Serializable and transient, then reload it with readObject and rebuild the derived total value.

Updated on

Exam Code Java Object-oriented programming Exam level Tested code

Exam Code: Solved exercise: Java serialization with transient fields
Object-Oriented Programming · Final examQuestion 4 · 3.0 points · 35 min

A hardware shop wants to store the state of its warehouse in a working file called warehouse.dat, located in the current directory. Write the program Main with two classes: Product (name, quantity and unit price in cents, both integers) and Warehouse (a list of products), both serializable.

Warehouse also keeps a field totalValue holding the sum of quantity * price over all products. This field is a derived cache and must not be written to the file: declare it transient and rebuild it on loading with a custom readObject. Also define serialVersionUID in both classes.

The program reads commands from standard input, one per line, until the end of the input. It starts with an empty warehouse. The commands are: ADD name quantity price, SELL name quantity, VALUE, SAVE, LOAD and LIST. Names contain no spaces; quantities and prices are non-negative integers.

ADD creates the product or, if it already exists, adds the quantity and replaces the price with the new one; it prints ADDED name. SELL removes units and, if all goes well, prints SOLD name remaining. Selling exactly all the remaining units is allowed, and a product whose stock reaches 0 stays in the warehouse: it is still shown by LIST and still counted by SAVED n. If the product does not exist print ERROR unknown product; if there are not enough units print ERROR insufficient stock; in both error cases nothing is modified.

VALUE prints VALUE total. SAVE serializes the whole warehouse into the file, overwriting it if it already exists, and prints SAVED n, where n is the number of distinct products. LOAD replaces the in-memory warehouse with the one read from the file, restoring the products in the order in which they were saved, and prints LOADED n; if the file does not exist it prints ERROR no file and leaves the warehouse unchanged. LIST prints one line per product, in order of first addition, formatted as name quantity price.

When finished, the program must delete warehouse.dat if it exists. Blank lines are ignored. If the input is empty nothing is printed.

Sample input

ADD nut 10 25
ADD screw 4 100
VALUE
SAVE
SELL nut 4
VALUE
LOAD
VALUE
LIST

Expected output

ADDED nut
ADDED screw
VALUE 650
SAVED 2
SOLD nut 6
VALUE 550
LOADED 2
VALUE 650
nut 10 25
screw 4 100
Classes Product and Warehouse correctly declared Serializable, with serialVersionUID
0.5
Field totalValue marked transient and maintained incrementally in ADD and SELL
0.75
Saving and loading with ObjectOutputStream/ObjectInputStream, using try-with-resources
0.75
Custom readObject that rebuilds the transient field after loading
0.75
Exact output, stock and unknown-product errors, and handling of a missing file
0.25

Hints

Hint 1 · What must be saved and what must not?

Think about which information is primary and which can be recomputed. The list of products is the real state; totalValue follows from it. A transient field returns to its default value (0 for a long) on deserialization, so somebody has to fill it in again.

Hint 2 · How do you hook your code into loading?

A Serializable class may declare a private method private void readObject(ObjectInputStream in) that the JVM invokes by reflection. Inside it, first call in.defaultReadObject() to read the non-transient fields, then recompute whatever is missing.

Hint 3 · And resource and error handling?

Open the streams with try-with-resources so they are always closed. Before loading, check with Files.exists (or catch NoSuchFileException/FileNotFoundException). Keep totalValue up to date incrementally in ADD and SELL so that VALUE does not have to walk the list.

Solution

Explained solution

The solution separates three responsibilities: the model (Product and Warehouse), persistence (save and load) and the command interpreter in main. The key point of the exercise is that totalValue is marked transient: it never travels to the file, and when the object is read back, readObject rebuilds it from the list.

import java.io.*;
import java.nio.file.*;
import java.util.*;

public class Main {

    static class Product implements Serializable {
        private static final long serialVersionUID = 1L;
        final String name;
        long quantity;
        long price;

        Product(String name, long quantity, long price) {
            this.name = name;
            this.quantity = quantity;
            this.price = price;
        }
    }

    static class Warehouse implements Serializable {
        private static final long serialVersionUID = 1L;
        private final List<Product> products = new ArrayList<>();
        private transient long totalValue = 0;

        private Product find(String name) {
            for (Product p : products) {
                if (p.name.equals(name)) return p;
            }
            return null;
        }

        void add(String name, long quantity, long price) {
            Product p = find(name);
            if (p == null) {
                p = new Product(name, quantity, price);
                products.add(p);
                totalValue += quantity * price;
            } else {
                totalValue -= p.quantity * p.price;
                p.quantity += quantity;
                p.price = price;
                totalValue += p.quantity * p.price;
            }
        }

        String sell(String name, long quantity) {
            Product p = find(name);
            if (p == null) return "ERROR unknown product";
            if (p.quantity < quantity) return "ERROR insufficient stock";
            p.quantity -= quantity;
            totalValue -= quantity * p.price;
            return "SOLD " + name + " " + p.quantity;
        }

        long value() {
            return totalValue;
        }

        int size() {
            return products.size();
        }

        List<Product> list() {
            return products;
        }

        private void readObject(ObjectInputStream in)
                throws IOException, ClassNotFoundException {
            in.defaultReadObject();
            totalValue = 0;
            for (Product p : products) {
                totalValue += p.quantity * p.price;
            }
        }
    }

    static final Path FILE = Paths.get("warehouse.dat");

    static void save(Warehouse w) throws IOException {
        try (ObjectOutputStream out = new ObjectOutputStream(
                new BufferedOutputStream(Files.newOutputStream(FILE)))) {
            out.writeObject(w);
        }
    }

    static Warehouse load() throws IOException, ClassNotFoundException {
        try (ObjectInputStream in = new ObjectInputStream(
                new BufferedInputStream(Files.newInputStream(FILE)))) {
            return (Warehouse) in.readObject();
        }
    }

    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        Warehouse warehouse = new Warehouse();
        StringBuilder sb = new StringBuilder();
        String line;
        while ((line = br.readLine()) != null) {
            line = line.trim();
            if (line.isEmpty()) continue;
            String[] t = line.split("\\s+");
            switch (t[0]) {
                case "ADD" -> {
                    warehouse.add(t[1], Long.parseLong(t[2]), Long.parseLong(t[3]));
                    sb.append("ADDED ").append(t[1]).append('\n');
                }
                case "SELL" -> sb.append(warehouse.sell(t[1], Long.parseLong(t[2]))).append('\n');
                case "VALUE" -> sb.append("VALUE ").append(warehouse.value()).append('\n');
                case "SAVE" -> {
                    save(warehouse);
                    sb.append("SAVED ").append(warehouse.size()).append('\n');
                }
                case "LOAD" -> {
                    if (!Files.exists(FILE)) {
                        sb.append("ERROR no file\n");
                    } else {
                        warehouse = load();
                        sb.append("LOADED ").append(warehouse.size()).append('\n');
                    }
                }
                case "LIST" -> {
                    for (Product p : warehouse.list()) {
                        sb.append(p.name).append(' ').append(p.quantity)
                          .append(' ').append(p.price).append('\n');
                    }
                }
                default -> { }
            }
        }
        System.out.print(sb);
        Files.deleteIfExists(FILE);
    }
}

Product and Warehouse implement Serializable and declare serialVersionUID, so version compatibility is explicit instead of depending on a hash computed by the JVM. In Warehouse, the field products is serialized while totalValue, being transient, is left out of the byte stream.

add and sell maintain totalValue incrementally. In add on an existing product the old contribution is subtracted first and the new one added, because the price is replaced: in the test "Repeated add" (nail 10 at 5, then 5 more at 8) the total goes from 50 to 15·8 = 120, not to 50 + 40.

The heart of the exercise is readObject. The JVM invokes it on deserialization; we call defaultReadObject() to restore products and then walk the list to recompute totalValue. Without that method the field would be 0 after LOAD. The test "Statement example" checks it: after SAVE the value is 650, after selling 4 nuts it drops to 550, and after LOAD it is 650 again, because the saved state is recovered and the transient field is rebuilt from it.

The method save uses try-with-resources with an ObjectOutputStream over a buffered stream, so the file is flushed and closed even if an exception occurs; Files.newOutputStream truncates an existing file, which is why SAVE overwrites it. load does the same with ObjectInputStream and casts the result to Warehouse; the list is read back in the order it was written. Before loading, main checks Files.exists; the test "Load without save" prints ERROR no file and the warehouse stays empty (VALUE 0).

The test "Save empty and reload" shows that loading replaces the in-memory warehouse: it is saved empty, a product is added (value 1), and LOAD returns 0 products and value 0. The errors of SELL (test "Failed sales") do not alter the state: the value stays at 1500. Finally, main accumulates the output in a StringBuilder and deletes the working file at the end so that successive runs are independent.

Code and tests on GitLab

Test cases

CaseInputExpected outputActual outputResult
Statement exampleADD nut 10 25 ADD screw 4 100 VALUE SAVE SELL nut 4 VALUE LOAD VALUE LISTADDED nut ADDED screw VALUE 650 SAVED 2 SOLD nut 6 VALUE 550 LOADED 2 VALUE 650 nut 10 25 screw 4 100ADDED nut ADDED screw VALUE 650 SAVED 2 SOLD nut 6 VALUE 550 LOADED 2 VALUE 650 nut 10 25 screw 4 100OK
Failed salesADD cable 3 500 SELL cable 5 SELL fuse 1 VALUEADDED cable ERROR insufficient stock ERROR unknown product VALUE 1500ADDED cable ERROR insufficient stock ERROR unknown product VALUE 1500OK
Load without saveLOAD VALUE LISTERROR no file VALUE 0ERROR no file VALUE 0OK
Repeated addADD nail 10 5 ADD nail 5 8 VALUE LISTADDED nail ADDED nail VALUE 120 nail 15 8ADDED nail ADDED nail VALUE 120 nail 15 8OK
Save empty and reloadSAVE ADD a 1 1 VALUE LOAD VALUE LISTSAVED 0 ADDED a VALUE 1 LOADED 0 VALUE 0SAVED 0 ADDED a VALUE 1 LOADED 0 VALUE 0OK
Empty input(empty)(empty)(empty)OK

Actual outputs: code compiled with Java 21.0.12.1 (Temurin) and run in an isolated container on 14 October 2026.

Complexity

Let p be the number of products and c the number of commands. find is a linear search, O(p), used by add and sell, so the command loop costs O(c·p) in the worst case. Maintaining totalValue incrementally does not change that bound: it only prevents VALUE from walking the list, taking that command from O(p) to O(1). To bring add and sell down to expected O(1) the names would have to be indexed with a HashMap.

SAVE, LOAD (including the recomputation in readObject) and LIST are O(p) in time, proportional to the size of the object graph written or read. Memory is O(p) for the list; serialization adds a temporary buffer and the class metadata that Java writes once per stream.

Common mistakes

  • Forgetting implements Serializable on Product: Warehouse implements it, but writing the list throws NotSerializableException on the first product.
  • Marking totalValue as transient and not rebuilding it: after LOAD the value would be 0 even though the products are there. transient always demands a plan to recover the data.
  • Writing readObject without calling in.defaultReadObject() first: the list would remain unread and deserialization would fail or leave the object inconsistent.
  • In add on an existing product, adding only the new quantity times the new price to totalValue, without subtracting the old contribution or valuing the units already in stock at the new price.
  • Not closing the streams (no try-with-resources): with a BufferedOutputStream the last bytes may never reach the file and the later load fails with EOFException.
  • Comparing names with == instead of equals: it works with literals, but names read from the file or from the input are distinct objects and the search fails.

Variants

Save only if there are changes

The professor may ask that SAVE not write anything if the warehouse has not changed since the last load or save. A field transient boolean dirty is enough: set it to true in add and sell, and to false after saving or loading. Because it is transient, it is false by default after deserialization, which is exactly the correct value.

Version control with serialVersionUID

If you add a new field to Product (say category) and keep serialVersionUID = 1L, Java still reads old files and leaves the new field at its default value. If you change the UID, reading throws InvalidClassException. With a custom readObject you can fill in a sensible value for the missing field.

Externalizable instead of Serializable

By implementing Externalizable you must write writeExternal and readExternal by hand, and the class needs a public no-argument constructor. You control the format completely, and the derived field is not even an issue: it is simply not written and is recomputed in readExternal.

On the syllabus

Object persistence is a standard topic in advanced object-oriented programming. The course ISIS1209 APO II Honores at Universidad de los Andes (Colombia) lists in its syllabus the study of class-reuse mechanisms and of contract definition, the foundation on which a serializable class rests: Serializable is itself a contract that the compiler does not verify but the JVM enforces at run time. For related OOP material see our solved exercise on interfaces, default methods and multiple implementation in Java.

Produced with AI support and reviewed by the newsroom · Reviewed by Kernel

Byline

· Chief editor · English edition · London

“What can be recomputed is not stored: transient forces you to decide what is truth and what is convenience.”