Lombok's Extension Methods
This deep dive into Lombok's @ExtensionMethod annotation considers its usefulness, use cases, tips, advice, and drawbacks.
Join the DZone community and get the full member experience.
Join For FreeLet's talk about another excellent Lombok feature — @ExtensionMethod
. An extension method is a feature of object-oriented-programming languages that allows methods defined outside of a class to be used with objects of that class (using the '.' operator) as if they are part of the class. Many programming languages such as C#, Scala, Kotlin, and TypeScript have this feature.
Java does not.
Lombok's ExtensionMethod annotation adds this functionality to Java.
Langauge Limitations
In Java, the methods callable to an object are those defined in the object class, superclasses, and interfaces (default methods). This is actually the point of object-oriented programming — to have execution code stay close to the data they operate on.
However, it is apparent that this is not sufficient or practical in a lot of cases. It is still true that we should put executions (methods) closer to the data, but it is not always practical to have all the possible executions included in the class definition.
For one thing, that can be a lot. For another, who can anticipate them all? Think about a class like String. There are an almost endless list of things applicable to String. Not only that, to keep integrity to the internal data and ensure predictable behaviors to this fundamental class, String is a final class in Java. So no matter how much you want it, extending it to add functionality is not possible. Until Java has that built in, Lombok's @ExtensionMethod
comes to the rescue.
Lombok's @ExtensionMethod
With @ExtensionMethod
, you can add methods to existing classes — or at least it appears so — as if they were written as part of the classes. It is essentially just syntactic sugar for calling static methods.
This can be used to make code read better — or align better with built-in methods. The method name is in the back of the object instead of in the front, so it lines up with other calls nearby. Let' looks at the example code below:
package nawaman.lombok.extensionmethods;
import lombok.experimental.ExtensionMethod;
class Extensions {
public static boolean isAllCap(String orgString) {
return !orgString.matches(".*[a-z].*");
}
}
@ExtensionMethod({
Extensions.class
})
public class MainFirst {
public static void main(String[] args) {
System.out.println("ONE".isAllCap()); // true
System.out.println("Two".isAllCap()); // false
}
}
Notice that on line #14 and line #15, I call method isAllCap
on a string literal. But isAllCap
is not a built-in method for the String
class. Instead, isAllCap
is an extension method declared on line #6. By declaring that this, the Main
class will use extension methods from the class Extensions
. Lombok converts any method call with the appropriate signature to be used as if it is part of that class.
Using @ExtensionMethod
To use @ExtensionMethod
, follow these steps:
- In a class, define the method (does not have to be in a separate class)
- Make the method static
- The first parameter of the method has to be the type this method will extend
- Add the annotation
@ExtensionMethod({ Extensions.class })
(replace 'Extensions' with the name of the class in the first step) to the calling class to tell Lombok where to look for extension methods. Multiple extension classes can be put there. - That is it! Now the method is available
Note: Eclipse is not smart enough to offer auto-completion to these methods, but it detects that the methods are static, so it displays in italic. It also enables 'Cntl' click (on Linux) and 'Command' click (on Mac) to navigate to the method definition.
When to Use?
Here are some of the places where/when I've found extension methods to be useful.
Extending Existing Classes
If you did not write a class but need to do something with it that the author did not intend, add them as extension methods. I found myself needing new methods for classes such as String
, BigDecimal
, ResultSet
, and JsonElement
(Gson) to name a few. For example, in one application, I need to convert from String to date and String to BigDecimal all over the place. The conversion is always the same (even if it is changed, it will change the same way).
public static BigDecimal toDecimal(String str) {
if ((str == null) ||
"null".equals(str))
return null;
return new BigDecimal(str).setScale(2, RoundingMode.HALF_DOWN);
}
public static LocalDate toLocalDate(String str) {
if ((str == null) ||
"null".equals(str) ||
"00000000".equals(str))
return null;
val format = str.contains("-") ?
DateTimeFormatter.ISO_DATE :
DateTimeFormatter.BASIC_ISO_DATE;
return LocalDate.parse(str, format);
}
With that, I can do ...
BigDecimal cost = "123.4567".toDecimal();
LocalDate date = "20171113".toLocalDate();
System.out.println(cost);// prints "123.46"
System.out.println(date); // prints "2017-11-13"
More examples:
- BigDecimal:
compare
,equals
,isZero
, andtoString
for 2-digit precision. - JsonElement: Get attributes from
JsonElement
and assumeJsonObject
— as I know what type they are.
Taming Influence
Once, I had a conversation with a senior developer who had a hard time getting the Stream API. The old way of doing things seems to blind her of getting the concept of it. After a while, I sensed that part of it was the name of those methods — "What does 'map' means? Map what with what?", "Can you explain 'reduce/collect'?" Another part is an overwhelming feeling when reading long chain method calls — made by the Juniors, of course. With some refactoring, these type of long chains can be much more readable.
val grandTotal = orderFiles.stream()
.map(toText)
.map(toGson)
.map(toOrder)
.filter(originatedIn2017)
.filter(shipped.or(coompleted))
.collect(summingInt(grandTotal));
The above is heavily refactored. Each of those functions contains a sizable amount of work. toText
, for example, opens the file, reads a string out of it and handles exception appropriately.
toGson
converts the text content of the file remove metadata and returns the actual order info.
originatedIn2017
gets the order date, parses the date, and checks the year, so on and so forth.
To refactor further, with built-in Java, your options are putting the first calls into a method, combining functions (with compose
and andThen
), and combining predicates (with or
and and
).
Both ways have advantages, disadvantages, and limitations and produce different results that read differently. With @ExtensionMethod
, you have another tool in your box. You can arbitrarily combine calls in the chain. You can use your own terminologies that make sense to your business domain (see ubiquitous language in DDD) instead of just map, filter, collect, etc. The following code can be changed to a DSL-like code ....
val grandTotal = selectOrdersFrom(orderFiles)
.orignatedIn(2017)
.withStatus(shipped, coompleted)
.getGrandTotal();
This is not to say which style is better. I just want to point out that you have more options to shape your code to your needs.
Taming Null
As you might notice from the above examples, extension methods can be made null safe. This is extremely useful. The whole host of operations can use extension methods to deal with null, for example, conversion methods like the ones above or checking methods like isBlank
, isEmpty
, isZero
, and isValid
. I am sure you can think of one easily. Outside of that, here are some examples of null specific methods that can be useful.
@ExtensionMethod({
MainNull.class
})
public class MainNull {
public static < T > T or(T obj, T defValue) {
return (obj != null) ? obj : defValue;
}
public static boolean isNull(Object obj) {
return (obj == null) ||
String.valueOf(obj).equals("null");
}
public static < T > Optional < T > whenNotNull(T obj) {
return Optional.ofNullable(obj);
}
public static void main(String...args) {
String str = null;
System.out.println(str.or("")); // prints ""
System.out.println(str.isNull()); // prints true
System.out.println(str.whenNotNull().map(String::length).orElse(0)); // prints 0
}
}
Limitations
Being an annotation processing trick, Lombok's @ExtensionMethod
is not a first-class citizen. Here are some noticeable limitations.
Lambda
Because a lambda is another compiler trick, the type information of a lambda is not accessible to Lombok. Thus, lambdas cannot be used to match the signature of a method, so it cannot be used as parameters for the extension methods. If you really want to use lambda with @ExtemsionMethod, you will have to give it a type — something along the line of the following code.
@ExtensionMethod({
MainLambda.class
})
public class MainLambda {
public static < T > T orElse(T obj, Supplier defSupplier) {
return (obj != null) ? obj : defSupplier.get();
}
public static < T > Supplier < T > S(Supplier defSupplier) {
return defSupplier;
}
public static void println(Object o) {
System.out.println(o);
}
public static void main(String...args) {
String str = null;
println(str.orElse((Supplier < String > )(() - > ("4" + "2")))); // Nasty cast
println(str.orElse(S(() - > ("4" + "2")))); // A bit less nasty
// Use the one above if you badly need closure // Otherwwise, avoid it like plague.
Supplier < String > nullString = () - > "4" + "2";
println(str.orElse(nullString)); // A bit less nasty
// Acceptable? Arguably.
}
}
Which looks ugly as hell, so avoid it.
Auto-Completion
Eclipse and IDEA, at the time of writing, do not offer to auto-complete for extension methods. You will have to know what you are going to use. This is one of the biggest limiting factors for me when using this. I will use it for plenty of other things if/when auto-completion is available. It is not all that bad though. Eclipse recognizes the method calls as static method calls, so it formats the method name properly (in italic). This provides a visual clue when reading the code.
Discussion
Does it break OO? No! Not even in the slightest. Extension methods are essentially just syntactic sugar for calling static methods. The method has no extra privilege to any of the private or protected field/methods of the object. @ExtemsionMethod
is a great tool to have in your toolbox. Just like every other feature, it should be used with consideration. Is your team familiar with it? Are you using an IDE that supports that? Use it when you see benefits.
Conclusion
Lombok's @ExtemsionMethod
is a great tool to have in the toolbox. It provides syntactic sugar for calling static methods as if the method is an instance method of your first arguments. It gives more options to add functionalities to existing classes, handling null, expressing and reusing Influence, etc. On top of that, it gives it to you in a natural looking way.
Happy coding!
Nawa Man
Code!
All the code in this article can be found on GitHub.
Published at DZone with permission of Nawa Manusitthipol. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments