1. Extending a record class: additional methods
Can you add methods to a record?
Absolutely! A record is like a move-in-ready apartment: the walls and floors are finished and can’t be changed, but you’re free to arrange the furniture however you like. Inside a record you can declare instance methods, static methods, and even store constants. This means you don’t have to move business logic into separate “utility” classes—you can neatly embed it right into the record itself.
Example: a method to compute distance between points
Suppose we have a record for a point on a plane:
public record Point(int x, int y) {
// Additional method
public double distanceTo(Point other) {
int dx = this.x - other.x;
int dy = this.y - other.y;
return Math.sqrt(dx * dx + dy * dy);
}
}
Now you can do this:
Point p1 = new Point(0, 0);
Point p2 = new Point(3, 4);
System.out.println(p1.distanceTo(p2)); // 5.0
As you can see, you can “enrich” a record class with your own methods—and it’s very convenient!
Example: a static method
public record Rectangle(int width, int height) {
public int area() {
return width * height;
}
public static Rectangle square(int size) {
return new Rectangle(size, size);
}
}
Now you can create a “square” with a single call:
Rectangle r = Rectangle.square(5);
System.out.println(r.area()); // 25
2. Compact constructor and data validation
Why do we need a “compact” constructor?
The canonical record constructor is generated automatically and assigns parameters to fields. But sometimes you want to add input validation (for example, disallow negative coordinates).
In a regular class we’d write:
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
if (x < 0 || y < 0) throw new IllegalArgumentException();
this.x = x;
this.y = y;
}
// ...
}
In a record class, you can declare a compact constructor—without repeating the parameter list and without explicitly assigning fields (the compiler does that for us).
Compact constructor syntax
public record Point(int x, int y) {
public Point {
if (x < 0 || y < 0) {
throw new IllegalArgumentException("Coordinates must be non-negative");
}
// No need to write: this.x = x; this.y = y;
}
}
- The constructor parameters automatically match the record components.
- The assignments this.x = x and this.y = y are performed by the compiler automatically after the constructor body completes successfully.
- If an exception is thrown, the object will not be created.
Validation example
Point p1 = new Point(3, 5); // OK
Point p2 = new Point(-1, 2); // Throws IllegalArgumentException!
Can you declare a “regular” constructor?
Yes, you can! If you need a constructor with a different parameter list or additional logic, declare it explicitly:
public record Range(int from, int to) {
public Range(int size) {
this(0, size); // calls the canonical constructor
}
}
3. Limitations of record classes
Unlike regular classes, record classes come with a number of restrictions. It’s important to remember these to avoid being surprised by compilation errors.
Components only—no additional instance fields
You cannot declare new instance fields in a record class:
public record Person(String name, int age) {
// int id; // Compilation error! You cannot add instance fields.
}
You can declare static fields and methods:
public record Person(String name, int age) {
public static final String SPECIES = "Homo sapiens";
}
A record is always final
A record class cannot be a parent (you cannot inherit from it) and itself cannot explicitly extend another class (other than the implicit inheritance from java.lang.Record). This means a record class is always a “final” structure.
public record User(String login) { }
// public class Admin extends User {} // Error: cannot extend a record!
You can implement interfaces
A record class can implement interfaces:
public interface Printable {
void print();
}
public record Invoice(int amount) implements Printable {
@Override
public void print() {
System.out.println("Amount: " + amount);
}
}
4. Examples: extended records in real tasks
Let’s look at a few practical examples where additional methods and compact constructors make a record class truly useful.
Record with a computed method
public record Circle(double x, double y, double radius) {
public double area() {
return Math.PI * radius * radius;
}
public double distanceTo(Circle other) {
double dx = x - other.x;
double dy = y - other.y;
return Math.sqrt(dx * dx + dy * dy);
}
}
Record with validation
public record Email(String value) {
public Email {
if (value == null || !value.contains("@")) {
throw new IllegalArgumentException("Invalid email: " + value);
}
}
}
Now you can’t create an invalid email:
Email e1 = new Email("test@example.com"); // OK
Email e2 = new Email("not-an-email"); // Throws IllegalArgumentException
Record with additional static methods
public record Temperature(double celsius) {
public static Temperature fromFahrenheit(double fahrenheit) {
return new Temperature((fahrenheit - 32) * 5 / 9);
}
public double toFahrenheit() {
return celsius * 9 / 5 + 32;
}
}
Usage:
Temperature t = Temperature.fromFahrenheit(98.6);
System.out.println(t.celsius()); // 37.0
System.out.println(t.toFahrenheit()); // 98.6
5. Compact constructor: nuances and limitations
When should you use a compact constructor?
- When you need to validate data.
- When you need to transform values before storing them (e.g., round a number or convert a string to upper case).
- When you want to avoid duplicating the parameter list.
How it works
- In a compact constructor you cannot explicitly assign to components (this.x = ...)—that will cause a compilation error, because the compiler performs the assignment itself after the constructor body finishes.
- In a compact constructor you cannot change parameter names—they always match the record component names.
Example: automatic rounding
public record Money(double amount) {
public Money {
amount = Math.round(amount * 100) / 100.0; // Round to cents
}
}
6. Practice: building out the training app
Suppose we’re implementing banking operations in a training app. Let’s say we have a record class Transaction that stores the amount, the sender, and the recipient.
public record Transaction(String from, String to, double amount) {
public Transaction {
if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
if (from == null || to == null) throw new IllegalArgumentException("Fields cannot be null");
}
public String description() {
return String.format("Transfer %.2f from %s to %s", amount, from, to);
}
}
Usage:
Transaction t = new Transaction("Alice", "Bob", 150.0);
System.out.println(t.description()); // Transfer 150.00 from Alice to Bob
Attempting to create an invalid transaction will throw an error:
Transaction t2 = new Transaction("Alice", "Bob", -10.0); // IllegalArgumentException
Table: what you can and cannot do in a record class
| Allowed in a record class | Not allowed in a record class |
|---|---|
| Regular methods | New instance fields |
| Static methods and fields | Extending other classes |
| Implement interfaces | Be a superclass for others |
| Compact and regular constructors | Mutate components after creation |
| Override methods | Use setters |
7. Common mistakes when working with record classes with a non-standard body
Mistake No. 1: trying to add an instance field.
Beginners often try to add “one more field for internal logic” to a record class—for example, a counter or a cache. This won’t work: the compiler will immediately report an error. If you need to store additional state, a record class is probably not the right choice.
Mistake No. 2: forgetting validation in the compact constructor.
If you want the object to always be valid, perform checks in the compact constructor. Don’t rely on the assumption that “the user won’t enter nonsense.”
Mistake No. 3: attempting to modify a component after creation.
Fields of a record class are final—you cannot change them either directly or via methods. If you need a mutable structure, use a regular class.
Mistake No. 4: duplicating logic in methods and in the constructor.
Sometimes validation logic and computation logic are duplicated both in methods and in the constructor. It’s better to perform all validation in the constructor and keep methods for “pure” business logic.
Mistake No. 5: forgetting about inheritance restrictions.
A record class is always final—you cannot create a subclass of it. If you’re designing a hierarchy that needs subclasses, use regular classes.
GO TO FULL VERSION