Идиома Java для лямбда с интерфейсами, отличными от SAM

В Java интерфейсы с одним абстрактным методом (т.е. типы SAM или функциональные интерфейсы) могут быть элегантно реализованы с помощью лямбда вместо анонимного класса:

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

можно заменить на:

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

Но для интерфейсов с несколькими абстрактными методами лямбда не может быть применена непосредственно. Например, java.awt.event.WindowListener имеет семь методов. Но часто кусок кода интересует только один из этих семи методов.

Чтобы реализовать поведение с переопределением анонимного класса, мы можем:

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

но есть ли более элегантный способ с лямбдами?

@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!");
    }
}

Примечание: @maythesource.com задал аналогичный, но более широкий вопрос: "Что бы кто-то сделал с MouseListener, если они хотели реализовать несколько методов в анонимном классе?" Самый одобренный и принятый ответ - использовать анонимную реализацию. Мой вопрос касается элегантного лямбда-решения для не-SAM-типов. Поэтому этот вопрос не является дубликатом Java 8 Lambda Expressions - о нескольких методах в вложенном классе.


Ответ 1

Самый элегантный способ, который я нашел, - использовать анонимный мост:

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

который, подобно сценарию типа SAM, более чист, чем анонимный адаптер:

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

но для этого требуется немного неудобный мост со статическим 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);
            }
        };
    }
}

Ответ 2

В Брайан Гетц ответил на другой вопрос, он предложил использовать статические методы factory. В этом случае это немного утомительно, так как WindowListener определяет семь методов обработчика, поэтому вам нужно определить семь статических методов factory. Однако это не так уж плохо, поскольку уже существует класс WindowAdapter, который предоставляет пустые реализации всех методов. (Если его нет, вам нужно будет определить свой собственный эквивалент.) Вот как бы я это сделал:

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); }
        };
    }

    // ...
}

(Другие 253 случая аналогичны.)

Каждый метод factory создает подкласс WindowAdapter, который переопределяет соответствующий метод для вызова передаваемого лямбда-выражения. Нет необходимости в дополнительных классах адаптера или моста.

Он будет использоваться следующим образом:

window.addWindowListener(WLFactory.windowOpened(we -> System.out.println("opened")));

Ответ 3

Я хотел бы предложить довольно общее решение для этого: для генерации реализации интерфейса можно использовать Динамические прокси-классы. Такой прокси мог просто игнорировать все методы, за исключением метода, для которого соответствующий Consumer был указан как лямбда.

Конечно, рефлексию всегда нужно использовать с осторожностью. Но преимущество заключается в том, что он работает "из коробки" с помощью любого MAM-интерфейса (Multiple Abstract Method).

Нет необходимости создавать десятки или сотни мостовых методов для всех интерфейсов и их методов. Просто создайте прокси-сервер, который является "пустой" реализацией интерфейса, и передайте единую реализацию метода как лямбда.

Ниже приведен пример реализации базового примера, показывающий, что он может использоваться кратко и в общем случае для разных интерфейсов, таких как WindowListener, MouseListener и 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());
    }
}