Skip to Content
Course content
Click on the "Edit" button in the top corner of the screen to edit your slide content.

Look, we've all been there. You need a simple class to carry some data from a database to a UI, or maybe just to group two values together so you aren't passing seven different arguments into a method. In the old days of Java, this meant writing a "POJO" (Plain Old Java Object). But let's be honest: calling it "plain" is a joke when you have to write 50 lines of code for a class that essentially does nothing.

The Boilerplate Tax

Imagine we're building a pricing engine. We need a way to represent a price along with its currency. The "correct" way to do this for years was to create an immutable class. If you didn't make it immutable, you'd risk some random service changing the currency of a price halfway through a transaction, which is a nightmare to debug.

public final class ProductPrice {
    private final BigDecimal amount;
    private final String currency;

    public ProductPrice(BigDecimal amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }

    public BigDecimal getAmount() {
        return amount;
    }

    public String getCurrency() {
        return currency;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        ProductPrice that = (ProductPrice) o;
        return Objects.equals(amount, that.amount) && 
               Objects.equals(currency, that.currency);
    }

    @Override
    public int hashCode() {
        return Objects.hash(amount, currency);
    }

    @Override
    public String toString() {
        return "ProductPrice{" + "amount=" + amount + ", currency='" + currency + '\'' + '}';
    }
}

Now, I know what you're thinking: "My IDE generates all of that for me." Sure it does. But that code still lives in your source file. It's visual noise. Worse, if you add a third field—say, a discountCode—you have to remember to update the constructor, the getters, the equals method, the hashCode method, and the toString method. If you forget just one of those, you've introduced a subtle bug that might only show up in production when two identical prices are treated as different objects in a HashMap.

The Record Shortcut

This is exactly why Java introduced Records. A record is essentially a signal to the compiler saying, "This class is just a transparent carrier for immutable data." When you use a record, the compiler does all that heavy lifting for you behind the scenes.

public record ProductPrice(BigDecimal amount, String currency) {}

That's it. That one line replaces the entire block of code I wrote above. You get the private final fields, the constructor, the equals(), the hashCode(), and a clean toString() automatically. I'll point out one small detail: you'll notice there are no "getters" in the record definition. Records don't use the getAmount() naming convention; they just use amount(). It's a subtle shift, but it emphasizes that you're accessing a component of the data, not calling a traditional JavaBean getter.

When Records Break the Rules

You might be wondering if you can just replace every class with a record. Not quite. The biggest trade-off is that records are immutable. You cannot have a setter method in a record. If your logic requires you to change the state of an object after it's been created, a record is the wrong tool. You'd be better off with a standard class.

However, if you need to add some basic validation—like ensuring the amount isn't negative—you don't have to go back to the old way. Records allow something called a "compact constructor." It's a concise way to validate data without having to repeat the assignment of every single field:

public record ProductPrice(BigDecimal amount, String currency) {
    public ProductPrice {
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("Price cannot be negative");
        }
    }
}

I love this pattern because it keeps the focus on the business logic (the validation) rather than the plumbing (assigning this.amount = amount). It makes the intent of the code immediately obvious to anyone reading it after you.




📋 Practical Task

Implementing a WeatherReading Data Carrier

You are building a meteorological application that collects data from various sensors. You need a data structure to hold a WeatherReading, which consists of a double temperature, a double humidity, and a String stationId.

Your Task:

  • Create a Java Record named WeatherReading with the three fields mentioned above.
  • Implement a compact constructor within the record to ensure that the humidity is between 0.0 and 100.0 (inclusive). If the humidity is outside this range, throw an IllegalArgumentException with the message "Humidity must be between 0 and 100".
  • In a main method, instantiate a valid WeatherReading and print it to the console to verify that the automatic toString() method is working as expected.
  • Attempt to instantiate an invalid WeatherReading (e.g., humidity of 110.0) and wrap it in a try-catch block to verify your validation logic works.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.