Program Tip

람다의 무한 while 루프 내부의 Thread.sleep에는 'catch (InterruptedException)'가 필요하지 않습니다.

programtip 2020. 12. 5. 10:31
반응형

람다의 무한 while 루프 내부의 Thread.sleep에는 'catch (InterruptedException)'가 필요하지 않습니다.


에 대한 내 질문은 InterruptedException으로부터 발생하는, Thread.sleep방법. 함께 작업하는 동안 ExecutorService이해하지 못하는 이상한 행동을 발견했습니다. 내가 의미하는 바는 다음과 같습니다.

ExecutorService executor = Executors.newSingleThreadExecutor();
    executor.submit(() -> {
        while(true)
        {
            //DO SOMETHING
            Thread.sleep(5000);
        }
    });

이 코드로, 컴파일러는 나에게 있다는 오류 또는 메시지 제공하지 않습니다 InterruptedException에서이 Thread.sleep잡힐되어야한다. 하지만 루프 조건을 변경하고 "true"를 다음과 같은 변수로 바꾸려고 할 때 :

ExecutorService executor = Executors.newSingleThreadExecutor();
    executor.submit(() -> {
        while(tasksObserving)
        {
            //DO SOMETHING
            Thread.sleep(5000);
        }
    });

컴파일러는 항상 InterruptedException처리해야한다고 불평합니다 . 누군가 이런 일이 발생하는 이유와 조건이 true로 설정된 경우 컴파일러가 InterruptedException을 무시하는 이유를 설명 할 수 있습니까?


그 이유는 이러한 호출이 실제로에서 사용할 수있는 두 개의 다른 오버로드 된 메서드에 대한 호출이기 때문입니다 ExecutorService. 각 메소드는 서로 다른 유형의 단일 인수를 사용합니다.

  1. <T> Future<T> submit(Callable<T> task);
  2. Future<?> submit(Runnable task);

그러면 컴파일러가 문제의 첫 번째 경우 람다를 Callable<?>함수 인터페이스 로 변환합니다 (첫 번째 오버로드 된 메서드 호출). 두 번째 문제의 경우 람다를 Runnable기능 인터페이스 로 변환하여 (따라서 두 번째 오버로드 된 메서드를 호출) Exceptionthrow 된 처리를 요구합니다 . 하지만 이전 경우에는 Callable.

두 기능 인터페이스 모두 인수를 사용하지 않지만 Callable<?> 값을 반환합니다 .

  1. 호출 가능 : V call() throws Exception;
  2. 실행 가능 : public abstract void run();

코드를 관련 부분으로 트리밍하는 예제로 전환하면 (호기심 많은 부분 만 쉽게 조사하기 위해) 원본 예제와 동일하게 작성할 수 있습니다.

    ExecutorService executor = Executors.newSingleThreadExecutor();

    // LAMBDA COMPILED INTO A 'Callable<?>'
    executor.submit(() -> {
        while (true)
            throw new Exception();
    });

    // LAMBDA COMPILED INTO A 'Runnable': EXCEPTIONS MUST BE HANDLED BY LAMBDA ITSELF!
    executor.submit(() -> {
        boolean value = true;
        while (value)
            throw new Exception();
    });

이러한 예제를 사용하면 첫 번째 항목이 a로 변환되고 Callable<?>두 번째 항목이 a로 변환되는 Runnable이유는 컴파일러 추론 때문 이라는 것을 쉽게 알 수 있습니다 .

두 경우 모두 블록의 모든 return 문이 형식을 갖기 때문에 람다 본문은 void 호환 가능 합니다 return;.

