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

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
LISTExpected 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.
Test cases
| Case | Input | Expected output | Actual output | Result |
|---|---|---|---|---|
| Statement example | ADD nut 10 25
ADD screw 4 100
VALUE
SAVE
SELL nut 4
VALUE
LOAD
VALUE
LIST | ADDED nut
ADDED screw
VALUE 650
SAVED 2
SOLD nut 6
VALUE 550
LOADED 2
VALUE 650
nut 10 25
screw 4 100 | ADDED nut
ADDED screw
VALUE 650
SAVED 2
SOLD nut 6
VALUE 550
LOADED 2
VALUE 650
nut 10 25
screw 4 100 | OK |
| Failed sales | ADD cable 3 500
SELL cable 5
SELL fuse 1
VALUE | ADDED cable
ERROR insufficient stock
ERROR unknown product
VALUE 1500 | ADDED cable
ERROR insufficient stock
ERROR unknown product
VALUE 1500 | OK |
| Load without save | LOAD
VALUE
LIST | ERROR no file
VALUE 0 | ERROR no file
VALUE 0 | OK |
| Repeated add | ADD nail 10 5
ADD nail 5 8
VALUE
LIST | ADDED nail
ADDED nail
VALUE 120
nail 15 8 | ADDED nail
ADDED nail
VALUE 120
nail 15 8 | OK |
| Save empty and reload | SAVE
ADD a 1 1
VALUE
LOAD
VALUE
LIST | SAVED 0
ADDED a
VALUE 1
LOADED 0
VALUE 0 | SAVED 0
ADDED a
VALUE 1
LOADED 0
VALUE 0 | OK |
| 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 SerializableonProduct:Warehouseimplements it, but writing the list throwsNotSerializableExceptionon the first product. - Marking
totalValueastransientand not rebuilding it: afterLOADthe value would be 0 even though the products are there.transientalways demands a plan to recover the data. - Writing
readObjectwithout callingin.defaultReadObject()first: the list would remain unread and deserialization would fail or leave the object inconsistent. - In
addon an existing product, adding only the new quantity times the new price tototalValue, 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 aBufferedOutputStreamthe last bytes may never reach the file and the later load fails withEOFException. - Comparing names with
==instead ofequals: 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


