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

j'ai un problème en essayant les expressions Lambda de Java 8. D'habitude ça marche bien, mais maintenant j'ai des méthodes qui jettent IOException 's. 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, parce que 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 compilera pas parce que je n'ai pas saisi l'Exception. Donc, soit il y a un bug dans JDK, ou 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?

139
demandé sur Jack 2013-11-03 23:51:07

12 réponses

vous devez attraper l'exception avant il échappe à la lambda:

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

considérez le fait que la lambda n'est pas évaluée à l'endroit où vous l'écrivez, mais à un endroit complètement différent, dans une classe JDK. Donc ce serait le point où cette exception contrôlée serait jetée, et à cet endroit elle n'est pas déclarée.

vous pouvez y faire face en utilisant un emballage de votre lambda qui traduit vérifié exceptions aux exceptions non vérifié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 de cette question Sans emballage; au lieu de cela j'utilise une méthode qui désamorce effectivement la vérification des exceptions par le compilateur. Il va sans dire que cela doit être géré avec soin et que tout le monde dans le projet doit être conscient qu'une exception contrôlée peut apparaître là où elle n'est pas déclarée. C'est le code de la 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 à recevoir un IOException jeté dans votre visage, même si collect ne le déclare pas. Dans la plupart, mais pas tous les cas de la vie réelle, vous voudriez simplement repenser l'exception, de toute façon, et le traiter comme un échec Générique. Dans tous les cas, rien n'est perdu dans la clarté ou l'exactitude. Il suffit de se méfier de ces autres cas, où vous voudriez réellement réagir à l'exception sur place. Le développeur ne sera pas mis au courant par le compilateur qu'il y a un IOException à attraper là et le compilateur se plaindra en fait si vous essayez de l'attraper parce que nous l'avons dupé en croyant qu'aucune telle exception ne peut être jetée.

180
répondu Marko Topolnik 2017-12-01 13:02:55

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

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

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

cette méthode semble être essentielle pour lambda méthode chaînage, donc j'espère qu'un jour, il sera ajouté à l'un des populaires libs ou cette multiplication comportement 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
répondu Andrey Chaschev 2013-11-03 23:53:32

cette classe de helper 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 coché . Le flux lui-même lance aussi ClassNotFoundException , et pas une exception d'emballage non contrôlé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 importation statique 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, désavantages et limitations suivants :

• si le code d'appel doit gérer l'exception vérifiée, vous devez l'ajouter à la clause des lancements de la méthode qui contient le flux. Le compilateur ne vous forcera plus à l'ajouter, il est donc plus facile de l'oublier.

"151990920 • * si le code d'appel gère déjà l'exception cochée, le compilateur vous rappellera d'ajouter le lance la clause à la déclaration de méthode celui-ci contient le flux (si vous ne le faites pas, il dira: Exception n'est jamais jeté dans le corps de la déclaration d'essai correspondante).

• dans tous les cas, vous ne pourrez pas entourer le flux lui-même pour attraper l'exception cochée à l'intérieur de la méthode qui contient le flux (si vous essayez, le compilateur dira: L'Exception n'est jamais lancée dans le corps de la déclaration d'essai correspondante).

• Si vous appelez une méthode qui littéralement ne peut jamais jeter l'exception qu'il déclare, alors vous ne devriez pas inclure la clause des lancements. Par exemple: new String(byteArr, "UTF-8") lance UnsupportedEncodingException, mais UTF-8 est garanti par la spécification Java d'être toujours présent. Ici, la déclaration des lancers est une nuisance et toute solution pour la réduire au silence avec un boilerplate minimal est la bienvenue.

"151990920 • * si vous détestez les exceptions vérifiées et que vous pensez qu'elles ne devraient jamais être ajoutées au langage Java pour commencer (a nombre croissant de gens pensent de cette façon, et je ne suis PAS l'un d'eux), puis il suffit de ne pas ajouter l'objet d'une exception à la clause throws de la méthode qui contient le flux. Le vérifié l'exception se comportera alors comme une exception non contrôlée.

"151990920 • * si vous implémentez une interface stricte où vous n'avez pas la possibilité d'ajouter une déclaration de lancers, et pourtant lancer une exception est entièrement approprié, puis envelopper une exception juste pour gagner le privilège de le jeter se traduit par un empiètement avec des exceptions fausses qui contribuer aucune information sur ce qui s'est mal passé. Un bon exemple est Praticable.run (), qui ne lance aucune exception cochée. Dans ce cas, vous pouvez décider de ne pas ajouter l'exception cochée à la clause des lancements de la méthode qui contient le flux.

• dans tous les cas, si vous décidez de ne pas ajouter (ou d'oublier d'ajouter) l'exception cochée à la clause des lancers de la méthode qui contient le flux, être conscient de ces 2 conséquences de jeter vérifié exceptions:

1) le code d'appel ne pourra pas l'attraper par son nom (si vous essayez, le compilateur dira: L'Exception n'est jamais lancée dans le corps de l'essai correspondant déclaration.) Il va bulle et probablement être accroché dans la boucle du programme principal par certains "catch Exception" ou "catch Throwable", qui peut être ce que vous voulez de toute façon.

2) il viole le principe de la moindre surprise: il ne sera pas plus long être assez pour attraper RuntimeException pour être en mesure de garantir la capture de tous les exceptions possibles. Pour cette raison, je pense que cela ne devrait pas être fait dans le code-cadre, mais seulement dans le code des affaires que vous contrôlez complètement.

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

