Java Record for Lightweight DTOs
Records (Java 14 preview, stable since Java 16) collapse boilerplate immutable data carriers into one line. This snippet shows the canonical record, a compact constructor for validation, and using records as map keys. The runnable code targets Java 13 syntax (since the test runner is OpenJDK 13), with the modern record equivalent shown inline in comments and explanations.
809 views
6
import java.util.Objects;
// On JDK 16+, this whole class collapses to: record Point(int x, int y) {}
final class Point {
private final int x;
private final int y;
Point(int x, int y) { this.x = x; this.y = y; }
public int x() { return x; }
public int y() { return y; }
@Override public boolean equals(Object o) {
if (!(o instanceof Point)) return false;
Point p = (Point) o;
return x == p.x && y == p.y;
}
@Override public int hashCode() { return Objects.hash(x, y); }
@Override public String toString() { return "Point[x=" + x + ", y=" + y + "]"; }
}
public class Main {
public static void main(String[] args) {
Point p1 = new Point(3, 4);
Point p2 = new Point(3, 4);
System.out.println(p1);
System.out.println("equal? " + p1.equals(p2));
System.out.println("x=" + p1.x() + " y=" + p1.y());
}
}This is the long-hand form of record Point(int x, int y) {}. The record declaration auto-generates exactly these members: a canonical constructor, accessor methods named after each component (x(), y()), and value-based equals, hashCode, and toString. Records are implicitly final and their components are final, so instances are immutable and safe to share across threads. Use them for DTOs, pair returns, and value objects, but reach for a regular class when you need mutability or inheritance.
// On JDK 16+ this is:
// record Range(int lo, int hi) {
// Range { if (lo > hi) throw new IllegalArgumentException("lo > hi"); }
// boolean contains(int n) { return n >= lo && n <= hi; }
// }
final class Range {
private final int lo;
private final int hi;
Range(int lo, int hi) {
if (lo > hi) throw new IllegalArgumentException("lo > hi");
this.lo = lo;
this.hi = hi;
}
public int lo() { return lo; }
public int hi() { return hi; }
public boolean contains(int n) { return n >= lo && n <= hi; }
@Override public String toString() { return "Range[" + lo + "," + hi + "]"; }
}
public class Main {
public static void main(String[] args) {
Range r = new Range(1, 10);
System.out.println(r + " contains 7? " + r.contains(7));
try { new Range(5, 1); } catch (IllegalArgumentException e) {
System.out.println("rejected: " + e.getMessage());
}
}
}Records on JDK 16+ accept a compact constructor (no parameter list) where you can validate or normalize the components before they are assigned to the implicit fields. This snippet shows the same pattern in regular-class form so it runs on JDK 13. You can also add extra instance methods like contains to a record exactly as shown here. Throw IllegalArgumentException for invariant violations so the failure surfaces at construction time rather than later.
// Long-hand for: record Pair(int a, int b) {}
final class Pair {
final int a, b;
Pair(int a, int b) { this.a = a; this.b = b; }
@Override public int hashCode() { return java.util.Objects.hash(a, b); }
@Override public boolean equals(Object o) {
if (!(o instanceof Pair)) return false;
Pair p = (Pair) o;
return a == p.a && b == p.b;
}
@Override public String toString() { return "(" + a + "," + b + ")"; }
}
public class Main {
public static void main(String[] args) {
java.util.Map<Pair, String> seen = new java.util.HashMap<>();
seen.put(new Pair(0, 0), "origin");
seen.put(new Pair(1, 2), "target");
System.out.println(seen.get(new Pair(0, 0)));
System.out.println(seen.containsKey(new Pair(1, 2)));
System.out.println("size=" + seen.size());
}
}Because records (and the equivalent class shown here) generate value-based equals and hashCode, they work as HashMap keys and HashSet members out of the box. This is the killer feature versus a plain class with reference equality: two newly constructed Pair(0, 0) instances hash the same and look up the same entry. On JDK 16+ the Pair class collapses to record Pair(int a, int b) {}. Avoid records as keys only if any component is mutable in a way that affects equality, but records enforce immutability for you, so this rarely happens in practice.
