Introduction

Lombok, short for Project Lombok, is a Java library that automatically plugs into your build process, allowing you to reduce boilerplate code and focus on the logic of your application.
By using Lombok, you can eliminate the need for getters, setters, constructors, and other tedious code, making your development process more efficient.

Extension methods, on the other hand, are a way to extend the functionality of existing classes and interfaces without modifying their source code.
This is particularly useful when working with third-party libraries or Java’s built-in classes, where you may want to add custom functionality without altering the original code.

By using extension methods, you can enhance the functionality of these classes and interfaces without altering their original behavior.

In this tutorial, you’ll learn how to use @ExtensionMethod to simplify your code, improve readability, and boost productivity.

What is @ExtensionMethod

@ExtensionMethod is a Lombok annotation that allows you to add new methods to existing classes and interfaces.
This is achieved by creating a static method in a utility class, which is then treated as if it were an instance method of the target class or interface.

Imagine you want to add a method to the built-in String class to convert all characters to uppercase.
For this, you would have to create a utility class with a static method, like this

public class StringUtil { 
  public static String getFirstChar(String str) { 
    return str.getCharAt(0) + ""; 
  } 
}

Then, you would have to call this method using the utility class

String original = "hello"; 
String uppercase = StringUtil.getFirstChar(original);

With @ExtensionMethod, you can add this method directly to the String class, making it look like it’s a native method

@ExtensionMethod(StringUtil.class) 
public static String getFirstChar(String self) { 
  return self.getCharAt(0) + ""; 
}

Now, you can call this method as if it’s a part of the String class

String original = "hello"; 
String uppercase = original.toUpperCase();

This makes your code more readable and easier to use.
You can apply this concept to any existing class or interface, giving you the flexibility to extend their functionality without modifying their source code.

@ExtensionMethod Working

You provide a class name as argument to @ExtensionMethod and define a method in this class that takes an argument T.

Now, you can call this newly added method on an object of type T, as if existed in T.

In the context of above example, we added a new method getFirstChar() in StringUtils class.
getFirstChar() accepts a string argument.

So, if we use @ExtensionMethod with StringUtils class, we can call getFirstChar() on String object, even though String does not have any method with this name, thereby extending the capabilities of inbuilt classes.

Lombok documentation for @ExtensionMethod states

You can make a class containing a bunch of publicstatic methods which all take at least 1 parameter.
These methods will extend the type of the first parameter, as if they were instance methods, using the @ExtensionMethod feature.

@ExtensionMethod Examples

Below are some of the examples for a better understanding of @ExtensionMethod annotation.

1. Incrementing String length

Suppose you have a string and you want to add a number to its length.

To do this with @ExtensionMethod, create a new utility class having a static method that accepts a string argument and an integer to be added to its length as below

public class LengthUtil {
  public static int incrementLength(String s, int addition) {
    return s.length()+addition;
  }
}

Remember that the method should be static.

Now, you can directly call this method on a string object(since string is the first argument of this method) at any place where @ExtensionMethod annotation is applied as below

@ExtensionMethod(LengthUtil.class)
public class Main {
  public static void main(String[] a) {
    String s = "codippa";
    s.incrementLength(3); // returns 10
  }
}

Notice that we are calling incrementLength() method on a string object since the first argument in its definition is a string.
Also, we are passing only 1 argument while it defines 2. This is because the first argument is implicitly supplied to it.

2. Counting vowels

Let’s define a method that counts vowels in a string.

public class StringUtil {
  public static int  countVowels(String s) { 
    int count = 0; 
    for (char c : s.toCharArray()) { 
      if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') { 
        count++; 
      } 
    } 
    return count; 
  }
}

We will call this method on a string object while string class does not contain any such method as below

@ExtensionMethod(StringUtil.class)
public class Main {
  public static void main(String[] a) {
    String s = "codippa";
    s.countVowels(); // returns 3
  }
}

In this example, the countVowels() method is an extension method for the String class.

Again note that we are not passing any argument to countVowels() method while it expects a string.
This is because the first argument is implicit.

3. Removing Whitespaces

With @ExtensionMethod, you can add a method directly to the String class as shown below

@ExtensionMethod(String.class) 
public class StringExtensions {
  public static String removeWhitespace(String str) { 
    return str.replaceAll("\\s+", ""); 
  }
}

Now, you can call the removeWhitespace() method directly on any string object, making your code more concise and readable.

Extending List interface

