Java 8: обязательная проверенная обработка исключений в лямбда-выражениях. Почему обязательно, а не по желанию?


я играю с новыми функциями лямбда в Java 8, и обнаружил, что методы, предлагаемые Java 8, действительно полезны. Однако, мне интересно, есть ли хороший способ сделать обход для следующего сценария. Предположим, у вас есть оболочка пула объектов, которая требует какой-то фабрики для заполнения пула объектов, например (используя java.lang.functions.Factory):

public class JdbcConnectionPool extends ObjectPool<Connection> {

    public ConnectionPool(int maxConnections, String url) {
        super(new Factory<Connection>() {
            @Override
            public Connection make() {
                try {
                    return DriverManager.getConnection(url);
                } catch ( SQLException ex ) {
                    throw new RuntimeException(ex);
                }
            }
        }, maxConnections);
    }

}

после преобразования функционального интерфейса в лямбда-выражение, приведенный выше код становится похожим что:

public class JdbcConnectionPool extends ObjectPool<Connection> {

    public ConnectionPool(int maxConnections, String url) {
        super(() -> {
            try {
                return DriverManager.getConnection(url);
            } catch ( SQLException ex ) {
                throw new RuntimeException(ex);
            }
        }, maxConnections);
    }

}

не так уж и плохо, но проверенное исключение java.sql.SQLException требует try/catch блок внутри лямбды. В моей компании мы уже давно используем два интерфейса:

  • IOut<T>, что эквивалентно java.lang.functions.Factory;
  • и специальный интерфейс для случаев, которые обычно требуют распространения проверенных исключений:interface IUnsafeOut<T, E extends Throwable> { T out() throws E; }.

и IOut<T> и IUnsafeOut<T> должны быть удалены во время миграции на Java 8, однако нет точного соответствия для IUnsafeOut<T, E>. Если лямбда-выражения могут иметь дело с проверенными исключениями, как они были сняты, можно было бы использовать просто как следующее В конструкторе выше:

super(() -> DriverManager.getConnection(url), maxConnections);

это выглядит намного чище. Я вижу, что могу переписать ObjectPool супер класс, чтобы принять наш IUnsafeOut<T>, но насколько я знаю, Java 8 еще не закончена, поэтому могут быть некоторые изменения, такие как:

  • реализация чего-то похожего на IUnsafeOut<T, E>? (честно говоря, я считаю, что грязно-субъект должен выбрать, что принять: либо Factory или "небезопасная фабрика", которая не может иметь совместимых сигнатур метода)
  • просто игнорируя проверенные исключения в лямбдах, так что нет необходимости в IUnsafeOut<T, E> суррогаты? (почему бы и нет? например, еще одно важное изменение: OpenJDK, который я использую,javac теперь не требует, чтобы переменные и параметры были объявлены как final для записи в анонимный класс [функциональный интерфейс] или лямбда выражение)

таким образом, вопрос, как правило, заключается в следующем: есть ли способ обойти проверенные исключения в lambdas или он планируется в будущем, пока Java 8 не будет окончательно выпущен?


обновление 1

Hm-m-m, насколько я понимаю, что у нас сейчас есть, кажется, что на данный момент нет никакого способа, несмотря на то, что упомянутая статья датируется 2010 годом: Брайан Гетц объясняет прозрачность исключений в Java. Если ничего не изменилось в Java 8, это может быть обдумал ответ. Также Брайан говорит, что interface ExceptionalCallable<V, E extends Exception> (что я упомянул как IUnsafeOut<T, E extends Throwable> из нашего кода наследие) в значительной степени бесполезно, и я согласен с ним.

я все еще скучаю по чему-то еще?

8 66

8 ответов:

Не уверен, что я действительно отвечу на ваш вопрос, но не могли бы вы просто использовать что-то вроде этого?

public final class SupplierUtils {
    private SupplierUtils() {
    }

    public static <T> Supplier<T> wrap(Callable<T> callable) {
        return () -> {
            try {
                return callable.call();
            }
            catch (RuntimeException e) {
                throw e;
            }
            catch (Exception e) {
                throw new RuntimeException(e);
            }
        };
    }
}

