Вопрос-ответ

How can I throw checked exceptions from inside Java 8 lambdas/streams?

Как я могу генерировать проверенные исключения из Java 8 lambdas / streams?

Как я могу генерировать проверенные исключения из Java 8 lambda, используемые, например, в потоке?

Другими словами, я хочу, чтобы подобный код компилировался:

public List<Class> getClasses() throws ClassNotFoundException {     

List<Class> classes =
Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
.map(className -> Class.forName(className))
.collect(Collectors.toList());
return classes;
}

Этот код не компилируется, поскольку Class.forName() метод, приведенный выше, выдает ClassNotFoundException, который проверяется.

Пожалуйста, обратите внимание, что я НЕ хочу переносить проверенное исключение внутрь исключения во время выполнения и вместо этого создавать обернутое непроверенное исключение. Я хочу генерировать само проверяемое исключение, и без добавления уродливого try/catches в поток.

Переведено автоматически
Ответ 1

Простой ответ на ваш вопрос таков: вы не можете, по крайней мере, напрямую. И это не ваша вина. Oracle все испортил. Они цепляются за концепцию проверяемых исключений, но непоследовательно забыли позаботиться о проверяемых исключениях при проектировании функциональных интерфейсов, потоков, лямбда и т.д. Все это льет воду на мельницу таких экспертов, как Роберт К. Мартин, которые называют проверенные исключения неудачным экспериментом.

На мой взгляд, это огромная ошибка в API и незначительная ошибка в языковой спецификации.

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

Ошибка в спецификации языка заключается в том, что она не позволяет параметру type выводить список типов вместо отдельного типа, пока параметр type используется только в ситуациях, когда список типов допустим (throws предложение).

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

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

public class CheckedStream {
// List variant to demonstrate what we actually had before refactoring.
public List<Class> getClasses(final List<String> names) throws ClassNotFoundException {
final List<Class> classes = new ArrayList<>();
for (final String name : names)
classes.add(Class.forName(name));
return classes;
}

// The Stream function which we want to compile.
public Stream<Class> getClasses(final Stream<String> names) throws ClassNotFoundException {
return names.map(Class::forName);
}
}

Однако это дает:

cher@armor1:~/playground/Java/checkedStream$ javac CheckedStream.java 
CheckedStream.java:13: error: incompatible thrown types ClassNotFoundException in method reference
return names.map(Class::forName);
^
1 error

Способ, которым определены функциональные интерфейсы, в настоящее время не позволяет компилятору пересылать исключение - нет объявления, которое сообщало бы также, Stream.map() что if Function.apply() throws E, Stream.map() throws E (или, более конкретно, поскольку потоки являются отложенными конвейерами, что любая операция терминала, подобная forEach() or reduce()).

Чего не хватает, так это объявления параметра типа для передачи через проверенные исключения из аргументов функции вниз по потоковому конвейеру к операциям терминала. Следующий код показывает, как такой параметр сквозного типа на самом деле мог быть объявлен с текущим синтаксисом (для простоты, не ленивый пример). За исключением особого случая в отмеченной строке, который является ограничением, обсуждаемым ниже, этот код компилируется и ведет себя так, как ожидалось.

import java.io.IOException;
interface Function<T, R, E extends Throwable> {
// Declare you throw E, whatever that is.
R apply(T t) throws E;
}

interface Stream<T> {
// Pass through E, whatever mapper defined for E.
<R, E extends Throwable> Stream<R> map(Function<? super T, ? extends R, E> mapper) throws E;
}

class Main {
public static void main(final String... args) throws ClassNotFoundException {
final Stream<String> s = null;

// Works: E is ClassNotFoundException.
s.map(Class::forName);

// Works: E is RuntimeException (probably).
s.map(Main::convertClass);

// Works: E is ClassNotFoundException.
s.map(Main::throwSome);

// Doesn't work: E is Exception.
s.map(Main::throwSomeMore); // error: unreported exception Exception; must be caught or declared to be thrown
}

public static Class convertClass(final String s) {
return Main.class;
}

static class FooException extends ClassNotFoundException {}

static class BarException extends ClassNotFoundException {}

public static Class throwSome(final String s) throws FooException, BarException {
throw new FooException();
}

public static Class throwSomeMore(final String s) throws ClassNotFoundException, IOException {
throw new FooException();
}
}

