Java 8: gestion obligatoire des exceptions vérifiées dans les expressions lambda. Pourquoi obligatoire, pas facultatif?
je joue avec les nouvelles fonctionnalités lambda de Java 8, et j'ai trouvé que les pratiques offertes par Java 8 sont vraiment utiles. Cependant, je me demande s'il y a une bonne façon de faire un contournement pour le scénario suivant. Supposons que vous ayez un emballage de pool d'objets qui nécessite une sorte d'usine 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 transformation de l'interface fonctionnelle en lambda expression, le code ci-dessus devient comme cela:
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'objet d'une exception java.sql.SQLException
exige try
/ catch
bloc à l'intérieur de la lambda. Dans mon entreprise, nous utilisons depuis longtemps deux interfaces:
-
IOut<T>
qui est l'équivalent dejava.lang.functions.Factory
; - et une interface spéciale pour les cas qui nécessitent habituellement une propagation contrôlée des exceptions:
interface IUnsafeOut<T, E extends Throwable> { T out() throws E; }
.
les deux 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 des exceptions vérifiées comme si elles n'étaient pas vérifiées, il pourrait être possible d'utiliser simplement comme suit dans le constructeur ci-dessus:
super(() -> DriverManager.getConnection(url), maxConnections);
qui semble beaucoup plus propre. Je vois que je peux réécrire la classe ObjectPool
super pour accepter notre IUnsafeOut<T>
, mais pour autant que je sache, Java 8 n'est pas encore terminé, donc il pourrait y avoir quelques changements comme:
- mettre en œuvre quelque chose de similaire à
IUnsafeOut<T, E>
? (pour être honnête, je considère que sale-le sujet doit choisir ce qu'il doit accepter: soitFactory
ou "usine dangereuse" qui ne peut pas avoir des signatures de méthode compatibles) - ignorant simplement les exceptions vérifiées dans lambdas, donc pas besoin de substituts dans
IUnsafeOut<T, E>
? (pourquoi pas? par exemple un autre changement important: OpenJDK, que j'utilise,javac
n'exige plus que les variables et les paramètres soient déclarés commefinal
à capturer dans une classe anonyme [interface fonctionnelle] ou expression lambda)
donc la question est généralement: y a-t-il un moyen de contourner les exceptions vérifiées dans lambdas ou est-ce prévu dans le futur jusqu'à ce que Java 8 soit finalement publié?
"1519220920 mise à jour" Update 1
Hm-m-m, dans la mesure où je comprends ce que nous avons actuellement, il semble qu'il n'y a aucune façon pour le moment, malgré l'article référencé est daté de 2010: Brian Goetz explique la transparence d'exception en Java . Si rien n'a beaucoup changé dans Java 8, cela pourrait être considéré comme une réponse. Aussi Brian dit que interface ExceptionalCallable<V, E extends Exception>
(ce que j'ai mentionné comme IUnsafeOut<T, E extends Throwable>
de notre héritage de code) est à peu près inutile, et je suis d'accord avec lui.
puis-je encore de rater quelque chose d'autre?
8 réponses
Je ne suis pas sûr de répondre à votre question, mais ne pourriez-vous pas 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);
}
}
dans la liste de diffusion lambda c'était tout en discutant . Comme vous pouvez le voir Brian Goetz suggéré là que l'alternative est d'é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, à moins que le temps qu'il a fallu pour l'écrire votre original e-mail. Et de la même façon une fois pour chaque type de SAM que vous utilisez.
Je préférerais que nous considérions cela comme "verre plein à 99%" plutôt que comme alternative. Tous les problèmes ne nécessitent pas de nouvelles fonctionnalités linguistiques car solution. (Sans oublier que les nouvelles caractéristiques linguistiques provoquent toujours de nouveaux problèmes.)
à l'époque, L'interface avec les consommateurs s'appelait Block.
je pense que cela correspond à réponse de JB Nizet .
plus tard Brian explique pourquoi cela a été conçu de cette façon (la raison du problème)
Oui, vous devez fournir vos propres SAMs exceptionnels. Mais alors lambda la conversion marcherait très bien avec eux.
le GE a discuté de la langue supplémentaire et de soutien de bibliothèque pour cette problème, et à la fin le sentiment que c'était un mauvais rapport coût/avantage compromis.
les solutions basées sur les bibliothèques provoquent une explosion 2x dans les types SAM (exceptionnelle vs non), qui interagir mal avec les explosions combinatoires existantes pour des primitifs de la spécialisation.
les solutions linguistiques disponibles étaient les perdants d'une compromis entre complexité et valeur. Bien qu'il existe quelques alternatives les solutions que nous allons continuer à explorer -- bien que manifestement pas pour 8 et probablement pas pour 9 soit.
en attendant, vous avez les outils pour faire ce que vous voulez. Je reçois ce que vous préférez que nous fournissons en dernier kilomètre pour vous (et, secondairement, votre demande est vraiment une demande à peine voilée pour "pourquoi ne pas vous abandonner les exceptions déjà vérifiées"), mais je pense que le courant état vous permet de faire votre travail.
septembre 2015:
vous pouvez utiliser ET pour cela. ET est une petite bibliothèque Java 8 pour la conversion/traduction d'exception.
avec ET vous pouvez écrire:
super(() -> et.withReturningTranslation(() -> DriverManager.getConnection(url)), maxConnections);
Multi-ligne version:
super(() -> {
return et.withReturningTranslation(() -> DriverManager.getConnection(url));
}, maxConnections);
Tout ce que vous devez faire avant, est de créer une nouvelle ExceptionTranslator
instance:
ExceptionTranslator et = ET.newConfiguration().done();
cette instance est sans fil une 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 vérifiées sont automatiquement converties en RuntimeException
.
(Disclaimer: je suis l'auteur de HE)
avez-vous envisagé d'utiliser une classe D'emballage RuntimeException (unchecked) pour faire sortir l'exception originale de l'expression lambda, puis de rejeter l'exception emballée back à son exception originale vérifiée?
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();
}
}
créer votre propre classe unique devrait empêcher toute probabilité de confondre une autre exception non contrôlée pour celui que vous avez enveloppé dans votre lambda, même si l'exception est sérialisée quelque part dans le pipeline avant de l'attraper et de le relancer.
Hmm... La seule chose que je vois qui est un problème ici est que vous faites cela à l'intérieur d'un constructeur avec un appel à super() qui, selon la loi, doit être la première déclaration dans votre constructeur. Est-ce que 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é avec cela. Nous avons décidé de le rendre public il y a deux mois.
C'est 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);
}
};
}
}
envelopper l'exception de la manière décrite ne fonctionne pas. Je l'ai essayé et j'obtiens encore des erreurs de compilateur, ce qui est en fait selon la spécification: l'expression lambda jette l'exception qui est incompatible avec le type cible de l'argument de méthode: Callable; call() ne le jette donc pas Je ne peux pas passer l'expression lambda comme un Callable.
Donc, fondamentalement, il n'y a pas de solution: nous sommes coincés avec l'écriture standard. La seule chose que nous pouvons faire est de notre voix l'opinion que ce besoin de fixation. Je pense que le spec ne devrait pas se contenter de rejeter aveuglément un type de Cible Basé sur des exceptions lancées incompatibles: Il devrait ensuite vérifier si l'exception lancée incompatible est attrapée ou déclarée comme lancers dans la portée invoquée. Et pour les expressions lambda qui ne sont pas inlined je propose que nous puissions les marquer comme des lancements silencieux d'exception cochée (silencieux dans le sens où le compilateur ne devrait pas vérifier, mais le runtime devrait toujours attraper). laisser la marque de ceux avec => opposition à l' -> 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é à travailler dessus quelques mois après que vous ayez posé votre question, donc vous avez probablement été une partie de l'inspiration pour cela!
vous remarquerez qu'il n'y a que 4 interfaces fonctionnelles dans Paguro vs. Les 43 interfaces incluses avec Java 8. C'est parce que Paguro préfère les génériques aux primitifs.
Paguro a des transformations en un seul passage dans ses collections immuables (copiées de Clojure). Ces transformes sont à peu près équivalentes aux transducteurs Clojure ou aux flux Java 8, mais elles acceptent les interfaces fonctionnelles qui enveloppent les exceptions vérifiées. Voir: les différences entre Paguro et Java 8 flux .
vous can lancer de vos lambdas, il suffit de les déclarer" à votre façon " (ce qui les rend, malheureusement, non réutilisables en 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 y a aussi une mention de l'utilisation de "sneakyThrow" pour ne pas déclarer les exceptions vérifiées, mais les jeter tout de même. Il ça me fait un peu mal à la tête, peut-être une option.