Как указать типы функций для методов void (не Void) в Java8?

149

Я экспериментирую с Java 8, чтобы понять, как он функционирует как первоклассный гражданин. У меня есть следующий фрагмент:

package test;

import java.util.*;
import java.util.function.*;

public class Test {

    public static void myForEach(List<Integer> list, Function<Integer, Void> myFunction) {
      list.forEach(functionToBlock(myFunction));
    }

    public static void displayInt(Integer i) {
      System.out.println(i);
    }


    public static void main(String[] args) {
      List<Integer> theList = new ArrayList<>();
      theList.add(1);
      theList.add(2);
      theList.add(3);
      theList.add(4);
      theList.add(5);
      theList.add(6);
      myForEach(theList, Test::displayInt);
    }
}

Я пытаюсь передать метод displayIntметоду, myForEachиспользуя ссылку на метод. Компилятор выдает следующую ошибку:

src/test/Test.java:9: error: cannot find symbol
      list.forEach(functionToBlock(myFunction));
                   ^
  symbol:   method functionToBlock(Function<Integer,Void>)
  location: class Test
src/test/Test.java:25: error: method myForEach in class Test cannot be applied to given ty
pes;
      myForEach(theList, Test::displayInt);
      ^
  required: List<Integer>,Function<Integer,Void>
  found: List<Integer>,Test::displayInt
  reason: argument mismatch; bad return type in method reference
      void cannot be converted to Void

Компилятор на это жалуется void cannot be converted to Void. Я не знаю, как указать тип интерфейса функции в сигнатуре myForEach, чтобы код компилировался. Я знаю, что могу просто изменить тип возвращаемого значения displayIntна, Voidа затем вернуться null. Однако могут возникнуть ситуации, когда невозможно изменить метод, который я хочу передать в другое место. Есть ли простой способ повторно использовать displayIntкак есть?

Валентин
источник

Ответы:

252

Вы пытаетесь использовать неправильный тип интерфейса. Тип Function не подходит в этом случае, потому что он получает параметр и имеет возвращаемое значение. Вместо этого вы должны использовать Consumer (ранее известный как Block)

Тип функции объявлен как

interface Function<T,R> {
  R apply(T t);
}

Однако тип Consumer совместим с тем, что вы ищете:

interface Consumer<T> {
   void accept(T t);
}

Таким образом, Consumer совместим с методами, которые получают T и ничего не возвращают (void). И это то, что вам нужно.

Например, если бы я хотел отобразить все элементы в списке, я мог бы просто создать для этого потребителя с помощью лямбда-выражения:

List<String> allJedi = asList("Luke","Obiwan","Quigon");
allJedi.forEach( jedi -> System.out.println(jedi) );

Выше видно, что в этом случае лямбда-выражение получает параметр и не имеет возвращаемого значения.

Теперь, если я хотел использовать ссылку на метод вместо лямбда-выражения для создания потребителя этого типа, тогда мне нужен метод, который получает String и возвращает void, верно?

Я мог бы использовать разные типы ссылок на методы, но в этом случае давайте воспользуемся ссылкой на printlnметод System.outобъекта, используя метод в объекте, например:

Consumer<String> block = System.out::println

Или я мог бы просто сделать

allJedi.forEach(System.out::println);

Этот printlnметод подходит, потому что он получает значение и имеет тип возвращаемого значения void, как и acceptметод в Consumer.

Итак, в вашем коде вам нужно изменить подпись метода примерно так:

public static void myForEach(List<Integer> list, Consumer<Integer> myBlock) {
   list.forEach(myBlock);
}

И тогда вы сможете создать потребителя, используя ссылку на статический метод, в вашем случае, выполнив:

myForEach(theList, Test::displayInt);

В конце концов, вы даже можете полностью избавиться от своего myForEachметода и просто сделать:

theList.forEach(Test::displayInt);

О функциях граждан первого класса

