Статьи

Исключения в Lambdas: элегантное решение проблемы

Рассмотрим следующую функцию для записи в файл:

Идея метода заключается в том, чтобы позволить пользователю передавать в метод различные реализации InputStream чтобы метод writeToFile можно было вызывать, например, с помощью GZIPOuputStream , SnappyOuputStream
(быстрое сжатие) или просто обычный FileInputStream .

1
2
3
4
5
6
7
private static void writeToFile(File file, String value,
        Function<OutputStream, OutputStream> writing) throws IOException{
    try (PrintWriter pw = new PrintWriter(new BufferedOutputStream
            (writing.apply(new FileOutputStream(file))))) {
        pw.write(value);
    }
}

Это аккуратная функция, которая может быть вызвана так:

01
02
03
04
05
06
07
08
09
10
11
public static void main(String[] args) {
    try {
        //Write with compression
        //DOES NOT COMPILE!!
        writeToFile(new File("test"), "Hello World", GZIPOutputStream::new);
        //Just use the FileOutputStream
        writeToFile(new File("test"), "Hello World", i->i);
    }catch(IOException e){
        //deal with exception as you choose
    }
}

К сожалению, как указано в комментарии, это не компилируется! Причина, по которой он не компилируется, заключается в том, что GZIPOutputStream генерирует IOException в своем конструкторе. Было бы неплохо, если бы исключение IOException было выброшено из лямбды и могло быть обработано в блоке try catch — но это не то, как работают лямбды 🙁

На самом деле, вот как вы должны иметь дело с исключением, чтобы получить код для компиляции:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
    try {
        //Write with compression
        //COMPILES BUT SO UGLY
        writeToFile(new File("test"), "Hello World", i -> {
            try {
                return new GZIPOutputStream(i);
            } catch (IOException e) {
                //HOW ARE WE SUPPOSED TO DEAL WITH THIS EXCEPTION??
                throw new AssertionError(e);
            }
        });
        //Just use the FileOutputStream
        writeToFile(new File("test"), "Hello World", i->i);
    }catch(IOException e){
        //deal with exception as you choose
    }
}

Это не только уродливо, но у вас осталась довольно неловкая проблема с тем, что делать с IOException. В этом случае мы просто перепаковали внутри AssertionError. Смотрите мой предыдущий пост «Обман с исключениями» о том, как правильно справиться с этим сценарием.

Но есть решение этой проблемы. Вместо использования функции java.util.function.Function которая принимает значение и возвращает значение, мы можем создать пользовательскую функцию, которая принимает значение, возвращает значение и выдает исключение . Таким образом, клиентский код writeToFile хорош и чист и может естественным образом обрабатывать исключения. Более того, лямбды теперь используются так, как они должны были сделать наш код красивее и проще для понимания.

Смотрите полный список кодов ниже:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package util;
  
import java.io.*;
import java.util.zip.GZIPOutputStream;
  
public class LambdaExceptions {
    public static void main(String[] args) {
        try {
            //Write with compression
            writeToFile(new File("test"), "Hello World", GZIPOutputStream::new);
            //Just use the FileOutputStream
            writeToFile(new File("test"), "Hello World", i->i);
        }catch(IOException e){
            //deal with exception as you choose
        }
    }
     
    private static void writeToFile(File file, String value,
                       ThrowingFunction<OutputStream, OutputStream, IOException> writing) throws IOException{
        try (PrintWriter pw = new PrintWriter(new BufferedOutputStream
                (writing.apply(new FileOutputStream(file))))) {
            pw.write(value);
        }
    }
  
    @FunctionalInterface
    public interface ThrowingFunction<I, O, T extends Throwable> {
        O apply(I i) throws T;
    }
}