public class JdbcConnectionPool extends ObjectPool<Connection> {

    public JdbcConnectionPool(int maxConnections, String url) {
        super(SupplierUtils.wrap(() -> DriverManager.getConnection(url)), maxConnections);
    }
}

в списке рассылки лямбда это было тщательно обсудили. Как вы можете видеть, Брайан Гетц предложил там, что альтернативой является написать свой собственный комбинатор:

или вы могли бы написать свой собственный тривиальный комбинатора:

static<T> Supplier<T> exceptionWrappingSupplier(Supplier<T> b) {
     return e -> {
         try { b.accept(e); }
         catch (Exception e) { throw new RuntimeException(e); }
     };
}

вы можете написать его один раз, за меньшее время, чем потребовалось для написания вашего оригинальный e-mail. И точно так же один раз для каждого вида Сэма вы используете.

Я бы предпочел, чтобы мы смотрели на это как на" стекло 99% полное", а чем альтернатива. Не все проблемы требуют новых языковых функций, как решения. (Не говоря уже о том, что новые языковые функции всегда вызывают новые трудности.)

в те времена интерфейс потребителя назывался блоком.

Я думаю, что это соответствует ответ JB Nizet.

Позже Брайан объясняет, почему Это было разработано таким образом (причина проблемы)

да, вам придется предоставить ваши собственные исключительные Сэмы. Но тогда лямбда преобразование будет отлично работать с ними.

например, обсуждается дополнительная поддержка языка и библиотеки для этого проблема, и в конце концов почувствовал, что это было плохо затраты / выгоды компромисс.

библиотечные решения вызывают взрыв 2x в типах SAM (исключительный vs not), которые плохо взаимодействуют с существующими комбинаторными взрывами для примитивной специализации.

доступный язык на основе решения были проигравшими от a компромисс сложности / ценности. Хотя есть некоторые альтернативы решения мы будем продолжать изучать, хотя явно не для 8 и, вероятно, не для 9 тоже.

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

сентября 2015 года:

можно использовать ET для этого. ET-это небольшая библиотека Java 8 для преобразования/перевода исключений.

С ET вы можете написать:

super(() -> et.withReturningTranslation(() -> DriverManager.getConnection(url)), maxConnections);

Multi line версия:

super(() -> {
  return et.withReturningTranslation(() -> DriverManager.getConnection(url));
}, maxConnections);

все, что вам нужно сделать раньше, это создать новый ExceptionTranslator например:

ExceptionTranslator et = ET.newConfiguration().done();

этот экземпляр является потокобезопасным и может использоваться несколькими компонентами. Можно настроить более конкретное преобразование исключений правила (например,FooCheckedException -> BarRuntimeException), если вам нравится. Если другие правила недоступны, проверенные исключения автоматически преобразуются в RuntimeException.

(отказ от ответственности: я являюсь автором ET)

вы рассматривали возможность использования класса-оболочки RuntimeException (unchecked) для контрабанды исходного исключения из лямбда-выражения, а затем приведения обернутого исключения назад чтобы это оригинальное проверенное исключение?

class WrappedSqlException extends RuntimeException {
    static final long serialVersionUID = 20130808044800000L;
    public WrappedSqlException(SQLException cause) { super(cause); }
    public SQLException getSqlException() { return (SQLException) getCause(); }
}

public ConnectionPool(int maxConnections, String url) throws SQLException {
    try {
        super(() -> {
            try {
                return DriverManager.getConnection(url);
            } catch ( SQLException ex ) {
                throw new WrappedSqlException(ex);
            }
        }, maxConnections);
    } catch (WrappedSqlException wse) {
        throw wse.getSqlException();
    }
}

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

Мда... Единственное, что я вижу, что проблема здесь заключается в том, что вы делаете это внутри конструктора с вызовом super (), который по закону должен быть первым оператором в вашем конструкторе. Делает try считать предыдущим утверждением? У меня это работает (без конструктора) в моем собственном коде.

мы разработали внутренний проект в моей компании, который помог нам в этом. Мы решили выйти на публику два месяца назад.

вот что мы придумали:

