How to Use Java Records to Write Better and More Efficient Code Oracle, the company who is responsible for developing and maintaining the JAVA language itself, introduces new features on a regular basis to make this more user-friendly and more maintainable. One of the cool features that was introduced there, is my favorite — JAVA Record. In this article, I am going to discuss record, including their purpose and usage. I also provide information about generated methods and how they relate to records. Purpose Commonly we use Classes to define objects to hold data like query results from databases or store/retrieve information. Often times we need to make the class immutable to ensure the validity of the data and prevent any change to provide consistency. To accomplish immutability we declare data class in the following manner
Let's look at an example. Suppose I have a class named Customer. It has two fields- firstName, and lastName. I want it to be immutable. That is once initialized it should not change its value. public class Customer { private final String firstName; private final String lastName; } Now let's create getter methods of the fields and add a public constructor with the necessary fields as arguments. The class will look like below: public class Customer { private final String firstName; private final String lastName; public Customer(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } public String getFirstName() { return firstName; } public String getLastName() { return lastName; } } Now it's time to create other helper methods like equals, hashCode, and toString. Let's do it. @Override public int hashCode() { return Objects.hash(firstName, lastName); } @Override public boolean equals(Object obj) { if(this == obj) { return true; } else if (!(obj instanceof Customer)) { return false; } else { Customer other = (Customer) obj; return Objects.equals(firstName, other.firstName) && Objects.equals(lastName, other.lastName); } } @Override public String toString() { return "Customer{" + "firstName='" + firstName + '#x27;' + ", lastName='" + lastName + '#x27;' + '}'; } So the final class will look like this: public class Customer { private final String firstName; private final String lastName; public Customer(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } public String getFirstName() { return firstName; } public String getLastName() { return lastName; } @Override public int hashCode() { return Objects.hash(firstName, lastName); } @Override public boolean equals(Object obj) { if(this == obj) { return true; } else if (!(obj instanceof Customer)) { return false; } else { Customer other = (Customer) obj; return Objects.equals(firstName, other.firstName) && Objects.equals(lastName, other.lastName); } } @Override public String toString() { return "Customer{" + "firstName='" + firstName + '#x27;' + ", lastName='" + lastName + '#x27;' + '}'; } } This accomplished our goal. But it has some drawbacks.
If we have several data classes, we need to go through the same tedious process, creating new fields for the data, creating equals, hashCode, and toString methods, and creating a constructor with the parameter that matches each field. Off course IDE has some built-in features to generate some of the code for us. But think about what happened when we add a new field to our class. Like we want to add the ‘age’ of the customer? Our IDE is not capable to add necessary codes for the newly added field. We have to update the code manually to achieve our goal. Another case is, this makes our code complicated. After all, this is a simple class with just two fields — firstName and lastName. This is where Java Records prove to be useful. The Basics: Now with the introduction of the Record, we can replace the data class with the record. A Record is essentially an immutable data class that only needs the field type and field name. The other methods like getter, hashCode, toString, and equals methods are generated automatically by the JAVA compiler. To create a record for our Customer we just simply declare the record as below: public record Customer(String firstName, String lastName) {} Constructor: With record equivalent public constructor is generated for us by the JAVA compiler. The equivalent constructor for our Customer record is: public Customer(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } This constructor can be used similar manner as we use to initialize class in Java. Customer customer = new Customer("John", "Doe"); See! It's just that simple! Getters: JAVA compiler has generated getters for our record. Let's test it: /** * Given valid firstName and lastName * When get firstName and LastName * the test case should pass */ @Test public void testCustomerRecordWithFirstNameAndLastName() { String firstName = "John"; String lastName = "Doe"; Customer customer = new Customer(firstName, lastName); assertEquals(firstName, customer.firstName()); assertEquals(lastName, customer.lastName()); } Equals: Just like a constructor, the equals method is also created for us. This method gives back “true” if the object given is of the same kind and all its fields have matching values: /** * Two object initialized * With same first name and last name * if check equals they should return true */ @Test public void testEqualsWithSameFirstNameAndLastName() { String firstName = "John"; String lastName = "Doe"; Customer customer1 = new Customer(firstName, lastName); Customer customer2 = new Customer(firstName, lastName); assertTrue(customer1.equals(customer2)); } hashCode: Just like the equals method we talked about earlier, a related hashCode method is also automatically created for us. Our hashCode method gives the same result when two Customer objects have all their field values matching: /** * With given Same FirstName And LastName * in hashCode method * Than the customer should be equal */ @Test public void hashCodeWithSameFirstNameAndLastName() { String firstName = "John"; String lastName = "Doe"; Customer customer1 = new Customer(firstName, lastName); Customer customer2 = new Customer(firstName, lastName); assertEquals(customer1.hashCode(), customer2.hashCode()); } toString: Lastly, our toString method. It creates a string that includes the record’s name, followed by each field’s name and its corresponding value in square brackets. So, if we make a Customer with the firstName “John” and lastName “Doe”, the toString result would be like this: Customer[firstName=John, lastName=Doe] Highly Configurable Constructors: Even though the public constructor for our record class is automatically created, it’s not limited to just that. We can customize it according to our needs. For example, we can make sure that our first name and last name provided to our constructor is not null. Check the code below: public Customer(String firstName, String lastName) { this.firstName = Objects.requireNonNull(firstName); this.lastName = Objects.requireNonNull(lastName); } Even we can also create different constructor matching our needs. For example, suppose our customer does not have any lastName! public Customer(String firstName) { this(firstName, ""); } Here we pass a blank string as the last name. You can test it on your own. Will get a similar result if you would do it in the traditional approach. Conclusion: In this article, we looked at the new “record” keyword in Java. We learned about the basic ideas and details. Of course, you can dig deeper into this by following official documentation. By using records along with their automatic methods, we can make our immutable classes better and write less repetitive code. Thank you for your time. See you next week |
I’m Iftekhar — a developer sharing what I learn about Java, Spring Boot, Spring Security, and related backend technologies like Docker, Kubernetes, and Kafka.Each week, I send one practical email with code examples, mini-projects, and real-world lessons to help you grow as a backend developer.
Monday morning, I was sipping coffee. On my desk. in my office Sarah, along with another Junior Developer, Tom, came to me. "Service works on my laptop. Crashes in staging. Same code. No idea why!" - Said Tom I checked the log. in the staging: ERROR - No qualifying bean of type 'EmailService' available ERROR - expected at least 1 bean which qualifies as autowire candidate But Tom's Local logs: INFO - Started Application in 8.2 seconds INFO - EmailService initialized successfully Same code....
It was 3 am. Cold night. I was in deep sleep. That's when Sarah called me. "Payment service down. It's 500 errors!!" I was terrified. Customers couldn't buy anything. My sleep... just gone. I opened my computer.... Logged on to the server to see what happened. The log showed: ERROR - BeanCreationException: Error creating bean 'paymentProcessor' ERROR - Could not resolve placeholder 'stripe.api.secret' I was exhausted. After a loooong week of development, I just wanted to take some rest. but...
Last mail was about 'How to Use Java Records to Write Better and More Efficient Code'. But Is it really Immutable? Java often receives criticism for being too formal, requiring developers to write a lot of code even for simple tasks. It has some good sides like, it making Java code more readable and at the same time helping the developer to write code that has fewer bugs. In some instances, however, it creates unnecessary overhead. The worst case scenario is when there is a need for a data...