
Adapter pattern… to explain java method references?
This quick blog post was inspired by some questions I got asked about method references in Java. This is not meant to be a tutorial on java method references and their specific binding rules (static method, constructor, bound instance method, unbound instance method) as there are many great resources out there already that can be found just by searching for “method reference” on any search engine. But sometimes some concepts are not trivial to understand even if we are able to use them, which was my case when I first started to use method references in my code. The code was compiling and executing fine, but it didn’t register in my mind why. Different people respond to different kind of explanations or point of views when reasoning about any particular concepts, so I thought I would share the mental trick I’m (still) using when applying method references in my code instead of just praying that typing the name of my class followed by::
and the method name will magically work.
Method References and Types
A method reference is syntactic sugar over a lamba expression, so it is technically a more succinct and readable way to pass functions around. But in java, functions are really interfaces (Single Abstract Method interfaces), so keeping in mind that a lamba expression actually translates to a type (interface) would be my first step to reason about them:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
interface DataProvider<T> { T getData(); } public class DefaultDataProvider<T> implements DataProvider<T> { @Override public T getData() { return data; // Fetch/get data from somewhere and return it } } DataProvider<String> dp = new DefaultDataProvider<>(); Supplier<String> supplier = dp::getData; |
Method References and Adapters
So what is going on here? One way to think about method references is to see them as straight type converters. If we forget about the functional aspect and just stick with the concept of java types (classes and interfaces), here we are kind of converting from type DataProvider
to type Supplier
, we are implicitly writing an adapter to perform that type conversion:
So thinking method references == adapter was a bit of a mental trick for me when I first got exposed to that new method reference java construct.
Method References and Type Conversion Chaining
To further illustrate the concept, below is an hypothetical example just to show how we can use method references to chain type conversions. The scenario is to convert from Supplier
to DataProvider
back to Supplier
, the use case being to reuse a piece of legacy code which doesn’t support the new java 8 default functional interfaces. Let first see how we would do it without method references nor lambas (i.e. pre java 8) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<T> DataProvider<T> toDataProvider(Supplier<T> supplier) { return new DataProvider<T>() { @Override public T getData() { return supplier.get(); } }; } <T> Supplier<T> toSupplier(DataProvider<T> provider) { return new Supplier<T>() { @Override public T get() { return provider.getData(); } }; } <T> DataProvider<T> cachedProvider(DataProvider<T> provider) { return provider; // Here we assume we have logic for caching the first invocation } <T> Supplier<T> cached(Supplier<T> supplier) { return toSupplier(cachedProvider(toDataProvider(supplier))); } |
Based on our observations from the first part of this blog post above, we could replace the adapter code with method references:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<T> DataProvider<T> toDataProvider(Supplier<T> supplier) { return supplier::get; } <T> Supplier<T> toSupplier(DataProvider<T> provider) { return provider::getData; } <T> DataProvider<T> cachedProvider(DataProvider<T> provider) { return provider; // Here we assume we have logic for caching the first invocation } <T> Supplier<T> cached(Supplier<T> supplier) { return toSupplier(cachedProvider(toDataProvider(supplier))); } |
But can we get rid of all type conversion helper methods and just use method references?
1 2 3 4 5 6 7 |
<T> DataProvider<T> cachedProvider(DataProvider<T> provider) { return provider; // Here we assume we have logic for caching the first invocation } <T> Supplier<T> cached(Supplier<T> supplier) { return cachedProvider(supplier::get)::getData; } |
which would be conceptually equivalent to:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<T> Supplier<T> cached(Supplier<T> supplier) { return new Supplier<>() { @Override public T get() { return (T) cachedProvider(new DataProvider<>(){ @Override public T getData() { return supplier.get(); } }); } }; } |
Conclusion
Although it’s not everybody in the java community who think method references are more suited than simple lamba expressions for better expressiveness with functional programming, I’m in that camp and using them profusely, whether to overcome the checked exceptions issue with java lambda expressions or when trying to solve microservice performance problems while exposing an easy to use api.
So the takeaway here (for myself at least), to better understand method references, think adapter pattern.
* featured image from www.freeimages.co.uk