Using Lombok’s @With Annotation to Clone Immutable Objects is a beneficial feature that helps developers minimize code replication and ceremonial code. It is the next best alternative to Copy Constructs in object-oriented programming. The @With annotation also requires @AllArgsConstructor to function correctly.
For rapid application development the maven project provided in this example used the spring initializr page https://start.spring.io to generate the initial source code.
The source code referenced by this article can be found at https://github.com/kapresoft/lombok-with.
In this simple example, we will model a resident of a house. The Resident class can be modeled in Lombok as:
@Value @Builder
public class Resident {
@With
String fullName;
@With
String phoneNumber;
String streetAddress;
String city;
String state;
String zipCode;
String country;
}
Source: Resident.java
In order to utilize the power @With annotation, you must use it in conjunction with @AllArgsConstructor. In the above class definition, @Value is used. The @Value is a simplification for configuring a class with immutable fields.
The @Value annotation can be similarly modeled by @Getter and final fields and other annotations as shown below:
@Getter
@FieldDefaults(makeFinal=true, level=AccessLevel.PRIVATE)
@AllArgsConstructor
@ToString
@EqualsAndHashCode
public class Resident {
@Getter @With
private final String fullName;
// ... etc
}
Please Refer to Lombok • Builders and Copy Constructors for more details on immutable objects in Lombok.
A few thing worth mentioning is that you must adhere to Lombok’s class field naming convention. Static fields are not supported by @With and $ symbols are ignored.
A single resident can then be created and verified as:
@Test
void createSingleResident() {
final Resident johnDoe = Resident.builder()
.streetAddress("100 Main St")
.city("Bellevue").state("WA")
.fullName("John Doe")
.phoneNumber("425-555-1000")
.city("Bellevue").state("WA").zipCode("98004")
.build();
assertThat(johnDoe.getFullName()).as("Full Name")
.isEqualTo("John Doe");
assertThat(johnDoe.getPhoneNumber()).as("Phone Number")
.isEqualTo("425-555-1000");
assertThat(johnDoe.getCity()).as("City")
.isEqualTo("Bellevue");
assertThat(johnDoe.getState()).as("State")
.isEqualTo("WA");
assertThat(johnDoe.getState()).as("ZipCode")
.isEqualTo("98004");
}
Source: ResidentTest.java
Suppose there are two other residents. Those additional residents can be cloned from the existing immutable instance johnDoe. A newly created clone and immutable Resident objects janeDoe and teenageDaughter with a slightly different information modified by withFullName(String)
and withPhoneNumber(String)
methods.
@Test
void createResidents() {
final Resident johnDoe = Resident.builder()
.streetAddress("100 Main St")
.city("Bellevue").state("WA")
.fullName("John Doe")
.phoneNumber("425-555-1000")
.city("Bellevue").state("WA").zipCode("98004")
.build();
assertThat(johnDoe.getFullName()).as("Full Name")
.isEqualTo("John Doe");
assertThat(johnDoe.getPhoneNumber()).as("Phone Number")
.isEqualTo("425-555-1000");
final Resident janeDoe = johnDoe
.withFullName("Jane Doe")
.withPhoneNumber("425-555-1001");
assertThat(janeDoe.getFullName()).as("Full Name")
.isEqualTo("Jane Doe");
assertThat(janeDoe.getPhoneNumber()).as("Phone Number")
.isEqualTo("425-555-1001");
final Resident teenageDaughter = johnDoe.withFullName("Jennifer Doe")
.withPhoneNumber("425-555-1002");
assertThat(teenageDaughter.getFullName()).as("Full Name")
.isEqualTo("Jennifer Doe");
assertThat(teenageDaughter.getPhoneNumber()).as("Phone Number")
.isEqualTo("425-555-1002");
}
Source: ResidentTest.java
The @With annotation is a class or field-level annotation.
In the main example, the @With was placed at the field-level. In this example, a class-level @With annotation means that a ‘with’ method is generated for each and every field.
@Value @Builder @With
public class Resident {
String fullName;
String phoneNumber;
String streetAddress;
String city;
String state;
String zipCode;
String country;
}
final Resident johnDoe = Resident.builder()
.streetAddress("100 Main St")
.city("Bellevue").state("WA")
.fullName("John Doe")
.phoneNumber("425-555-1000")
.city("Bellevue").state("WA").zipCode("98004")
.build();
final Resident janeDoe = johnDoe
.withFullName("Jane Doe")
.withPhoneNumber("425-555-1001")
.withStreetAddress("100 Rodeo Drive")
.withCity("Beverly Hills")
.withState("CA")
.withZipCode("90210");
The @With may also be configured with an access level:
@With(value = AccessLevel.PACKAGE)
String fullName;
The above configuration will produce a method:
Resident withFullName(String fullName) {
// ... function body here ...
}
This article covered how to use Lombok’s @With annotation at a class and field level; an example use-case to generate clones of immutable objects with a change in multiple fields. We also covered configuring the method access level (private, protected, public, or package-level) of the generated “with” methods in Lombok.
Full code examples are available at Github https://github.com/kapresoft/lombok-with.