---
title: 'Records and Enumerations'
source: 'https://academia.sh/en/courses/java-object-model/records-and-enumerations'
course: 'Object-Oriented Java'
language: en
updated: '2026-08-17T18:09:41+00:00'
license: 'CC BY-SA 4.0'
---

# Records and Enumerations

A one-line record declaration produces ten members in the class file, and the produced class cannot be extended, meaning its declared type and runtime type can never come apart. In an enum, a constant with its own body written takes its own class; a record's produced equality, in turn, protects only shallowly.

The previous lesson had the compiler produce classes never written in the source, and give
them hidden fields and hidden constructor parameters. What everything produced shared was
taking on a binding job a programmer would otherwise write by hand — but what was produced
stayed invisible: none of it was a surface callable from the source.

This lesson puts that same production ability to a visible job. In Java, a data carrier's
entire surface — its field readers, equality, hash value, and string conversion — can be
produced from a single line; that line's name is the **record** declaration. An
**enumeration**, defining a fixed set, does a similar production. There are three questions:
how many members does one line produce, which side do the produced members leave the decision
to, and how far does the produced equality protect?

## The Measurement Core

The measurement compiles a source, reads the field and method names in the class file it
produces, reports whether the class can be extended, and also returns whether a given source
compiles at all.

- **CI36** — The oracle is the source itself: the record declaration's body is empty, so every
  member appearing in the class file is one the compiler produced.
- **CI37** — The member list is read from the class file; the count is based on the file
  produced, not the source's spelling.
- **CI38** — Compile trials run in their own temporary directories, and the result is read
  through a single exit code.
- **CI39** — In the equality measurement, two objects are constructed separately; identity is
  compared only with `==`, and no identity number is printed.
- **CI40** — Hash value is invoked only with the question of whether two objects' hashes are
  **equal to each other**, and that question is asked only for components producing a
  value-based hash.

```java
// Gauge.java — members in the class file and the extension trial
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.classfile.*;
import java.lang.reflect.AccessFlag;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;

class Gauge {
    record ClassInfo(boolean isFinal, List<String> fields, List<String> methods) {}

    static Path newDir() throws Exception { return Files.createTempDirectory("gauge"); }

    static boolean compile(Path d, String name, String source) throws Exception {
        Path f = d.resolve(name + ".java");
        Files.writeString(f, source);
        PrintWriter sink = new PrintWriter(Writer.nullWriter());
        return ToolProvider.findFirst("javac").orElseThrow()
                .run(sink, sink, "-d", d.toString(), "-cp", d.toString(), f.toString()) == 0;
    }

    static ClassInfo read(String source, String name) throws Exception {
        Path d = newDir();
        if (!compile(d, name, source)) throw new IllegalStateException("did not compile: " + name);
        ClassModel cm = ClassFile.of().parse(d.resolve(name + ".class"));
        List<String> fields = new ArrayList<>();
        for (FieldModel f : cm.fields())
            fields.add(f.fieldName().stringValue()
                    + (f.flags().has(AccessFlag.FINAL) ? " [final]" : ""));
        List<String> methods = new ArrayList<>();
        for (MethodModel m : cm.methods()) methods.add(m.methodName().stringValue());
        return new ClassInfo(cm.flags().has(AccessFlag.FINAL), fields, methods);
    }
}
```

## One Line, Ten Members

The measured source is a single line, and its body is empty. The course's concrete axis
continues here too: a three-field inventory record — name, count, and weight in grams.

```java
// Produced.java — how many members come out of a one-line record declaration
import java.util.TreeSet;

public class Produced {
    static final String MANUAL = """
        class Manual {
            private final String name; private final int count; private final long weight;
            Manual(String name, int count, long weight) {
                this.name = name; this.count = count; this.weight = weight;
            }
            public String name() { return name; }
            public int count() { return count; }
            public long weight() { return weight; }
            @Override public boolean equals(Object o) {
                return o instanceof Manual e
                        && name.equals(e.name) && count == e.count && weight == e.weight;
            }
            @Override public int hashCode() {
                return java.util.Objects.hash(name, count, weight);
            }
            @Override public String toString() {
                return "Manual[name=" + name + ", count=" + count + ", weight=" + weight + "]";
            }
        }
        """;

    public static void main(String[] args) throws Exception {
        String source = "record Entry(String name, int count, long weight) { }";
        Gauge.ClassInfo k = Gauge.read(source, "Entry");
        System.out.println("source: " + source);
        System.out.println("fields (" + k.fields().size() + "): " + k.fields());
        System.out.println("methods (" + k.methods().size() + "): " + k.methods());
        System.out.println("class final: " + k.isFinal());
        System.out.println();
        Gauge.ClassInfo e = Gauge.read(MANUAL, "Manual");
        System.out.println("source lines: record 1, hand-written " + MANUAL.strip().lines().count());
        System.out.println("member count : record " + (k.fields().size() + k.methods().size())
                + ", hand-written " + (e.fields().size() + e.methods().size()));
        System.out.println("method names equal: "
                + new TreeSet<>(k.methods()).equals(new TreeSet<>(e.methods())));
        System.out.println();
        System.out.printf("%-34s -> does it compile: %s%n", "extending the record",
                Gauge.compile(Gauge.newDir(), "T", source + " class T extends Entry { }"));
        System.out.printf("%-34s -> does it compile: %s%n", "record implementing an interface",
                Gauge.compile(Gauge.newDir(), "T", "interface T { }"
                        + " record Entry(String name) implements T { }"));
        System.out.printf("%-34s -> does it compile: %s%n", "reassigning its field",
                Gauge.compile(Gauge.newDir(), "T",
                        "record Entry(String name) { } class T { void f(Entry k) { k.name = \"x\"; } }"));
    }
}
```

