16 November 2017
CATEGORY
Coding
TAGS
LombokExtension

Lombok's extension methods

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

Introduction

Let's talk about another excellent Lombok feature -- @ExtensionMethod. "Extension method" is a feature of object-orieted-programming langauges that allows methods defined outside of a class to be used with object of that class (using '.' operator) as if they are part of the class. Many programming langauges such as C#, Scala, Kotlin, 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, super classes and interfaces (default methods). This is actually the point of object-orieted programming -- to have execution codes stay close to the data they operate on. However, it is appearant 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, it is not always practical, however, 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 class like String. There are almost endless list of things applicable to String. Not only that, to keep integety to the internal data and ensure preditable 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 have that build 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 writen as part of the classes. It is essencially just syntactic sugar for calling static methods. This can be used to make code read better -- align better with build-in methods. The method name is in the back of the object instead of in the front so it lines up with other calls near by. 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 'String' class. Instead, 'isAllCap' is an extension method declared on line #6. By declaring that this '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,

  • 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 annotation @ExtensionMethod({ Extensions.class }) (replace 'Extensions' with the name of the class on 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 enable '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 found extension methods to be useful.

Extending Existing Classes

If you did not write the class but need to do thing with it out of what the author has intended, add them as extension methods. I found myself needing new methods to classes such as String, BigDecimal, ResultSet, JsonElement (Gson) to name a few. For example, in one application I need to convert String to date and String to BigDecimal all over the place. The conversion is always the same (even if it is changed, it will changed to 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, toString all those for 2 digits.
  • JsonElement: get attribute from JsonElement assume JsonObject -- as I know what type they are

Taming Influence

Once, I had a conversation with a senior developer who has a hard time getting the Stream API. The old way of doing things seems to blind her of getting the concept of it. After awhile, I sense that part of it is 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 aiming to have it reads. Each of those functions contain a sizable amount of work. toText, for example, opens the file, read string out of it and handle exception appropriately. toGson converts the text content of the file remove meta data and return the actual order info. originatedIn2017 get the order date, parse the date and check the year. ... so on and so forth. To refactor further, with build-in Java, your options are putting first calls into a method, combine functions (with compose and andThen) combine predicates (with or and and). Each of which has advantages, disvantages and limitations and produce different results which 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, .... 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 that which style is better. But just want to point out that you have more options to shape your code the way that fits to your needs.

Taming Null

As you might notice from the above examples, extension methods can be made null safe. This is extremely useful. Whole host of operations can use extension methods to deal with null. Conversion methods like the ones above. Checking methods like isBlank, isEmpty, isZero, 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 noticible limitations.

  • Lambda: Because lambda is another compiler tricks, the type information of lambda is not accessible to Lombok. Thus, lambda cannot be used to match the signature of the 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 orElse(T obj, Supplier defSupplier) {
          return (obj != null) ? obj : defSupplier.get();
      }
      public static Supplier 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, does not offer to auto-complete the extension methods. You will have to know what you are going to use. This is one biggest limiting factor for me on using this. I will use it for much more things if auto-completion is available. It is not all that bad though, Eclipse is recognizing the method calls as static method calls so it formats the method name properly (in italic). This provide visual clue when reading the code.

Dicussions

Does it breaks OO? No! Not even in a 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 the toolbox. Just like every other features, it should be used with considerations. Is your team familiar with it? Are you using IDE that support that? Use it when you see benefits. :-D

Conclusion

Lombok's @ExtemsionMethod is a great tool to have in the toolbox. It provides syntactic sugar on calling static method as if the method is an instant method of the first arguments. It give more options to add functionalities to existing class, handling null, express and reuse Influence. All those in a natural looking way.

Happy coding!
Nawa Man

Code!

All the code in this article can be found on GitHub .

Comments

Thank you for keeping the comment section positive, constructive and respectful environment. I do appreciate constructive criticism & respectful disagreement! I have ZERO tolerant for disrespect, harassment, threats, cyber-bullying, racism, sexism attacks, foul language, and spam. Comments will be actively moderated and abusive users will be blocked. Keep it civil! :-)