0Pricing
Java Academy · Lesson

Custom Methods on Records

Add instance methods and static factory methods to enrich record functionality.

Custom Methods on Records is a free Java Academy lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Java Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Custom Methods on Records

Records can contain instance methods, static methods, and static fields — any behavior that derives from or relates to the record's components.

Instance Methods on Records

Add methods to records to compute derived values or provide useful operations based on the components.

record Money(long cents, String currency) {
    public double amount() { return cents / 100.0; }
    public String display() { return String.format("%s %.2f", currency, amount()); }
    public Money add(Money other) {
        if (!currency.equals(other.currency))
            throw new IllegalArgumentException("Currency mismatch");
        return new Money(cents + other.cents, currency);
    }
    public Money multiply(int factor) { return new Money(cents * factor, currency); }
}

Money price = new Money(1999, "USD");
Money tax = new Money(160, "USD");
System.out.println(price.add(tax).display()); // USD 21.59

Static Factory Methods

Static factory methods provide named, readable ways to construct records — hiding the raw constructor when it is less clear.

import java.time.*;

record DateRange(LocalDate start, LocalDate end) {
    DateRange { if (start.isAfter(end)) throw new IllegalArgumentException(); }

    public static DateRange of(LocalDate start, LocalDate end) {
        return new DateRange(start, end);
    }

    public static DateRange thisMonth() {
        LocalDate now = LocalDate.now();
        return new DateRange(now.withDayOfMonth(1),
            now.withDayOfMonth(now.lengthOfMonth()));
    }

    public long durationDays() {
        return ChronoUnit.DAYS.between(start, end) + 1;
    }
}

System.out.println(DateRange.thisMonth().durationDays());

Predicate Methods

Records frequently expose boolean predicate methods to check conditions on the data they hold.

record User(String email, String plan, boolean emailVerified) {
    public boolean isPro()      { return "PRO".equals(plan); }
    public boolean canPublish() { return isPro() && emailVerified; }
    public boolean isFree()     { return "FREE".equals(plan); }

    public User withPlan(String newPlan) {
        return new User(email, newPlan, emailVerified); // "update"
    }
}

User u = new User("alice@example.com", "FREE", true);
System.out.println(u.canPublish()); // false
User upgraded = u.withPlan("PRO");
System.out.println(upgraded.canPublish()); // true

Wither Methods (Copy-With)

Since records are immutable, "updating" means creating a new record. Wither methods follow the pattern withX(newValue).

record ServerConfig(String host, int port, boolean ssl, int timeout) {
    public ServerConfig withHost(String h)    { return new ServerConfig(h, port, ssl, timeout); }
    public ServerConfig withPort(int p)       { return new ServerConfig(host, p, ssl, timeout); }
    public ServerConfig withSsl(boolean s)    { return new ServerConfig(host, port, s, timeout); }
    public ServerConfig withTimeout(int t)    { return new ServerConfig(host, port, ssl, t); }
}

ServerConfig dev = new ServerConfig("localhost", 8080, false, 30);
ServerConfig prod = dev
    .withHost("api.example.com")
    .withPort(443)
    .withSsl(true)
    .withTimeout(60);
System.out.println(prod);

Records Implementing Functional Interfaces

A record that also implements a functional interface can be passed directly as a lambda replacement.

import java.util.function.*;

record Validator<T>(Predicate<T> rule, String message)
    implements Predicate<T> {

    public boolean test(T value) { return rule.test(value); }

    public String validate(T value) {
        return test(value) ? null : message;
    }
}

Validator<String> notEmpty = new Validator<>(
    s -> !s.isBlank(), "Must not be blank");
Validator<String> validEmail = new Validator<>(
    s -> s.contains("@"), "Invalid email");

System.out.println(notEmpty.validate(""));        // Must not be blank
System.out.println(validEmail.validate("a@b.c")); // null (valid)

Aggregation Methods

Records that hold collections can expose stream-based aggregation methods.

import java.util.*;

record SalesReport(String region, List<Double> dailySales) {
    SalesReport { dailySales = List.copyOf(dailySales); }

    public double total()   { return dailySales.stream().mapToDouble(Double::doubleValue).sum(); }
    public double average() { return dailySales.stream().mapToDouble(Double::doubleValue).average().orElse(0); }
    public double max()     { return dailySales.stream().mapToDouble(Double::doubleValue).max().orElse(0); }
    public int    days()    { return dailySales.size(); }
}

