Статьи

Обман с исключениями — Java 8 Lambdas

Обман с исключениями — Java 8 Lambdas

Оставляя в стороне религиозные дебаты об исключениях Checked vs Runtime, бывают ситуации, когда из-за плохо сконструированных библиотек работа с проверенными примерами может свести вас с ума.

Рассмотрим этот фрагмент кода, который вы можете написать:

1
2
3
4
5
public void createTempFileForKey(String key) {
  Map<String, File> tempFiles = new ConcurrentHashMap<>();
  //does not compile because it throws an IOException!!
  tempFiles.computeIfAbsent(key, k -> File.createTempFile(key, ".tmp"));
}

Для его компиляции вам нужно поймать исключение, которое оставляет вас с этим кодом:

01
02
03
04
05
06
07
08
09
10
11
public void createTempFileForKey(String key) {
    Map<String, File> tempFiles = new ConcurrentHashMap<>();
    tempFiles.computeIfAbsent(key, k -> {
        try {
            return File.createTempFile(key, ".tmp");
        }catch(IOException e) {
            e.printStackTrace();
            return null;
        }
    });
}

Хотя это компилируется, IOException было эффективно поглощено. Пользователь этого метода должен быть проинформирован о том, что было сгенерировано исключение.

Для решения этой проблемы вы можете заключить IOException в общее RuntimeException, как показано ниже:

01
02
03
04
05
06
07
08
09
10
public void createTempFileForKey(String key) throws RuntimeException {
    Map<String, File> tempFiles = new ConcurrentHashMap<>();
    tempFiles.computeIfAbsent(key, k -> {
        try {
            return File.createTempFile(key, ".tmp");
        }catch(IOException e) {
            throw new RuntimeException(e);
        }
    });
}

Этот код генерирует исключение, но не фактическое исключение IOException, которое должно было быть выдано кодом. Возможно, что те, кто поддерживает только исключения RuntimeException, будут довольны этим кодом, особенно если решение может быть усовершенствовано для создания настраиваемого исключения IORuntimeException. Несмотря на то, что большинство людей File.createTempFile код, они ожидают, что их метод сможет генерировать проверенное IOException из метода File.createTempFile .

Естественный способ сделать это немного запутанным и выглядит так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public void createTempFileForKey(String key) throws IOException{
        Map<String, File> tempFiles = new ConcurrentHashMap<>();
        try {
            tempFiles.computeIfAbsent(key, k -> {
                try {
                    return File.createTempFile(key, ".tmp");
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
        }catch(RuntimeException e){
            if(e.getCause() instanceof IOException){
                throw (IOException)e.getCause();
            }
        }
}

Внутри лямбды вам нужно будет перехватить IOException, обернуть его в RuntimeException и выбросить это RuntimeException. Лямбда должна была бы перехватить распаковку RuntimeException и сбросить IOException. Все действительно ужасно!

В идеальном мире нам нужно уметь выдавать проверенное исключение из лямбды без необходимости изменять объявление computeIfAbsent. Другими словами, генерировать исключение проверки, как если бы это было исключение времени выполнения. Но, к сожалению, Java не позволяет нам сделать это …

Это не, если мы не обманываем! Вот два способа сделать именно то, что мы хотим, выбрасывая проверенное исключение, как если бы оно было исключением во время выполнения.

Метод 1 — Использование дженериков:

01
02
03
04
05
06
07
08
09
10
11
12
public static void main(String[] args){
        doThrow(new IOException());
    }
 
    static void doThrow(Exception e) {
        CheckedException.<RuntimeException> doThrow0(e);
    }
 
    static <E extends Exception>
      void doThrow0(Exception e) throws E {
          throw (E) e;
    }

Обратите внимание, что мы создали и сгенерировали исключение IOException без его объявления в основном методе.

Способ 2 — Использование небезопасных:

01
02
03
04
05
06
07
08
09
10
11
12
13
public static void main(String[] args){
        getUnsafe().throwException(new IOException());
    }
 
    private static Unsafe getUnsafe(){
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            return (Unsafe) theUnsafe.get(null);
        } catch (Exception e) {
            throw new AssertionError(e);
        }
    }

Снова нам удалось создать исключение IOException, не объявив его в методе.

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

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
public void createTempFileForKey(String key) throws IOException{
        Map<String, File> tempFiles = new ConcurrentHashMap<>();
 
        tempFiles.computeIfAbsent(key, k -> {
            try {
                return File.createTempFile(key, ".tmp");
            } catch (IOException e) {
                throw doThrow(e);
            }
        });
    }
     
    private RuntimeException doThrow(Exception e){
        getUnsafe().throwException(e);
        return new RuntimeException();
    }
 
    private static Unsafe getUnsafe(){
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            return (Unsafe) theUnsafe.get(null);
        } catch (Exception e) {
            throw new AssertionError(e);
        }
    }

doThrow() метод doThrow() будет инкапсулирован в некоторый служебный класс, оставляя ваш код в createTempFileForKey() довольно чистым.

Ссылка: Обман с исключениями — Java 8 Lambdas от нашего партнера по JCG Дэниела Шая в блоге Rational Java .