If you are using any traditional approach to transfer data from one layer to another without changing its value in your project, it is advisable that you can start using record in order to reduce a big amount of boilerplate code. Additionally, if you use the โrecordโ, you donโt have to write a single line of code in order to make a class immutable. In this article, we will explore the concept of record in Java with examples, including their syntax, how to create and use them, and some other features in detail.
When to use Record in Java?
If you want to transfer immutable data between different layers of your application, then using a record in Java can be a good choice. By default, Records are immutable in Java, which means that we canโt change their properties after they are created. It can also be helpful in avoiding bugs and improving code reliability. In simple words, we can auto-generate classes using Record in Java.
Where to use a Record in Java?
Generally, we can use records in any situation where you need to declare simple data containers with immutable properties and auto-generated methods. For example, below are a few use cases where records can be useful:
Data transfer objects (DTOs): We can use Records to declare simple data transfer objects that hold data. This is useful when transferring data between different layers of an application, such as between the service layer and the database layer.
Configuration objects: Records can be used to declare configuration objects that hold a set of configuration properties for an application or a module. These objects typically have immutable properties, making them thread-safe and easy to reason about.
Value objects: Records can be used to declare value objects that hold a set of values that represent a particular concept or domain model.
API Responses: When building REST APIs, it is common to return data in the form of JSON or XML. In such cases, you might want to define a simple data structure that represents the API response. Records are ideal for this because they allow you to define a lightweight and immutable data structure that can be easily serialized to JSON or XML.
Test Data: When writing unit tests, it is often necessary to create test data that represents a specific scenario. In such cases, you might want to define a simple data structure that represents the test data. Records can be perfect for this because they allow us to define a lightweight and an immutable data structure with minimal boilerplate code.
Tuple-like objects: Records can be used to declare tuple-like objects that hold a fixed number of related values. This can be useful when returning multiple values from a method or when working with collections of related values.
What is a Record in Java?
A record in Java is a class that is intended to store data. It is similar to a traditional Java class, but in addition it is more lightweight and has some unique features. Records are immutable by default, which means that their state cannot be changed once they are created. This makes them ideal for storing data that should not be modified (immutable data), such as configuration settings or values returned from a database query.
The syntax for creating a record in Java is as follows:
record Record_Name(Fieldsโฆ.)
Example:
public record Book(String name, double price) { }
This creates a new record called โBookโ with two fields: โnameโ and โpriceโ.
You may also read: Java 14 Features andย Java 17 Features
How to create and use Record in Java With Examples?
Letโs understand how to create and use a record in java programmatically.
Creating a Record
Programmatically, creating a record is not very similar to creating a regular class in Java. We use โrecordโ keyword instead of โclassโ. Furthermore, declare the fields with data types within the bracket of record name. For example, below code demonstrates how to create a record in Java:
public record Book(String name, double price) { }
As shown in this example, we have created a record called โBookโ with two fields: โnameโ and โpriceโ. The โpublicโ keyword indicates that this record can be accessed from outside the package it is defined in.
Using a Record
To use a record in Java, we can create an instance of it using the โnewโ keyword, just like we do with a regular class. Here is an example:
Book book = new Book("Core Java", 324.25);
This creates a new instance of the โBookโ record with the โnameโ field set to โCore Javaโ and the โpriceโ field set to 324.25. Once a record has been created, its fields can be accessed using the field name itself. There are no getter & setter methods. Instead, field name becomes the method name.
String name = book.name();
double price = book.price();
This retrieves the value of the โnameโ and โpriceโ fields from the โbookโ record.
In addition to the fields, records also have some built-in methods that can be used to manipulate them. For example, toString(), equals() and hashCode().
Records in Java are immutable by default, which means that once they have been created, their state cannot be changed.
Letโs take a look at some other example of how to create & use records in Java.
Example:
Letโs take an example of a record for storing student information.
public record Student(String name, int age, String subject) {}
This creates a record called โStudentโ with three fields: โnameโ, โageโ, and โsubjectโ. We can create an instance of this record as follows:
Student student = new Student("John Smith", 20, "Computer Science");
We can then retrieve the values of the fields using the field name:
String name = student.name();
int age = student.age();
String subject= student.subject();
How does a Record look like after Compilation?
As record is simply a special kind of a class and the compiler also converts it into a normal class, but with some restrictions, which makes it slightly different from the typical classes. When the compiler converts the record (Java file) into the bytecode after compilation process, the produced โ.classโ file contains some additional declarations of the record class. For example, below is the bytecode produced for the Student record by the java compiler:
Record in Java Fileย
record Student(String name, int age) {ย ย }
Record in .class file (After Compilation)
We can also find below converted class if we use javap tool and apply below command from command prompt. It is important to note that the Java command-line includes the โjavapโ tool, which can be used to view details about the fields, constructors, and methods of a class file.
>javap Student
Conclusion:ย If we check the bytecode closely, we can have some observations:
- The compiler has replaced the record keyword byย class.
- The compiler has declared class as final. This indicates that this class cannot be extended. It also means that it cannot be inherited, and is immutable in nature.
- The converted class extends java.lang.Record. It indicates that all records are a subclass of the Record class defined under java.lang package.
- There is a parameterized constructor added by the compiler.
- The compiler has auto-generated the toString(), hashCode(), and equals() methods.
- The compiler has added methods to access the fields. Note that naming convention of the methods. They are exactly matching with the names of the fields, no get or set before the field names.
Fields in Records
Records in Java specify their state via a set of fields, each with its own name and type. The fields of a record are declared in the recordโs header. For example:
public record Person(String name, int age) {}
This record has two fields: โnameโ of type String and โageโ of type int. The fields of a record are implicitly final and cannot be reassigned after the record is created.
We can add a new field, but not recommended. A new field added to the record must beย static. For example:
public record Person(String name, int age) { static String sex; }
Constructors in Records
Like traditional class constructors, constructors in records are used to create instances of records. There are two concepts of constructors in Records: Canonical Constructor and Compact Constructor. The compact constructor provides a more concise way of initializing state variables in a record, while the canonical constructor provides a more traditional way with greater flexibility.
Canonical Constructor
Java compiler by default provides us an all-arguments constructor (a constructor with all fields) that assigns its arguments to the corresponding fields. It is known as the canonical constructor. We can also add business logic such as conditional statements to validate data. Below is the example:
public record Person(String name, int age) { ย ย ย public Person(String name, int age) { ย ย ย ย ย if (age < 18) { ย ย ย ย ย ย ย throw new IllegalArgumentException("You are not allowed to participate in general elections"); ย ย ย ย } ย ย } }
Compact Constructor
Compact constructors ignore all arguments, including the parentheses. The assignment of respective fields happens automatically. For example, below code demonstrates the concept of a compact constructor in records:
public record Person(String name, int age) { ย ย ย public Personย { ย ย ย ย ย if (age < 18) { ย ย ย ย ย ย ย throw new IllegalArgumentException("You are not allowed to participate in general elections"); ย ย ย ย } ย ย } }
In addition to the compact constructor, you can define regular constructors in a record just like in a regular class. However, you must ensure that all constructors initialize all fields of the record.
Methods in Records
Records in Java automatically generate a set of methods for each field in the record. These methods are called accessor methods and have the same name as the field they are associated with. For example, if a record has a field called โpriceโ, then it will automatically have a method called โprice()โ that returns the value of the โpriceโ field.
In addition to the automatically generated accessor methods, we can also define our own methods in a record, just like in a regular class. For example:
public record Person(String name, int age) { public void sayHello() { System.out.println("Hello, my name is " + name); } }
This record has a method called โsayHello()โ that prints out a greeting using the โnameโ field.
How does Record help in reducing Boilerplate code?
Letโs take a simple example to see how records in Java can help eliminate boilerplate code. Suppose we have a class called Person that represents a person with a name, age, and email address. Here is how we define the class in the traditional way.
Code Without Using Record
public class Person {
private String name;
private int age;
private String email;
public Person(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
publicString getEmail() {
return email;
}
public void setName(String name) {
this.name = name;
}
public voidsetAge(int age) {
this.age = age;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", email='" + email + '\'' +
'}';
}
}
As we can see, this class requires a lot of boilerplate code to define the fields, constructor, getters, setters, and toString() method.
Apart from this, suppose we want to make this class immutable. Then we have to do some additional steps such as:
- Declare the class as final, so it cannot be extended.
- Declare all fields as private and final, so they cannot be modified outside the constructor.
- Do not provide any setter methods for the fields.
- If any of the fields are mutable, return a copy of them instead of returning the original object.
Code Using Record
Now letโs see how we can define the same class using a Java record:
public record Person(String name, int age, String email) {}
Thatโs it! With just one line of code, we have defined a class that has the same fields, constructor, getters, setters, and toString() method as the traditional class. The record syntax takes care of all the boilerplate code for us.
Additionally, with the record syntax, we donโt have to do anything extra to make the class immutable. By default, all fields in a record are final, and the record class itself is immutable. Hence, creating an immutable class using the record syntax is much simpler and requires less code than the traditional approach. With records, we donโt have to declare the fields as final, provide a constructor that initializes the fields, or provide getters for all the fields.
Generic Record Classes
In Java, it is also possible to define generic record classes. A generic record class is a record class that has one or more type parameters. Here is an example:
public record Pair<T, U>(T first, U second) {}
In this example, Pair is a generic record class that takes two type parameters T and U. We can create an instance of this record as follows:
Pair<String, Integer> pair = new Pair<>("Hello", 123);
Nested Class Inside a Record
It is also possible to define nested classes and interfaces inside a record. This can be useful for grouping related classes and interfaces together, and can help to improve the organization and maintainability of a codebase.
Here is an example of a record that contains a nested class:
public record Person(String name, int age, Contact contact){
public static class Contact {
private final String email;
private final String phone;
public Contact(String email, String phone){
this.email = email;
this.phone = phone;
}
public String getEmail(){
return email;
}
public String getPhone(){
return phone;
}
}
}
In this example, Person is a record that contains a nested class Contact. Contact is a static nested class that contains two private final fields email and phone. It also has a constructor that takes in email and phone, and two getter methods getEmail() and getPhone().
We can create an instance of Person as follows:
Person person = new Person("John",30, new Person.Contact("[email protected]", "123-456-7890"));
In this example, we created a new Person object with name โJohnโ, age 30, and a new Contact object with email โ[email protected]โ, and phone โ123-456-7890โ.
Nested Interface Inside a Record
Here is an example of a record that contains a nested interface:
public record Book(String title, String author, Edition edition){ public interface Edition{ String getName(); } }
In this example, Book is a record that contains a nested interface Edition. Editionis an interface that defines a single method getName().
We can create an instance of Book as follows:
Book book = new Book("The Hitchhiker's Guide to the Galaxy", "Douglas Adams", new Book.Edition() { public String getName() { return "Science Fiction"; } });
In this example, we create a new Book object with title โThe Hitchhikerโs Guide to the Galaxyโ, author โDouglas Adamsโ, and a new anonymous implementation of the Edition interface that returns the name โScience Fictionโ when the getName() method is called.
What else Records can do?
- Records can define custom constructors. Records support parameterized constructors, which can invoke the default constructor with the provided parameters within their bodies. Additionally, Records also support compact constructors, which are similar to default constructors, but can include extra functionality such as checks within the constructor body.
- Just like any other class in Java, Records can define and use instance methods. This means we can create and invoke methods specific to the record class.
- In Java Records, defining instance variables as class members is not allowed, as they can only be specified as constructor parameters. However, Records do support static fields and static methods, which can be used to store and access data that is common to all instances of the record class.
FAQ
Can a record implement interfaces?
public interface Printable { void print(); }
public record Person(String name, int age) implements Printable {
public void print() {
System.out.println("Name: " + name + ", Age: " + age);
}
}
In this example, we have defined an interface Printable that has a single method print(). We have also defined a record Person that implements the Printable interface. The Person record has two fields name and age, and overrides the print method of the Printable interface to print the values of these fields.
We can create an instance of the Person record and call its print method as follows:
Person person = new Person("John", 30); person.print();
This will output Name: John, Age: 30 to the console.
As shown in the example, records in Java can implement interfaces just like regular classes. This can be useful for adding behavior to a record or for ensuring that a record conforms to a specific contract defined by an interface.
References
https://docs.oracle.com/en/java/javase/14/language/records.html
https://www.oracle.com/java/technologies/javase/17-relnote-issues.html#NewFeature