```
source: record Entry(String name, int count, long weight) { }
fields (3): [name [final], count [final], weight [final]]
methods (7): [<init>, toString, hashCode, equals, name, count, weight]
class final: true

source lines: record 1, hand-written 19
member count : record 10, hand-written 10
method names equal: true

extending the record               -> does it compile: false
record implementing an interface   -> does it compile: true
reassigning its field              -> does it compile: false
```

A single declaration line was written in the source; the class file carries **three fields**
and **seven methods**, **ten members** in total. All three fields carry the `final` flag. Of
the seven methods, three are **component readers** (`name`, `count`, `weight`), three are the
surface Java expects from every object (`toString`, `hashCode`, `equals`), and one is the
**canonical constructor** taking the components. This is where it differs from the previous
lesson's hidden fields: these are not hidden, they are a callable surface.

The middle three lines measure what this surface costs when written by hand. A class building
the same ten members by hand is **19 lines**, and it also leaves **10 members** in its class
file; the two classes' method names are identical. A record brings no new mechanism — what
turns up at the class-file level is an ordinary class. What it brings is that **the mistakes
possible in writing these ten members by hand disappear**: when a component is added, it is
possible to forget it in a hand-written equality method; not so in the produced one.

The Java Fundamentals course measured this same thing for enumerations too, and showed that a
declaration writing only two names in the source carries fields and methods absent from the
source in its class file. The two forms use the same mechanism; what differs is what they
produce, not whether they produce.

The three trials in the bottom section show one more side of what is produced. A record
**cannot be extended**: the class carries the `final` flag, and a class extending it does not
compile. Implementing an interface, though, is free. Reassigning its field does not compile
either, because all the fields are `final` — the restriction the previous lesson measured is
enforced here without ever being written in the source.

## Which Side Do the Produced Members Leave the Decision To

A record's not being extendable brings the course's question to an odd place. Since a record
type cannot have a subtype, a name declared with that type will always have the same runtime
type. Does the question, then, lose its meaning for the produced members?

```java
// Side.java — which side do the produced members leave the decision to
import java.util.*;

public class Side {
    interface Carrier { }
    record Entry(String name, int count) implements Carrier { }

    enum Operation {
        ADD { @Override int apply(int a) { return a + 1; } },
        KEEP;
        int apply(int a) { return a; }
    }

    public static void main(String[] args) {
        Carrier t = new Entry("bolt", 12);
        Object n = t;
        System.out.println("declared type Object, toString      : " + n);
        Entry a = new Entry("bolt", 12), b = new Entry("bolt", 12);
        System.out.println("a == b                               : " + (a == b));
        System.out.println("a.equals(b)                          : " + a.equals(b));
        System.out.println("hash codes equal                     : " + (a.hashCode() == b.hashCode()));
        Map<Entry, String> m = new HashMap<>();
        m.put(a, "first");
        System.out.println("read with key b                      : " + m.get(b));
        System.out.println();
        for (Operation i : Operation.values())
            System.out.printf("%-5s class is Operation itself: %-5s | declaring type: %s | apply(1)=%d%n",
                    i, i.getClass() == Operation.class, i.getDeclaringClass().getSimpleName(), i.apply(1));
        System.out.println("Operation.ADD and valueOf(\"ADD\") same object: "
                + (Operation.ADD == Operation.valueOf("ADD")));
    }
}
```

```
declared type Object, toString      : Entry[name=bolt, count=12]
a == b                               : false
a.equals(b)                          : true
hash codes equal                     : true
read with key b                      : first

ADD   class is Operation itself: false | declaring type: Operation | apply(1)=2
KEEP  class is Operation itself: true  | declaring type: Operation | apply(1)=1
Operation.ADD and valueOf("ADD") same object: true
```

The first line answers the question. The declared type is `Object`, and `Object`'s own
toString method does not give what the record gives; the printed text, though, is exactly the
form the record produces. The produced `toString` is an **overriding**, and like every
overriding, leaves the decision to the runtime type. Because a record cannot be extended, that
type is the **only known type** — but the decision still comes from that side. Not being
extendable does not change which side the decision comes from; it makes the answer
**knowable in advance**.

The next four lines show what the produced equality is. `a` and `b` are two separately
constructed objects, and `==` gives **false**: identity is separate. `equals`, though, gives
**true**, because the produced implementation compares the components. The hash values are
equal too, and the observable consequence of this is the last line: a map entry added with `a`
can be read with `b`. These three lines, where identity and equality come apart, are the
definition of how a data carrier should behave — and not a single character of it was written
in the source.

