Java 8: Lambda-Streams, Filtrer par méthode avec Exception


J'ai un problème à essayer les expressions Lambda de Java 8. Habituellement, cela fonctionne bien, mais maintenant j'ai des méthodes qui jettent IOException. C'est mieux si vous regardez le code suivant:

class Bank{
    ....
    public Set<String> getActiveAccountNumbers() throws IOException {
        Stream<Account> s =  accounts.values().stream();
        s = s.filter(a -> a.isActive());
        Stream<String> ss = s.map(a -> a.getNumber());
        return ss.collect(Collectors.toSet());
    }
    ....
}

interface Account{
    ....
    boolean isActive() throws IOException;
    String getNumber() throws IOException;
    ....
}

Le problème est qu'il ne compile pas, car je dois attraper les exceptions possibles des méthodes isActive - et getNumber. Mais même si j'utilise explicitement un try-catch-Block comme ci-dessous, il ne compile toujours pas parce que je n'attrape pas l'exception. Donc, soit il y a un bug dans JDK, soit je ne sais pas comment attraper ces exceptions.

class Bank{
    ....
    //Doesn't compile either
    public Set<String> getActiveAccountNumbers() throws IOException {
        try{
            Stream<Account> s =  accounts.values().stream();
            s = s.filter(a -> a.isActive());
            Stream<String> ss = s.map(a -> a.getNumber());
            return ss.collect(Collectors.toSet());
        }catch(IOException ex){
        }
    }
    ....
}

Comment puis-je le faire fonctionner? Quelqu'un peut-il me suggérer la bonne solution?

Author: Jack, 2013-11-03

12 answers

Vous devez attraper l'exception avant que n'échappe au lambda:

s = s.filter(a -> { try { return a.isActive(); } 
                    catch (IOException e) { throw new UncheckedIOException(e); }}});

Considérez le fait que le lambda n'est pas évalué à l'endroit où vous l'écrivez, mais à un endroit complètement indépendant, dans une classe JDK. Ce serait donc le point où cette exception vérifiée serait levée, et à cet endroit, elle n'est pas déclarée.

Vous pouvez y faire face en utilisant un wrapper de votre lambda qui traduit les exceptions vérifiées en exceptions non cochées:

public static <T> T uncheckCall(Callable<T> callable) {
  try { return callable.call(); }
  catch (RuntimeException e) { throw e; }
  catch (Exception e) { throw new RuntimeException(e); }
}

Votre exemple serait écrit comme

return s.filter(a -> uncheckCall(a::isActive))
        .map(Account::getNumber)
        .collect(toSet());

Dans mes projets, je traite ce problème sans encapsuler; à la place, j'utilise une méthode qui désamorce efficacement la vérification des exceptions par le compilateur. Inutile de dire que cela doit être géré avec soin et tout le monde sur le projet doit être conscient qu'une exception vérifiée peut apparaître lorsqu'elle n'est pas déclarée. Ceci est le code de plomberie:

public static <T> T uncheckCall(Callable<T> callable) {
  try { return callable.call(); }
  catch (Exception e) { return sneakyThrow(e); }
}
public static void uncheckRun(RunnableExc r) {
  try { r.run(); } catch (Exception e) { sneakyThrow(e); }
}
public interface RunnableExc { void run() throws Exception; }


@SuppressWarnings("unchecked")
private static <T extends Throwable> void sneakyThrow(Throwable t) throws T {
  throw (T) t;
}

Et vous pouvez vous attendre à obtenir un IOException jeté au visage, même si collect ne le déclare pas. Dans la plupart, mais pas tous les cas réels, vous voudriez simplement repenser l'exception, de toute façon, et la gérer comme un échec générique. Dans tous ces cas, rien n'est perdu dans la clarté ou l'exactitude. Méfiez-vous simplement de ces autres cas, où vous voudriez réellement réagir à l'exception sur place. Le développeur ne sera pas informé par le compilateur qu'il y a un IOException à attraper là-bas et le compilateur se plaindra en fait si vous essayez de l'attraper parce que nous l'avons trompé en lui faisant croire qu'aucun tel exception peut être lancée.

 180
