Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java idiom for lambdas with non-SAM interfaces

In Java, interfaces with a single abstract method (i.e., SAM types or functional interfaces) can be elegantly implemented with lambda instead of an anonymous class:

    // SAM ActionListener with anonymous implementation
    button.addActionListener(
        new ActionListener(){
            public void actionPerformed(Event e){
                System.out.println("button via anon!");
            }
        }
    );

can be replaced with:

    // SAM ActionListener with lambda implementation
    button.addActionListener(
        e -> System.out.println("button via lambda!")
    );

But for interfaces with multiple abstract methods, lambda cannot be directly applied. For example, java.awt.event.WindowListener has seven methods. But often a chunk a code is only interested in defining one of these seven methods.

To implement the behavior with an anonymous class override, we can:

    // non-SAM with adapter implementation with override
    window.addWindowListener(
        new WindowAdapter() {
            @Override
            public void windowOpened(Event e){
                System.out.println("WindowAdapter opened via override!");
            }
        }
    );

but is there a more elegant way with lambdas?

@FunctionalInterface
public interface ActionListener {
    void actionPerformed(Event e);
}

public interface WindowListener {
    void windowOpened(Event e);

    void windowClosing(Event e);
}

public class WindowAdapter implements WindowListener {

    public void windowOpened(Event e){
        System.out.println("windowOpened in adapter!");
    }

    public void windowClosing(Event e){
        System.out.println("windowClosing in adapter!");
    }
}

Note: @maythesource.com asked a similar, but broader question: "What would someone do with a MouseListener if they wanted to implement multiple methods within the anonymous class?" The most upvoted and accepted answer is to use an anonymous implementation. My question is about an elegant lambda solution for non-SAM types. Therefore, this question is not a duplicate of Java 8 Lambda Expressions - what about multiple methods in nested class.


like image 598
Jan Nielsen Avatar asked Aug 14 '14 03:08

Jan Nielsen


3 Answers

In Brian Goetz' answer to the other question, he suggested using static factory methods. In this case it's a bit tedious, since WindowListener defines seven handler methods, so you'd need to define seven static factory methods. This isn't that bad, though, since there is already a WindowAdapter class that provides empty implementations of all of the methods. (If there isn't one, you'd have to define your own equivalent.) Here's how I'd do it:

class WLFactory {
    public static WindowListener windowOpened(Consumer<WindowEvent> c) {
        return new WindowAdapter() {
            @Override public void windowOpened(WindowEvent e) { c.accept(e); }
        };
    }

    public static WindowListener windowClosing(Consumer<WindowEvent> c) {
        return new WindowAdapter() {
            @Override public void windowClosing(WindowEvent e) { c.accept(e); }
        };
    }

    // ...
}

(The other 253 cases are analogous.)

Each factory method creates a subclass of WindowAdapter that overrides the appropriate method to call the lambda expression that's passed in. No need for additional adapter or bridge classes.

It would be used as follows:

window.addWindowListener(WLFactory.windowOpened(we -> System.out.println("opened")));
like image 143
Stuart Marks Avatar answered Nov 05 '22 14:11

Stuart Marks


The most elegant way I have found is to use an anonymous bridge:

    // SAM bridge with lambda implementation
    window.addWindowListener(
        WindowBridge.windowOpened(
            b -> System.out.println("opening via lambda!")
        )
    );

which, like the SAM type scenario, is cleaner than the anonymous adapter:

    // non-SAM with adapter implementation with override
    window.addWindowListener(
        new WindowAdapter() {
            @Override
            public void windowOpened(Event e){
                System.out.println("WindowAdapter opened via override!");
            }
        }
    );

but it does require a slightly awkward bridge with a static factory:

import java.util.function.Consumer;

public interface WindowBridge {

    // SAM for this method
    public abstract class WindowOpened extends WindowAdapter {
        public abstract void windowOpened(Event e);
    }

    // factory bridge
    public static WindowOpened windowOpened(Consumer<Event> c) {
        return new WindowOpened() {
            public void windowOpened(Event e){
                c.accept(e);
            }
        };
    }

    // SAM for this method
    public abstract class WindowClosing extends WindowAdapter {
        public abstract void windowClosing(Event e);
    }

    // factory bridge
    public static WindowClosing windowClosing(Consumer<Event> c) {
        return new WindowClosing() {
            public void windowClosing(Event e){
                c.accept(e);
            }
        };
    }
}
like image 39
Jan Nielsen Avatar answered Nov 05 '22 16:11

