[java] Java 8 : Lambda-Streams, 예외가있는 메소드 별 필터링

Java 8의 Lambda 표현식을 시도하는 데 문제가 있습니다. 일반적으로 정상적으로 작동하지만 이제는 throw하는 메소드가 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-Methods의 가능한 예외를 잡아야하기 때문에 컴파일되지 않는다는 것입니다. 그러나 아래처럼 try-catch-Block을 명시 적으로 사용하더라도 예외를 포착하지 않기 때문에 여전히 컴파일되지 않습니다. 따라서 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){
        }
    }
    ....
}

어떻게 작동합니까? 누군가가 올바른 해결책을 알려줄 수 있습니까?



답변

람다를 탈출 하기 전에 예외 잡아야합니다 .

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

그리고 당신은 그것을 선언하지 IOException않더라도 당신의 얼굴에 던져 질 것을 기대할 수 있습니다 collect. 에서 전부는 아니지만 대부분의 실제 사례 당신은 어쨌든, 예외를 다시 발생하고, 일반 오류로 처리하는 것입니다. 이 모든 경우에 명확성 또는 정확성이 손실되지 않습니다. 그 자리에서 예외에 실제로 반응하기를 원하는 다른 경우에주의하십시오. 개발자는 IOException컴파일러가이를 포착 할 수 있다는 사실을 인식하지 못하며 실제로 예외를 던질 수 없다고 믿었 기 때문에이를 포착하려고하면 실제로 불만을 제기하게됩니다.


답변

람다로 정적 통증을 전파 할 수 있으므로 모든 것이 읽기 쉽습니다.

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

propagate여기서 java.util.concurrent.Callable매개 변수로 수신 하고 호출 중에 발생한 예외를로 변환합니다 RuntimeException. 비슷한 변환 방법이 있습니다. 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);
    }
}


답변

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 절을 추가하도록 알려줍니다 (그렇지 않은 경우 : 해당 try 문의 본문에 예외가 발생하지 않습니다). ).

• 어떤 경우에도 스트림을 포함하는 메서드 내부에서 확인 된 예외를 포착하기 위해 스트림 자체를 둘러 쌀 수 없습니다 (시도하면 컴파일러는 다음과 같이 말합니다. 해당 try 문의 본문에는 예외가 발생하지 않습니다).

• 선언 된 예외를 문자 그대로 던질 수없는 메서드를 호출하는 경우 throws 절을 포함하지 않아야합니다. 예를 들어, new String (byteArr, “UTF-8”)은 UnsupportedEncodingException을 발생 시키지만 Java 스펙에서는 UTF-8이 항상 존재하도록 보장합니다. 던지기 선언은 성가신 일이며 최소한의 상용구로 침묵시키는 해결책은 환영합니다.

• 확인 된 예외를 증오하고 처음으로 Java 언어에 추가해서는 안된다고 생각하는 경우 (많은 사람들이 이런 식으로 생각하며 그 중 하나가 아닙니다), 확인 된 예외를 추가하지 마십시오. 스트림을 포함하는 메소드의 절을 던집니다. 그런 다음 확인 된 예외는 확인되지 않은 예외처럼 작동합니다.

• throws 선언을 추가 할 수있는 옵션이없는 엄격한 인터페이스를 구현하고 있지만 예외를 throw하는 것이 전적으로 적합한 경우 예외를 래핑하는 권한을 얻기 위해 예외를 래핑하면 가짜 예외가있는 스택 추적이 발생합니다. 실제로 무엇이 잘못되었는지에 대한 정보를 제공하지 않습니다. 확인 된 예외를 발생시키지 않는 Runnable.run ()이 좋은 예입니다. 이 경우, 점검 된 예외를 스트림을 포함하는 메소드의 throws 절에 추가하지 않기로 결정할 수 있습니다.

• 어떤 경우에도 스트림을 포함하는 메소드의 throws 절에 확인 된 예외를 추가하지 않거나 추가하는 것을 잊은 경우 CHECKED 예외가 발생하는 다음 두 가지 결과에 유의하십시오.

1) 호출 코드는 이름으로 붙잡을 수 없습니다 (시도하면 컴파일러는 다음과 같이 말합니다 : 해당 try 문 본문에는 예외가 발생하지 않습니다). 그것은 아마도 “캐치 예외”또는 “캐치 던지기 가능”에 의해 메인 프로그램 루프에서 잡히고 어쩌면 원하는 것일 수도 있습니다.

2) 최소한의 놀라움의 원칙을 위반합니다. 더 이상 RuntimeException을 포착하여 가능한 모든 예외를 포착 할 수있을만큼 충분하지 않습니다. 따라서 프레임 워크 코드에서는 수행하지 말고 완전히 제어하는 ​​비즈니스 코드에서만 수행해야한다고 생각합니다.

결론 : 나는 여기의 한계가 심각하지 않다고 생각하며 UtilException클래스는 두려움없이 사용될 수 있습니다. 그러나 그것은 당신에게 달려 있습니다!


답변

Stream람다를 감싸서 검사되지 않은 예외를 throw 한 다음 나중에 터미널 작업에서 검사되지 않은 예외를 풀어서 변형을 롤링 할 수 있습니다 .

@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클래스 (및 더 많은 것)에 대해 여기에서 설명한 것을 정확하게 수행하는 것이 좋습니다 .


답변

#propagate () 메소드를 사용하십시오. Sam Beran의 Java 8 블로그 에서 비 구아바 구현 샘플 :

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


답변

이것은 질문에 직접 대답하지 않지만 (많은 다른 답변이 있습니다) 먼저 문제를 피하려고 시도합니다.

내 경험상 예외 Stream(또는 다른 람다 식) 에서 예외를 처리해야 할 필요성 은 예외가 발생해서는 안되는 메소드에서 발생하도록 선언되었다는 사실에서 비롯됩니다. 이것은 종종 비즈니스 로직과 입출력을 혼합하여 발생합니다. 귀하의 Account인터페이스는 완벽한 예입니다 :

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

IOException각 게터 를 던지는 대신 다음 디자인을 고려하십시오.

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

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

이 메소드 AccountReader.readAccount(…)는 데이터베이스 나 파일 등에서 계정을 읽고 성공하지 못하면 예외를 던질 수 있습니다. Account사용할 준비가 된 모든 값이 이미 포함 된 객체를 구성합니다 . 값이 이미로드되었으므로readAccount(…) 이므로 getter는 예외를 발생시키지 않습니다. 따라서 예외를 감싸거나 숨기거나 숨길 필요없이 람다에서 자유롭게 사용할 수 있습니다.

물론 내가 설명한 방식으로 항상 할 수있는 것은 아니지만 종종 가능하며 더 깨끗한 코드 (IMHO)로 이어집니다.

  • 더 나은 우려 분리단일 책임 원칙 준수
  • 보일러 플레이트 감소 : 코드를 복잡하게 만들 필요가 없습니다. throws IOException 컴파일러를 만족시키기 위해
  • 오류 처리 : 필드 값을 얻으려는 경우에만 비즈니스 논리 중간 위치 대신 파일 또는 데이터베이스에서 읽을 때 오류가 발생하는 위치에서 오류를 처리합니다.
  • 불변 으로 만들 수 있습니다Account 그것의 장점 (예를 들어 스레드 안전)에서 이익
  • Account람다 (예 :에서 Stream) 에 사용하기 위해 “더러운 트릭”이나 해결 방법이 필요하지 않습니다.

답변

AbacusUtil 에서 StreamTry 를 사용 하여 간단한 코드 아래에서 해결할 수 있습니다 .

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

공개 : 저는 개발자입니다 AbacusUtil.