util functional example java lambda java-8 functional-interface

functional - java 8 supplier



¿Por qué un cambio lambda se sobrecarga cuando arroja una excepción de tiempo de ejecución? (5)

Tenga paciencia conmigo, la introducción es un poco largo, pero este es un rompecabezas interesante.

Tengo este código:

public class Testcase { public static void main(String[] args){ EventQueue queue = new EventQueue(); queue.add(() -> System.out.println("case1")); queue.add(() -> { System.out.println("case2"); throw new IllegalArgumentException("case2-exception");}); queue.runNextTask(); queue.add(() -> System.out.println("case3-never-runs")); } private static class EventQueue { private final Queue<Supplier<CompletionStage<Void>>> queue = new ConcurrentLinkedQueue<>(); public void add(Runnable task) { queue.add(() -> CompletableFuture.runAsync(task)); } public void add(Supplier<CompletionStage<Void>> task) { queue.add(task); } public void runNextTask() { Supplier<CompletionStage<Void>> task = queue.poll(); if (task == null) return; try { task.get(). whenCompleteAsync((value, exception) -> runNextTask()). exceptionally(exception -> { exception.printStackTrace(); return null; }); } catch (Throwable exception) { System.err.println("This should never happen..."); exception.printStackTrace(); } } } }

Estoy tratando de agregar tareas en una cola y ejecutarlas en orden. Esperaba que los 3 casos invocasen el método de add(Runnable) ; sin embargo, lo que realmente ocurre es que el caso 2 se interpreta como un Supplier<CompletionStage<Void>> que arroja una excepción antes de devolver un CompletionStage para que el bloque de código "esto nunca debería suceder" se active y el caso 3 nunca se ejecute.

Confirmé que el caso 2 está invocando el método incorrecto al recorrer el código usando un depurador.

¿Por qué no se Runnable método Runnable para el segundo caso?

Aparentemente, este problema solo ocurre en Java 10 o superior, así que asegúrese de probar bajo este entorno.

ACTUALIZACIÓN : de acuerdo con JLS §15.12.2.1. Identificar métodos potencialmente aplicables y más específicamente JLS §15.27.2. Lambda Body parece que () -> { throw new RuntimeException(); } () -> { throw new RuntimeException(); } cae dentro de la categoría de "compatible con vacíos" y "compatible con valores". Entonces, es claro que hay cierta ambigüedad en este caso, pero ciertamente no entiendo por qué el Supplier es más apropiado de una sobrecarga que Runnable aquí. No es como si el primero arrojara alguna excepción que el último no.

No entiendo lo suficiente sobre la especificación para decir lo que debería suceder en este caso.

Archivé un informe de error que está visible en https://bugs.openjdk.java.net/browse/JDK-8208490


El problema es que hay dos métodos:

void fun(Runnable r) y void fun(Supplier<Void> s) .

Y una expresión fun(() -> { throw new RuntimeException(); }) .

¿Qué método se invocará?

Según JLS §15.12.2.1 , el cuerpo lambda es a la vez compatible con el vacío y compatible con el valor:

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

Si el tipo de función de T tiene un tipo de retorno (no válido), entonces el cuerpo lambda es una expresión o un bloque compatible con el valor (§15.27.2).

Entonces, ambos métodos son aplicables a la expresión lambda.

Pero hay dos métodos, por lo que el compilador de Java necesita averiguar qué método es más específico

En §15.12.2.5 . Dice:

Una interfaz funcional de tipo S es más específica que una interfaz funcional de tipo T para una expresión e si todos los siguientes son verdaderos:

Uno de los siguientes es:

Deje que RS sea el tipo de retorno de MTS, adaptado a los parámetros de tipo de MTT, y que RT sea el tipo de retorno de MTT. Uno de los siguientes debe ser cierto:

Uno de los siguientes es:

RT es nulo.

Entonces, S (es decir, el Supplier ) es más específico que T (es decir, Runnable ) porque el tipo de devolución del método en Runnable es void .

Entonces el compilador elige Supplier lugar de Runnable .


He considerado erróneamente que esto es un error, pero parece ser correcto de acuerdo con §15.27.2 . Considerar:

import java.util.function.Supplier; public class Bug { public static void method(Runnable runnable) { } public static void method(Supplier<Integer> supplier) { } public static void main(String[] args) { method(() -> System.out.println()); method(() -> { throw new RuntimeException(); }); } }

javac Bug.java javap -c Bug

public static void main(java.lang.String[]); Code: 0: invokedynamic #2, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable; 5: invokestatic #3 // Method add:(Ljava/lang/Runnable;)V 8: invokedynamic #4, 0 // InvokeDynamic #1:get:()Ljava/util/function/Supplier; 13: invokestatic #5 // Method add:(Ljava/util/function/Supplier;)V 16: return

Esto sucede con jdk-11-ea + 24, jdk-10.0.1 y jdk1.8u181.

La respuesta de zhh me llevó a encontrar este caso de prueba aún más simple:

import java.util.function.Supplier; public class Simpler { public static void main(String[] args) { Supplier<Integer> s = () -> { throw new RuntimeException(); }; } }

