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)
.
-
En el primer caso (con
while (true)
), tantosubmit(Callable)
comosubmit(Runnable)
coinciden, por lo que el compilador debe elegir entre ellos-
submit(Callable)
se elige sobresubmit(Runnable)
porqueCallable
es más específico queRunnable
-
Callable
haCallable
throws Exception
encall()
, por lo que no es necesario detectar una excepción en su interior.
-
-
En el segundo caso (con la función
while (tasksObserving)
) solosubmit(Runnable)
, así el compilador la elige-
Runnable
no tiene declaración dethrows
en su métodorun()
, por lo que es un error de compilación no detectar la excepción dentro del métodorun()
.
-
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 :
- 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
- 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;
formularioreturn 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:
<T> Future<T> submit(Callable<T> task);
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!
):
- Llamable:
V call() throws Exception;
- 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:
-
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>()
. - 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.
- Dado que la lambda no está manejando la excepción, el compilador asume de manera predeterminada que estas excepciones deben volver a producirse.
- Con seguridad infiere que este lambda debe coincidir con una interfaz funcional que lanza Excepción .
-
Como
Callable<?>
Es la única interfaz funcional coincidente disponible para los métodos sobrecargados disponibles, la selecciona, convierte el lambda en unCallable<?>
Y crea una referencia de invocación al método sobrecargado desubmit(Callable<?>)
.
En el segundo caso, el compilador hace lo siguiente:
- Detecta que puede haber rutas de ejecución en la lambda que NO declaran excepciones de lanzamiento (dependiendo de la lógica a evaluar ).
- 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.
-
El compilador descarta
Callable<?>
Como una interfaz funcional coincidente para lambda, ya queCallable
declara excepciones de lanzamiento. (una) -
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 desubmit(Runnable)
. Todo esto viene al precio de delegar al usuario, la responsabilidad de manejar cualquierException
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.