@FunctionalInterface
public interface ThrowingFunction<T,R,E extends Throwable> {
R apply(T arg) throws E;

/**
 * @param <T> type
 * @param <E> checked exception
 * @return a function that accepts one argument and returns it as a value.
 */
static <T, E extends Exception> ThrowingFunction<T, T, E> identity() {
    return t -> t;
}

/**
 * @return a Function that returns the result of the given function as an Optional instance.
 * In case of a failure, empty Optional is returned
 */
static <T, R, E extends Exception> Function<T, Optional<R>> lifted(ThrowingFunction<T, R, E> f) {
    Objects.requireNonNull(f);

    return f.lift();
}

static <T, R, E extends Exception> Function<T, R> unchecked(ThrowingFunction<T, R, E> f) {
    Objects.requireNonNull(f);

    return f.uncheck();
}

default <V> ThrowingFunction<V, R, E> compose(final ThrowingFunction<? super V, ? extends T, E> before) {
    Objects.requireNonNull(before);

    return (V v) -> apply(before.apply(v));
}

default <V> ThrowingFunction<T, V, E> andThen(final ThrowingFunction<? super R, ? extends V, E> after) {
    Objects.requireNonNull(after);

    return (T t) -> after.apply(apply(t));
}

/**
 * @return a Function that returns the result as an Optional instance. In case of a failure, empty Optional is
 * returned
 */
default Function<T, Optional<R>> lift() {
    return t -> {
        try {
            return Optional.of(apply(t));
        } catch (Throwable e) {
            return Optional.empty();
        }
    };
}

/**
 * @return a new Function instance which wraps thrown checked exception instance into a RuntimeException
 */
default Function<T, R> uncheck() {
    return t -> {
        try {
            return apply(t);
        } catch (final Throwable e) {
            throw new WrappedException(e);
        }
    };
}

}

https://github.com/TouK/ThrowingFunction/

обертывание исключения описанным способом не работает. Я попробовал это, и я все еще получаю ошибки компилятора, что на самом деле соответствует спецификации: лямбда-выражение выбрасывает исключение, которое несовместимо с целевым типом аргумента метода: Callable; call() не выбрасывает его, поэтому я не могу передать лямбда-выражение как вызываемое.

Так что в принципе нет никакого решения: мы застряли с написанием шаблона. Единственное, что мы можем сделать, это высказать свое мнение, что это нужно исправить. Я думаю, что спецификация не должна просто слепо отбрасывать целевой тип на основе несовместимых брошенных исключений: она должна впоследствии проверить, поймано ли брошенное несовместимое исключение или объявлено как Броски в области вызова. А для лямбда-выражений, которые не встроены, я предлагаю пометить их как молчаливое выбрасывание проверенного исключения (молчание в том смысле, что компилятор не должен проверять, но среда выполнения все равно должна ловить). давайте пометим те с => в отличие от - > Я знаю, что это не дискуссионный сайт, но поскольку это единственное решение вопроса, позвольте себе быть услышанным и давайте изменим эту спецификацию!

Paguro предоставляет функциональные интерфейсы, которые обертывают проверенные исключения. Я начал работать над ним через несколько месяцев после того, как вы задали свой вопрос, так что вы, вероятно, были частью вдохновения для него!

вы заметите, что в Paguro есть только 4 функциональных интерфейса против 43 интерфейсов, включенных в Java 8. Это потому, что Пагуро предпочитает дженерики примитивам.

Paguro имеет однопроходные преобразования, встроенные в его неизменяемые коллекции (скопировано с Clojure). Эти преобразования примерно эквивалентны преобразователям Clojure или потокам Java 8, но они принимают функциональные интерфейсы, которые обертывают проверенные исключения. Смотрите:различия между потоками Paguro и Java 8.

вы можете бросьте из своих лямбд, они просто должны быть объявлены "по-своему" (что делает их, к сожалению, не повторно используемыми в стандартном коде JDK, но эй, мы делаем то, что мы можем).

@FunctionalInterface
public interface SupplierIOException {
   MyClass get() throws IOException;
}

или более универсальная версия:

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

ref здесь. Существует также упоминание об использовании "sneakyThrow", чтобы не объявлять проверенные исключения, но затем все равно бросать их. Это немного болит голова, может быть, вариант.