Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
108: Records
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
WeatherReadingwith the three fields mentioned above. - Implement a compact constructor within the record to ensure that the
humidityis between 0.0 and 100.0 (inclusive). If the humidity is outside this range, throw anIllegalArgumentExceptionwith the message "Humidity must be between 0 and 100". - In a main method, instantiate a valid
WeatherReadingand print it to the console to verify that the automatictoString()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.
There are no comments for now.