1. Overview
Lombok is a popular Java library that simplifies code writing by reducing boilerplate code. One of its powerful features is the @ExtensionMethod annotation, which enhances the readability and conciseness of our code.
In this tutorial, we’ll dive deep into what the @ExtensionMethod annotation is, how it works, and when to use it effectively.
2. What Is @ExtensionMethod?
The @ExtensionMethod annotation allows us to add static method extensions to existing classes. This means we can call methods defined in other classes as part of the original class. It’s beneficial for enhancing the functionality of third-party libraries or existing classes without modifying their source code.
3. How Does @ExtensionMethod Work?
To use @ExtensionMethod, we annotate a class with @ExtensionMethod and specify the classes containing the static methods we want to extend. Lombok then generates the necessary code to make these methods available as if they’re part of the annotated class.
Let’s say we have a utility class StringUtils with a method reverse() that reverses a string. We want to use this method as if it were a method of the String class. Lombok’s @ExtensionMethod can help us achieve this.
First, we need to add the Lombok dependency to our project. If we are using Maven, we can do this by adding the following to our pom.xml:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
This dependency can easily be found here.
3.1. Example With String
Now, let’s create the StringUtils class with a reverse() method:
public static String reverse(String str) {
return new StringBuilder(str).reverse().toString();
}
Next, let’s create a test class where we use the @ExtensionMethod annotation:
@ExtensionMethod(StringUtils.class)
public class StringUtilsUnitTest {
@Test
public void givenString_whenUsingExtensionMethod_thenReverseString() {
String original = "Lombok Extension Method";
String reversed = original.reverse();
assertEquals("dohteM noisnetxE kobmoL", reversed);
}
}
In the above code, the StringUtils class contains a static method reverse() that takes a String and returns its reversed version. The StringUtilsUnitTest class is annotated with @ExtensionMethod This tells Lombok to treat the static methods of StringUtils as if they were extension methods of other classes. In the test method, we call original.reverse().
Even though String doesn’t have a reverse() method, Lombok allows this call because StringUtils has a static method reverse() that takes a String as its first parameter.
If we review the Lombok-generated class, Lombok rewrites the original.reverse() call to StringUtils.reverse(original) during the compilation process. This transformation clarifies that original.reverse() is syntactic sugar provided by Lombok to enhance code readability:
Here’s what the Lombok-generated class might look like, including a generated static method:
private static String reverse(String str) {
return StringUtils.reverse(str);
}
Let’s also have a look at the test case and see which part is transformed by Lombok:
@Test
public void givenString_whenUsingExtensionMethod_thenReverseString() {
String original = "Lombok Extension Method";
String reversed = reverse(original);
assertEquals("dohteM noisnetxE kobmoL", reversed);
}
Lombok transforms the code part where the reversed variable is assigned a value.
Now, let’s suppose we don’t use the @ExtensionMethod annotation in the above example. In that case, we’d need to call the utility methods directly from the utility class, making the code more verbose and less intuitive.
Here’s how the code would look without the @ExtensionMethod annotation:
public class StringUtilsWithoutAnnotationUnitTest {
@Test
public void givenString_whenNotUsingExtensionMethod_thenReverseString() {
String original = "Lombok Extension Method";
String reversed = StringUtils.reverse(original);
assertEquals("dohteM noisnetxE kobmoL", reversed);
}
}
In the above code, we used StringUtils to call the reverse() method.
3.2. Example With List
Let’s create an example using a List and demonstrate how to use the @ExtensionMethod annotation to add a utility method that operates on List objects.
We’ll create a utility class ListUtils with a method sum() that calculates the sum of all elements in a list of integers. Then, we’ll use the @ExtensionMethod annotation to use this method as part of the List class:
public static int sum(List<? extends Number> list) {
return list.stream().mapToInt(Number::intValue).sum();
}
Now, let’s see the respective test class to test our sum() method for Integer and Double types:
@ExtensionMethod(ListUtils.class)
public class ListUtilsUnitTest {
@Test
public void givenIntegerList_whenUsingExtensionMethod_thenSum() {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int total = numbers.sum();
assertEquals(15, total, "The sum of the list should be 15");
}
@Test
public void givenDoubleList_whenUsingExtensionMethod_thenSum() {
List<Double> numbers = Arrays.asList(1.0, 2.0, 3.0);
int total = numbers.sum();
assertEquals(6, total, "The sum of the list should be 6");
}
}
The test class uses the @ExtensionMethod(ListUtils.class) annotation to treat sum() as an extension method of the List class.
This allows calling numbers.sum() directly.
This also highlights how generics are fully applied to figure out extension methods in Lombok. This allows the @ExtensionMethod annotation to work with specific types of collections.
4. Conclusion
In this article, we saw that by using the @ExtensionMethod annotation in Lombok, we can enhance the functionality of existing classes without modifying their source code. This makes our code more expressive and easier to maintain. The example discussed demonstrates how to apply custom utility methods to a String and List. We can apply the same principle to any class and any set of static methods, providing great flexibility in our Java projects.
The source code of all these examples is available over on GitHub.