java multithreading

java - Thread.sleep dentro de infinite while loop no lanza excepción, ¿por qué?



multithreading (2)

Mi pregunta es sobre InterruptedException , que se lanza desde el método Thread.sleep . Mientras trabajaba con ExecutorService noté un comportamiento extraño que no entiendo; esto es lo que quiero decir:

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

Con este código, el compilador no me da ningún error o mensaje de que se debe Thread.sleep InterruptedException de Thread.sleep . Pero cuando intento cambiar la condición de bucle y reemplazar "verdadero" con una variable como esta:

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

El compilador se queja constantemente de que InterruptedException tiene que ser manejado. ¿Alguien puede explicarme por qué sucede esto y por qué si la condición se establece como verdadera el compilador ignora la excepción InterruptedException?


Brevemente

ExecutorService tiene ambos submit(Callable) y de submit(Runnable) .

  1. En el primer caso (con while (true) ), tanto submit(Callable) como submit(Runnable) coinciden, por lo que el compilador debe elegir entre ellos
    • submit(Callable) se elige sobre submit(Runnable) porque Callable es más específico que Runnable
    • Callable ha Callable throws Exception en call() , por lo que no es necesario detectar una excepción en su interior.
  2. En el segundo caso (con la función while (tasksObserving) ) solo submit(Runnable) , así el compilador la elige
    • Runnable no tiene declaración de throws en su método run() , por lo que es un error de compilación no detectar la excepción dentro del método run() .

La historia completa

La especificación del lenguaje Java describe cómo se elige el método durante la compilación del programa en $15.2.2 :

  1. Identifique los métodos potencialmente aplicables ( $15.12.2.1 ) que se realizan en 3 fases para una invocación de $15.12.2.1 estricta, flexible y variable
  2. Elija el método más específico ( $15.12.2.5 ) de los métodos que se encuentran en el primer paso.

Analicemos la situación con 2 métodos submit() en dos fragmentos de código proporcionados por el OP:

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

y

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

(donde tasksObserving no es una variable final).

Identificar métodos potencialmente aplicables

Primero, el compilador debe identificar los métodos potencialmente aplicables : $ 15.12.2.1

Si el miembro es un método de aridad fija con aridad n, la aridad de la invocación del método es igual a n, y para todo i (1 ≤ i ≤ n), el enésimo argumento de la invocación del método es potencialmente compatible , como se define A continuación, con el tipo del parámetro i''th del método.

Y un poco más lejos en la misma sección.

Una expresión es potencialmente compatible con un tipo de destino de acuerdo con las siguientes reglas:

Una expresión lambda (§15.27) es potencialmente compatible con un tipo de interfaz funcional (§9.8) si todo lo siguiente es verdadero:

La aridad del tipo de función del tipo de destino es la misma que la aridad de la expresión lambda.

Si el tipo de función del tipo de destino tiene un retorno de vacío, entonces el cuerpo lambda es una expresión de instrucción (§14.8) o un bloque compatible con el vacío (§15.27.2).

Si el tipo de función del tipo de destino tiene un tipo de retorno (no vacío), entonces el cuerpo lambda es una expresión o un bloque compatible con valores (§15.27.2).

Notemos que en ambos casos, la lambda es un bloque lambda.

También Runnable que Runnable tiene un tipo de retorno de void , por lo que para ser potencialmente compatible con Runnable , un bloque lambda debe ser un bloque compatible con el vacío . Al mismo tiempo, Callable tiene un tipo de retorno no nulo, por lo que para ser potencialmente compatible con Callable , un bloque lambda debe ser un bloque compatible con valores .

$ 15.27.2 define qué son un bloque compatible con el vacío y un bloque compatible con el valor .

Un cuerpo lambda de bloque es nulo compatible si cada declaración de retorno en el bloque tiene el formulario de return; .

Un cuerpo lambda de bloque es compatible con los valores si no puede completarse normalmente (§14.21) y cada declaración de retorno en el bloque tiene la return Expression; formulario return Expression; .

Veamos $ 14.21, párrafo sobre el bucle while:

Una instrucción while puede completarse normalmente si al menos uno de los siguientes es verdadero:

La instrucción while es accesible y la expresión de condición no es una expresión constante (§15.28) con valor verdadero.

Hay una instrucción break accesible que sale de la instrucción while.

En los casos borh, las lambdas son en realidad bloques lambdas.

