web-dev-qa-db-fra.com

Java 8: Gestion des exceptions vérifiées obligatoires dans les expressions lambda. Pourquoi obligatoire, pas optionnel?

Je joue avec les nouvelles fonctionnalités de lambda dans Java 8 et découvre que les pratiques proposées par Java 8 sont vraiment utiles. Cependant, je me demande s’il existe un bon moyen de contourner le scénario suivant. Supposons que vous ayez un wrapper de pool d'objets qui nécessite une sorte de fabrique pour remplir le pool d'objets, par exemple (en utilisant 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);
    }

}

Après avoir transformé l'interface fonctionnelle en expression lambda, le code ci-dessus devient comme ceci:

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

}

Pas si mal en effet, mais l'exception vérifiée Java.sql.SQLException nécessite un bloc try/catch à l'intérieur du lambda. Dans mon entreprise, nous utilisons deux interfaces depuis longtemps:

  • IOut<T> qui est équivalent à Java.lang.functions.Factory;
  • et une interface spéciale pour les cas qui nécessitent généralement la propagation des exceptions vérifiées: interface IUnsafeOut<T, E extends Throwable> { T out() throws E; }.

IOut<T> et IUnsafeOut<T> sont supposés être supprimés lors de la migration vers Java 8, cependant, il n'y a pas de correspondance exacte pour IUnsafeOut<T, E>. Si les expressions lambda pouvaient traiter les exceptions vérifiées comme si elles étaient désélectionnées, il pourrait être possible d'utiliser simplement le même résultat dans le constructeur ci-dessus:

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

Cela a l'air beaucoup plus propre. Je vois que je peux réécrire la super classe ObjectPool pour accepter notre IUnsafeOut<T>, mais pour autant que je sache, Java 8 n’est pas encore terminé, il est donc possible que des modifications telles que:

  • implémenter quelque chose de similaire à IUnsafeOut<T, E>? (pour être honnête, j’estime que c’est sale - le sujet doit choisir ce qu’il accepte: soit Factory, soit une "fabrique non sécurisée" qui ne peut pas avoir de signatures de méthode compatibles)
  • simplement en ignorant les exceptions vérifiées dans lambdas, donc pas besoin dans les substituts IUnsafeOut<T, E>? (pourquoi pas? Par exemple, un autre changement important: OpenJDK, que j’utilise, javac ne nécessite plus que les variables et les paramètres soient déclarés comme final pour être capturés dans une classe anonyme [interface fonctionnelle] ou une expression lambda)

La question est donc généralement la suivante: existe-t-il un moyen de contourner les exceptions vérifiées dans lambdas ou est-il prévu dans le futur jusqu'à la sortie définitive de Java 8?


Mise à jour 1

Hm-m-m, pour autant que je sache ce que nous avons actuellement, il semble qu'il n'y ait aucun moyen pour le moment, bien que l'article cité en référence date de 2010: Brian Goetz explique la transparence des exceptions en Java . Si rien ne change beaucoup dans Java 8, cela peut être considéré comme une réponse. Brian dit aussi que interface ExceptionalCallable<V, E extends Exception> (ce que j'ai mentionné comme étant IUnsafeOut<T, E extends Throwable> de notre héritage de code) est quasiment inutile, et je suis d'accord avec lui.

Est-ce que je manque encore quelque chose?

68
Lyubomyr Shaydariv

Pas sûr que je réponde vraiment à votre question, mais pourriez-vous simplement utiliser quelque chose comme ça?

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

Dans la liste de diffusion lambda, cela a été discuté en détail . Comme vous pouvez le constater, Brian Goetz a suggéré que l’alternative consiste à écrire votre propre combinateur:

Ou vous pourriez écrire votre propre combinateur trivial:

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

Vous pouvez l'écrire une fois, en moins que le temps qu'il a fallu pour écrire votre e-mail d'origine. Et pareillement une fois pour chaque type de SAM que vous utilisez.

Je préférerais que nous considérions cela comme "du verre rempli à 99%" plutôt que du alternative. Tous les problèmes ne nécessitent pas de nouvelles fonctionnalités linguistiques, telles que solutions. (Sans compter que les nouvelles fonctionnalités linguistiques causent toujours De nouveaux problèmes.)

A cette époque, l'interface consommateur s'appelait Block.

Je pense que cela correspond à la réponse de JB Nizet .

Plus tard, Brian explique pourquoi ceci a été conçu de cette manière (la raison du problème)

Oui, vous devez fournir vos propres SAM exceptionnelles. Mais alors lambda la conversion fonctionnerait bien avec eux.

Le Groupe d’experts a discuté de l’ajout de langues et de bibliothèques dans ce domaine problème, et a finalement estimé que c’était un mauvais rapport coût/bénéfice troquer.

Les solutions basées sur la bibliothèque provoquent une explosion 2x des types SAM (exceptionnels Vs. Non), qui interagissent mal avec les explosions combinatoires existantes pour la spécialisation primitive.