Author: Marko Topolnik, 2017-12-01 13:02:55

Vous pouvez également propager votre douleur statique avec des lambdas, de sorte que le tout semble lisible:

s.filter(a -> propagate(a::isActive))

propagate ici reçoit java.util.concurrent.Callable en tant que paramètre et convertit toute exception interceptée pendant l'appel en RuntimeException. Il existe une méthode de conversion similaire Throwables#propagate(Throwable) dans Goyave.

Cette méthode semble être essentielle pour le chaînage de la méthode lambda, donc j'espère qu'un jour elle sera ajoutée à l'une des bibliothèques populaires ou que ce comportement de propagation serait par défaut.

public class PropagateExceptionsSample {
    // a simplified version of Throwables#propagate
    public static RuntimeException runtime(Throwable e) {
        if (e instanceof RuntimeException) {
            return (RuntimeException)e;
        }

        return new RuntimeException(e);
    }

    // this is a new one, n/a in public libs
    // Callable just suits as a functional interface in JDK throwing Exception 
    public static <V> V propagate(Callable<V> callable){
        try {
            return callable.call();
        } catch (Exception e) {
            throw runtime(e);
        }
    }

    public static void main(String[] args) {
        class Account{
            String name;    
            Account(String name) { this.name = name;}

            public boolean isActive() throws IOException {
                return name.startsWith("a");
            }
        }


        List<Account> accounts = new ArrayList<>(Arrays.asList(new Account("andrey"), new Account("angela"), new Account("pamela")));

        Stream<Account> s = accounts.stream();

        s
          .filter(a -> propagate(a::isActive))
          .map(a -> a.name)
          .forEach(System.out::println);
    }
}
 23
Author: Andrey Chaschev, 2013-11-03 23:53:32

Cette classe d'assistance UtilException vous permet d'utiliser toutes les exceptions vérifiées dans les flux Java, comme ceci:

Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
      .map(rethrowFunction(Class::forName))
      .collect(Collectors.toList());

Note Class::forName jette ClassNotFoundException, qui est vérifié. Le flux lui-même lève également ClassNotFoundException, et NON une exception d'emballage non cochée.

public final class UtilException {

@FunctionalInterface
public interface Consumer_WithExceptions<T, E extends Exception> {
    void accept(T t) throws E;
    }

@FunctionalInterface
public interface BiConsumer_WithExceptions<T, U, E extends Exception> {
    void accept(T t, U u) throws E;
    }

@FunctionalInterface
public interface Function_WithExceptions<T, R, E extends Exception> {
    R apply(T t) throws E;
    }

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

@FunctionalInterface
public interface Runnable_WithExceptions<E extends Exception> {
    void run() throws E;
    }

/** .forEach(rethrowConsumer(name -> System.out.println(Class.forName(name)))); or .forEach(rethrowConsumer(ClassNameUtil::println)); */
public static <T, E extends Exception> Consumer<T> rethrowConsumer(Consumer_WithExceptions<T, E> consumer) throws E {
    return t -> {
        try { consumer.accept(t); }
        catch (Exception exception) { throwAsUnchecked(exception); }
        };
    }

public static <T, U, E extends Exception> BiConsumer<T, U> rethrowBiConsumer(BiConsumer_WithExceptions<T, U, E> biConsumer) throws E {
    return (t, u) -> {
        try { biConsumer.accept(t, u); }
        catch (Exception exception) { throwAsUnchecked(exception); }
        };
    }

/** .map(rethrowFunction(name -> Class.forName(name))) or .map(rethrowFunction(Class::forName)) */
public static <T, R, E extends Exception> Function<T, R> rethrowFunction(Function_WithExceptions<T, R, E> function) throws E {
    return t -> {
        try { return function.apply(t); }
        catch (Exception exception) { throwAsUnchecked(exception); return null; }
        };
    }

/** rethrowSupplier(() -> new StringJoiner(new String(new byte[]{77, 97, 114, 107}, "UTF-8"))), */
public static <T, E extends Exception> Supplier<T> rethrowSupplier(Supplier_WithExceptions<T, E> function) throws E {
    return () -> {
        try { return function.get(); }
        catch (Exception exception) { throwAsUnchecked(exception); return null; }
        };
    }

/** uncheck(() -> Class.forName("xxx")); */
public static void uncheck(Runnable_WithExceptions t)
    {
    try { t.run(); }
    catch (Exception exception) { throwAsUnchecked(exception); }
    }

/** uncheck(() -> Class.forName("xxx")); */
public static <R, E extends Exception> R uncheck(Supplier_WithExceptions<R, E> supplier)
    {
    try { return supplier.get(); }
    catch (Exception exception) { throwAsUnchecked(exception); return null; }
    }

/** uncheck(Class::forName, "xxx"); */
public static <T, R, E extends Exception> R uncheck(Function_WithExceptions<T, R, E> function, T t) {
    try { return function.apply(t); }
    catch (Exception exception) { throwAsUnchecked(exception); return null; }
    }

@SuppressWarnings ("unchecked")
private static <E extends Throwable> void throwAsUnchecked(Exception exception) throws E { throw (E)exception; }

}