For an enumeration, the same question is asked from the opposite end. `ADD` and `KEEP` share
the same declared type, but `ADD`'s runtime class is **not the enumeration itself**: a
constant with its own body written takes its own class. For `KEEP`, the two are the same. The
two constants give separate answers to the same method call — **2** and **1** — and what
gives the answer, again, is the runtime type. The declaring type stays `Operation` for both;
this shows that, at the point where the declared type and the runtime type come apart, which
set a constant belongs to is not lost. The last line, in turn, measures the set's being fixed:
a constant found by name is the same object as the one found with `==`, because there is
exactly one of every constant.

Why the two forms sit side by side in this lesson is read from here. A record is written for
carriers whose **count is unknown**, and leaves each one's identity to its value; an
enumeration defines a set whose **count is known**, and keeps each one's identity in itself.
In one, `equals` can be true while `==` is false; in the other, the two always answer
together. What they share is the source of their produced members: both declarations have the
compiler produce a surface, and the produced surface's decision comes from the runtime type.

## A Bounding Measurement: A Record Is Shallowly Immutable

All the produced fields were `final`, and the produced equality compared components. Read
together, these two facts make a record look "immutable." The boundary sits exactly here.

```java
// Shallow.java — a record is shallowly immutable
import java.util.Arrays;

public class Shallow {
    record Batch(String name, int[] counts) { }

    record Safe(String name, int[] counts) {
        Safe { counts = counts.clone(); }
    }

    public static void main(String[] args) {
        int[] outside = {1, 2, 3};
        Batch p = new Batch("bolt", outside);
        Safe g = new Safe("bolt", outside);
        System.out.println("after construction  : " + Arrays.toString(p.counts())
                + " | cloning: " + Arrays.toString(g.counts()));
        outside[0] = 99;
        System.out.println("written from outside: " + Arrays.toString(p.counts())
                + " | cloning: " + Arrays.toString(g.counts()));
        System.out.println();
        Batch q = new Batch("bolt", new int[] {99, 2, 3});
        System.out.println("name fields equal   : " + p.name().equals(q.name()));
        System.out.println("array content equal : " + Arrays.equals(p.counts(), q.counts()));
        System.out.println("p.equals(q)         : " + p.equals(q));
    }
}
```

```
after construction  : [1, 2, 3] | cloning: [1, 2, 3]
written from outside: [99, 2, 3] | cloning: [1, 2, 3]

name fields equal   : true
array content equal : true
p.equals(q)         : false
```

After `p` was constructed, none of its members were ever written to; even so, its `counts`
component changed. The side making the change never even saw the record — it wrote to the
array it had in hand, and the record was holding that very same array. As measured in the
previous lesson, a `final` field protects only the binding; the object at the end of that
binding sits outside the restriction. A record applies that restriction per component, so its
protection is only as deep as the components' type — it is **shallow**.

The bottom three lines show the same boundary on the equality side. `p` and `q` now carry the
same name and an array with the same content; both comparisons give true. `p.equals(q)`, even
so, is **false**. The produced equality compares every component by that component's own
equality; an array's own equality is identity. Inside the same record type, one component
looks at value, the other at identity, and nowhere in the source is this distinction written.

Both defects have their remedy in the same place. The `Safe` record clones the array in its
canonical constructor and is unaffected by the outside change; the output's second line shows
this. What the compiler produces is a starting point, not a guarantee: the produced members
inherit the components' behavior, and fixing it is left to whoever writes it.

## Summary

- A one-line record declaration with an empty body produces **three fields** and **seven
  methods** in the class file; all three fields carry the `final` flag.
- The seven produced methods are three component readers, three shared-surface methods
  (`toString`, `hashCode`, `equals`), and the canonical constructor taking the components.
- A record cannot be extended, can implement an interface, and reassigning its field stops
  compilation.
- The produced members leave the decision to the **runtime type**: even when the declared
  type is `Object`, the printed text is the form the record produces. Not being extendable
  does not change which side the decision comes from; it makes the answer knowable in advance.
- For two separately constructed records, `==` is false, `equals` is true, and their hash
  values are equal; in an enumeration, a constant with its own body written takes its own
  class and gives a separate answer to the same call.
- A record's protection is shallow: an array component changes from outside, and the produced
  equality looks at that component's identity, not its value; cloning in the canonical
  constructor fixes both.

## Next Step

Throughout this topic, the same question was measured: did the declared type give the
decision, or the runtime type? In the last two lessons, a third side joined the question — the
compiler itself. It produced the hidden fields, the constructor parameters, and a data
carrier's entire surface; everything it produced sat in the class file and could be read at
runtime. So what happens to another piece of information the compiler holds — **type
information itself**? Which type of element a container holds is written in the source, and
the compiler checks against that writing. The next topic measures how much of that information
survives at runtime: can two containers written with two separate type parameters be told
apart at runtime, and once the side doing the checking is gone, who pays the cost of writing
the wrong thing?