Les solutions linguistiques disponibles étaient les perdants d’un compromis complexité/valeur. Bien qu'il existe une alternative solutions que nous allons continuer à explorer - bien que ce ne soit clairement pas le cas pour 8 et probablement pas pour 9 non plus.

En attendant, vous avez les outils pour faire ce que vous voulez. Je comprends ça vous préférez nous fournir ce dernier kilomètre pour vous (et, secondairement, votre demande est vraiment une demande à peine voilée pour "pourquoi ne pas simplement abandonner les exceptions vérifiées déjà"), mais je actuel l'état vous permet de faire votre travail.

34
Edwin Dalorzo

Septembre 2015:

Vous pouvez utiliser ET pour cela. ET est une petite bibliothèque Java 8 pour la conversion/traduction des exceptions.

Avec ET vous pouvez écrire:

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

Version multi ligne:

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

Tout ce que vous devez faire avant, c'est créer une nouvelle instance ExceptionTranslator:

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

Cette instance est thread-safe et peut être partagée par plusieurs composants. Vous pouvez configurer des règles de conversion d'exception plus spécifiques (par exemple, FooCheckedException -> BarRuntimeException) si vous le souhaitez. Si aucune autre règle n'est disponible, les exceptions cochées sont automatiquement converties en RuntimeException.

(Avertissement: je suis l'auteur d'ET)

5
micha

Avez-vous envisagé d’utiliser une classe wrapper RuntimeException (non cochée) pour passer l’exception d’origine en dehors de l’expression lambda, puis de transtyper l’exception encapsulée back vers son exception vérifiée d’origine?

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

La création de votre propre classe unique devrait permettre d'éviter de confondre une autre exception non vérifiée avec celle que vous avez enveloppée dans votre lambda, même si cette exception est sérialisée quelque part dans le pipeline avant que vous ne la récupériez.

Hmm ... La seule chose qui me pose problème ici, c'est que vous faites cela dans un constructeur avec un appel à super () qui, selon la loi, doit être la première déclaration de votre constructeur. try compte-t-il comme une déclaration précédente? J'ai ce travail (sans le constructeur) dans mon propre code.

4
GlenPeterson

Nous avons développé un projet interne dans mon entreprise qui nous a aidés avec cela. Nous avons décidé de rendre public il y a deux mois.

Voici ce que nous avons trouvé:

@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

Paguro fournit des interfaces fonctionnelles qui encapsulent les exceptions vérifiées . J'ai commencé à travailler dessus quelques mois après que vous ayez posé votre question, alors vous en avez probablement fait partie de l'inspiration!

Vous remarquerez qu'il n'y a que 4 interfaces fonctionnelles dans Paguro par rapport aux 43 interfaces incluses dans Java 8. C'est parce que Paguro préfère les génériques aux primitives.

Paguro intègre des transformations en un seul passage dans ses collections immuables (copiées de Clojure). Ces transformations sont à peu près équivalentes aux transducteurs Clojure ou aux flux Java 8, mais acceptent des interfaces fonctionnelles qui encapsulent les exceptions vérifiées. Voir: les différences entre les flux Paguro et Java 8 .

1
GlenPeterson

Emballer l'exception de la manière décrite ne fonctionne pas. J'ai essayé et j'ai toujours des erreurs de compilation, ce qui est en fait conforme à la spécification: l'expression lambda lève l'exception qui est incompatible avec le type cible de l'argument de méthode: Callable; call () ne le jette pas, donc je ne peux pas passer l'expression lambda en Callable.

Donc, fondamentalement, il n'y a pas de solution: nous sommes coincés avec l'écriture passe-partout. La seule chose que nous puissions faire est d'exprimer notre opinion que cela doit être corrigé. Je pense que la spécification ne devrait pas simplement ignorer aveuglément un type de cible basé sur des exceptions levées incompatibles: elle devrait ensuite vérifier si l'exception incompatible levée est interceptée ou déclarée comme des jetons dans l'étendue appelante . Et pour les expressions lambda non alignées I proposer que nous puissions les marquer comme levant silencieusement les exceptions vérifiées (silencieux dans le sens où le compilateur ne devrait pas vérifier, mais le runtime devrait quand même intercepter) . marquons ceux avec => par opposition à -> Je le sais n’est pas un site de discussion, mais puisque ceci IS est la seule solution à la question, faites-vous entendre et changeons cette spécification!

1

Vous pouvez jeter de vos lambdas, ils doivent simplement être déclarés "à votre façon" (ce qui les rend, malheureusement, non réutilisables dans le code JDK standard, mais bon, nous faisons ce que nous pouvons).

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

Ou la version plus générique:

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

ref ici . Il est également question d'utiliser "sneakyThrow" pour ne pas déclarer les exceptions vérifiées, mais les lancer tout de même. Cela me fait un peu mal à la tête, peut-être une option.

0
rogerdpack