Let’s say you want to add a method to the List interface that allows you to find the first element that matches a certain predicate.
Again, you can use @ExtensionMethod to achieve this as shown below

public class ListExtensions { 
  public static String findFirst(List<String> self, Predicate<String> predicate) { 
    for (String element : self) { 
      if (predicate.test(element)) { 
        return element; 
      } 
    }  
    return null; 
  }
}

Now, you can use the findFirst() method on any List object, like this

@ExtensionMethod(ListExtensions.class)
public class Main {
  public static void main(String[] a) {
    List list = Arrays.asList("apple", "banana", "cherry"); 
    String first = list.findFirst(f -> f.startsWith("b")); // Output: "banana"
  }
}

The argument to findFirst() is a predicate that tests whether a string in the list starts with “b”.

Best Practices

Following are some of the best practices that you can follow while using @ExtensionMethod annotation in Lombok.

  • Only use @ExtensionMethod when you need to add functionality to an existing class or interface that you don’t own or can’t modify.
    This annotation is not meant to replace inheritance or composition, but rather to provide a convenient way to extend the behavior of third-party classes or built-in Java classes.
  • Avoid method name conflicts and overriding.
    When you use @ExtensionMethod, you’re actually adding a new method to an existing class or interface.
    If a method with the same name and signature already exists, you’ll get a compiler error.
  • Do not over use the annotation, leading to cluttered code and decreased maintainability.
  • Document extension methods, making it easier for others to understand their purpose and behavior.
  • Consider the impact of extension methods on existing code, potentially introducing conflicts or overriding issues.

@ExtensionMethod with Generics and Type Parameters

You often need to create methods that can work with different types of objects.
This is where type parameters come into play.
By using type parameters, you can create methods that are flexible and can be used with various types of data.
But how do you combine this with @ExtensionMethod?

Lombok’s @ExtensionMethod annotation supports generics and type parameters seamlessly.
You can use type parameters in your extension methods just like you would in any other method.
Let’s take a look at an example

public class Utils {
  public static <T> T orElse(T obj, T sub) {
    return obj != null ? obj : sub;
  }
}

In this example, we are creating a method orElse() that takes a parameter of type T, where T is a type parameter.
This means you can use this method with any type of object, not just `String`.

Now, let’s say you want to use this method with a String object

@ExtensionMethod(Utils.class)
public class Main {
  public static void main(String[] a) {
    String s = "default";
    String s1 = null;
    s1.orElse(s); // default
  }
}

In this case, the orElse() method will return a String.

You can use orElse() with any type of object now.

@ExtensionMethod with Other Lombok Features

Lombok’s @ExtensionMethod annotation can be used with its other annotations as well.
One of the most powerful combinations is using @ExtensionMethod with Lombok’s @Data annotation. When you use @Data on a class, Lombok automatically generates getters, setters, and other boilerplate code for you.
By combining @Data with @ExtensionMethod, you can create classes that not only have automatically generated boilerplate code but also provide additional functionality through extension methods.

For example, let’s say you have a class called Person with properties name and age.
You can use @Data to generate getters and setters, and then use @ExtensionMethod to add a new method called getFullName() that concatenates the first and last names as shown below

@Data 
public class Person { 
  private String firstName; 
  private String lastName; 
  private int age; 

  @ExtensionMethod 
  public static String getFullName(Person person) { 
    return person.getFirstName() + " " + person.getLastName(); 
  } 
}

With this combination, you can now use the getFullName() method on any instance of Person class, making your code more expressive and concise.

Another Lombok feature that works well with @ExtensionMethod is @Builder.
When you use @Builder on a class, Lombok generates a builder pattern for you, allowing you to create objects in a more fluent and flexible way.
By combining @Builder with @ExtensionMethod, you can create builders that not only allow you to set properties but also provide additional functionality through extension methods.

For example, let’s say you have a class called Address with properties for street, city and state.
You can use @Builder to generate a builder pattern, and then use @ExtensionMethod to add a new method called validate() that checks if the address is valid

@Builder 
public class Address { 
  private String street; 
  private String city; 
  private String state; 

  @ExtensionMethod 
  public static boolean validate(Address address) { 
    // implementation of validation logic 
  } 
}

With this combination, you can now use the validate() method on any instance of the Address class, making your code more robust and maintainable.

Conclusion

In this article, we learnt how using Lombok’s @ExtensionMethod annotation, we can add support for new methods in existing classes and third party libraries with ease.
By applying @ExtensionMethod annotation to your methods, you can simplify your code, improve readability, and boost productivity.