With Java 8 lambdas being available to us as a programming tool, there is a “new” and elegant way of constructing objects. I put “new” in quotes, because it’s not new. It used to be called
the strategy pattern, but as I’ve written on this blog before,
many GoF patterns will no longer be implemented in their classic OO way, now that we have lambdas.
A simple example from jOOQ
jOOQ knows a simple type called
Converter. It’s a simple
SPI, which allows users to implement custom data types and inject data type conversion into jOOQ’s type system. The interface looks like this:
public interface Converter<T, U> {
U from(T databaseObject);
T to(U userObject);
Class<T> fromType();
Class<U> toType();
}
Users will have to implement 4 methods:
- Conversion from a database (JDBC) type
T to the user type U
- Conversion from the user type
U to the database (JDBC) type T
- Two methods providing a
Class reference, to work around generic type erasure
Now, an implementation that converts hex strings (database) to integers (user type):
public class HexConverter implements Converter<String, Integer> {
@Override
public Integer from(String hexString) {
return hexString == null
? null
: Integer.parseInt(hexString, 16);
}
@Override
public String to(Integer number) {
return number == null
? null
: Integer.toHexString(number);
}
@Override
public Class<String> fromType() {
return String.class;
}
@Override
public Class<Integer> toType() {
return Integer.class;
}
}
That wasn’t difficult to write, but it’s quite boring to write this much boilerplate:
- Why do we need to give this class a name?
- Why do we need to override methods?
- Why do we need to handle nulls ourselves?
Now, we could write some object oriented libraries, e.g. abstract base classes that take care at least of the
fromType() and
toType() methods, but much better: The API designer can provide a “constructor API”, which allows users to provide “strategies”, which is just a fancy name for “function”. One function (i.e. lambda) for each of the four methods. For example:
public interface Converter<T, U> {
...
static <T, U> Converter<T, U> of(
Class<T> fromType,
Class<U> toType,
Function<? super T, ? extends U> from,
Function<? super U, ? extends T> to
) {
return new Converter<T, U>() { ... boring code here ... }
}
static <T, U> Converter<T, U> ofNullable(
Class<T> fromType,
Class<U> toType,
Function<? super T, ? extends U> from,
Function<? super U, ? extends T> to
) {
return of(
fromType,
toType,
// Boring null handling code here
t -> t == null ? null : from.apply(t),
u -> u == null ? null : to.apply(u)
);
}
}
From now on, we can easily write converters in a functional way. For example, our
HexConverter would become:
Converter<String, Integer> converter =
Converter.ofNullable(
String.class,
Integer.class,
s -> Integer.parseInt(s, 16),
Integer::toHexString
);
Wow! This is really nice, isn’t it? This is the pure essence of what it means to write a
Converter. No more overriding, null handling, type juggling, just the bidirectional conversion logic.
Other examples
A more famous example is the JDK 8
Collector.of() constructor, without which it would be much more tedious to implement a collector. For example, if we want to find the second largest element in a stream… easy!
for (int i : Stream.of(1, 8, 3, 5, 6, 2, 4, 7)
.collect(Collector.of(
() -> new int[] { Integer.MIN_VALUE, Integer.MIN_VALUE },
(a, t) -> {
if (a[0] < t) {
a[1] = a[0];
a[0] = t;
}
else if (a[1] < t)
a[1] = t;
},
(a1, a2) -> {
throw new UnsupportedOperationException(
"Say no to parallel streams");
}
)))
System.out.println(i);
Run this, and you get:
8
7
Bonus exercise: Make the collector parallel capable by implementing the
combiner correctly. In a sequential-only scenario, we don’t need it
(until we do, of course…).
Conclusion
The concrete examples are nice examples of API usage, but the key message is this:
If you have an interface of the form:
interface MyInterface {
void myMethod1();
String myMethod2();
void myMethod3(String value);
String myMethod4(String value);
}
Then, just add a convenience constructor to the interface, accepting Java 8 functional interfaces like this:
// You write this boring stuff
interface MyInterface {
static MyInterface of(
Runnable function1,
Supplier<String> function2,
Consumer<String> function3,
Function<String, String> function4
) {
return new MyInterface() {
@Override
public void myMethod1() {
function1.run();
}
@Override
public String myMethod2() {
return function2.get();
}
@Override
public void myMethod3(String value) {
function3.accept(value);
}
@Override
public String myMethod4(String value) {
return function4.apply(value);
}
}
}
}
As an API designer, you write this boilerplate only once. And your users can then easily write things like these:
// Your users write this awesome stuff
MyInterface.of(
() -> { ... },
() -> "hello",
v -> { ... },
v -> "world"
);
Easy! And your users will love you forever for this.