Overview
In this article, we'll try to convert the Student class from a java object with Lombok annotations, to a Java17 record. We'll assume that this class is used in many places inside our project:
@Data
@AllArgsConstructor
public class Student {
private String firstName;
private String lastName;
}
To make sure we don't break anything, we'll use a very simple test that is using the getters, setters, equals, and the constructor. We will treat this test as if it was our production code. Therefore, we'll have to refactor it — or rather, let the IDE refactor it automatically:
@Test
void janeDoeTest() {
Student student = new Student("john", "doe");
student.setFirstName("jane");
assertEquals("jane", student.getFirstName());
assertEquals("doe", student.getLastName());
assertEquals(student, new Student("jane", "doe"));
}
Our goal is to convert the Student class into a Java 17 record, remove the Lombok annotations, and, all these, without having uncompilable code or a failing test at any given time.
1. Delombok
Firstly, let's get rid of Lombok: right-click on the class name, then go to refactor > Delombok > All Lombok annotations.
As a result, the @Data and @AllArgsConstructor are gone, and we have a class that looks like this:
static class Student {
private String firstName;
private String lastName;
public Student(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return this.firstName;
}
public String getLastName() {
return this.lastName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
// toStirng, equals and hashCode
}
2. Rename the Getters
As we already know, the record classes expose methods to access the fields, but these methods do not contain the "get" word. Therefore, we need to rename the getters to comply with the new name.
With most IDEs, we can easily do this. In IntelliJ, we can do it by right-clicking on the method and then going to refactor > rename. Alternatively, we can put the cursor over the method and press Shift + F6. After we rename it and hit enter, all the places where the field was used will be updated.
@Test
void janeDoeTest() {
Student student = new Student("john", "doe");
student.setFirstName("jane");
assertEquals("jane", student.firstName());
assertEquals("doe", student.lastName());
assertEquals(student, new Student("jane", "doe"));
}
static class Student {
private String firstName;
private String lastName;
public Student(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String firstName() {
return this.firstName;
}
public String lastName() {
return this.lastName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
// toStirng, equals and hashCode
}
We can notice that IntelliJ correctly updated the test — we can run it and check that it is still green.
3. Remove Useless Setters
Dealing with the setters might be a bit more complicated. If the setter generated by Lombok was never used, Intellij will signal this by showing the method name greyed out.
If we place the cursor over the greyed-out methods and hit Alt + enter, the IDE will show some suggestions. Now, if we hit enter again, we will safely delete the method.
4. Convert Used Setters To Withers
On the other hand, if the setters are used, we have to cautiously check if we can replace them with "withers". A "wither" would be the immutable counterpart of a setter.
Unfortunately, there is no IDE support for this and we'll have to do it manually, in 3 steps. After each of the steps, the code should be compilable and all the tests should pass:
1) Make the setter return this. Then quickly fix the complication error with Alt + enter (IDE suggestions) + enter again, to make setFisrtName() return Student.
2) Re-assign the variable on the caller's side. Control + click on the method to see all the places where it is called from. After that, we'll make sure we are re-assigning the variable all the time:
Student student = new Student("john", "doe");
student = student.setFirstName("jane");
3) Rename the method and make it return a new instance. We already know how to rename the method (Shift + F6) — instead of setFirstName we'll call it withFirstName. Now, let's make it return a new instance of a Student, instead of changing state:
public Student setFirstName(String newFirstName) {
return new Student(newFirstName, this.lastName);
}
5. Make Fields Final
At this point in time, we are no longer changing any of the fields from the Student class. Therefore, we can make them final.
Place the cursor over one of the fields and press Alt + enter for suggestions: the first one should be "make field final". If you find it, hit enter again:
After that, we will repeat the process for the other fields.
6. Convert To Record
So far so good! We managed to remove Lombok and make the class immutable. Luckily enough, IntelliJ already noticed it and it is already suggesting we should convert the class into a record.
To do so, we will hover over the class name and use our favorite shortcut (Alt + enter) > convert to record > and then enter again:
7. Remove Unnecessary Code
Since we are using the record type now, we no longer need to manually overwrite the equals, hashcode, and toString methods, so we can just remove them. In the end, all we were left with is:
static record Student(String firstName, String lastName) {
public Student withFirstName(String newFirstName) {
return new Student(newFirstName, this.lastName);
}
}
Conclusion
In this article, we learned how to use various IntelliJ tricks to convert a class annotated with Lombok's @Data to a java 17 immutable record.
Maybe it looks like there are many steps to make this (apparently) simple conversion:
//from
@Data
@AllArgsConstructor
public class Student {
private String firstName;
private String lastName;
}
// to
public record Student(String firstName, String lastName) {
public Student withFirstName(String newFirstName) {
return new Student(newFirstName, this.lastName);
}
}
But, if we follow these steps, we can be sure that all the callers of the Student class are updated properly and the source code was in a compilable state all the time. The benefit of the latest is that it allows us to run the suite of tests much more often and find potential problems early.
Thank You!
Thanks for reading the article and please let me know what you think! Any feedback is welcome.
If you want to read more about clean code, design, unit testing, functional programming, and many others, make sure to check out my other articles. Do you like the content? Consider following or subscribing to the email list.
Finally, if you consider becoming a Medium member and supporting my blog, here's my referral.
Happy Coding!