SalesReport r = new SalesReport("North", List.of(1200.0, 1450.0, 980.0, 1600.0));
System.out.printf("Total: %.0f, Avg: %.0f, Max: %.0f%n", r.total(), r.average(), r.max());
// Total: 5230, Avg: 1307, Max: 1600

Custom toString Override

While the auto-generated toString is useful for debugging, you may want to override it for user-facing display.

record Temperature(double celsius) {
    @Override
    public String toString() {
        return String.format("%.1f\u00b0C / %.1f\u00b0F",
            celsius, toFahrenheit());
    }

    public double toFahrenheit() { return celsius * 9.0/5.0 + 32; }
    public double toKelvin()     { return celsius + 273.15; }

    public static Temperature fromFahrenheit(double f) {
        return new Temperature((f - 32) * 5.0/9.0);
    }
}

Temperature boiling = new Temperature(100.0);
System.out.println(boiling); // 100.0°C / 212.0°F

Records as Value Objects in DDD

In Domain-Driven Design, records map naturally to Value Objects — immutable types defined by their attributes, not their identity.

// Value objects in a banking domain
record AccountId(String value) {
    AccountId { if (!value.matches("ACC-[0-9]{8}")) throw new IllegalArgumentException(); }
}
record Amount(long cents, String currency) {
    public static Amount usd(long cents) { return new Amount(cents, "USD"); }
    public boolean isPositive() { return cents > 0; }
}
record TransactionId(java.util.UUID value) {
    public static TransactionId generate() {
        return new TransactionId(java.util.UUID.randomUUID());
    }
}

AccountId id = new AccountId("ACC-12345678");
Amount amount = Amount.usd(5000);
TransactionId txId = TransactionId.generate();
System.out.println(id + " " + amount + " " + txId);

Comparing Records with Comparator

Records are sortable using Comparator.comparing with method references pointing to accessors.

import java.util.*;

record Employee(String name, String dept, double salary) {}

List<Employee> employees = List.of(
    new Employee("Alice",   "Engineering", 95000),
    new Employee("Bob",     "Marketing",   72000),
    new Employee("Charlie", "Engineering", 105000),
    new Employee("Diana",   "Marketing",   81000)
);

// Sort by dept, then salary descending
employees.stream()
    .sorted(Comparator.comparing(Employee::dept)
        .thenComparing(Comparator.comparingDouble(Employee::salary).reversed()))
    .forEach(e -> System.out.printf("%-10s %-15s %.0f%n",
        e.name(), e.dept(), e.salary()));

Static Utility Methods on Records

Records can have static methods that work with multiple record instances — useful for transformations and comparisons.

import java.util.*;
import java.util.stream.*;

record Product(String id, String category, double price) {
    public static Map<String, DoubleSummaryStatistics> statsByCategory(
            List<Product> products) {
        return products.stream().collect(
            Collectors.groupingBy(Product::category,
                Collectors.summarizingDouble(Product::price)));
    }

    public static Optional<Product> cheapest(List<Product> products) {
        return products.stream()
            .min(Comparator.comparingDouble(Product::price));
    }
}

Quick Check

You want to "update" a record field. What is the correct approach?

Recap: Custom Methods on Records

Key takeaways:

  • Records can have instance methods, static methods, and static fields
  • Add computed properties as methods (area(), total(), display())
  • Use static factory methods for readable named constructors
  • Implement wither methods (withX) for immutable updates
  • Override toString() for user-facing display instead of debug output
  • Records map naturally to Value Objects in Domain-Driven Design

Frequently asked questions

Is the “Custom Methods on Records” lesson free?

Yes — the full text of “Custom Methods on Records” is free to read here on the web, and the Java Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Java Academy course, upgrade to CoddyKit PRO.

What will I learn in “Custom Methods on Records”?

Add instance methods and static factory methods to enrich record functionality. You practise Java Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Java Academy?

No prior experience is required. Java Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Custom Methods on Records” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Java Academy lesson?

Yes. Every Java Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Introducing Records
  2. Compact Constructors and Validation
  3. Custom Methods on Records
  4. Records vs Classes vs Lombok
← Back to Java Academy