17
répondu MarcG 2018-02-26 19:16:31

vous pouvez éventuellement lancer votre propre variante Stream en enveloppant votre lambda pour lancer une exception non contrôlée et ensuite déballer cette exception non contrôlée sur les opérations de 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));
    }
}

alors vous pouvez l'utiliser de la même manière 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 boilerplate, donc je vous suggère de jeter un oeil à la bibliothèque j'ai déjà fait qui fait exactement ce que j'ai décrit ici l'ensemble Stream classe (et plus encore!).

8
répondu Jeffrey 2015-08-26 04:02:55

utiliser la méthode #propagate (). Exemple d'implémentation non-Guava du Blog Java 8 de 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
répondu n0mer 2015-09-24 12:05:03

il peut être résolu par le code ci-dessous 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
répondu user_3380739 2017-02-07 18:03:03

pour ajouter correctement le code de manipulation 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 maintenant est que le IOException devra être capturé comme un RuntimeException et converti de nouveau à un IOException -- et cela ajoutera encore plus de code à la méthode ci-dessus.

Pourquoi utiliser Stream quand il peut être fait juste comme ceci -- 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
répondu Austin Powers 2017-12-05 06:59:13

Extending @marcg solution, vous pouvez normalement jeter et attraper un coché exception dans les cours d'eau; c'est-à-dire, compilateur vous demandera d'attraper/rejouer comme si vous étiez hors des cours d'eau!!

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

alors, 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
répondu PaoloC 2016-07-06 08:29:46

cela ne répond pas directement à la question (Il ya beaucoup d'autres réponses qui le font), mais tente d'éviter le problème en premier lieu:

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

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 un fichier ou que ce soit et de lever une exception si elle n'y parvient 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 lanceraient pas d'exception. Ainsi vous pouvez les utiliser librement dans lambdas sans avoir besoin d'emballer, masquer ou cacher les exceptions.

bien sûr, il n'est pas toujours possible de le faire comme je l'ai décrit, mais souvent il est et il conduit à un code plus propre tout à fait (IMHO):

  • Mieux la séparation des préoccupations et après principe de responsabilité unique
  • moins boilerplate: vous n'avez pas à encombrer votre code avec throws IOException pour aucun usage mais pour satisfaire le compilateur
  • gestion des erreurs: vous gérez les erreurs là où elles se produisent - lors de la lecture d'un fichier ou d'une base de données-au lieu de quelque part au milieu de votre logique d'affaires seulement 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)
  • You ne pas avoir besoin de" trucs cochons "ou de solutions de rechange pour utiliser Account dans lambdas (p.ex. dans un Stream )
2
répondu siegi 2017-12-02 09:35:10

gardant cette question à l'esprit, j'ai développé une petite bibliothèque pour traiter les exceptions vérifiées et lambdas. Adaptateurs personnalisés vous permettent d'intégrer avec les types fonctionnels existants:

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

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

1
répondu Grzegorz Piwowarek 2016-02-19 18:22:41

votre exemple peut s'écrire 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 classe Unthrow peut être prise ici https://github.com/SeregaLBN/StreamUnthrower

1
répondu SeregaLBN 2016-03-04 13:18:01

si cela ne vous dérange pas d'utiliser des bibliothèques tierces, AOL cyclops-react lib, disclosure::I am a contributor, has a Exceptitionsoftener class that can help here.

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