it-swarm.com.ru

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 для захвата в анонимном классе [функциональный интерфейс] или лямбда-выражении)

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


Обновление 1

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

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

68
Lyubomyr Shaydariv

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

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);
    }
}
42
JB Nizet

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

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

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

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

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

В те дни интерфейс Consumer назывался Block.

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

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

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

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

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

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

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

34
Edwin Dalorzo

Сентябрь 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
micha

Рассматривали ли вы возможность использования класса-оболочки 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
GlenPeterson

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

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

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

3
Grzegorz Piwowarek

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

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

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

1
GlenPeterson

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

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

1
Waldo auf der Springe

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

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

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

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

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

0
rogerdpack