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

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


обновление 1

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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


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

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

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

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

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