Java 8: Lambda-Streams, Filtra per metodo con eccezione


Ho un problema a provare le espressioni Lambda di Java 8. Di solito funziona bene, ma ora ho metodi che lanciano IOException. È meglio se guardi il seguente codice:

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

Il problema è che non viene compilato, perché devo cogliere le possibili eccezioni dei metodi isActive e getNumber. Ma anche se uso esplicitamente un blocco try-catch come di seguito, non viene ancora compilato perché non cattura l'eccezione. Quindi o c'è un bug in JDK, o non lo so come catturare queste eccezioni.

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){
        }
    }
    ....
}

Come posso farlo funzionare? Qualcuno mi può suggerire la soluzione giusta?

Author: Jack, 2013-11-03

12 answers

È necessario rilevare l'eccezione prima che sfugga al lambda:

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

Considera il fatto che il lambda non viene valutato nel luogo in cui lo scrivi, ma in un luogo completamente estraneo, all'interno di una classe JDK. Quindi quello sarebbe il punto in cui quell'eccezione controllata verrebbe lanciata, e in quel punto non viene dichiarata.

Puoi gestirlo usando un wrapper del tuo lambda che traduce le eccezioni controllate in quelle non controllate:

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

Il tuo esempio sarebbe scritto come

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

Nei miei progetti mi occupo di questo problema senza wrapping; invece uso un metodo che disinnesca efficacemente il controllo delle eccezioni da parte del compilatore. Inutile dire che questo dovrebbe essere gestito con cura e tutti sul progetto devono essere consapevoli che un'eccezione controllata può apparire dove non è dichiarata. Questo è il codice idraulico:

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

E puoi aspettarti di ricevere un IOException gettato in faccia, anche se collect non lo dichiara. In la maggior parte, ma non tutti i casi di vita reale si vorrebbe solo ripensare l'eccezione, comunque, e gestirla come un errore generico. In tutti questi casi, nulla è perso in chiarezza o correttezza. Fai attenzione a quegli altri casi, in cui vorresti effettivamente reagire all'eccezione sul posto. Lo sviluppatore non sarà reso consapevole dal compilatore che c'è un IOException da catturare lì e il compilatore si lamenterà se si tenta di catturarlo perché lo abbiamo ingannato facendolo credere che non esiste l'eccezione può essere generata.

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

Puoi anche propagare il tuo dolore statico con lambda, quindi il tutto sembra leggibile:

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

propagate qui riceve java.util.concurrent.Callable come parametro e converte qualsiasi eccezione rilevata durante la chiamata in RuntimeException. Esiste un metodo di conversione simile Throwables#propagate(Throwable) in Guava.

Questo metodo sembra essere essenziale per il concatenamento del metodo lambda, quindi spero che un giorno verrà aggiunto a una delle librerie popolari o questo comportamento di propagazione sarebbe di predefinito.

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

Questa classe helper UtilException consente di utilizzare eventuali eccezioni controllate nei flussi Java, in questo modo:

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

Nota Class::forName genera ClassNotFoundException, che è controllato. Il flusso stesso genera anche ClassNotFoundException, e NON qualche eccezione di wrapping non selezionata.

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

}

Molti altri esempi su come usarlo (dopo l'importazione staticaUtilException):

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

Ma non usarlo prima di comprendere i seguenti vantaggi, svantaggi e limitazioni:

* Se il calling-code è quello di gestire l'eccezione selezionata è necessario aggiungerlo alla clausola throws del metodo che contiene il flusso. Il compilatore non ti costringerà più ad aggiungerlo, quindi è più facile dimenticarlo.

• Se il codice chiamante gestisce già l'eccezione selezionata, il compilatore ti ricorderà di aggiungere la clausola throws alla dichiarazione del metodo che contiene il flusso (se non lo fai dirà: L'eccezione non viene mai generata nel corpo della corrispondente istruzione try).

• In in ogni caso, non sarà possibile circondare il flusso stesso per rilevare l'eccezione selezionata ALL'interno del metodo che contiene il flusso (se ci provi, il compilatore dirà: L'eccezione non viene mai generata nel corpo dell'istruzione try corrispondente).

• Se stai chiamando un metodo che letteralmente non può mai lanciare l'eccezione che dichiara, allora non dovresti includere la clausola throws. Ad esempio: new String (byteArr, "UTF-8") genera UnsupportedEncodingException, ma UTF-8 è garantito da le specifiche Java per essere sempre presenti. Qui, la dichiarazione di lancio è un fastidio e qualsiasi soluzione per silenziarla con un minimo di boilerplate è benvenuta.

• Se odi le eccezioni controllate e ritieni che non dovrebbero mai essere aggiunte al linguaggio Java per cominciare (un numero crescente di persone la pensa in questo modo, e io non sono uno di loro), quindi non aggiungere l'eccezione controllata alla clausola throws del metodo che contiene il flusso. Il controllato l'eccezione, quindi, si comporterà proprio come un Eccezione deselezionata.

* Se si sta implementando un'interfaccia rigorosa in cui non si ha l'opzione per aggiungere una dichiarazione di throws, eppure lanciare un'eccezione è del tutto appropriato, quindi avvolgere un'eccezione solo per ottenere il privilegio di lanciarla si traduce in uno stacktrace con eccezioni spurie che contribuire nessuna informazione su ciò che in realtà è andato storto. Un buon esempio è eseguibile.run (), che non genera eccezioni controllate. In questo caso, si può decidere di non aggiungere l'eccezione selezionata alla clausola throws del metodo che contiene il flusso.

