0% completed
Records and Enums
On This Page
What the Compiler Writes for a Record
The Compact Constructor
Records as Data Transfer Objects
Enums With Fields
Switch Expressions
Key Takeaways
Practice Questions
A record is a class whose only job is to carry a fixed set of values, declared in one line:
public record BookingRequest(long eventId, String seatLabel) {}
From it, the compiler writes a constructor, a reading method for each value, equals, hashCode, and toString. The Java documentation calls a record a shallowly immutable, transparent carrier for a fixed set of values, called the record components. Here the components are eventId and seatLabel.
The booking service needs several such carriers, starting with the body of POST /bookings and its response. It also needs a fixed set of states for a booking. The sections below cover records, the constructor that checks their values, enums with fields, and switch expressions over those enums.
What the Compiler Writes for a Record
For each record, the compiler generates a private final field and a reading method per component, a constructor that takes all components, and equals, hashCode, and toString. The reading methods, called accessors, use the component names without a "get" prefix: request.eventId(), not getEventId().
Equality is by value. Two BookingRequest objects with event 7 and seat A12 are equal, although they are separate objects. A record whose components cannot change, such as this one, is therefore safe as a map key and easy to compare in tests.
The compiler also enforces what a record may not do. When we tried, javac rejected two changes:
- An extra instance field in the body failed with "field declaration must be static", and the hint "consider replacing field with record component".
- Assigning a component from outside failed with "cannot assign a value to final variable seatLabel".
A record therefore cannot gain hidden state, and no code can assign its components again after construction. It can still declare methods, static fields, and extra constructors.
The Compact Constructor
A compact constructor is a record constructor written without a parameter list, and it runs before the fields are assigned. It can therefore check and clean the values. The BookingRequest of the booking service uses one:
public record BookingRequest(long eventId, String seatLabel) { public BookingRequest { if (eventId <= 0) { throw new IllegalArgumentException("eventId must be positive"); } if (seatLabel == null || seatLabel.isBlank()) { throw new IllegalArgumentException("seatLabel is required"); } seatLabel = seatLabel.trim().toUpperCase(); } }
The last line assigns to the parameter, not to the field. After the body ends, each parameter is assigned to its field, so the cleaned label is stored. No BookingRequest can exist with a blank or lowercase label.
The program below runs this record, a response record, an enum, and a switch expression. Run it and compare part 2 with the constructor above.
It prints the following:
1. What the compiler wrote for the record toString: BookingRequest[eventId=7, seatLabel=A12] accessors: eventId=7, seatLabel=A12 equal values, equal records: true 2. The compact constructor checks and cleans input " a12 " becomes: A12 input " " fails: seatLabel is required event 0 fails: eventId must be positive 3. A record is shallowly immutable caller's list changed, record still holds: [A12] adding through the accessor: UnsupportedOperationException 4. Enum constants with a field, and a switch expression CONFIRMED: holds seat true, message "Your seat is booked" CANCELED: holds seat false, message "Your booking was canceled" response: BookingResponse[id=1, eventId=7, seatLabel=A12, status=CONFIRMED] 5. valueOf needs the exact name valueOf("CANCELED"): CANCELED valueOf("CANCELLED"): No enum constant Solution.BookingStatus.CANCELLED
Part 3 shows the meaning of "shallowly" immutable. The SeatMap record holds a list, and its compact constructor stores a copy made with List.copyOf. The caller's later change does not reach the record, and the copy rejects changes. Without the copy, the field would still point to the same list, but that list could change.
Records as Data Transfer Objects
A data transfer object, or DTO, is an object whose only job is to carry data across a boundary such as the HTTP API. A record fits that job exactly. In Spring MVC, a controller can receive a record as the request body and return one:
@RestController class BookingController { private final AtomicLong nextId = new AtomicLong(1); @PostMapping("/bookings") BookingResponse book(@RequestBody BookingRequest request) { return new BookingResponse(nextId.getAndIncrement(), request.eventId(), request.seatLabel(), BookingStatus.CONFIRMED.name()); } }
This controller does not charge or store anything yet. It only shows how records travel. We started the application, sent one booking for seat A12, and then sent a lowercase label with spaces:
curl -X POST localhost:8102/bookings -H 'Content-Type: application/json' -d '{"eventId":7,"seatLabel":" a12 "}'
{"id":2,"eventId":7,"seatLabel":"A12","status":"CONFIRMED"}
Jackson, the JSON library from Inside a Spring Boot Project, matched the JSON fields to the record components by name. It then called the constructor, so the compact constructor cleaned the label. A blank label produced a 400 Bad Request response, and the log said why:
JSON parse error: Cannot construct instance of `com.example.booking.bookings.BookingRequest`, problem: seatLabel is required
Notice that the check ran before any controller code. A request with a blank label never reached the method book, because no BookingRequest could be built from it.
Enums With Fields
An enum is a type with a fixed set of named constants, and each constant is an object that can carry fields and methods. Each booking state carries a field that says whether it holds the seat:
public enum BookingStatus { CONFIRMED(true), CANCELED(false); private final boolean holdsSeat; BookingStatus(boolean holdsSeat) { this.holdsSeat = holdsSeat; } public boolean holdsSeat() { return holdsSeat; } }
Code that builds the seat map can ask status.holdsSeat() instead of comparing names in several places. A state added later gets its answer once, next to the constant.
Enum names must match exactly when they come from outside. Part 5 shows that valueOf("CANCELLED"), with the British spelling, fails with "No enum constant". Any JSON body or database column that stores a status by name must use CANCELED.
Switch Expressions
A switch expression is a switch that produces a value. In the arrow form used here, case CONFIRMED ->, each case runs only its own code and never falls through to the next case. The method customerMessage in the program uses one:
static String customerMessage(BookingStatus status) { return switch (status) { case CONFIRMED -> "Your seat is booked"; case CANCELED -> "Your booking was canceled"; }; }
A switch expression must cover every possible value. When we removed the CANCELED case, javac refused the code with "the switch expression does not cover all possible input values". That error is useful. If someone adds a third status, every switch expression over BookingStatus without a default stops compiling until it handles the new state. The older switch statement, which produces no value, gets no such check: ours compiled with a case missing.
Notice that the failure happens at compile time, not in production. A default case would hide it, so a switch over your own enum usually lists every constant.
Key Takeaways
- A record declares its components once, and the compiler writes the fields, constructor, accessors, equals, hashCode, and toString.
- A compact constructor checks and cleans values before they are stored, so an invalid record cannot exist.
- Records are shallowly immutable: copy a list with List.copyOf if the record must not change.
- Spring MVC turns JSON into a request record through its constructor and turns a response record back into JSON.
- Enum constants can carry fields, and a switch expression over an enum must cover every constant.
Spring found the controller above without being told its name, and it knew that book handles POST /bookings. The answer lies in the annotations on the class and the method, which Annotations and Reflection reads while the program runs.
Practice Questions
1. A client sends {"eventId":7,"seatLabel":"b7"} to POST /bookings. What does the controller receive as seatLabel, and where did the change happen?
It receives "B7". Jackson called the record's constructor with "b7", and the compact constructor trimmed and upper-cased it before the field was assigned. The controller never sees the original text.
</details>2. A developer wants BookingRequest to count how many times it was validated, and adds private int checks; to the record body. What happens, and what does it tell you about records?
The code does not compile: javac reports "field declaration must be static". A record may hold only its declared components as instance state. That rule keeps equals, hashCode, and toString complete, because they are built from the components alone.
</details>3. A team adds REFUNDED to BookingStatus. Which code breaks at compile time, and which code might misbehave silently?
<details><summary>Show answer</summary>Every switch expression over BookingStatus without a default stops compiling, which shows exactly where the new state needs a decision. Code with a default case compiles and quietly treats REFUNDED as the default. Switch statements, if statements, and code that compares strings also compile, and may treat a refunded booking as if it still held its seat. REFUNDED also needs its own holdsSeat value, which the constructor call forces the developer to write.
</details>4. A record SeatMap holds List<String> takenSeats without a compact constructor. A caller creates it with a list and then adds "A13" to that list. What does the record now contain, and how do you prevent it?
The record now contains A13 as well, because it holds a reference to the caller's list, not a copy. A record is only shallowly immutable: the field cannot point to another list, but the list itself can change. Storing List.copyOf(takenSeats) in a compact constructor makes the record independent of the caller's list and rejects later changes.
Reading Progress
0%
On This Page
What the Compiler Writes for a Record
The Compact Constructor
Records as Data Transfer Objects
Enums With Fields
Switch Expressions
Key Takeaways
Practice Questions