En el primer caso, como puede verse, hay un bucle while con una expresión constante con valor true (sin sentencias de break ), por lo que no puede completarse normalmente (por $ 14.21); además, no tiene declaraciones de devolución, por lo tanto, la primera lambda es compatible con los valores .

Al mismo tiempo, no hay declaraciones de return en absoluto, por lo que también es compatible con el vacío . Entonces, al final, en el primer caso, la lambda es compatible tanto con el vacío como con el valor .

En el segundo caso, el bucle while puede completarse normalmente desde el punto de vista del compilador (debido a que la expresión del bucle ya no es una expresión constante), por lo que la lambda en su totalidad puede completarse normalmente , por lo que no es un valor compatible bloque Pero sigue siendo un bloque compatible con el vacío porque no contiene declaraciones de return .

El resultado intermedio es que, en el primer caso, la lambda es tanto un bloque compatible con el vacío como un bloque compatible con el valor ; en el segundo caso es solo un bloque compatible con el vacío .

Recordando lo que notamos antes, esto significa que en el primer caso, la lambda será potencialmente compatible tanto con Callable como con Runnable ; en el segundo caso, la lambda solo será potencialmente compatible con Runnable .

Elija el método más específico

Para el primer caso, el compilador tiene que elegir entre los dos métodos porque ambos son potencialmente aplicables . Lo hace usando el procedimiento llamado "Elija el método más específico" y se describe en $ 15.12.2.5. Aquí hay un extracto:

Un tipo de interfaz funcional S es más específico que un tipo de interfaz funcional T para una expresión e si T no es un subtipo de S y uno de los siguientes es verdadero (donde U1 ... Uk y R1 son los tipos de parámetros y el tipo de retorno de el tipo de función de la captura de S, y V1 ... Vk y R2 son los tipos de parámetros y el tipo de retorno del tipo de función de T):

Si e es una expresión lambda explícitamente escrita (§15.27.1), entonces se cumple una de las siguientes condiciones:

R2 es nulo.

Ante todo,

Una expresión lambda con cero parámetros se escribe explícitamente.

Además, ninguno de Runnable y Callable es una subclase entre sí, y el tipo de retorno de Runnable es void , por lo que tenemos una coincidencia: Callable es más específico que Runnable . Esto significa que entre submit(Callable) y submit(Runnable) en el primer caso se Callable el método con Callable .

En cuanto al segundo caso, solo tenemos un método potencialmente aplicable , submit(Runnable) , por lo que se elige.

Entonces, ¿por qué surge el cambio?

Entonces, al final, podemos ver que en estos casos el compilador elige diferentes métodos. En el primer caso, se infiere que la lambda es una Callable que tiene throws Exception en su método call() , de modo que sleep() llamada sleep() compila. En el segundo caso, es Runnable que run() no declara ninguna excepción lanzable, por lo que el compilador se queja de que no se Runnable una excepción.


La razón de esto es que estas invocaciones son, de hecho, invocaciones a dos métodos sobrecargados diferentes, tomando dos tipos diferentes de argumentos, cada tipo con diferentes especificaciones de manejo de excepciones:

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

Entonces, lo que sucede es que el compilador está convirtiendo la lambda en el primer caso de su problema en una interfaz funcional Callable<?> (Invocando el primer método sobrecargado); y en el segundo caso de su problema, la lambda se convierte en una interfaz funcional Runnable (invocando así el segundo método sobrecargado).

Aunque ambas interfaces funcionales no aceptan argumentos, Callable<?> Devuelve un valor y throws Exception muy importante! ):

  1. Llamable: V call() throws Exception;
  2. Ejecutable: public abstract void run();

Si cambiamos a ejemplos que recortan el código a las partes relevantes (para investigar fácilmente solo los bits curiosos), entonces podemos escribir, de manera equivalente a los ejemplos originales:

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

Con estos ejemplos, puede ser más fácil observar que la razón por la que el primero se convierte en un Callable<?> , Mientras que el segundo se convierte en un Runnable se debe a inferencias del compilador .