В случае throwSomeMore мы хотели бы видеть, что IOException пропущено, но на самом деле оно пропущено Exception.

Это не идеально, потому что вывод типа, похоже, ищет один тип, даже в случае исключений. Поскольку для вывода типа требуется один тип, E необходимо преобразовать в общий super из ClassNotFoundException и IOException, который является Exception.

Необходима настройка определения вывода типов, чтобы компилятор искал несколько типов, если параметр type используется там, где допустим список типов (throws предложение). Тогда тип исключения, сообщаемый компилятором, был бы таким же конкретным, как исходное throws объявление проверенных исключений метода, на который ссылается ссылка, а не единого универсального супертипа.

Плохая новость в том, что это означает, что Oracle все перепутала. Конечно, они не нарушат пользовательский код, но введение параметров типа исключения в существующие функциональные интерфейсы нарушит компиляцию всего пользовательского кода, который явно использует эти интерфейсы. Им придется изобрести какой-нибудь новый синтаксический сахар, чтобы исправить это.

Еще хуже то, что эта тема уже обсуждалась Брайаном Гетцем в 2010 году (https://blogs.oracle.com/briangoetz/entry/exception_transparency_in_java, http://mail.openjdk.java.net/pipermail/lambda-dev/2010-June/001484.html) но мне сообщили, что это расследование в конечном итоге не увенчалось успехом, и что в Oracle, насколько я знаю, в настоящее время нет работы по смягчению взаимодействия между проверяемыми исключениями и лямбдами.

Ответ 2

Этот LambdaExceptionUtil вспомогательный класс позволяет использовать любые проверенные исключения в Java streams, например:

Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
.map(rethrowFunction(Class::forName))
.collect(Collectors.toList());

Обратите внимание на Class::forName выбрасываниеClassNotFoundException, которое проверяется. Сам поток также генерирует ClassNotFoundException, а НЕ какое-то непроверенное исключение.

public final class LambdaExceptionUtil {

@FunctionalInterface
public interface Consumer_WithExceptions<T, E extends Exception> {
void accept(T t) throws E;
}

@FunctionalInterface
public interface BiConsumer_WithExceptions<T, U, E extends Exception> {
void accept(T t, U u) throws E;
}

@FunctionalInterface
public interface Function_WithExceptions<T, R, E extends Exception> {
R apply(T t) throws E;
}

@FunctionalInterface
public interface Supplier_WithExceptions<T, E extends Exception> {
T get() throws E;
}

@FunctionalInterface
public interface Runnable_WithExceptions<E extends Exception> {
void run() throws E;
}

/** .forEach(rethrowConsumer(name -> System.out.println(Class.forName(name)))); or .forEach(rethrowConsumer(ClassNameUtil::println)); */
public static <T, E extends Exception> Consumer<T> rethrowConsumer(Consumer_WithExceptions<T, E> consumer) throws E {
return t -> {
try { consumer.accept(t); }
catch (Exception exception) { throwAsUnchecked(exception); }
};
}

public static <T, U, E extends Exception> BiConsumer<T, U> rethrowBiConsumer(BiConsumer_WithExceptions<T, U, E> biConsumer) throws E {
return (t, u) -> {
try { biConsumer.accept(t, u); }
catch (Exception exception) { throwAsUnchecked(exception); }
};
}

/** .map(rethrowFunction(name -> Class.forName(name))) or .map(rethrowFunction(Class::forName)) */
public static <T, R, E extends Exception> Function<T, R> rethrowFunction(Function_WithExceptions<T, R, E> function) throws E {
return t -> {
try { return function.apply(t); }
catch (Exception exception) { throwAsUnchecked(exception); return null; }
};
}

/** rethrowSupplier(() -> new StringJoiner(new String(new byte[]{77, 97, 114, 107}, "UTF-8"))), */
public static <T, E extends Exception> Supplier<T> rethrowSupplier(Supplier_WithExceptions<T, E> function) throws E {
return () -> {
try { return function.get(); }
catch (Exception exception) { throwAsUnchecked(exception); return null; }
};
}

/** uncheck(() -> Class.forName("xxx")); */
public static void uncheck(Runnable_WithExceptions t)
{
try { t.run(); }
catch (Exception exception) { throwAsUnchecked(exception); }
}

/** uncheck(() -> Class.forName("xxx")); */
public static <R, E extends Exception> R uncheck(Supplier_WithExceptions<R, E> supplier)
{
try { return supplier.get(); }
catch (Exception exception) { throwAsUnchecked(exception); return null; }
}

/** uncheck(Class::forName, "xxx"); */
public static <T, R, E extends Exception> R uncheck(Function_WithExceptions<T, R, E> function, T t) {
try { return function.apply(t); }
catch (Exception exception) { throwAsUnchecked(exception); return null; }
}

@SuppressWarnings ("unchecked")
private static <E extends Throwable> void throwAsUnchecked(Exception exception) throws E { throw (E)exception; }

}

Много других примеров того, как это использовать (после статического импорта LambdaExceptionUtil):

@Test
public void test_Consumer_with_checked_exceptions() throws IllegalAccessException {
Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
.forEach(rethrowConsumer(className -> System.out.println(Class.forName(className))));

Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
.forEach(rethrowConsumer(System.out::println));
}

@Test
public void test_Function_with_checked_exceptions() throws ClassNotFoundException {
List<Class> classes1
= Stream.of("Object", "Integer", "String")
.map(rethrowFunction(className -> Class.forName("java.lang." + className)))
.collect(Collectors.toList());

List<Class> classes2
= Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
.map(rethrowFunction(Class::forName))
.collect(Collectors.toList());
}

@Test
public void test_Supplier_with_checked_exceptions() throws ClassNotFoundException {
Collector.of(
rethrowSupplier(() -> new StringJoiner(new String(new byte[]{77, 97, 114, 107}, "UTF-8"))),
StringJoiner::add, StringJoiner::merge, StringJoiner::toString);
}

@Test
public void test_uncheck_exception_thrown_by_method() {
Class clazz1 = uncheck(() -> Class.forName("java.lang.String"));

Class clazz2 = uncheck(Class::forName, "java.lang.String");
}

@Test (expected = ClassNotFoundException.class)
public void test_if_correct_exception_is_still_thrown_by_method() {
Class clazz3 = uncheck(Class::forName, "INVALID");
}

ОБНОВЛЕНИЕ от ноября 2015 г. Код был улучшен с помощью @PaoloC, пожалуйста, проверьте его ответ ниже и поддержите его. Он помог решить последнюю проблему: теперь компилятор попросит вас добавить предложения throw, и все будет так, как если бы вы могли генерировать проверенные исключения изначально в потоках Java 8.

Note 1

The rethrow methods of the LambdaExceptionUtil class above may be used without fear, and are OK to use in any situation.

Note 2

The uncheck methods of the LambdaExceptionUtil class above are bonus methods, and may be safely removed them from the class if you don't want to use them. If you do used them, do it with care, and not before understanding the following use cases, advantages/disadvantages and limitations:


  • You may use the uncheck methods if you are calling a method which literally can never throw the exception that it declares. For example: new String(byteArr, "UTF-8") throws UnsupportedEncodingException, but UTF-8 is guaranteed by the Java spec to always be present. Here, the throws declaration is a nuisance and any solution to silence it with minimal boilerplate is welcome:


    String text = uncheck(() -> new String(byteArr, "UTF-8"));


  • You may use the uncheck methods if you are implementing a strict interface where you don't have the option for adding a throws declaration, and yet throwing an exception is entirely appropriate. Wrapping an exception just to gain the privilege of throwing it results in a stacktrace with spurious exceptions which contribute no information about what actually went wrong. A good example is Runnable.run(), which does not throw any checked exceptions.



  • In any case, if you decide to use the uncheck methods, be aware of these two consequences of throwing CHECKED exceptions without a throws clause:



    1. The calling-code won't be able to catch it by name (if you try, the compiler will say: "Exception is never thrown in body of corresponding try statement"). It will bubble and probably be caught in the main program loop by some catch Exception or catch Throwable, which may be what you want anyway.

    2. It violates the principle of least surprise: it will no longer be enough to catch RuntimeException to be able to guarantee catching all possible exceptions. For this reason, I believe this should not be done in framework code, but only in business code that you completely control.



References

Ответ 3

Вы можете!

Расширяя @marcg UtilException и добавляя throw E там, где это необходимо: таким образом, компилятор попросит вас добавить предложения throw и все будет так, как если бы вы могли генерировать проверенные исключения изначально в потоках Java 8.

Инструкции: просто скопируйте / вставьте LambdaExceptionUtil в вашу IDE, а затем используйте его, как показано ниже LambdaExceptionUtilTest.

public final class LambdaExceptionUtil {

@FunctionalInterface
public interface Consumer_WithExceptions<T, E extends Exception> {
void accept(T t) throws E;
}

@FunctionalInterface
public interface Function_WithExceptions<T, R, E extends Exception> {
R apply(T t) throws E;
}

/**
* .forEach(rethrowConsumer(name -> System.out.println(Class.forName(name))));
*/

public static <T, E extends Exception> Consumer<T> rethrowConsumer(Consumer_WithExceptions<T, E> consumer) throws E {
return t -> {
try {
consumer.accept(t);
} catch (Exception exception) {
throwActualException(exception);
}
};
}

/**
* .map(rethrowFunction(name -> Class.forName(name))) or .map(rethrowFunction(Class::forName))
*/

public static <T, R, E extends Exception> Function<T, R> rethrowFunction(Function_WithExceptions<T, R, E> function) throws E {
return t -> {
try {
return function.apply(t);
} catch (Exception exception) {
throwActualException(exception);
return null;
}
};
}

@SuppressWarnings("unchecked")
private static <E extends Exception> void throwActualException(Exception exception) throws E {
throw (E) exception;
}

}

Несколько тестов, чтобы показать использование и поведение:

public class LambdaExceptionUtilTest {

@Test(expected = MyTestException.class)
public void testConsumer() throws MyTestException {
Stream.of((String)null).forEach(rethrowConsumer(s -> checkValue(s)));
}

private void checkValue(String value) throws MyTestException {
if(value==null) {
throw new MyTestException();
}
}

private class MyTestException extends Exception { }

@Test
public void testConsumerRaisingExceptionInTheMiddle() {
MyLongAccumulator accumulator = new MyLongAccumulator();
try {
Stream.of(2L, 3L, 4L, null, 5L).forEach(rethrowConsumer(s -> accumulator.add(s)));
fail();
} catch (MyTestException e) {
assertEquals(9L, accumulator.acc);
}
}

private class MyLongAccumulator {
private long acc = 0;
public void add(Long value) throws MyTestException {
if(value==null) {
throw new MyTestException();
}
acc += value;
}
}

@Test
public void testFunction() throws MyTestException {
List<Integer> sizes = Stream.of("ciao", "hello").<Integer>map(rethrowFunction(s -> transform(s))).collect(toList());
assertEquals(2, sizes.size());
assertEquals(4, sizes.get(0).intValue());
assertEquals(5, sizes.get(1).intValue());
}

private Integer transform(String value) throws MyTestException {
if(value==null) {
throw new MyTestException();
}
return value.length();
}

@Test(expected = MyTestException.class)
public void testFunctionRaisingException() throws MyTestException {
Stream.of("ciao", null, "hello").<Integer>map(rethrowFunction(s -> transform(s))).collect(toList());
}

}
Ответ 4

Вы не можете сделать это безопасно. Вы можете жульничать, но тогда ваша программа сломается, и это неизбежно вернется, чтобы укусить кого-нибудь (это должны быть вы, но часто наше жульничество сказывается на ком-то другом).

Вот немного более безопасный способ сделать это (но я все равно не рекомендую это.)

class WrappedException extends RuntimeException {
Throwable cause;

WrappedException(Throwable cause) { this.cause = cause; }
}

static WrappedException throwWrapped(Throwable t) {
throw new WrappedException(t);
}

try
source.stream()
.filter(e -> { ... try { ... } catch (IOException e) { throwWrapped(e); } ... })
...
}
catch (WrappedException w) {
throw (IOException) w.cause;
}

Здесь то, что вы делаете, это перехватываете исключение в lambda, выдаете сигнал из конвейера stream, который указывает на исключительную ошибку вычисления, перехватываете сигнал и действуете по этому сигналу, чтобы выдать базовое исключение. Ключ в том, что вы всегда перехватываете синтетическое исключение, а не позволяете проверяемому исключению просачиваться, не объявляя, что генерируется исключение.

java lambda java-8 java-stream