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

Je joue avec les nouvelles fonctionnalités lambda de Java 8 et j’ai constaté que les pratiques proposées par Java 8 étaient 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’objects nécessitant une sorte de fabrique pour remplir le pool d’objects, par exemple (en utilisant java.lang.functions.Factory ):

 public class JdbcConnectionPool extends ObjectPool { public ConnectionPool(int maxConnections, Ssortingng url) { super(new Factory() { @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 cela:

 public class JdbcConnectionPool extends ObjectPool { public ConnectionPool(int maxConnections, Ssortingng 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 équivalent à java.lang.functions.Factory ;
  • et une interface spéciale pour les cas qui nécessitent généralement une propagation d’exceptions vérifiées: interface IUnsafeOut { T out() throws E; } interface IUnsafeOut { T out() throws E; } .

IOut et IUnsafeOut doivent tous deux être supprimés lors de la migration vers Java 8, mais il n’y a pas de correspondance exacte pour IUnsafeOut . Si les expressions lambda pouvaient traiter des exceptions vérifiées comme si elles étaient décochées, il serait possible d’utiliser simplement comme suit dans le constructeur ci-dessus:

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

Cela semble beaucoup plus propre. Je vois que je peux réécrire la super-classe ObjectPool pour accepter notre IUnsafeOut , mais pour autant que je sache, Java 8 n’est pas encore terminé, il pourrait donc y avoir quelques changements comme:

  • implémenter quelque chose de similaire à IUnsafeOut ? (pour être honnête, je considère cela comme sale – le sujet doit choisir quoi accepter: soit Factory ou “usine non sûre” qui ne peut pas avoir de signatures de méthodes compatibles)
  • ignorer simplement les exceptions vérifiées dans lambdas, donc pas besoin dans IUnsafeOut substituts? (pourquoi pas? par exemple un autre changement important: OpenJDK, que j’utilise, javac maintenant ne nécessite pas que les variables et parameters soient déclarés comme final pour être capturés dans une classe anonyme [interface fonctionnelle] ou une expression lambda)

Donc, la question est 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’à ce que Java 8 soit finalement publié?


Mise à jour 1

Hm-mm, pour autant que je comprenne ce que nous avons actuellement, il semble qu’il n’y ait aucun moyen pour le moment, malgré le fait que l’article référencé date de 2010: Brian Goetz explique la transparence des exceptions en Java . Si rien ne changeait beaucoup dans Java 8, cela pourrait être considéré comme une réponse. Brian dit aussi que l’ interface ExceptionalCallable (ce que j’ai mentionné comme IUnsafeOut de notre inheritance de code) est quasiment inutile, et je suis d’accord avec lui.

Est-ce que je manque encore quelque chose d’autre?

Je ne suis pas sûr de bien répondre à votre question, mais ne pourriez-vous pas simplement utiliser quelque chose comme ça?

 public final class SupplierUtils { private SupplierUtils() { } public static  Supplier wrap(Callable callable) { return () -> { try { return callable.call(); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } }; } } public class JdbcConnectionPool extends ObjectPool { public JdbcConnectionPool(int maxConnections, Ssortingng url) { super(SupplierUtils.wrap(() -> DriverManager.getConnection(url)), maxConnections); } } 

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

Ou vous pourriez écrire votre propre combinateur sortingvial:

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

Vous pouvez l’écrire une fois, en moins du temps qu’il a fallu pour écrire votre e-mail d’origine. Et de même une fois pour chaque type de SAM que vous utilisez.

Je préfère que nous considérions cela comme du “verre à 99% plein” plutôt que comme une alternative. Tous les problèmes ne nécessitent pas de nouvelles fonctionnalités linguistiques en tant que solutions. (Sans compter que les nouvelles fonctionnalités de la langue causent toujours de nouveaux problèmes.)

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

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

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

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

Le groupe d’experts a discuté de la question de la langue et de la bibliothèque pour ce problème et a finalement estimé qu’il s’agissait d’un compromis coût / bénéfice.

Les solutions basées sur les bibliothèques provoquent une explosion de 2x dans les types SAM (exceptionnels vs non), qui interagissent mal avec les explosions combinatoires existantes pour la spécialisation primitive.

Les solutions basées sur la langue disponibles étaient les perdants d’un compromis complexité / valeur. Bien qu’il y ait des solutions alternatives que nous allons continuer à explorer – même si ce n’est clairement pas pour 8 et probablement pas pour 9 autres.

En attendant, vous avez les outils pour faire ce que vous voulez. Je comprends que vous préférez que nous fournissions ce dernier kilomètre pour vous (et, en second lieu, votre demande est vraiment une demande à peine voilée pour “pourquoi ne renoncez-vous pas déjà aux exceptions vérifiées”), mais je pense que vous faites votre travail.

Septembre 2015:

Vous pouvez utiliser ET pour cela. ET est une petite bibliothèque Java 8 pour la conversion / conversion 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 de créer une nouvelle instance d’ 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’exceptions plus spécifiques (par exemple, FooCheckedException -> BarRuntimeException ) si vous le souhaitez. Si aucune autre règle n’est disponible, les exceptions vérifiées sont automatiquement converties en RuntimeException .

(Disclaimer: je suis l’auteur de ET)

Avez-vous envisagé d’utiliser une classe wrapper RuntimeException (non vérifiée) pour transférer l’exception d’origine hors de l’expression lambda, puis en renvoyant l’ exception regroupée à 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, Ssortingng url) throws SQLException { try { super(() -> { try { return DriverManager.getConnection(url); } catch ( SQLException ex ) { throw new WrappedSqlException(ex); } }, maxConnections); } catch (WrappedSqlException wse) { throw wse.getSqlException(); } } 

Créer votre propre classe unique devrait empêcher toute probabilité de confondre une autre exception non vérifiée avec celle que vous avez enveloppée dans votre lambda, même si l’exception est sérialisée quelque part dans le pipeline avant que vous ne l’attrapiez et la relancez.

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

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

C’est ce que nous avons imaginé:

 @FunctionalInterface public interface ThrowingFunction { R apply(T arg) throws E; /** * @param  type * @param  checked exception * @return a function that accepts one argument and returns it as a value. */ static  ThrowingFunction 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  Function> lifted(ThrowingFunction f) { Objects.requireNonNull(f); return f.lift(); } static  Function unchecked(ThrowingFunction f) { Objects.requireNonNull(f); return f.uncheck(); } default  ThrowingFunction compose(final ThrowingFunction before) { Objects.requireNonNull(before); return (V v) -> apply(before.apply(v)); } default  ThrowingFunction andThen(final ThrowingFunction 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> 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 uncheck() { return t -> { try { return apply(t); } catch (final Throwable e) { throw new WrappedException(e); } }; } 

}

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

L’emballage de l’exception de la manière décrite ne fonctionne pas. Je l’ai essayé et j’ai toujours des erreurs de compilation, ce qui correspond en fait à la spécification: l’expression lambda renvoie l’exception incompatible avec le type cible de l’argument de la méthode: Callable; call () ne le lance pas, donc je ne peux pas passer l’expression lambda en tant que callable.

Donc, fondamentalement, il n’y a pas de solution: nous sums coincés avec l’écriture passe-partout. La seule chose que nous pouvons faire, c’est d’exprimer notre opinion selon laquelle 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 lancée est interceptée ou déclarée comme jetée dans la scope d’appel. Et pour les expressions lambda qui ne sont pas en ligne, je propose de les marquer comme étant une exception vérifiée en silence (silencieuse dans le sens où le compilateur ne doit pas vérifier, mais le moteur d’exécution doit toujours être en attente). marquons ceux avec => par opposition à -> Je sais que ce n’est pas un site de discussion, mais puisque c’est la seule solution à la question, laissez-vous entendre et changeons cette spécification!

Paguro fournit des interfaces fonctionnelles qui enveloppent les exceptions vérifiées . J’ai commencé à y travailler quelques mois après avoir posé votre question, vous avez donc 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 avec Java 8. C’est parce que Paguro préfère les génériques aux primitives.

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

Vous pouvez jeter à partir de vos lambda, il suffit de les déclarer “à votre guise” (ce qui, malheureusement, ne sont pas 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 get() throws E; } 

réf ici Il y a aussi une mention d’utiliser “sneakyThrow” pour ne pas déclarer les exceptions vérifiées, mais ensuite les lancer. Ça me fait un peu mal à la tête, peut-être une option.