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

Я играю с новыми функциями лямбда в Java 8 и обнаружил, что практика, предлагаемая Java 8, действительно полезна. Тем не менее, мне интересно, есть ли хороший способ сделать обход для следующего сценария. Предположим, у вас есть оболочка пула объектов, для которой требуется некоторый вид factory для заполнения пула объектов, например (с помощью 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, либо "небезопасный factory", который не может иметь совместимые сигнатуры методов)
  • просто игнорирует проверенные исключения в lambdas, поэтому нет необходимости в IUnsafeOut<T, E> суррогатах? (почему бы и нет? Например, еще одно важное изменение: OpenJDK, который я использую, javac теперь не требует, чтобы переменные и параметры объявлялись как final для захвата в анонимном классе [функциональный интерфейс] или лямбда-выражение)

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


Обновление 1

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

Я еще что-то пропустил?

Ответы

Ответ 1

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

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);
    }
}

Ответ 2

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

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

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

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

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

В те дни пользовательский интерфейс назывался Block.

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

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

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

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

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

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

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

Ответ 3

Считаете ли вы использование класса оболочки 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 как предыдущее утверждение? У меня есть эта работа (без конструктора) в моем собственном коде.

Ответ 4

Сентябрь 2015:

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

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

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

Многострочная версия:

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

Все, что вам нужно сделать до этого, создает новый экземпляр ExceptionTranslator:

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

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

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

Ответ 5

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

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

@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/

Ответ 6

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

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

Ответ 7

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

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

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

Ответ 8

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

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

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

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

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