Ограничения 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);
Сообщение об ошибке сообщает мне, что
-
forEach
в Arraylist observers
не применим к типу TemperatureObserver::react
-
TemperatureObserver
не определяет метод react(TemperatureObserver)
Достаточно справедливо, поскольку forEach
ожидает в качестве аргумента a Consumer<? super TemperatureObserver>
, и мой интерфейс, хотя и функциональный, не соответствует Consumer
из-за другого аргумента react
(a BigDecimal
в моем случае).
Итак, можно ли это решить, или это случай, когда лямбда не имеет соответствующей ссылки метода?
Ответы
Ответ 1
Существует три типа ссылок на методы, которые могут использоваться, когда одно единственное значение доступно из потока:
-
Метод с меньшим параметром потокового объекта.
class Observer {
public void act() {
// code here
}
}
observers.forEach(Observer::act);
observers.forEach(obs -> obs.act()); // equivalent lambda
Потоковый объект становится объектом this
метода.
-
Статический метод с потоковым объектом как параметром.
class Other {
public static void act(Observer o) {
// code here
}
}
observers.forEach(Other::act);
observers.forEach(obs -> Other.act(obs)); // equivalent lambda
-
Нестатический метод с потоковым объектом как параметром.
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);
Ответ 2
Взгляните на Список ссылок на методы в учебнике 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)
.
Как вы уже подозреваете, есть случаи, когда вы не можете найти эквивалентную ссылку на метод для лямбда. Lambdas более гибкие, хотя IMHO иногда они менее читаемы, чем ссылки на методы (но это вопрос вкуса, многие люди думают наоборот).
Способ использования ссылки на метод будет иметь вспомогательный метод:
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));
Но, честно говоря, я предпочитаю использовать лямбду.
Ответ 3
Это не работает, потому что вы перебираете обработчики, а не над параметрами.
Например, этот код работает:
ArrayList<BigDecimal> temps = new ArrayList<>();
TemperatureObserver observer = new TemperatureObserverImpl();
temps.forEach(observer::react);