Beaucoup d'autres exemples sur la façon de l'utiliser (après statiquement l'importation UtilException):

@Test
public void test_Consumer_with_checked_exceptions() throws IllegalAccessException {
    Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
          .forEach(rethrowConsumer(className -> System.out.println(Class.forName(className))));

    Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
          .forEach(rethrowConsumer(System.out::println));
    }

@Test
public void test_Function_with_checked_exceptions() throws ClassNotFoundException {
    List<Class> classes1
          = Stream.of("Object", "Integer", "String")
                  .map(rethrowFunction(className -> Class.forName("java.lang." + className)))
                  .collect(Collectors.toList());

    List<Class> classes2
          = Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
                  .map(rethrowFunction(Class::forName))
                  .collect(Collectors.toList());
    }

@Test
public void test_Supplier_with_checked_exceptions() throws ClassNotFoundException {
    Collector.of(
          rethrowSupplier(() -> new StringJoiner(new String(new byte[]{77, 97, 114, 107}, "UTF-8"))),
          StringJoiner::add, StringJoiner::merge, StringJoiner::toString);
    }

@Test    
public void test_uncheck_exception_thrown_by_method() {
    Class clazz1 = uncheck(() -> Class.forName("java.lang.String"));

    Class clazz2 = uncheck(Class::forName, "java.lang.String");
    }

@Test (expected = ClassNotFoundException.class)
public void test_if_correct_exception_is_still_thrown_by_method() {
    Class clazz3 = uncheck(Class::forName, "INVALID");
    }

Mais ne l'utilisez pas avant de comprendre les avantages, les inconvénients et les limites:

• Si le calling-code consiste à gérer l'exception vérifiée, vous DEVEZ l'ajouter à la clause throws de la méthode qui contient le flux. Le compilateur ne vous forcera plus à l'ajouter, il est donc plus facile de l'oublier.

{[9]• * Si le code appelant gère déjà l'exception vérifiée, le compilateur vous rappellera d'ajouter la clause throws à la déclaration de méthode qui contient le flux (si vous ne le faites pas, il dira: L'exception n'est jamais levée dans le corps de l'instruction try correspondante).

* Dans dans tous les cas, vous ne pourrez pas entourer le flux lui-même pour attraper l'exception vérifiée DANS la méthode qui contient le flux (si vous essayez, le compilateur dira: L'exception n'est jamais levée dans le corps de l'instruction try correspondante).

• Si vous appelez une méthode qui ne peut littéralement jamais lever l'exception qu'elle déclare, vous ne devez pas inclure la clause throws. Par exemple: new String (byteArr, "UTF-8") lève UnsupportedEncodingException, mais UTF-8 est garanti par la spécification Java doit toujours être présente. Ici, la déclaration throws est une nuisance et toute solution pour la faire taire avec un minimum de passe-partout est la bienvenue.

{[9]• * Si vous détestez les exceptions vérifiées et pensez qu'elles ne devraient jamais être ajoutées au langage Java pour commencer (un nombre croissant de personnes pensent de cette façon, et je NE suis PAS l'un d'entre eux), alors n'ajoutez pas l'exception vérifiée à la clause throws de la méthode qui contient le flux. Le vérifié l'exception se comportera alors comme un Exception non cochée. {[9]• * Si vous implémentez une interface stricte où vous n'avez pas la possibilité d'ajouter une déclaration throws, et pourtant lancer une exception est tout à fait approprié, puis envelopper une exception juste pour avoir le privilège de la lancer entraîne une stacktrace avec des exceptions parasites qui contribuer aucune information sur ce qui a réellement mal tourné. Un bon exemple est Praticable.run (), qui ne lève aucune exception vérifiée. Dans ce cas, vous pouvez décider de ne pas ajoutez l'exception vérifiée à la clause throws de la méthode qui contient le flux. {[9]• * Dans tous les cas, si vous décidez de NE PAS ajouter (ou d'oublier d'ajouter) l'exception cochée à la clause throws de la méthode qui contient le flux, soyez conscient de ces 2 conséquences de lancer des exceptions vérifiées:

1) Le code appelant ne pourra pas l'attraper par son nom (si vous essayez, le compilateur dira: L'exception n'est jamais levée dans le corps de l'essai correspondant déclaration). Il va bouillonner et probablement être capté dans la boucle principale du programme par une "exception de capture" ou "catch Throwable", qui peut être ce que vous voulez de toute façon.

2) Cela viole le principe de la moindre surprise: il ne suffira plus d'attraper RuntimeException pour pouvoir garantir la capture de tous exceptions possibles. Pour cette raison, je pense que cela ne devrait pas être fait dans le code framework, mais seulement dans le code métier que vous contrôlez complètement.

En conclusion: Je crois que les limites ici ne sont pas sérieux, et la classe UtilException peut être utilisée sans crainte. Toutefois, il est à vous!

 17
Author: MarcG, 2018-02-26 19:16:31

Vous pouvez potentiellement lancer votre propre variante Stream en enveloppant votre lambda pour lancer une exception non cochée, puis en déballant plus tard cette exception non cochée sur les opérations du terminal:

@FunctionalInterface
public interface ThrowingPredicate<T, X extends Throwable> {
    public boolean test(T t) throws X;
}

@FunctionalInterface
public interface ThrowingFunction<T, R, X extends Throwable> {
    public R apply(T t) throws X;
}

@FunctionalInterface
public interface ThrowingSupplier<R, X extends Throwable> {
    public R get() throws X;
}

public interface ThrowingStream<T, X extends Throwable> {
    public ThrowingStream<T, X> filter(
            ThrowingPredicate<? super T, ? extends X> predicate);

    public <R> ThrowingStream<T, R> map(
            ThrowingFunction<? super T, ? extends R, ? extends X> mapper);

    public <A, R> R collect(Collector<? super T, A, R> collector) throws X;

    // etc
}

class StreamAdapter<T, X extends Throwable> implements ThrowingStream<T, X> {
    private static class AdapterException extends RuntimeException {
        public AdapterException(Throwable cause) {
            super(cause);
        }
    }

    private final Stream<T> delegate;
    private final Class<X> x;

    StreamAdapter(Stream<T> delegate, Class<X> x) {
        this.delegate = delegate;
        this.x = x;
    }

    private <R> R maskException(ThrowingSupplier<R, X> method) {
        try {
            return method.get();
        } catch (Throwable t) {
            if (x.isInstance(t)) {
                throw new AdapterException(t);
            } else {
                throw t;
            }
        }
    }

    @Override
    public ThrowingStream<T, X> filter(ThrowingPredicate<T, X> predicate) {
        return new StreamAdapter<>(
                delegate.filter(t -> maskException(() -> predicate.test(t))), x);
    }

    @Override
    public <R> ThrowingStream<R, X> map(ThrowingFunction<T, R, X> mapper) {
        return new StreamAdapter<>(
                delegate.map(t -> maskException(() -> mapper.apply(t))), x);
    }

    private <R> R unmaskException(Supplier<R> method) throws X {
        try {
            return method.get();
        } catch (AdapterException e) {
            throw x.cast(e.getCause());
        }
    }

    @Override
    public <A, R> R collect(Collector<T, A, R> collector) throws X {
        return unmaskException(() -> delegate.collect(collector));
    }
}

Ensuite, vous pouvez utiliser cela de la même manière exacte qu'un Stream:

Stream<Account> s = accounts.values().stream();
ThrowingStream<Account, IOException> ts = new StreamAdapter<>(s, IOException.class);
return ts.filter(Account::isActive).map(Account::getNumber).collect(toSet());

Cette solution nécessiterait un peu de passe-partout, donc je vous suggère de jeter un oeil à la bibliothèque que j'ai déjà faite qui fait exactement ce que j'ai décrit ici pour toute la classe Stream (et plus encore!).

 8
Author: Jeffrey, 2015-08-26 04:02:55

Utilisez la méthode #propagate (). Exemple d'implémentation non-Goyave de Java 8 Blog par Sam Beran:

public class Throwables {
    public interface ExceptionWrapper<E> {
        E wrap(Exception e);
    }

    public static <T> T propagate(Callable<T> callable) throws RuntimeException {
        return propagate(callable, RuntimeException::new);
    }

    public static <T, E extends Throwable> T propagate(Callable<T> callable, ExceptionWrapper<E> wrapper) throws E {
        try {
            return callable.call();
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw wrapper.wrap(e);
        }
    }
}
 5
Author: n0mer, 2015-09-24 12:05:03

, Il peut être résolu en-dessous de code simple avec Stream et Try dans AbacusUtil:

Stream.of(accounts).filter(a -> Try.call(a::isActive)).map(a -> Try.call(a::getNumber)).toSet();

Divulgation: je suis le développeur de AbacusUtil.

 3
Author: user_3380739, 2017-02-07 18:03:03

Pour ajouter correctement le code de gestion IOException (à RuntimeException), votre méthode ressemblera à ceci:

Stream<Account> s =  accounts.values().stream();

s = s.filter(a -> { try { return a.isActive(); } 
  catch (IOException e) { throw new RuntimeException(e); }});

Stream<String> ss = s.map(a -> { try { return a.getNumber() }
  catch (IOException e) { throw new RuntimeException(e); }});

return ss.collect(Collectors.toSet());

Le problème est maintenant que le IOException devra être capturé en tant que RuntimeException et converti en IOException -- et cela ajoutera encore plus de code à la méthode ci-dessus.

Pourquoi utiliser Stream quand cela peut être fait comme ça -- et la méthode lance IOException donc aucun code supplémentaire n'est nécessaire pour cela aussi:

Set<String> set = new HashSet<>();
for(Account a: accounts.values()){
  if(a.isActive()){
     set.add(a.getNumber());
  } 
}
return set;
 3
Author: Austin Powers, 2017-12-05 06:59:13

En étendant la solution @ marcg, vous pouvez normalement lancer et attraper une exception cochée dans les flux; c'est-à-dire que le compilateur vous demandera d'attraper/relancer car vous étiez en dehors des flux!!

@FunctionalInterface
public interface Predicate_WithExceptions<T, E extends Exception> {
    boolean test(T t) throws E;
}

/**
 * .filter(rethrowPredicate(t -> t.isActive()))
 */
public static <T, E extends Exception> Predicate<T> rethrowPredicate(Predicate_WithExceptions<T, E> predicate) throws E {
    return t -> {
        try {
            return predicate.test(t);
        } catch (Exception exception) {
            return throwActualException(exception);
        }
    };
}

@SuppressWarnings("unchecked")
private static <T, E extends Exception> T throwActualException(Exception exception) throws E {
    throw (E) exception;
}

Ensuite, votre exemple serait écrit comme suit (en ajoutant des tests pour le montrer plus clairement):

@Test
public void testPredicate() throws MyTestException {
    List<String> nonEmptyStrings = Stream.of("ciao", "")
            .filter(rethrowPredicate(s -> notEmpty(s)))
            .collect(toList());
    assertEquals(1, nonEmptyStrings.size());
    assertEquals("ciao", nonEmptyStrings.get(0));
}

private class MyTestException extends Exception { }

private boolean notEmpty(String value) throws MyTestException {
    if(value==null) {
        throw new MyTestException();
    }
    return !value.isEmpty();
}

@Test
public void testPredicateRaisingException() throws MyTestException {
    try {
        Stream.of("ciao", null)
                .filter(rethrowPredicate(s -> notEmpty(s)))
                .collect(toList());
        fail();
    } catch (MyTestException e) {
        //OK
    }
}
 2
Author: PaoloC, 2016-07-06 08:29:46

Cela ne répond pas directement à la question (il y a beaucoup d'autres réponses qui le font) mais essaie d'éviter le problème en premier lieu:

D'après mon expérience, la nécessité de gérer les exceptions dans un Stream (ou une autre expression lambda) vient souvent du fait que les exceptions sont déclarées comme étant jetées à partir de méthodes où elles ne devraient pas être jetées. Cela vient souvent du mélange de la logique métier avec l'entrée et la sortie. Votre interface Account est un exemple parfait:

interface Account {
    boolean isActive() throws IOException;
    String getNumber() throws IOException;
}

Au lieu de lancer un IOException sur chaque getter, considérez cette conception:

interface AccountReader {
    Account readAccount(…) throws IOException;
}

interface Account {
    boolean isActive();
    String getNumber();
}

La méthode AccountReader.readAccount(…) pourrait lire un compte à partir d'une base de données ou d'un fichier ou autre et lancer une exception si elle ne réussit pas. Il construit un objet Account qui contient déjà toutes les valeurs, prêt à être utilisé. Comme les valeurs ont déjà été chargées par readAccount(…), les getters ne lèveraient pas d'exception. Ainsi, vous pouvez les utiliser librement dans les lambdas sans avoir besoin d'envelopper, de masquer ou de cacher les exceptions.

De bien sûr, il n'est pas toujours possible de le faire comme je l'ai décrit, mais souvent c'est le cas et cela conduit à un code plus propre (À mon humble avis):

  • Meilleur séparation des préoccupations et la suite principe de responsabilité unique
  • , Moins passe-partout: Vous n'avez pas à encombrer votre code avec throws IOException pour aucune utilisation, mais pour satisfaire le compilateur
  • Gestion des erreurs: Vous gérez les erreurs là où elles se produisent-lors de la lecture à partir d'un fichier ou d'une base de données - au lieu de quelque part dans le milieu de votre logique métier uniquement parce que vous voulez obtenir une valeur de champs
  • Vous pouvez être en mesure de faire Account immuable et profiter des avantages de celui-ci (par exemple, la sécurité du fil)
  • Vous n'avez pas besoin de" trucs sales " ou de solutions de contournement pour utiliser Account dans les lambdas (par exemple dans un Stream)
 2
Author: siegi, 2017-12-02 09:35:10

Gardant ce problème à l'esprit, j'ai développé une petite bibliothèque pour traiter les exceptions vérifiées et les lambdas. Les adaptateurs personnalisés vous permettent d'intégrer des types fonctionnels existants:

stream().map(unchecked(URI::new)) //with a static import

Https://github.com/TouK/ThrowingFunction/

 1
Author: Grzegorz Piwowarek, 2016-02-19 18:22:41

Votre exemple peut être écrit comme:

import utils.stream.Unthrow;

class Bank{
   ....
   public Set<String> getActiveAccountNumbers() {
       return accounts.values().stream()
           .filter(a -> Unthrow.wrap(() -> a.isActive()))
           .map(a -> Unthrow.wrap(() -> a.getNumber()))
           .collect(Collectors.toSet());
   }
   ....
}

La classeUnthrow peut être prise ici https://github.com/SeregaLBN/StreamUnthrower

 1
Author: SeregaLBN, 2016-03-04 13:18:01

Si cela ne vous dérange pas d'utiliser des bibliothèques tierces, la bibliothèque cyclops-reactd'AOL, disclosure::Je suis un contributeur, a une classe ExceptionSoftener qui peut aider ici.

 s.filter(softenPredicate(a->a.isActive()));
 0
Author: John McClean, 2016-02-24 17:36:20