이제 첫 번째 경우 컴파일러는 다음을 수행합니다.

  1. 람다의 모든 실행 경로가 확인 된 예외를 던지는 것을 선언하는지 감지합니다 (이제부터는 'exception'으로 참조하고 'checked exceptions' 만 의미 함 ). 여기에는 예외 발생을 선언하는 모든 메서드 호출과에 대한 명시 적 호출이 포함됩니다 throw new <CHECKED_EXCEPTION>().
  2. 람다 WHOLE 본문이 예외를 던지는 코드 블록과 동일 하다는 것을 올바르게 결론 지었습니다 . 물론 이는 반드시 이 될 : 처리 또는 재 발생합니다.
  3. 람다가 예외를 처리하지 않기 때문에 컴파일러는 기본적으로 이러한 예외가 다시 발생해야한다고 가정합니다.
  4. 안전이 람다 기능 인터페이스를 할 수없는 일치해야 추론 complete normally때문에 것은 값 호환 .
  5. 이후 Callable<?>Runnable이 람다 대한 잠재적 인 일치되어, 컴파일러는 최 일치 (모든 시나리오 다루)을 선택; Callable<?>, 람다를 인스턴스로 변환하고 submit(Callable<?>)오버로드 된 메서드에 대한 호출 참조를 만듭니다 .

두 번째 경우 컴파일러는 다음을 수행합니다.

  1. 람다에 예외 발생을 선언 하지 않는 실행 경로가있을 수 있음을 감지합니다 ( 평가 대상 논리 에 따라 다름 ).
  2. 모든 실행 경로가 던지는 예외를 선언하기 때문에, 컴파일러는 람다의 몸이라고 결론 필요는 없다 선언 예외를 던지는 코드 블록에 해당 - 코드의 어떤 부분들이 수 있음을 선언 할 경우, 컴파일러는 / 유료 관심을 상관하지 않는다 , 전신이 그렇지 않은 경우에만.
  3. 람다가 값과 호환 되지 않는다고 안전하게 추론합니다 . 5 월 이후 complete normally.
  4. 람다를 변환 할 수 Runnable있는 유일한 피팅 기능 인터페이스 이므로 선택 하고 submit(Runnable)오버로드 된 메서드에 대한 호출 참조를 만듭니다 . 사용자에게 위임의 가격으로 오는이 모든 것은, 어떤 처리의 책임은 Exception그들이 어디에 던져 s의 수도 람다 몸의 일부에서 발생합니다.

이것은 좋은 질문이었습니다. 나는 그것을 쫓아 다니며 많은 재미를 보았습니다. 감사합니다!


간단히

ExecutorServicesubmit(Callable)submit(Runnable)방법 이 모두 있습니다.

  1. 첫 번째 경우 (사용 while (true)), 둘 다 submit(Callable)submit(Runnable)일치하므로 컴파일러는 중에서 선택해야합니다.
    • submit(Callable)이상 선택 submit(Runnable)하기 때문 Callable입니다 보다 구체적인 이상Runnable
    • Callable에 포함 throws Exception되어 call()있으므로 내부에서 예외를 포착 할 필요가 없습니다.
  2. 두 번째 경우 (와 함께 while (tasksObserving)) 만 submit(Runnable)일치하므로 컴파일러가 선택합니다.
    • Runnable메서드 throws에 대한 선언 이 없으므로 run()메서드 내에서 예외를 포착하지 않는 것은 컴파일 오류 run()입니다.

전체 이야기

Java 언어 사양은 $ 15.2.2 에서 프로그램 컴파일 중에 메소드가 선택되는 방법을 설명합니다 .

  1. 엄격하고 느슨하며 가변적 인 arity 호출을 위해 3 단계로 수행되는 잠재적으로 적용 가능한 방법 식별 ( $ 15.12.2.1 )
  2. 첫 번째 단계에서 찾은 방법 중에서 가장 구체적인 방법 ( $ 15.12.2.5 )을 선택합니다 .

Let's analyze the situation with 2 submit() methods in two code snippets provided by the OP:

ExecutorService executor = Executors.newSingleThreadExecutor();
    executor.submit(() -> {
        while(true)
        {
            //DO SOMETHING
            Thread.sleep(5000);
        }
    });

and

ExecutorService executor = Executors.newSingleThreadExecutor();
    executor.submit(() -> {
        while(tasksObserving)
        {
            //DO SOMETHING
            Thread.sleep(5000);
        }
    });

