6

Java 8: Как использовать лямбда-функцию, которая выбрасывает исключение?

27

Я знаю, как создать ссылку на метод, который имеет параметр типа String и возвращает int, это делается следующим образом:

Function<String, Integer>

Однако это не сработает, если функция генерирует исключение. Например, если метод определён так:

Integer myMethod(String s) throws IOException

Как мне определить такую ссылку?

5 ответ(ов)

5

Вам необходимо сделать одно из следующих действий:

  • Если это ваш код, то определите собственный функциональный интерфейс, который будет объявлять проверяемое исключение:

    @FunctionalInterface
    public interface CheckedFunction<T, R> {
        R apply(T t) throws IOException;
    }
    

    Затем используйте его:

    void foo(CheckedFunction f) { ... }
    
  • Если это не ваш код, оберните метод Integer myMethod(String s) в метод, который не объявляет проверяемое исключение:

    public Integer myWrappedMethod(String s) {
        try {
            return myMethod(s);
        } catch(IOException e) {
            throw new UncheckedIOException(e);
        }
    }
    

    После этого вы можете сделать так:

    Function<String, Integer> f = (String t) -> myWrappedMethod(t);
    

    Или так:

    Function<String, Integer> f =
        (String t) -> {
            try {
                return myMethod(t);
            } catch(IOException e) {
                throw new UncheckedIOException(e);
            }
        };
    
0

Это не специфично для Java 8. Вы пытаетесь скомпилировать что-то, эквивалентное следующему коду:

interface I {
    void m();
}
class C implements I {
    public void m() throws Exception {} // не компилируется
}

В данном случае, метод m() в классе C не может объявлять проверяемое исключение (checked exception), поскольку метод m() в интерфейсе I не объявляет это исключение. Чтобы код скомпилировался, метод m() в классе C должен соответствовать сигнатуре метода в интерфейсе, а это значит, что он не может бросать исключения, которые не указаны в декларации метода интерфейса.

0

Примечание: Я еще не использовал Java 8, только читал о ней.

Function<String, Integer> не может выбрасывать IOException, поэтому вы не можете реализовать код, который бы throws IOException. Если вы вызываете метод, который ожидает Function<String, Integer>, то лямбда-выражение, которое вы передаете этому методу, не может выбрасывать IOException, и точка. Вы можете написать лямбду таким образом (я думаю, что это синтаксис лямбды, точно не уверён):

(String s) -> {
    try {
        return myMethod(s);
    } catch (IOException ex) {
        throw new RuntimeException(ex);
        // (Или сделать что-то другое с ним...)
    }
}

Либо, если метод, которому вы передаете лямбду, написан вами, вы можете определить новый функциональный интерфейс и использовать его в качестве типа параметра вместо Function<String, Integer>:

public interface FunctionThatThrowsIOException<I, O> {
    O apply(I input) throws IOException;
}
0

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

@FunctionalInterface
public interface UseInstance<T, X extends Throwable> {
    void accept(T instance) throws X;
}

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

import java.io.FileWriter;
import java.io.IOException;

// лямбда-выражения и паттерн "выполнение вокруг метода" (EAM) для 
// управления ресурсами

public class FileWriterEAM {
    private final FileWriter writer;

    private FileWriterEAM(final String fileName) throws IOException {
        writer = new FileWriter(fileName);
    }

    private void close() throws IOException {
        System.out.println("Закрытие вызвано автоматически...");
        writer.close();
    }

    public void writeStuff(final String message) throws IOException {
        writer.write(message);
    }
    
    //...

    public static void use(final String fileName, final UseInstance<FileWriterEAM, IOException> block) throws IOException {
        final FileWriterEAM writerEAM = new FileWriterEAM(fileName);
        try {
            block.accept(writerEAM);
        } finally {
            writerEAM.close();
        }
    }

    public static void main(final String[] args) throws IOException {
        FileWriterEAM.use("eam.txt", writerEAM -> writerEAM.writeStuff("хорошо"));

        FileWriterEAM.use("eam2.txt", writerEAM -> {
            writerEAM.writeStuff("как");
            writerEAM.writeStuff("хорошо");
        });

        FileWriterEAM.use("eam3.txt", FileWriterEAM::writeIt);
    }