Sin embargo, duvduv señaló §15.27.2, en particular, esta regla:

Un bloque lambda body es compatible con el valor si no puede completarse normalmente (§14.21) y cada declaración de retorno en el bloque tiene la forma return Expression ;

Por lo tanto, un bloque lambda es trivialmente compatible con el valor, incluso si no contiene ningún enunciado de retorno. Hubiera pensado, porque el compilador necesita inferir su tipo, que requeriría al menos una Expresión de retorno; Holgar y otros han señalado que esto no es necesario con métodos ordinarios tales como:

int foo() { for(;;); }

Pero en ese caso, el compilador solo necesita asegurarse de que no haya devolución que contradiga el tipo de devolución explícita; no necesita inferir un tipo. Sin embargo, la regla en el JLS está escrita para permitir la misma libertad con el bloque lambdas que con los métodos ordinarios. Tal vez debería haberlo visto antes, pero no lo hice.

Archivé un error con Oracle, pero desde entonces le envié una actualización que hace referencia a §15.27.2 y que declaro que creo que mi informe original es erróneo.


Lo primero es lo primero:

El punto clave es que los métodos de sobrecarga o los constructores con diferentes interfaces funcionales en la misma posición de argumento causan confusión. Por lo tanto, no sobrecargue métodos para tomar diferentes interfaces funcionales en la misma posición de argumento.

Joshua Bloch, - Java efectivo.

De lo contrario, necesitarás un yeso para indicar la sobrecarga correcta:

queue.add((Runnable) () -> { throw new IllegalArgumentException(); }); ^

El mismo comportamiento es evidente cuando se usa un bucle infinito en lugar de una excepción de tiempo de ejecución:

queue.add(() -> { for (;;); });

En los casos que se muestran arriba, el cuerpo lambda nunca se completa normalmente, lo que aumenta la confusión: ¿qué sobrecarga elegir ( compatible con void o compatible con el valor ) si la lambda está implícitamente tipada? Debido a que en esta situación ambos métodos se vuelven aplicables, por ejemplo, puede escribir:

queue.add((Runnable) () -> { throw new IllegalArgumentException(); }); queue.add((Supplier<CompletionStage<Void>>) () -> { throw new IllegalArgumentException(); }); void add(Runnable task) { ... } void add(Supplier<CompletionStage<Void>> task) { ... }

Y, como se indica en esta answer , se elige el método más específico en caso de ambigüedad:

queue.add(() -> { throw new IllegalArgumentException(); }); ↓ void add(Supplier<CompletionStage<Void>> task);

Al mismo tiempo, cuando el cuerpo lambda se completa normalmente (y solo es compatible con el vacío):

queue.add(() -> { for (int i = 0; i < 2; i++); }); queue.add(() -> System.out.println());

se elige el método void add(Runnable task) , porque no hay ambigüedad en este caso.

Como se establece en el JLS §15.12.2.1 , cuando un cuerpo lambda es compatible tanto con el vacío como compatible con el valor , la definición de aplicabilidad potencial va más allá de una verificación de ariadidad básica para tener también en cuenta la presencia y la forma de los tipos de objetivo de interfaz funcional.


Parece que al lanzar una Excepción, el compilador elige la interfaz que devuelve una referencia.

interface Calls { void add(Runnable run); void add(IntSupplier supplier); } // Ambiguous call calls.add(() -> { System.out.println("hi"); throw new IllegalArgumentException(); });

sin embargo

interface Calls { void add(Runnable run); void add(IntSupplier supplier); void add(Supplier<Integer> supplier); }

quejas

Error: (24, 14) java: la referencia para agregar es ambigua. Tanto el método add (java.util.function.IntSupplier) en Main.Calls como el método add (java.util.function.Supplier) en Main.Calls coinciden

Finalmente

interface Calls { void add(Runnable run); void add(Supplier<Integer> supplier); }

compila bien

Muy extraño

  • void vs int es ambiguo
  • int vs Integer es ambiguo
  • void vs Integer NO es ambiguo.

Entonces creo que algo está roto aquí.

He enviado un informe de error al oráculo.


Primero, de acuerdo con §15.27.2 la expresión:

() -> { throw ... }

Es a la vez compatible con el void compatible con el valor, por lo que es compatible ( §15.27.3 ) con el Supplier<CompletionStage<Void>> :

class Test { void foo(Supplier<CompletionStage<Void>> bar) { throw new RuntimeException(); } void qux() { foo(() -> { throw new IllegalArgumentException(); }); } }

(mira que compila)

Segundo, de acuerdo con §15.12.2.5 Supplier<T> (donde T es un tipo de referencia) es más específico que Runnable :

Dejar:

  • S : = Supplier<T>
  • T : = Runnable
  • e : = () -> { throw ... }

Así que eso:

  • MTs : = T get() ==> Rs : = T
  • MTt : = void run() ==> Rt : = void

Y:

  • S no es una superinterfaz o una subinterfaz de T
  • MTs y MTt tienen los mismos parámetros de tipo (ninguno)
  • Sin parámetros formales, por lo que el punto 3 también es cierto
  • e es una expresión lambda explícitamente tipada y Rt es void