In Java, both classes and records are used to define new data types, but they serve different purposes and have distinct characteristics. In this blog post, we will discuss the comparison between Java records (introduced in Java 16) and traditional classes with examples.
What is a Java Record?
Java Records, introduced as a preview feature in Java 14 and made standard in Java 16, represent a significant addition to the Java language. A record is a special kind of class in Java designed specifically for holding immutable data. The primary use case for records is to act as simple data carriers without the need for additional encapsulation or behavior.Records are a concise way to create classes that are primarily data carriers. They reduce boilerplate code significantly by automatically generating field accessors, constructors, equals(), hashCode(), and toString() methods.
Here's an example:
public record User(Long id, String firstName, String lastName, String email) {}
This single line generates:- Private final fields for id, firstName, lastName, and email.
- A public constructor.
- Public getter methods.
- Overridden equals(), hashCode(), and toString() methods.
Traditional Java Classes
public class User {
private final Long id;
private final String firstName;
private final String lastName;
private final String email;
public User(Long id, String firstName, String lastName, String email) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
public Long getId() {
return id;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public String getEmail() {
return email;
}
@Override
public boolean equals(Object o) {
// Custom implementation of equals
}
@Override
public int hashCode() {
// Custom implementation of hashCode
}
@Override
public String toString() {
// Custom implementation of toString
}
}
Key Differences with Examples
1. Boilerplate Code
Class
public class User {
private final Long id;
private final String firstName;
private final String lastName;
private final String email;
public User(Long id, String firstName, String lastName, String email) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
public Long getId() {
return id;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public String getEmail() {
return email;
}
@Override
public boolean equals(Object o) {
// Custom implementation of equals
}
@Override
public int hashCode() {
// Custom implementation of hashCode
}
@Override
public String toString() {
// Custom implementation of toString
}
}
Record
public record User(Long id, String firstName, String lastName, String email) {}
2. Immutability
Class
Classes can define mutable objects. The fields of a class can be modified after the object is created unless they are explicitly declared as final.
Example: Classes can be designed to be immutable, but it's up to the developer.
public class User {
// Same as previous example
// Setters are not provided, making this implementation immutable
}
Record
Records are inherently immutable. The state of a record is defined at the time of its creation and cannot be changed later. All fields in a record are final.
User user = new User(1L, "John", "Doe", "john.doe@email.com");
// user.id = 2L; // This would result in a compilation error
3. Inheritance
Class
A class can extend another class and can be extended by other classes, supporting inheritance.
Example: PremiumUser class extends User class:
public class PremiumUser extends User {
private final String membershipLevel;
public PremiumUser(Long id, String firstName, String lastName, String email, String membershipLevel) {
super(id, firstName, lastName, email);
this.membershipLevel = membershipLevel;
}
public String getMembershipLevel() { return membershipLevel; }
}
Record
Records cannot extend any other class and cannot be extended, thus not supporting inheritance. They implicitly extend java.lang.Record.
Example: Records cannot extend another class and are implicitly final.
// Cannot extend any class
public record User(Long id, String firstName, String lastName, String email) {}
4. Flexibility
Class
Classes offer more flexibility in design. They can contain a mix of data and methods and can be designed to change state.
Example: Can include various behaviors and states.
public class User {
// Same as previous example
public void printUserInfo() {
System.out.println("User Info: " + this.toString());
}
}
Record
Records are less flexible but more concise. They are intended to be simple carriers of data and do not support methods that change state.
Example: Limited to data representation.
public record User(Long id, String firstName, String lastName, String email) {
// Cannot add additional behavior or state
}
5. Field Access
Class
Classes can have private, protected, or public fields, and can include custom getter and setter methods.
Example: We'll create a User class with private fields and public getter methods, then access these fields and print the data.
class User {
private Long id;
private String firstName;
private String lastName;
private String email;
public User(Long id, String firstName, String lastName, String email) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
public Long getId() { return id; }
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public String getEmail() { return email; }
}
public class ClassDemo {
public static void main(String[] args) {
User user = new User(1L, "John", "Doe", "john.doe@example.com");
System.out.println("ID: " + user.getId());
System.out.println("First Name: " + user.getFirstName());
System.out.println("Last Name: " + user.getLastName());
System.out.println("Email: " + user.getEmail());
}
}
Record
Records generate public getter methods for all fields automatically, but these fields are not directly accessible; they are accessed through these getter methods.
Example: We'll create a User record, access its fields using the getter methods, and print the data.
public record User(Long id, String firstName, String lastName, String email) {}
public class RecordDemo {
public static void main(String[] args) {
User user = new User(1L, "John", "Doe", "john.doe@example.com");
System.out.println("ID: " + user.id());
System.out.println("First Name: " + user.firstName());
System.out.println("Last Name: " + user.lastName());
System.out.println("Email: " + user.email());
}
}
In both examples, we create a User instance, access its fields through the available methods (which are automatically generated in the case of the record), and then print the values. The key difference lies in the syntax and the automatic generation of these methods in records.
6. Constructor Customization
Class
Classes can have multiple constructors with different parameters.
public class User {
public User(String email) {
// Initialize with email only
}
public User(String firstName, String lastName) {
// Initialize with first and last name
}
}
Record
Records can have custom constructors, but they must be delegated to the primary constructor, and they must initialize all fields.
Example: In this record, there's a compact constructor performing validation. It still initializes all fields implicitly.
public record User(Long id, String firstName, String lastName, String email) {
public User {
if (id == null) {
throw new IllegalArgumentException("ID cannot be null");
}
}
}
7. Serialization
Class
Serialization with classes can be customized using methods like writeObject and readObject.
public class User implements Serializable {
private void writeObject(ObjectOutputStream out) throws IOException {
// Custom serialization logic
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
// Custom deserialization logic
}
}
This allows for fine-grained control over the serialization process.
Record
Records are serializable as long as all their components are serializable. However, customization in the serialization process is limited.
public record User(Long id, String firstName, String lastName, String email) implements Serializable {}
In records, the serialization process is straightforward but less customizable.
Cheat Sheet
Conclusion
Java records offer a way to define data-centric, immutable types with less code. They're ideal for simple, straightforward data carriers. Traditional classes, however, provide more flexibility and control over the behavior and state of objects, making them suitable for more complex scenarios. The choice between them hinges on the specific needs of your application, whether you prioritize brevity and immutability or require more complex behavior and mutable state.
Comments
Post a Comment
Leave Comment