* In ogni caso, se si decide di NON aggiungere (o dimenticare di aggiungere) l'eccezione selezionata alla clausola throws del metodo che contiene il flusso, essere consapevoli di queste 2 conseguenze del lancio di eccezioni CONTROLLATE:

1) Il codice chiamante non sarà in grado di catturarlo per nome (se ci provi, il compilatore dirà: L'eccezione non viene mai generata nel corpo della prova corrispondente dichiarazione). Sarà bolla e probabilmente essere catturato nel ciclo principale del programma da qualche "eccezione di cattura" o "catch Throwable", che potrebbe essere quello che voglio comunque.

2) Viola il principio della minima sorpresa: non sarà più sufficiente catturare RuntimeException per poter garantire la cattura di tutti possibili eccezioni. Per questo motivo, credo che questo non dovrebbe essere fatto nel codice framework, ma solo nel codice aziendale che controlli completamente.

In conclusione: credo che le limitazioni qui non lo siano grave, e la classe UtilException può essere utilizzata senza paura. Tuttavia, dipende da te!

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

È possibile eseguire il rollback della propria variante Stream avvolgendo il lambda per generare un'eccezione non selezionata e successivamente scartare l'eccezione non selezionata sulle operazioni del terminale:

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

Quindi potresti usarlo nello stesso modo esatto di 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());

Questa soluzione richiederebbe un po ' di boilerplate, quindi ti suggerisco di dare un'occhiata alla libreria che ho già creato che fa esattamente quello che ho descritto qui per l'intera classe Stream (e altro ancora!).

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

Usa il metodo #propagate (). Esempio di implementazione non Guava da Java 8 Blog di 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

Può essere risolto dal seguente codice semplice con Stream e Prova in AbacusUtil :

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

Divulgazione: Sono lo sviluppatore di AbacusUtil.

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

Per aggiungere correttamente il codice di gestione IOException (to RuntimeException), il tuo metodo sarà simile a questo:

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

Il problema ora è che IOException dovrà essere catturato come RuntimeException e convertito in IOException -- e questo aggiungerà ancora più codice al metodo precedente.

Perché usare Stream quando può essere fatto proprio così -- e il metodo genera IOException quindi non è necessario alcun codice aggiuntivo anche per questo:

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

Estendendo la soluzione @marcg, puoi normalmente lanciare e catturare un'eccezione controllata nei Flussi; cioè, il compilatore ti chiederà di catturare/ri-lanciare come se fossi fuori dai flussi!!

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

Quindi, il tuo esempio sarebbe scritto come segue (aggiungendo test per mostrarlo più chiaramente):

@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

Questo non risponde direttamente alla domanda (ci sono molte altre risposte che lo fanno) ma cerca di evitare il problema in primo luogo:

Nella mia esperienza la necessità di gestire le eccezioni in un Stream (o altra espressione lambda) spesso deriva dal fatto che le eccezioni sono dichiarate per essere lanciate da metodi in cui non dovrebbero essere lanciate. Questo spesso deriva dalla miscelazione logica di business con in-e uscita. La tua interfaccia Account è un esempio perfetto:

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

Invece di lanciare un IOException su ogni getter, considera questo disegno:

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

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

Il metodo AccountReader.readAccount(…) potrebbe leggere un account da un database o un file o qualsiasi altra cosa e generare un'eccezione se non riesce. Costruisce un oggetto Account che contiene già tutti i valori, pronto per essere utilizzato. Poiché i valori sono già stati caricati da readAccount(…), i getter non genererebbero un'eccezione. Quindi puoi usarli liberamente in lambda senza la necessità di avvolgere, mascherare o nascondere le eccezioni.

Di naturalmente non è sempre possibile farlo nel modo in cui ho descritto, ma spesso lo è e porta a un codice più pulito del tutto (IMHO):

  • Meglio separazione delle preoccupazioni e seguendo principio di responsabilità unica
  • Less boilerplate: non devi ingombrare il tuo codice con throws IOException senza alcun uso ma per soddisfare il compilatore
  • Gestione degli errori: gestisci gli errori dove si verificano-durante la lettura da un file o un database-invece che da qualche parte in la metà della tua logica di business solo perché vuoi ottenere un valore di campi
  • Potresti essere in grado di fare Account immutabile e trarre profitto dai suoi vantaggi (ad esempio sicurezza del filo)
  • Non hai bisogno di" trucchi sporchi " o soluzioni alternative per usare Account in lambda (ad esempio in Stream)
 2
Author: siegi, 2017-12-02 09:35:10

Tenendo presente questo problema ho sviluppato una piccola libreria per gestire eccezioni controllate e lambda. Gli adattatori personalizzati consentono di integrarsi con i tipi funzionali esistenti:

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

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

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

Il tuo esempio può essere scritto come:

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 può essere presa qui https://github.com/SeregaLBN/StreamUnthrower

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

Se non ti dispiace usare le librerie di terze parti, AOL cyclops-reactlib, disclosure::Sono un contributore, ha una classe ExceptionSoftener che può aiutare qui.

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