Jan Nielsen


I'd like to propose a rather generic solution for this: One can use Dynamic Proxy Classes to generate the implementation of the interface. Such a proxy could simply ignore all methods, except for the method for which an appropriate Consumer was specified as a lambda.

Of course, reflection always has to be used with care. But the advantage is that it works "out of the box" with any MAM-interface-type (Multiple Abstract Method).

There's no need to create dozens or hundreds of bridge methods for all the interfaces and their methods. Just create a proxy that is an "empty" implementation of the interface, and pass in a single method implementation as a lambda.

A basic example implementation is here, showing that it may be used concisely and generically for different interfaces, like WindowListener, MouseListener and ComponentListener:

import java.awt.event.ComponentListener;
import java.awt.event.MouseListener;
import java.awt.event.WindowListener;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.function.Consumer;
import java.util.function.Function;

class LambdaDelegatorTest
{
    public static void main(String args[])
    {
        WindowListener w =
            LambdaDelegators.create(WindowListener.class, "windowClosed",
                e -> System.out.println("Window closed"));

        w.windowActivated(null);
        w.windowClosed(null);

        MouseListener m =
            LambdaDelegators.create(MouseListener.class, "mouseExited",
                e -> System.out.println("Mouse exited"));

        m.mouseClicked(null);
        m.mouseExited(null);

        ComponentListener c =
            LambdaDelegators.create(ComponentListener.class, "componentShown",
                e -> System.out.println("Component shown"));

        c.componentHidden(null);
        c.componentShown(null);

    }
}

class LambdaDelegators
{
    public static <T> T create(Class<T> c, String methodName,
        Consumer<Object[]> consumer)
    {
        Function<Object[], Object> function = new Function<Object[], Object>()
        {
            @Override
            public Object apply(Object[] t)
            {
                consumer.accept(t);
                return null;
            }
        };
        return createFromFunction(c, methodName, function);
    }

    @SuppressWarnings("unchecked")
    private static <T> T createFromFunction(Class<T> c, String methodName,
        Function<Object[], Object> function)
    {
        Class<?> classes[] = new Class[1];
        classes[0] = c;
        Object proxy =
            Proxy.newProxyInstance(c.getClassLoader(), classes,
                new LambdaDelegator(methodName, function));
        return (T) proxy;
    }

    private LambdaDelegators()
    {

    }
}

class LambdaDelegator implements InvocationHandler
{
    private static final Method hashCodeMethod;
    private static final Method equalsMethod;
    private static final Method toStringMethod;
    static
    {
        try
        {
            hashCodeMethod = Object.class.getMethod(
                "hashCode", (Class<?>[]) null);
            equalsMethod = Object.class.getMethod(
                "equals", new Class[] { Object.class });
            toStringMethod = Object.class.getMethod(
                "toString", (Class<?>[]) null);
        }
        catch (NoSuchMethodException e)
        {
            throw new NoSuchMethodError(e.getMessage());
        }
    }

    private final String methodName;
    private final Function<Object[], Object> function;

    public LambdaDelegator(String methodName,
        Function<Object[], Object> function)
    {
        this.methodName = methodName;
        this.function = function;
    }

    public Object invoke(Object proxy, Method m, Object[] args)
        throws Throwable
    {
        Class<?> declaringClass = m.getDeclaringClass();
        if (declaringClass == Object.class)
        {
            if (m.equals(hashCodeMethod))
            {
                return proxyHashCode(proxy);
            }
            else if (m.equals(equalsMethod))
            {
                return proxyEquals(proxy, args[0]);
            }
            else if (m.equals(toStringMethod))
            {
                return proxyToString(proxy);
            }
            else
            {
                throw new InternalError(
                    "unexpected Object method dispatched: " + m);
            }
        }
        else
        {
            if (m.getName().equals(methodName))
            {
                return function.apply(args);
            }
        }
        return null;
    }

    private Integer proxyHashCode(Object proxy)
    {
        return new Integer(System.identityHashCode(proxy));
    }

    private Boolean proxyEquals(Object proxy, Object other)
    {
        return (proxy == other ? Boolean.TRUE : Boolean.FALSE);
    }

    private String proxyToString(Object proxy)
    {
        return proxy.getClass().getName() + '@' +
            Integer.toHexString(proxy.hashCode());
    }
}
like image 34
Marco13 Avatar answered Nov 05 '22 15:11

Marco13