Вам нужно передать лямбда-обработчик в стандартный функциональный интерфейс без throws: почему вызов проверяемого исключения внутри него не компилируется?
Лямбда получает тип от целевого функционального интерфейса. Если его единственный метод не объявляет проверяемое исключение, тело лямбды не может необработанно выбросить такое исключение: компилятор требует перехватить его или преобразовать в непроверяемое.
Причина в том, что контракт интерфейса должен гарантировать вызывающему коду допустимый набор исключений. Лямбда не может незаметно расширить этот контракт.
Проверяемые исключения в Java предназначены для явного контроля ситуаций, которые вызывающий код потенциально способен обработать. До появления лямбд такой контракт задавался объявлением метода интерфейса или класса в throws.
С появлением лямбд Java сохранила эту модель: лямбда не является самостоятельным типом, а получает функциональный тип из контекста. Поэтому ограничения на проверяемые исключения определяются методом функционального интерфейса, которому соответствует лямбда.
Например, Consumer<T> описывает метод accept, не объявляющий проверяемых исключений. Если передать ему обработчик, который вызывает API вроде чтения файла, компилятор не может считать, что вызывающий код умеет обработать IOException.
Если разрешить такую лямбду, исключение могло бы выйти через метод, чей контракт его не допускает. Это нарушило бы проверку исключений при вызове и сделало бы поведение API зависимым от конкретной реализации обработчика.
Проверка выполняется по целевому типу лямбды. У интерфейса с методом без throws тело лямбды должно соответствовать этому методу: проверяемое исключение нужно обработать внутри либо завернуть, например, в UncheckedIOException.
Если функциональный интерфейс объявляет подходящее проверяемое исключение, лямбда может передать его вызывающему коду:
Вызов useChecked допустим, потому что его контракт содержит throws IOException. Вызов use требует локальной обработки: после оборачивания исключение становится непроверяемым, но исходная причина сохраняется как причина (cause).
Создание собственного интерфейса с throws обычно лучше, когда ошибка является ожидаемой частью API и вызывающий код должен её обработать. Оборачивание в непроверяемое исключение удобнее для потоковых API и колбэков, но переносит обработку на более высокий уровень и может усложнить единообразие ошибок.
Метод-ссылка подчиняется тому же правилу: её проверяемые исключения должны быть совместимы с throws метода целевого интерфейса. Объявление throws у внешнего метода не меняет контракт Consumer и само по себе не разрешает выбросить IOException из его лямбды.
Сервис обходит список файлов через стандартный Consumer<Path>, но чтение каждого файла может завершиться IOException. Рассматривались три варианта: подавить ошибку, завернуть её в UncheckedIOException или заменить Consumer на собственный ThrowingConsumer.
Подавление ошибки имеет простой код, но приводит к потере данных и затрудняет диагностику. Оборачивание сохраняет исходную причину и совместимо с существующим API, однако обработку нужно централизовать выше, иначе ошибка завершит обход неожиданно. Собственный интерфейс сохраняет проверяемую семантику, но требует дополнительных перегрузок и адаптеров.
Для пакетной обработки, где ошибка одного файла должна остановить операцию и попасть в общий обработчик, выбран собственный интерфейс с throws IOException. Для инфраструктурного обхода, уже построенного на Consumer, выбран адаптер с UncheckedIOException и обязательным перехватом на границе операции. Это сохраняет причину и не заставляет скрывать сбой внутри callback.
Почему объявление throws IOException у метода, содержащего вызов Consumer, не исправляет ошибку внутри лямбды?
Объявление внешнего метода определяет, что может выбросить сам этот метод. Оно не расширяет сигнатуру Consumer.accept, которая уже зафиксирована типом параметра. Сначала лямбда должна соответствовать контракту Consumer, поэтому IOException необходимо обработать или преобразовать непосредственно в её теле.
Почему перехват проверяемого исключения с последующим выбросом RuntimeException не нарушает проверку, но всё равно может быть плохим решением?
RuntimeException является непроверяемым, поэтому его можно выбросить из метода без соответствующего throws. Однако изменение типа меняет контракт обработки: вызывающий код больше не обязан реагировать на ошибку, а корректная реакция может быть отложена до общего обработчика. Решение приемлемо, если граница API действительно использует непроверяемые ошибки и исходное исключение сохранено как причина.
Что произойдёт, если лямбда совместима сразу с несколькими функциональными интерфейсами, но у них разные объявления throws?
Лямбда получает тип из контекста, поэтому при неоднозначном контексте компилятор сначала потребует выбрать целевой тип, например явным приведением или передачей в конкретный метод. После выбора применяются ограничения именно этого интерфейса. Возможность лямбды выбросить проверяемое исключение не определяется наиболее широким набором throws среди всех потенциальных интерфейсов автоматически.