Pertanyaan Java 8: Lambda-Streams, Filter by Method with Exception


Saya memiliki masalah dalam mencoba ekspresi Lambda dari Java 8. Biasanya berfungsi dengan baik, tapi sekarang saya punya metode yang dilemparkan IOException's. Lebih baik jika Anda melihat kode berikut:

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

Masalahnya adalah, itu tidak dikompilasi, karena saya harus menangkap kemungkinan pengecualian dari isActive- dan getNumber-Methods. Tetapi bahkan jika saya secara eksplisit menggunakan Blok try-catch-seperti di bawah ini, masih belum dikompilasi karena saya tidak menangkap Exception. Jadi ada bug di JDK, atau saya tidak tahu cara menangkap Pengecualian ini.

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

Bagaimana saya bisa membuatnya bekerja? Bisakah seseorang memberi saya solusi yang tepat?


134
2017-11-03 19:51


asal


Jawaban:


Anda harus menangkap pengecualian sebelum itu lepas lambda:

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

Pertimbangkan fakta bahwa lambda tidak dievaluasi di tempat Anda menulisnya, tetapi di beberapa tempat yang sama sekali tidak terkait, di dalam kelas JDK. Jadi itu akan menjadi titik di mana pengecualian yang dicentang akan dibuang, dan di tempat itu tidak dideklarasikan.

Anda dapat mengatasinya dengan menggunakan pembungkus lambda Anda yang menerjemahkan pengecualian yang dicentang ke yang tidak dicentang:

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

Contoh Anda akan ditulis sebagai

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

Dalam proyek saya, saya berurusan dengan masalah ini tanpa membungkus; sebagai gantinya saya menggunakan metode yang secara efektif meredakan pemeriksaan pengecualian compiler. Tak perlu dikatakan, ini harus ditangani dengan hati-hati dan semua orang di proyek harus menyadari bahwa pengecualian yang dicentang dapat muncul di mana tidak dinyatakan. Ini adalah kode perpipaan:

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

dan Anda bisa berharap mendapatkan IOException dilemparkan ke wajahmu, meskipun collect tidak menyatakannya. Di sebagian besar, tetapi tidak semua kasus-kasus nyata Anda ingin hanya mengekor ulang pengecualian, bagaimanapun, dan menanganinya sebagai kegagalan umum. Dalam semua kasus itu, tidak ada yang hilang dalam kejelasan atau kebenaran. Waspadalah terhadap kasus-kasus lain, di mana Anda sebenarnya ingin bereaksi terhadap pengecualian di tempat. Pengembang tidak akan dibuat sadar oleh kompiler yang ada IOException untuk menangkap di sana dan kompiler sebenarnya akan mengeluh jika Anda mencoba untuk menangkapnya karena kami telah mengelabui ke dalam keyakinan bahwa tidak ada pengecualian yang dapat dilemparkan.


176
2017-11-03 20:04



Anda juga dapat menyebarkan rasa sakit statis Anda dengan lambdas, sehingga semuanya terlihat terbaca:

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

propagate disini menerima java.util.concurrent.Callable sebagai parameter dan mengubah pengecualian apa pun yang tertangkap selama panggilan masuk RuntimeException. Ada metode konversi yang serupa Throwables # propagate (Throwable) di Guava.

Metode ini tampaknya penting untuk metode lambda chaining, jadi saya harap suatu hari nanti akan ditambahkan ke salah satu libs populer atau perilaku propagasi ini akan secara default.

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
2017-11-03 23:47



Ini UtilException kelas penolong memungkinkan Anda menggunakan pengecualian yang dicentang di aliran Java, seperti ini:

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

Catatan Class::forName melempar ClassNotFoundException, yang mana diperiksa. Aliran itu sendiri juga melempar ClassNotFoundException, dan BUKAN beberapa pembungkusan tanpa terkendali pengecualian.

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

}

Banyak contoh lain tentang cara menggunakannya (setelah mengimpor secara statis 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");
    }

Tetapi jangan menggunakannya sebelum memahami kelebihan, kerugian, dan keterbatasan berikut ini:

• Jika kode panggil untuk menangani pengecualian yang dicentang Anda HARUS menambahkannya ke klausa lemparan dari metode yang berisi aliran.  Compiler tidak akan memaksa Anda untuk menambahkannya lagi, jadi lebih mudah melupakannya.

• Jika pemanggilan-kode sudah menangani pengecualian yang diperiksa, kompiler AKAN mengingatkan Anda untuk menambahkan klausa lempar ke deklarasi metode  yang berisi aliran (jika Anda tidak akan mengatakan: Eksepsi tidak pernah dilemparkan ke dalam badan pernyataan percobaan yang sesuai).

• Dalam hal apa pun, Anda tidak akan dapat mengelilingi streaming itu sendiri untuk menangkap pengecualian yang dicentang DI DALAM metode yang berisi aliran  (Jika Anda mencoba, kompilator akan berkata: Eksepsi tidak pernah dilemparkan ke dalam badan pernyataan percobaan yang sesuai).