Как уже было сказано, правда в том, что Java 8 не будет иметь функций в качестве первоклассных граждан, поскольку структурный тип функции не будет добавлен к языку. Java просто предложит альтернативный способ создания реализаций функциональных интерфейсов из лямбда-выражений и ссылок на методы. В конечном итоге лямбда-выражения и ссылки на методы будут привязаны к ссылкам на объекты, поэтому все, что у нас есть, - это объекты как первоклассные граждане. Важным моментом является функциональность, поскольку мы можем передавать объекты в качестве параметров, связывать их со ссылками на переменные и возвращать их как значения из других методов, тогда они в значительной степени служат той же цели.

Эдвин Далорцо
источник
1
BlockConsumer
Между
4
В последнем абзаце замалчиваются некоторые важные факты: лямбда-выражения и ссылки на методы - это больше, чем просто синтаксическое дополнение. Сама JVM предоставляет новые типы для более эффективной обработки ссылок на методы, чем с анонимными классами (что наиболее важно MethodHandle), и, используя это, компилятор Java может создавать более эффективный код, например, реализуя лямбда-выражения без закрытия как статические методы, предотвращая тем самым создание дополнительные занятия.
Feuermurmel
7
И правильная версия Function<Void, Void>есть Runnable.
OrangeDog
2
@OrangeDog Это не совсем так. В комментариях к Runnableи Callableинтерфейсам написано , что они должны быть использованы в сочетании с нитями . Досадным последствием этого является то, что некоторые инструменты статического анализа (например, Sonar) будут жаловаться, если вы вызовете run()метод напрямую.
Денис
Исходя исключительно из опыта работы с Java, мне интересно, в каких конкретных случаях Java не может рассматривать функции как первоклассные граждане даже после введения функциональных интерфейсов и лямбда-выражений со времен Java8?
Vivek Sethi
22

Когда вам нужно принять функцию в качестве аргумента, которая не принимает аргументов и не возвращает результата (void), на мой взгляд, все же лучше иметь что-то вроде

  public interface Thunk { void apply(); }

где-то в вашем коде. В моих курсах функционального программирования для описания таких функций использовалось слово thunk. Почему этого нет в java.util.function, я не понимаю.

В других случаях я обнаружил, что даже когда в java.util.function есть что-то, что соответствует подписи, которую я хочу, это все равно не всегда правильно, когда именование интерфейса не соответствует использованию функции в моем коде. Я предполагаю, что это то же самое, что и в другом месте здесь, в отношении «Runnable» - термина, связанного с классом Thread - так что, хотя у него может быть нужная мне подпись, она все же может запутать читателя.

LOAS
источник
В частности Runnable, не допускаются проверенные исключения, что делает его бесполезным для многих приложений. Поскольку Callable<Void>это тоже бесполезно, вам, кажется, нужно определить свое собственное. Некрасиво.
Джесси Глик
Что касается проблемы с проверенными исключениями, новые функции Java в целом борются с этим. это огромный блокиратор некоторых вещей, таких как Stream API. Очень плохой дизайн с их стороны.
user2223059
1

Установите тип возврата Voidвместо voidиreturn null

// Modify existing method
public static Void displayInt(Integer i) {
    System.out.println(i);
    return null;
}

ИЛИ

// Or use Lambda
myForEach(theList, i -> {System.out.println(i);return null;});
Додзё
источник
-2

Я считаю, что вам следует использовать пользовательский интерфейс вместо Function<T, R>.

Потребитель - это, по сути, функциональный интерфейс, предназначенный для принятия значения и ничего не возвращающего (т.е. void).

В вашем случае вы можете создать потребителя в другом месте вашего кода следующим образом:

Consumer<Integer> myFunction = x -> {
    System.out.println("processing value: " + x);    
    .... do some more things with "x" which returns nothing...
}

Затем вы можете заменить свой myForEachкод приведенным ниже фрагментом:

public static void myForEach(List<Integer> list, Consumer<Integer> myFunction) 
{
  list.forEach(x->myFunction.accept(x));
}

Вы относитесь к myFunction как к первоклассному объекту.

Энтони Аньянву
источник
2
Что это добавляет помимо существующих ответов?
Мэтью Рид