En el primer caso, el compilador hace lo siguiente:

  1. Detecta que todas las rutas de ejecución en la lambda declaran lanzar excepciones comprobadas (de ahora en adelante nos referiremos como ''excepción'' , lo que implica solo ''excepciones revisadas'' ). Esto incluye la invocación de cualquier método que declare excepciones de lanzamiento y la invocación explícita para throw new <CHECKED_EXCEPTION>() .
  2. Concluye correctamente que el cuerpo ENTERO de la lambda es equivalente a un bloque de código que declara excepciones de lanzamiento; el cual, por supuesto, DEBE ser: manipulado o devuelto.
  3. Dado que la lambda no está manejando la excepción, el compilador asume de manera predeterminada que estas excepciones deben volver a producirse.
  4. Con seguridad infiere que este lambda debe coincidir con una interfaz funcional que lanza Excepción .
  5. Como Callable<?> Es la única interfaz funcional coincidente disponible para los métodos sobrecargados disponibles, la selecciona, convierte el lambda en un Callable<?> Y crea una referencia de invocación al método sobrecargado de submit(Callable<?>) .

En el segundo caso, el compilador hace lo siguiente:

  1. Detecta que puede haber rutas de ejecución en la lambda que NO declaran excepciones de lanzamiento (dependiendo de la lógica a evaluar ).
  2. Dado que no todas las rutas de ejecución declaran excepciones de lanzamiento, el compilador concluye que el cuerpo de la lambda NO ES NECESARIAMENTE equivalente a un bloque de código que declara excepciones de lanzamiento. , solo si todo el cuerpo lo hace o no.
  3. El compilador descarta Callable<?> Como una interfaz funcional coincidente para lambda, ya que Callable declara excepciones de lanzamiento. (una)
  4. Selecciona Runnable como la interfaz funcional de adaptación restante para la lambda que se va a convertir y crea una referencia de invocación al método sobrecargado de submit(Runnable) . Todo esto viene al precio de delegar al usuario, la responsabilidad de manejar cualquier Exception lanzada dondequiera que pueda ocurrir dentro de las partes del cuerpo lambda.

(a) El compilador no tiene ninguna razón para no convertir por defecto todas las lambdas a Callable<?> (y facilita las complicaciones internas), aparte de tener Callable<?> una interfaz funcional más restrictiva que las alternativas disponibles y viables (es decir, Runnable ). Este comportamiento está en línea con el principio de ''ser tan restrictivo como uno DEBE; Pero tan irrestricto como uno puede '' .

Esta fue una gran pregunta. Me divertí mucho persiguiéndolo, ¡gracias!

EDITAR (Respuesta a @Roman Correction):

Mi respuesta es correcta. La razón por la que el compilador decide compilar el primer lambda de la pregunta como un Callable<T> y el segundo como un Runnable es debido a sus CARACTERÍSTICAS DE EXCEPCIONES DECLARADAS IMPLÍCITAS . El siguiente es incluso un ejemplo más simple de esto para llevar el punto a casa:

// LAMBDA COMPILED INTO A ''Callable<?>'' Executors.newSingleThreadExecutor().submit(() -> { throw new Exception(); }); // LAMBDA COMPILED INTO A ''Runnable'' Executors.newSingleThreadExecutor().submit(() -> { });

Por lo tanto, respondí correctamente a la pregunta originalmente formulada, caso cerrado.

Ahora, en un sentido más general, es obviamente cierto que el compilador considera TODAS LAS CARACTERÍSTICAS DE LA FIRMA de los lambdas para determinar la interfaz funcional para compilar CUALQUIER lambda en (incluidos los tipos de argumentos y el tipo de retorno); Pero este no es el caso de la pregunta; Como se muestra claramente en el ejemplo anterior más simple.

Respecto al ejemplo en el que basaste tu argumento completo:

Executors.newSingleThreadExecutor().submit(() -> { while (true) { Thread.sleep(5000); } });

a pesar de tus reclamaciones ...

Por ejemplo, la siguiente construcción NO compilará

El compilador simplemente no puede decir si es un Runnable o un Callable; Puede ser cualquiera de ellos.

... este ejemplo sí compila. El compilador PUEDE decir que la lambda cumple con la firma de un Callable<T> . De hecho, ese ejemplo es el mismo que el primero de la pregunta, así que no sé de qué está hablando.

Finalmente, los ejemplos que incluyó a continuación NO SON EQUIVALENTES con los ejemplos presentados en la pregunta. Usted agregó declaraciones de return ; que cambia la pregunta, porque agrega características a las lambdas que no están presentes en la pregunta original (es decir, el tipo de retorno de las lambdas para discriminar en qué interfaz funcional compilarla) y que requiere una respuesta diferente; pero OTRA VEZ que no estaba en ninguna parte en la pregunta; y lo que es peor ahora, veo sus nuevos comentarios en la pregunta que solicitan modificaciones / modificaciones a la pregunta para que se adapten a su respuesta.