Функциональные интерфейсы, лямбды и Stream API
Вопрос 1
Что будет, если попытаться откомпилировать и запустить следующий код:
List<String> lines = new LinkedList<>();
lines.add("Some puzzle");
for (String line : (Iterable<String>) lines::iterator) {
System.out.println(line);
}
а. Код не откомпилируется, потому что в круглых скобках оператора for после двоеточия могут быть только массивы и коллекции.
б. Код не откомпилируется, потому что ссылка на метод lines::iterator не может быть приведена к интерфейсу Iterable<String>.
в. Код откомпилируется и выполнится без проблем, выведя на консоль надпись "Some puzzle".
в. Код откомпилируется и выполнится без проблем, выведя на консоль надпись "Some puzzle".
В круглых скобках оператора for после двоеточия могут быть либо массив, либо любые объекты, классы которых имплементируют интерфейс Iterable. Коллекции имплементируют этот интерфейс и поэтому широко используются в таком цикле for. Но вообще ничто не запрещает написать свой класс, который бы имплементировал этот интерфейс и такой класс станет возможно использовать в for, как любую библиотечную коллекцию.
Ссылки на методы вообще прекрасно приводятся к интерфейсам, если это функциональный интерфейс и сигнатура, а также возвращаемое значение метода, совпадают с сигнатурой и возвращаемым значением единственного абстрактного метода функционального интерфейса. Метод lines::iterator прекрасно приводится к Iterable<String>. Собственно этот метод и добавлен в java.util.Collection (который в итоге наследует и имплементирует наш LinkedList), чтобы имплементировать интерфейс Iterable<E>.
На самом деле приведённый пример лишь иллюстрирует, что происходит в операторе for "под капотом". Когда мы помещаем справа от двоеточия коллекцию, оператор смотрит на неё не как на коллекцию, а как на сущность, имплементирующую интерфейс Iterable, а значит у которой можно выполнить метод iterator, который вернёт собственно итератор коллекции, с помощью которого можно будет перебирать элементы этой самой коллекции.
Вопрос 2
Что будет, если попытаться откомпилировать и запустить следующий код:
Arrays.asList(Math::random, Math::random)
.forEach(rand -> {
System.out.println(rand.get());
});
а. Код не откомпилируется.
б. Код откомпилируется, но во время выполнения программы возникнет исключительная ситуация.
в. Код откомпилируется и выведет в консоль два случайных числа.
а. Код не откомпилируется.
Компилятор выдаст ошибку: varargs mismatch; Object is not a functional interface. Проблема в этой части выражения:
Arrays.asList(Math::random, Math::random)
Метод asList() определён следующим образом:
public static <T> List<T> asList(T... a)
Использование vararg'ов означает, что компилятор гарантирует нам, что все аргументы (у нас их два) будут одного типа. Но в данном случае компилятор не может вывести тип ни одной из ссылок. Поэтому первую ссылку он сочтёт типом Object и при сравнении со второй ссылкой придёт к выводу, что Object не состоит в одной иерархии наследования (is not a) с функциональным интерфейсом. Что по-существу, верно.
Для решение этой проблемы компилятору нужно просто каким-то образом подсказать, к какому конкретно типу (конкретному функциональному интерфейсу) должны сводиться аргументы метода toList(). Например это можно сделать так:
List<Supplier> generators = Arrays.asList(Math::random, Math::random);
generators.forEach(rand -> {
System.out.println(rand.get());
});
Из первой строки компилятор может вывести, что тип параметра списка (List<T>) должен совпадать с типом аргументов метода asList(). Слева от знака равенства переменная объявлена как List<Supplier>, что означает, что аргументы метода asList() должны являться или сводиться (is a) к функциональному интерфейсу Supplier. И действительно, метод Math::random сводится к этому функциональному интерфейсу.
В данном случае у компилятора есть контекст, из которого он может определить, что Math::random -- не просто Object, а Supplier. Поэтому исправленный код откомпилируется.
Кроме того, есть и другие способы помочь компилятору.