• Jika Anda memanggil metode yang benar-benar tidak dapat membuang pengecualian yang dinyatakannya, maka Anda tidak boleh memasukkan klausa lemparan.  Sebagai contoh: String baru (byteArr, "UTF-8") melempar UnsupportedEncodingException, tetapi UTF-8 dijamin oleh spesifikasi Java untuk selalu hadir.  Di sini, deklarasi melempar adalah gangguan dan solusi untuk membungkamnya dengan boiler minimal diterima.

• Jika Anda benci mengecek pengecualian dan merasa mereka tidak boleh ditambahkan ke bahasa Jawa untuk memulai (semakin banyak orang berpikir seperti ini,  dan saya bukan salah satu dari mereka), maka jangan menambahkan pengecualian yang dicentang pada klausa lemparan metode yang berisi aliran. Yang diperiksa  pengecualian akan, kemudian, berperilaku seperti pengecualian yang tidak terkendali.

• Jika Anda menerapkan antarmuka yang ketat di mana Anda tidak memiliki opsi untuk menambahkan deklarasi melempar, namun melempar pengecualian  sepenuhnya tepat, lalu membungkus pengecualian hanya untuk mendapatkan hak istimewa melemparkannya menghasilkan stacktrace dengan pengecualian palsu yang  tidak memberikan informasi tentang apa yang sebenarnya salah. Contoh yang baik adalah Runnable.run (), yang tidak membuang pengecualian yang dicentang.  Dalam hal ini, Anda dapat memutuskan untuk tidak menambahkan pengecualian yang diperiksa ke klausa lemparan dari metode yang berisi aliran.

• Dalam kasus apa pun, jika Anda memutuskan TIDAK menambahkan (atau lupa menambahkan) pengecualian yang dicentang pada klausa lemparan metode yang berisi aliran,  Waspadai 2 konsekuensi ini dari melempar PERIKSA pengecualian:

1) Panggilan-kode tidak akan dapat menangkapnya dengan nama (jika Anda mencoba, kompilator akan berkata: Pengecualian tidak pernah dilemparkan dalam tubuh yang sesuai mencoba  pernyataan). Ini akan menggelembung dan mungkin akan ditangkap di loop program utama oleh beberapa "catch Exception" atau "catch Throwable", yang mungkin menjadi apa yang Anda  mau tetap.

2) Ini melanggar prinsip paling tidak mengejutkan: itu tidak lagi cukup untuk menangkap RuntimeException agar dapat menjamin penangkapan semua  kemungkinan pengecualian. Untuk alasan ini, saya yakin ini tidak boleh dilakukan dalam kode kerangka, tetapi hanya dalam kode bisnis yang sepenuhnya Anda kendalikan.

Kesimpulannya: Saya percaya keterbatasan di sini tidak serius, dan UtilException kelas dapat digunakan tanpa rasa takut. Namun, terserah Anda!


18
2017-12-26 20:15



Anda dapat berpotensi menggulir milik Anda sendiri Stream varian dengan membungkus lambda Anda untuk membuang pengecualian yang tidak dicentang dan kemudian membuka bukaan pengecualian yang tidak dicentang pada operasi 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));
    }
}

Maka Anda bisa menggunakan ini dengan cara yang persis sama dengan 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());

Solusi ini akan membutuhkan sedikit boiler, jadi saya sarankan Anda melihat pada perpustakaan saya sudah dibuat yang persis seperti yang saya jelaskan di sini untuk keseluruhan Stream kelas (dan banyak lagi!).


8
2018-04-20 23:15



Gunakan metode #propagate (). Contoh penerapan non-Guava dari Java 8 Blog oleh 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
2017-07-02 19:50



Ini dapat diatasi dengan kode sederhana di bawah ini Aliran dan Mencoba di AbacusUtil:

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

Pengungkapan: Saya pengembang AbacusUtil.


3
2017-12-02 01:45



Untuk menambahkan kode penanganan IOException (ke RuntimeException) dengan benar, metode Anda akan terlihat seperti ini:

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

Masalahnya sekarang adalah bahwa IOException harus ditangkap sebagai RuntimeException dan diubah kembali menjadi IOException - dan itu akan menambahkan lebih banyak kode ke metode di atas.

Mengapa digunakan? Stream ketika itu bisa dilakukan seperti ini - dan metode melempar IOException jadi tidak diperlukan kode tambahan untuk itu juga:

Set<String> set = new HashSet<>();
for(Account a: accounts.values()){
  if(a.isActive()){
     set.add(a.getNumber());
  } 
}
return set;

3
2017-11-04 02:10



Memperluas solusi @marcg, Anda biasanya dapat melempar dan menangkap diperiksa pengecualian dalam Streaming; itu adalah, kompiler akan meminta Anda untuk menangkap / melempar ulang seperti Anda berada di luar aliran !!

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

Kemudian, contoh Anda akan ditulis sebagai berikut (menambahkan tes untuk menunjukkannya lebih jelas):

@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
2018-06-26 11:00