(where tasksObserving is not a final variable).

Identify Potentially Applicable Methods

First, the compiler has to identify the potentially applicable methods: $15.12.2.1

If the member is a fixed arity method with arity n, the arity of the method invocation is equal to n, and for all i (1 ≤ i ≤ n), the i'th argument of the method invocation is potentially compatible, as defined below, with the type of the i'th parameter of the method.

and a bit further in the same section

An expression is potentially compatible with a target type according to the following rules:

A lambda expression (§15.27) is potentially compatible with a functional interface type (§9.8) if all of the following are true:

The arity of the target type's function type is the same as the arity of the lambda expression.

If the target type's function type has a void return, then the lambda body is either a statement expression (§14.8) or a void-compatible block (§15.27.2).

If the target type's function type has a (non-void) return type, then the lambda body is either an expression or a value-compatible block (§15.27.2).

Let's note that in both cases, the lambda is a block lambda.

Let's also note that Runnable has void return type, so to be potentially compatible with Runnable, a block lambda must be void-compatible block. At the same time, Callable has a non-void return type, so to be potentially comtatible with Callable, a block lambda must be value-compatible block.

$15.27.2 defines what a void-compatible-block and value-compatible-block are.

A block lambda body is void-compatible if every return statement in the block has the form return;.

A block lambda body is value-compatible if it cannot complete normally (§14.21) and every return statement in the block has the form return Expression;.

Let's look at $14.21, paragraph about while loop:

A while statement can complete normally iff at least one of the following is true:

The while statement is reachable and the condition expression is not a constant expression (§15.28) with value true.

There is a reachable break statement that exits the while statement.

In borh cases, lambdas are actually block lambdas.

In the first case, as it can be seen, there is a while loop with a constant expression with value true (without break statements), so it cannot complete normallly (by $14.21); also it has no return statements, hence the first lambda is value-compatible.

At the same time, there are no return statements at all, so it is also void-compatible. So, in the end, in the first case, the lambda is both void- and value-compatible.

In the second case, the while loop can complete normally from the point of view of the compiler (because the loop expression is not a constant expression anymore), so the lambda in its entirety can complete normally, so it is not a value-compatible block. But it is still a void-compatible block because it contains no return statements.

The intermediate result is that in the first case the lambda is both a void-compatible block and a value-compatible block; in the second case it is only a void-compatible block.

Recalling what we noted earlier, this means that in the first case, the lambda will be potentially compatible both with Callable and Runnable; in the second case, the lambda will only be potentially compatible with Runnable.

Choose the Most Specific Method

For the first case, the compiler has to choose between the two methods because both are potentially applicable. It does so using the procedure called 'Choose the Most Specific Method' and described in $15.12.2.5. Here is an excerpt:

A functional interface type S is more specific than a functional interface type T for an expression e if T is not a subtype of S and one of the following is true (where U1 ... Uk and R1 are the parameter types and return type of the function type of the capture of S, and V1 ... Vk and R2 are the parameter types and return type of the function type of T):

If e is an explicitly typed lambda expression (§15.27.1), then one of the following is true:

R2 is void.

First of all,

A lambda expression with zero parameters is explicitly typed.

Also, neither of Runnable and Callable is a subclass of one another, and Runnable return type is void, so we have a match: Callable is more specific than Runnable. This means that between submit(Callable) and submit(Runnable) in the first case the method with Callable will be chosen.

As for the second case, there we only have one potentially applicable method, submit(Runnable), so it is chosen.

So why does the change surface?

So, in the end, we can see that in these cases different methods are chosen by the compiler. In the first case, the lambda is inferred to be a Callable which has throws Exception on its call() method, so that sleep() call compiles. In the second case, it's Runnable which run() does not declare any throwable exceptions, so the compiler complains about an exception not being caught.

참고URL : https://stackoverflow.com/questions/56149752/thread-sleep-inside-infinite-while-loop-in-lambda-doesnt-require-catch-interr

반응형