Как компилятор определяет тип лямбда-выражения, переданного операции Stream API?
Тип лямбда-выражения определяется не самой лямбдой, а контекстом целевого типа. Для операции Stream API таким контекстом обычно служит параметр функционального интерфейса, например Function, Predicate, Consumer или ToIntFunction.
Лямбда должна соответствовать единственному абстрактному методу целевого функционального интерфейса. Поэтому одно и то же выражение может иметь разный целевой тип в зависимости от вызываемой операции.
В Java 8 появились лямбда-выражения и Stream API, чтобы передавать поведение без создания множества анонимных классов. При этом Java сохранила статическую типизацию: компилятор должен установить типы параметров, результат и проверяемые исключения до запуска программы.
Для этого используется механизм target typing — вывода типа по месту использования выражения. Он позволяет не указывать типы параметров лямбды явно, если они однозначно следуют из сигнатуры метода.
Лямбда сама по себе не является объектом конкретного класса и не имеет самостоятельного функционального типа. Поэтому компилятор не может определить тип выражения, если отсутствует контекст, задающий единственный подходящий функциональный интерфейс.
Ошибка или неоднозначность возникают, когда подходящих интерфейсов несколько, их сигнатуры совместимы с лямбдой или перегруженные методы дают разные целевые типы. Неверное понимание этого механизма приводит к неожиданным ошибкам компиляции при использовании перегрузок, var и ссылок на методы.
При вызове операции Stream API компилятор сначала анализирует сигнатуру этой операции. Например, map ожидает Function, возвращающую значение, а filter — Predicate, возвращающий boolean. На основании этого контекста выводятся типы параметров лямбды и проверяется тип результата.
Одна и та же ссылка на метод может соответствовать разным функциональным интерфейсам. map использует обобщённый ссылочный результат, тогда как mapToInt ожидает специализированный интерфейс ToIntFunction, возвращающий примитивный int.
В первом случае ссылка String::length подстраивается под Function<String, Integer> с упаковкой результата в Integer. Во втором случае она подстраивается под ToIntFunction<String> и возвращает примитивный int без промежуточной упаковки.
Если целевой тип отсутствует, лямбду нельзя вывести по её тексту. Например, присваивание лямбды переменной через var не задаёт функциональный интерфейс, поэтому компилятор не знает, какой тип объекта нужно создать.
Для перегруженных методов целевой тип может быть неоднозначным. В таком случае применяют явное приведение к нужному функциональному интерфейсу, промежуточную переменную с объявленным типом или проектируют перегруженные методы с различающимися именами.
В библиотеке была универсальная функция преобразования, перегруженная для Function и ToIntFunction. Передача ссылки на метод, возвращающий int, стала неоднозначной: подходили как вариант с результатом Integer, так и вариант с примитивным int.
Рассматривались три решения. Явное приведение к нужному интерфейсу быстро устраняет ошибку, но делает вызов менее читаемым. Промежуточная переменная явно фиксирует тип и облегчает отладку, но добавляет локальную сущность. Разные имена методов устраняют двусмысленность на уровне API, однако требуют изменения публичного контракта.
Выбрали разные имена методов для публичной библиотеки, а внутри Stream API использовали map или mapToInt в соответствии с требуемым типом результата. Это сделало выбор семантики явным и одновременно позволило избежать ненужной упаковки чисел при примитивной обработке.
Почему нельзя присвоить лямбду переменной через var без явного типа?
var выводит тип из уже типизированного выражения, но лямбда не имеет самостоятельного типа. Ей нужен целевой функциональный интерфейс, например Function<String, Integer> или Predicate<String>.
Поэтому сначала указывают тип интерфейса, после чего лямбда проверяется относительно его единственного абстрактного метода. Само ключевое слово var такой контекст не создаёт.
Почему ссылка на один метод может одновременно подходить для Function и ToIntFunction?
Ссылка на метод проверяется относительно целевого типа. Если целевой интерфейс допускает результат Integer, примитивный int может быть упакован; если интерфейс ожидает int, используется примитивный результат метода.
Это не означает, что ссылка имеет два типа одновременно. В каждом конкретном месте вызова компилятор выбирает тип функционального интерфейса из контекста операции или переменной.
Почему перегрузка метода по Function и ToIntFunction может сделать вызов лямбды неоднозначным?
Компилятор рассматривает каждую перегрузку как источник целевого типа. Если лямбда совместима с обеими сигнатурами и ни одна перегрузка не является однозначно более подходящей, вызов отклоняется.
Проблему решают явным указанием функционального интерфейса, передачей заранее типизированной переменной или изменением имен методов. При проектировании API предпочтительнее не создавать перегрузки, которые различаются только близкими функциональными интерфейсами и одинаково принимают типичные лямбды.