1:HeapObject.java
package memory;

import java.util.*;

/**
 * Models an identified heap allocation with named fields and explicit reference
 * edges.
 */
public final class HeapObject {
    private final long id;
    private final String type;
    private final Map<String, Object> fields = new LinkedHashMap<>();

    public HeapObject(long id, String type, Set<String> fieldNames) {
        if (id < 1)
            throw new IllegalArgumentException("id must be positive");
        if (type == null || type.isBlank())
            throw new IllegalArgumentException("type must not be blank");
        this.id = id;
        this.type = type;
        for (String field : Set.copyOf(Objects.requireNonNull(fieldNames, "fieldNames"))) {
            if (field.isBlank())
                throw new IllegalArgumentException("field must not be blank");
            fields.put(field, null);
        }
    }

    public long id() {
        return id;
    }

    public String type() {
        return type;
    }

    public Object readField(String name) {
        requireField(name);
        return fields.get(name);
    }

    public void writeField(String name, Object value) {
        requireField(name);
        fields.put(name, Objects.requireNonNull(value, "value"));
    }

    public Set<HeapObject> references() {
        Set<HeapObject> references = new LinkedHashSet<>();
        for (Object value : fields.values()) {
            if (value instanceof HeapObject reference)
                references.add(reference);
        }
        return Set.copyOf(references);
    }

    private void requireField(String name) {
        if (name == null || !fields.containsKey(name)) {
            throw new IllegalArgumentException("unknown field: " + name);
        }
    }
}