Chapter 5 of 18
05 — Structs (records)
Records as immutable data with behaviour, and float comparisons with a delta.
Kata: Rectangle, Circle, Triangle with perimeter() and area().
New ideas: records (data + behaviour in one declaration), behaviour-driven method naming,
floating-point comparison with a delta.
Records: data + behaviour in one line
A record is Java’s way to declare an immutable data type where the compiler gives you the
constructor, accessors, equals, hashCode and toString for free:
public record Rectangle(double width, double height) {
public double perimeter() {
return 2 * (width + height);
}
public double area() {
return width * height;
}
}
No boilerplate: width and height become constructor parameters, and the accessors are
width() / height() — not getWidth() / getHeight(). Methods that need the data live
right next to it.
Step 1 — write the test first
@Test
void calculatesPerimeter() {
Rectangle rectangle = new Rectangle(10, 5);
assertEquals(30, rectangle.perimeter(), 0.0001);
}
RED — Rectangle doesn’t exist. Write a stub, not the working code:
public record Rectangle(double width, double height) {
}
Compiles, perimeter() doesn’t exist → still RED. Good: the test names the method before the
compiler has ever seen it.
Step 2 — a subtle Java gotcha: floating-point
Now the honest, important part. assertEquals(10, circle.perimeter(), 0.0001) — see the third
argument?
assertEquals(expected, actual, delta);
double arithmetic (0.1 + 0.2 != 0.3 in Java as in every language) means exact equality on
floating point is fragile. The delta (0.0001) says “equal within one ten-thousandth”. If you
forget it, JUnit 5 has a specific overload: assertEquals(double, double) warns you to add a
delta.
Use it on all geometry:
@Test
void calculatesPerimeter() {
Rectangle rectangle = new Rectangle(10, 5);
assertEquals(30, rectangle.perimeter(), 0.0001);
}
GREEN.
Step 3 — Circle and Triangle prove the pattern
Circle.area() = π·r², Triangle.area() = Heron’s formula √(s(s−a)(s−b)(s−c)).
Same shape: test drives the method, delta compares floats. (Also: Math.sqrt, Math.PI — the
Math class is your stdlib maths.)
REFACTOR — parameterize
Three near-identical cases already? Table-test them:
@ParameterizedTest(name = "Rectangle({0} x {1}) perimeter = {2}")
@CsvSource({ "10, 5, 30", "3, 4, 14", "1, 1, 4" })
void calculatesPerimeter(double width, double height, double expected) {
assertEquals(expected, new Rectangle(width, height).perimeter(), 0.0001);
}
Why this matters for interviews
Records are the default answer to “what’s a plain data holder in modern Java?” (interviewers
will drag you toward class Foo { private fields; getters; setters; equals... } — you respond
“a record, unless I need mutability”). Immutability by default is exactly what test-driven
designs want: no hidden mutable state, no setUp noise, equals is honest.
Run it
gradle test --tests "structs.*"
Next: 06 — interfaces — make the shapes polymorphic through an interface.