Вопрос-ответ

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

Java 8: Лямбда -потоки, фильтровать по методу с исключением

У меня проблема с опробованием лямбда-выражений Java 8. Обычно это работает нормально, но теперь у меня есть методы, которые выдают IOException'ы. Лучше всего, если вы посмотрите на следующий код:

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

Проблема в том, что он не компилируется, потому что я должен перехватывать возможные исключения методов isActive и getNumber . Но даже если я явно использую блок try-catch, как показано ниже, он все равно не компилируется, потому что я не улавливаю Исключение. Итак, либо в JDK есть ошибка, либо я не знаю, как перехватить эти исключения.

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

Как я могу заставить это работать? Кто-нибудь может подсказать мне правильное решение?

Переведено автоматически
Ответ 1

Вы должны перехватить исключение, прежде чем оно экранирует лямбда:

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

Учтите тот факт, что лямбда-поток вычисляется не в том месте, где вы его пишете, а в каком-то совершенно не связанном месте внутри класса JDK. Таким образом, это будет точка, в которой будет сгенерировано это проверяемое исключение, и в этом месте оно не объявлено.

Вы можете справиться с этим, используя оболочку вашего лямбда-кода, которая преобразует проверенные исключения в непроверенные:

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

Ваш пример будет записан как

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

В своих проектах я решаю эту проблему без переноса; вместо этого я использую метод, который эффективно отключает проверку исключений компилятором. Излишне говорить, что с этим следует обращаться осторожно, и все участники проекта должны знать, что проверяемое исключение может появиться там, где оно не объявлено. Это вспомогательный код.:

public static <T> T uncheckCall(Callable<T> callable) {
try {
return callable.call();
} catch (Exception e) {
sneakyThrow(e);
return null; // Unreachable but needed to satisfy compiler
}
}

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

и вы можете ожидать, что вам IOException бросят вызов, даже если collect он не объявляется. В большинстве, но не во всех реальных случаях вам все равно захочется просто повторно создать исключение и обработать его как общий сбой. Во всех этих случаях ясность и корректность не теряются. Просто остерегайтесь других случаев, когда вы действительно захотите отреагировать на исключение на месте. Компилятор не поставит разработчика в известность о том, что там есть IOException для перехвата, и компилятор фактически будет жаловаться, если вы попытаетесь перехват, потому что мы обманули его, заставив поверить, что такое исключение не может быть выдано.

Ответ 2

Вы также можете распространять свою статическую ошибку с помощью лямбд, чтобы все это выглядело читабельным:

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

propagate здесь получает java.util.concurrent.Callable в качестве параметра и преобразует любое исключение, перехваченное во время вызова , в RuntimeException. В Guava есть аналогичный метод преобразования Throwables#propagate(Throwable).

Этот метод, по-видимому, важен для цепочки лямбда-методов, поэтому я надеюсь, что однажды он будет добавлен в одну из популярных библиотек, или такое распространяющееся поведение будет по умолчанию.

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);
}
}
Ответ 3

Этот UtilException вспомогательный класс позволяет использовать любые проверенные исключения в потоках Java, например:

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

Обратите внимание, что Class::forName выдаетClassNotFoundException, который проверяется. Сам поток также выдает ClassNotFoundException, а НЕ какое-то непроверенное исключение.

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

}

Много других примеров того, как это использовать (после статического импорта 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");
}

Но не используйте его, пока не поймете следующие преимущества, недостатки и ограничения:

• Если вызывающий код должен обрабатывать проверенное исключение, вы ДОЛЖНЫ добавить его в предложение throws метода, который содержит поток. Компилятор больше не будет заставлять вас добавлять его, поэтому его легче забыть.

• Если вызывающий код уже обрабатывает проверенное исключение, компилятор напомнит вам добавить предложение throws к объявлению метода, содержащего поток (если вы этого не сделаете, он скажет: Exception is never thrown in body соответствующего оператора try ).

• В любом случае, вы не сможете окружить сам поток, чтобы перехватить проверяемое исключение ВНУТРИ метода, который содержит поток (если вы попытаетесь, компилятор скажет: Exception is never thrown in body соответствующего оператора try ).

• Если вы вызываете метод, который буквально никогда не может выдать исключение, которое он объявляет, то вам не следует включать предложение throws . Например: новая строка (byteArr, "UTF-8") вызывает исключение UnsupportedEncodingException, но спецификация Java гарантирует, что UTF-8 всегда присутствует. Здесь объявление throws является помехой, и любое решение, позволяющее отключить его с минимальными шаблонами, приветствуется.

• Если вы ненавидите проверяемые исключения и считаете, что их никогда не следует добавлять в язык Java с самого начала (так думает все большее число людей, и я НЕ один из них), то просто не добавляйте проверяемое исключение в предложение throws метода, который содержит поток. Тогда проверенное исключение будет вести себя точно так же, как непроверенное исключение.

• Если вы реализуете строгий интерфейс, в котором у вас нет возможности добавить объявление throws , и все же выдача исключения вполне уместна, то перенос исключения только для получения привилегии выдавать его приводит к stacktrace с ложными исключениями, которые не предоставляют никакой информации о том, что на самом деле пошло не так. Хорошим примером является Runnable.run() , который не выдает никаких проверенных исключений. В этом случае вы можете решить не добавлять проверенное исключение в предложение throws метода, который содержит поток.

• В любом случае, если вы решите НЕ добавлять (или забудете добавить) проверяемое исключение в предложение throws метода, который содержит поток, имейте в виду эти 2 последствия выбрасывания ПРОВЕРЯЕМЫХ исключений:

1) Вызывающий код не сможет перехватить его по имени (если вы попытаетесь, компилятор скажет: Exception никогда не выдается в теле соответствующего оператора try). Он будет пузыриться и, вероятно, будет перехвачен в основном цикле программы каким-нибудь "catch Exception" или "catch Throwable", что в любом случае может быть тем, что вы хотите.

2) Это нарушает принцип наименьшего удивления: больше не будет достаточно перехватывать RuntimeException, чтобы иметь возможность гарантировать перехват всех возможных исключений. По этой причине я считаю, что это следует делать не в коде фреймворка, а только в бизнес-коде, который вы полностью контролируете.

В заключение: я считаю, что ограничения здесь несерьезны, и UtilException класс можно использовать без опасений. Впрочем, решать вам!

Ответ 4

Потенциально вы можете создать свой собственный Stream вариант, обернув свою лямбду для создания непроверенного исключения, а затем позже развернув это непроверенное исключение при операциях терминала:

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

Тогда вы могли бы использовать это точно так же, как 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());

Для этого решения потребуется совсем немного шаблонности, поэтому я предлагаю вам взглянуть на библиотеку, которую я уже создал, которая делает именно то, что я описал здесь для всего Stream класса (и многое другое!).

java exception lambda java-8