Ограничения forEach с ссылками на метод экземпляра в Java 8


Предположим, что у меня есть следующий функциональный интерфейс:

public interface TemperatureObserver {
    void react(BigDecimal t);
}

, а затем в другом классе уже заполненный ArrayList объектов типа TemperatureObserver. Предполагая, что temp является BigDecimal, я могу вызвать react в цикле, используя:

observers.forEach(item -> item.react(temp));

Мой вопрос : могу ли я использовать ссылку на метод для кода выше?

Не работает следующее:

observers.forEach(TemperatureObserver::react);

Сообщение об ошибке говорит мне, что

  1. forEach в Arraylist observers не применимо к типу TemperatureObserver::react
  2. TemperatureObserver не определяет метод react(TemperatureObserver)

Достаточно справедливо, так как forEach ожидает в качестве аргумента a Consumer<? super TemperatureObserver>, и мой интерфейс, хотя и функциональный, не соответствует Consumer из-за другого аргумента react (a BigDecimal в моем случае).

Так можно ли это решить, или это случай, в котором лямбда не имеет соответствующей ссылки на метод?

3 16

3 ответа:

Существует три вида ссылок на методы, которые можно использовать, когда одно значение доступно из потока:

  1. Беспараметрический метод потокового объекта.

    class Observer {
        public void act() {
            // code here
        }
    }
    
    observers.forEach(Observer::act);
    
    observers.forEach(obs -> obs.act()); // equivalent lambda
    

    Потоковый объект становится объектом метода this.

  2. Статический метод с потоковым объектом в качестве параметра.

    class Other {
        public static void act(Observer o) {
            // code here
        }
    }
    
    observers.forEach(Other::act);
    
    observers.forEach(obs -> Other.act(obs)); // equivalent lambda
    
  3. Нестатический метод с потоковым объектом в качестве параметра.

    class Other {
        void act(Observer o);
    }
    
    Other other = new Other();
    observers.forEach(other::act);
    
    observers.forEach(obs -> other.act(obs)); // equivalent lambda
    

Существует также конструктор ссылка, но это не имеет отношения к данному вопросу.

Поскольку у вас есть внешнее значение temp, и вы хотите использовать ссылку на метод, вы можете сделать третий вариант:

class Temp {
    private final BigDecimal temp;
    public Temp(BigDecimal temp) {
        this.temp = temp;
    }
    public void apply(TemperatureObserver observer) {
        observer.react(this.temp);
    }
}

Temp tempObj = new Temp(temp);

observers.forEach(tempObj::apply);

Взгляните на раздел ссылок на методы в руководстве по Java. Там написано:

Существует четыре вида ссылок на методы:
  • Ссылка на статический метод: ContainingClass::staticMethodName

  • Ссылка на метод экземпляра конкретного объекта: containingObject::instanceMethodName

  • Ссылка на метод экземпляра произвольного объекта определенного типа: ContainingType::methodName

  • Ссылка на конструктор: ClassName::new

Там он объясняет, что т. е. TemperatureObserver::react будет ссылкой на метод 3-го типа: ссылкой на метод экземпляра произвольного объекта определенного типа. В контексте вашего вызова метода Stream.forEach Эта ссылка на метод будет эквивалентна следующему лямбда-выражению:
(TemperatureObserver item) -> item.react()

Или просто:

item -> item.react()

, который не соответствует вашей сигнатуре метода void TemperatureObserver.react(BigDecimal t).

Как вы уже подозреваете, есть случаи, для которых вы не можете найдите эквивалентную ссылку на метод для лямбды. Лямбды гораздо более гибкие, хотя ИМХО иногда они менее читабельны, чем ссылки на методы (но это дело вкуса, многие люди думают наоборот).

Способ все еще использовать ссылку на метод - это использовать вспомогательный метод:

public static <T, U> Consumer<? super T> consumingParam(
        BiConsumer<? super T, ? super U> biConsumer,
        U param) {

    return t -> biConsumer.accept(t, param);
}

Который вы могли бы использовать следующим образом:

observers.forEach(consumingParam(TemperatureObserver::react, temp));
Но, честно говоря, я предпочитаю использовать лямбду.

Это не работает, потому что вы перебираете обработчики, а не параметры.

Например, этот код работает:

    ArrayList<BigDecimal> temps = new ArrayList<>();

    TemperatureObserver observer = new TemperatureObserverImpl();

    temps.forEach(observer::react);