    void writeIt() throws IOException {
        this.writeStuff("Как ");
        this.writeStuff("хорошо ");
        this.writeStuff("это");
    }
}

Этот код демонстрирует, как удобно управлять ресурсами в Java с использованием лямбда-выражений в сочетании с пользовательским интерфейсом функциональных интерфейсов. При помощи паттерна EAM вы можете быть уверены, что ресурсы будет корректно закрывать, даже если в процессе выполнения возникнет исключение.

0

Здесь уже опубликовано много отличных ответов. Я просто пытаюсь подойти к решению проблемы с другой точки зрения. Это всего лишь мое мнение, пожалуйста, поправьте меня, если я где-то неправ.

Использование throws в FunctionalInterface — не лучшая идея

Считаю, что навязывать throws IOException в FunctionalInterface — плохая практика по следующим причинам:

  • Это выглядит как анти-шаблон для Stream и Lambda. Основная идея заключается в том, что вызывающий код сам решает, какой код предоставить и как обрабатывать исключения. Во многих сценариях IOException может быть неуместным для клиента. Например, если клиент получает значение из кеша/памяти, а не выполняет фактический I/O.

  • Обработка исключений в потоках становится поистине ужасной. Например, вот как будет выглядеть мой код, если я использую ваш API:

    acceptMyMethod(s -> {
        try {
            Integer i = doSomeOperation(s);
            return i;
        } catch (IOException e) {
            // блок try-catch из-за throws
            // в функциональном методе, хотя doSomeOperation
            // может вообще не выбрасывать исключение.
            e.printStackTrace();
        }
        return null;
    });
    

    Неприятно, не так ли? Более того, как я уже упоминал, метод doSomeOperation может выбрасывать IOException или нет (в зависимости от реализации клиента/вызывающего), но из-за throws в вашем FunctionalInterface мне всегда приходится писать блок try-catch.

Что делать, если я точно знаю, что этот API выбрасывает IOException

  • Вероятно, мы путаем FunctionalInterface с типичными интерфейсами. Если вы знаете, что этот API будет выбрасывать IOException, то, скорее всего, вы также знаете о каком-то предопределённом/абстрактном поведении. Я думаю, вам следует определить интерфейс и предоставить вашу библиотеку (с предопределённой/абстрактной реализацией) следующим образом:

    public interface MyAmazingAPI {
        Integer myMethod(String s) throws IOException;
    }
    

    Но проблема с блоком try-catch все еще остается для клиента. Если я использую ваш API в потоке, мне все равно нужно обрабатывать IOException в ужасном блоке try-catch.

  • Предоставьте API, удобный для работы с потоками, следующим образом:

    public interface MyAmazingAPI {
        Integer myMethod(String s) throws IOException;
    
        default Optional<Integer> myMethod(String s, Consumer<? super Exception> exceptionConsumer) {
            try {
                return Optional.ofNullable(this.myMethod(s));
            } catch (Exception e) {
                if (exceptionConsumer != null) {
                    exceptionConsumer.accept(e);
                } else {
                    e.printStackTrace();
                }
            }
    
            return Optional.empty();
        }
    }
    

    Дефолтный метод принимает объект Consumer в качестве аргумента, который будет отвечать за обработку исключения. Теперь с точки зрения клиента код будет выглядеть так:

    strStream.map(str -> amazingAPIs.myMethod(str, Exception::printStackTrace))
                .filter(Optional::isPresent)
                .map(Optional::get).collect(toList());
    

    Звучит хорошо, не так ли? Конечно, вместо Exception::printStackTrace можно использовать логгер или другую логику обработки.

  • Вы также можете предоставить метод, похожий на https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CompletableFuture.html#exceptionally-java.util.function.Function-. Это означает, что вы можете предоставить другой метод, который будет содержать исключение из предыдущего вызова метода. Недостаток в том, что вы делаете свои API состоянием, что означает, что вам нужно учитывать потокобезопасность, а это может привести к снижению производительности. Это просто вариант, который стоит рассмотреть.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь