Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Lombok's Extension Methods

DZone's Guide to

Lombok's Extension Methods

This deep dive into Lombok's @ExtensionMethod annotation considers its usefulness, use cases, tips, advice, and drawbacks.

· Java Zone ·
Free Resource

Download Microservices for Java Developers: A hands-on introduction to frameworks and containers. Brought to you in partnership with Red Hat.

Spicing Java with Lombok!Let'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, and toString for 2-digit precision.
  • JsonElement: Get attributes from JsonElement and assume JsonObject — 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.

Download Building Reactive Microservices in Java: Asynchronous and Event-Based Application Design. Brought to you in partnership with Red Hat

Topics:
java ,lombok ,extension methods ,tutorial ,readable code

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}