web-dev-qa-db-fra.com

Pourquoi un lambda change-t-il les surcharges lorsqu'il déclenche une exception d'exécution?

Restez avec moi, l'introduction est un peu longue mais c'est un puzzle intéressant.

J'ai ce code:

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

J'essaie d'ajouter des tâches dans une file d'attente et de les exécuter dans l'ordre. Je m'attendais à ce que les 3 cas invoquent la méthode add(Runnable); cependant, ce qui se passe réellement, c'est que le cas 2 est interprété comme un Supplier<CompletionStage<Void>> qui lève une exception avant de renvoyer un CompletionStage de sorte que le bloc de code "cela ne devrait jamais arriver" est déclenché et le cas 3 ne s'exécute jamais.

J'ai confirmé que le cas 2 invoque la mauvaise méthode en parcourant le code à l'aide d'un débogueur.

Pourquoi la méthode Runnable n'est-elle pas invoquée pour le deuxième cas?

Apparemment, ce problème ne se produit que sur Java 10 ou supérieur, alors assurez-vous de tester dans cet environnement.

[~ # ~] mise à jour [~ # ~] : selon JLS §15.12.2.1. Identifier les méthodes potentiellement applicables et plus précisément JLS §15.27.2. Lambda Body il semble que () -> { throw new RuntimeException(); } tombe dans la catégorie à la fois "compatible avec le vide" et "compatible avec la valeur". Il est donc clair qu'il y a une certaine ambiguïté dans ce cas, mais je ne comprends certainement pas pourquoi Supplier est plus approprié à une surcharge que Runnable ici. Ce n'est pas comme si le premier levait des exceptions que le second ne faisait pas.

Je ne comprends pas assez la spécification pour dire ce qui devrait arriver dans ce cas.

J'ai déposé un rapport de bogue visible sur https://bugs.openjdk.Java.net/browse/JDK-820849

47
Gili

D'abord, selon §15.27.2 l'expression:

() -> { throw ... }

Est à la fois void- et valeur-compatible, donc il est compatible ( §15.27. ) avec Supplier<CompletionStage<Void>>:

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

(voir qu'il compile)

Deuxièmement, selon §15.12.2.5Supplier<T> (Où T est un type de référence) est plus spécifique que Runnable:

Laisser:

  • [~ # ~] s [~ # ~] : = Supplier<T>
  • [~ # ~] t [~ # ~] : = Runnable
  • e : = () -> { throw ... }

Pour que:

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

Et:

  • S n'est pas une superinterface ou une sous-interface de T
  • MTs et MTt ont les mêmes paramètres de type (aucun)
  • Pas de paramètres formels donc la puce 3 est également vraie
  • e est une expression lambda explicitement typée et Rt est void
10
duvduv

Le problème est qu'il existe deux méthodes:

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

Et une expression fun(() -> { throw new RuntimeException(); }).

Quelle méthode sera invoquée?

Selon JLS §15.12.2.1 , le corps lambda est à la fois compatible avec le vide et avec la valeur:

Si le type de fonction de T a un retour void, alors le corps lambda est soit une expression d'instruction (§14.8), soit un bloc compatible void (§15.27.2).

Si le type de fonction de T a un type de retour (non nul), alors le corps lambda est soit une expression soit un bloc compatible avec les valeurs (§15.27.2).

Les deux méthodes sont donc applicables à l'expression lambda.

Mais il y a deux méthodes donc Java doit trouver quelle méthode est plus spécifique

Dans JLS §15.12.2.5 . Ça dit:

Une interface fonctionnelle de type S est plus spécifique qu'une interface fonctionnelle de type T pour une expression e si toutes les conditions suivantes sont remplies:

L'un des éléments suivants est:

Soit RS le type de retour de MTS, adapté aux paramètres de type de MTT, et soit RT le type de retour de MTT. L'une des conditions suivantes doit être vraie:

L'un des éléments suivants est:

RT est nul.

Donc S (c'est-à-dire Supplier) est plus spécifique que T (c'est-à-dire Runnable) parce que le type de retour de la méthode dans Runnable est void.

Le compilateur choisit donc Supplier au lieu de Runnable.

19
zhh

Il apparaît que lors du lancement d'une exception, le compilateur choisit l'interface qui renvoie une référence.

interface Calls {
    void add(Runnable run);

    void add(IntSupplier supplier);
}

// Ambiguous call
calls.add(() -> {
        System.out.println("hi");
        throw new IllegalArgumentException();
    });

Toutefois

interface Calls {
    void add(Runnable run);

    void add(IntSupplier supplier);

    void add(Supplier<Integer> supplier);
}

se plaint

Erreur: (24, 14) Java: la référence à ajouter est ambiguë, la méthode add (Java.util.function.IntSupplier) dans Main.Calls et la méthode add (Java.util.function.Supplier) dans la correspondance Main.Calls correspondent

Enfin

interface Calls {
    void add(Runnable run);

    void add(Supplier<Integer> supplier);
}

compile bien.

Tellement bizarrement;

  • void vs int est ambigu
  • int vs Integer est ambigu
  • void vs Integer n'est PAS ambigu.

Je pense donc que quelque chose est cassé ici.

J'ai envoyé un rapport de bogue à Oracle.

7
Peter Lawrey

Tout d'abord:

Le point clé est que la surcharge des méthodes ou des constructeurs avec différentes interfaces fonctionnelles dans la même position d'argument provoque la confusion. Par conséquent, ne surchargez pas les méthodes pour prendre différentes interfaces fonctionnelles dans la même position d'argument.

Joshua Bloch, - Java efficace.

Sinon, vous aurez besoin d'un cast pour indiquer la surcharge correcte:

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

Le même comportement est évident lors de l'utilisation d'une boucle infinie au lieu d'une exception d'exécution:

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

Dans les cas indiqués ci-dessus, le corps lambda ne se termine jamais normalement, ce qui ajoute à la confusion: quelle surcharge choisir ( void-compatible ou value-compatible) si le lambda est implicitement typé? Parce que dans cette situation, les deux méthodes deviennent applicables, par exemple, vous pouvez écrire:

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

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

void add(Runnable task) { ... }
void add(Supplier<CompletionStage<Void>> task) { ... }

Et, comme indiqué dans ce réponse - la méthode la plus spécifique est choisie en cas d'ambiguïté:

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

Dans le même temps, lorsque le corps lambda se termine normalement (et est compatible avec le vide uniquement):

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

la méthode void add(Runnable task) est choisie, car il n'y a pas d'ambiguïté dans ce cas.

Comme indiqué dans le JLS §15.12.2.1 , lorsqu'un corps lambda est à la fois compatible avec le vide et compatible avec la valeur, le la définition de l'applicabilité potentielle va au-delà d'un contrôle d'arité de base pour prendre également en compte la présence et la forme des types cibles d'interfaces fonctionnelles.

5
Oleksandr Pyrohov

J'ai considéré à tort cela comme un bug, mais il semble être correct selon §15.27.2 . Considérer:

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

Cela se produit avec jdk-11-ea + 24, jdk-10.0.1 et jdk1.8u181.

la réponse de zhh m'a amené à trouver ce cas de test encore plus simple:

import Java.util.function.Supplier;

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

Cependant, duvduv a souligné le §15.27.2, en particulier, cette règle:

Un corps lambda de bloc est compatible avec les valeurs s'il ne peut pas se terminer normalement (§14.21) et chaque instruction de retour dans le bloc a la forme return Expression ;.

Ainsi, un bloc lambda est trivialement compatible avec les valeurs même s'il ne contient aucune instruction return. J'aurais pensé, parce que le compilateur doit inférer son type, qu'il nécessiterait au moins un retour Expression ;. Holgar et d'autres ont souligné que cela n'est pas nécessaire avec des méthodes ordinaires telles que:

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

Mais dans ce cas, le compilateur doit seulement s'assurer qu'il n'y a pas de retour qui contredit le type de retour explicite; il n'a pas besoin d'en déduire un type. Cependant, la règle dans le JLS est écrite pour permettre la même liberté avec les lambdas de bloc qu'avec les méthodes ordinaires. J'aurais peut-être dû le voir plus tôt, mais je ne l'ai pas vu.

J'ai déposé n bug avec Oracle mais depuis, j'ai envoyé une mise à jour référençant le §15.27.2 et déclarant que je pense que mon rapport d'origine était erroné.

2
David Conrad