Tuesday, July 7, 2015

Optional Parameters in Java 8 Lambda expressions

There is no such thing.

Still, using method overloading, polymorphism and default methods we can have something that looks a bit like optional parameters in lambda-expression and avoids instanceof when calling them.

Examples, the lazy way to grok things. Here’s one

Imagine a generic event bus that sends events and an optional header to registered event handlers.

Here’s what we would like to achieve:

bus.register(event -> System.out.println("I gots an event"));
// or!
bus.register((event,header) -> System.out.println("I gots an event w/ header"));

The first line registers an event handler that doesn’t really care about the headers and the second event handler wants it all: event and headers.

Obviously we need to use method overloading and support a functional interface with one parameter in the first case and with two parameters in the second case.

Here’s how Bus.java could look like so far.
(And for now we are ignoring thread-safety etc.)

public class Bus {
    public static class Event {}
    public static class Header {}
    List<Object> handlers = new ArrayList();

    public void register(Consumer<Event> handler) {
        handlers.add(handler);
    }

    public void register(BiConsumer<Event,Header> handler) {
        handlers.add(handler);
    }
...
}

The ‘trick’ here is to use method overloading and since Java 8 comes with a bunch of generic interfaces (Consumer, BiConsumer), we simply use those.

Problem solved!

Almost.

Note that handlers needs to be of type Object (and we don’t want two handler lists either).

Let’s look at the event dispatcher of our hypothetical event bus.

public void dispatch(Event event, Header header) {
        for (Object obj : handlers) {
            if (obj instanceof Consumer) {
                Consumer<Event> c = (Consumer<Event>) obj;
                c.accept(event);
            } else if (obj instanceof BiConsumer) {
                BiConsumer<Event, Header> b = (BiConsumer<Event, Header>) obj;
                b.accept(event, header);
            }
        }
    }

That looks ugly. Do we really have to deal with instanceof and funny looking, error-prone else-if constructs?

There’s no common super-interface of Consumer and BiConsumer and even if there was, it couldn’t have a common method since that would violate the Single Abstract Method rule for lambda expressions.

Default methods to the rescue!

While a functional interface can only have one abstract method, it can have as many default methods as it wants and we’ll use that to make our dispatch method very simple:

public void dispatch(Event event, Header header) {
    handlers.forEach(handler -> handler.dispatch(event, header));
}

Here’s how.
We need to define our own interfaces for the handlers:

// our common super-interface, defining a default method
public static interface BaseHandler {
    default void dispatch(Event evt, Header header) {}
}
// the one-argument handler
public static interface EventHandler extends BaseHandler {
    void handle(Event evt);
    default void dispatch(Event evt, Header header) {
        handle(evt); // ignore header
    }
}
// the two-argument handler
public static interface EventAndHeaderHandler extends   BaseHandler {
    void handle(Event evt, Header header);
    default void dispatch(Event evt, Header header) {
        handle(evt, header); 
    }
}

We delegate the actual call to the handle method to the overridden default method dispatch (which simply ignores the header argument in EventHandler)

Now we can change our handlers list to be of type List<BaseHandler> and fix the register methods.

List<BaseHandler> handlers = new ArrayList();

public void register(EventHandler handler) {
    handlers.add(handler);
}

public void register(EventAndHeaderHandler handler) {
    handlers.add(handler); 
}

The initial code snippet still works as intended and we have hopefully made it a bit easier for our users to use the event bus (and don’t get warnings about un-used parameters, for example).

Cheers!


Here's a gist with the full source code:


Written with StackEdit.