46 votes

Pourquoi un lambda change-t-il les surcharges lorsqu’il fait une exception de temps d’exécution ?

Ours avec moi, l'introduction est un peu longue haleine, 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(); }
        }
    }
}

Je suis en train d'ajouter des tâches sur une file d'attente et de les exécuter dans l'ordre. Je m'attendais à tous les 3 cas d'invoquer l' add(Runnable) méthode; cependant, ce qui se passe réellement est que le cas 2 est interprété comme un Supplier<CompletionStage<Void>> qui lève une exception avant de retourner un CompletionStage afin de les "cela ne devrait jamais se produire" bloc de code est déclenchée et le 3 n'est jamais à court.

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

Pourquoi n'est-ce pas l' Runnable méthode de prise en invoquée pour le deuxième cas?

Apparemment, ce problème se produit uniquement sur Java 10 ou plus, alors assurez-vous de tester sous cet environnement.

Mise à JOUR: Selon JLS §15.12.2.1. Identifier Potentiellement Méthodes Applicables et plus particulièrement JLS §15.27.2. Corps de Lambda, il semble qu' () -> { throw new RuntimeException(); } relèvent de la catégorie de "vide-compatible" et de la "valeur-compatible". Donc clairement il y a une certaine ambiguïté dans ce cas, mais je ne comprends pas pourquoi, Supplier est plus approprié à une surcharge de Runnable ici. C'est pas comme si l'ancien jette les exceptions que les seconds ne le sont pas.

Je ne comprends pas assez sur la spécification de dire ce qui doit se passer dans ce cas.

J'ai déposé un rapport de bug qui est visible https://bugs.openjdk.java.net/browse/JDK-8208490

19voto

zhh Points 1684

Le problème, c'est qu'il y a deux méthodes:

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

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

La méthode qui sera invoquée?

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

Si le type de fonction de T a un vide de retour, puis le corps de lambda est une déclaration de l'expression (§14.8) ou un vide-compatible bloc (§15.27.2).

Si le type de fonction de T (non-nulle) type de retour, puis le corps de lambda est soit une expression ou une valeur compatible avec le bloc (§15.27.2).

Donc, les deux méthodes sont applicables à l'expression lambda.

Mais il y a deux méthodes java compilateur a besoin de savoir quelle méthode est la plus spécifique

Dans JLS §15.12.2.5. Il 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'une des conditions suivantes:

Laissez-RS-être le type de retour de MTS, adapté pour les paramètres de type de MTT, et laissez-RT-être le type de retour de MTT. L'une des conditions suivantes doit être remplie:

L'une des conditions suivantes:

RT est nulle.

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

Ainsi, le compilateur choisissez Supplier au lieu de Runnable.

10voto

duvduv Points 581

Tout d'abord, selon §15.27.2 l'expression:

() -> { throw ... }

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

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

(voir la compilation)

En deuxième lieu, selon §15.12.2.5 Supplier<T> (où T est un type de référence) est plus spécifique que l' Runnable:

Laissez:

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

De sorte que:

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

Et:

  • S n'est pas un superinterface ou un subinterface d' T
  • MTs et MTt ont les mêmes paramètres de type (aucun)
  • Pas de paramètres formels donc le point 3 est également vrai
  • e est explicitement tapé expression lambda et Rt est - void

7voto

Peter Lawrey Points 229686

Il semble que lors de la levée 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();
    });

Cependant

interface Calls {
    void add(Runnable run);

    void add(IntSupplier supplier);

    void add(Supplier<Integer> supplier);
}

se plaint

Erreur:(24, 14) java: une référence à ajouter est ambigu à la fois la méthode add(java.util.fonction.IntSupplier) dans la Principale.Des appels et de la méthode add(java.util.fonction.Fournisseur) dans la Principale.Appels match

Enfin

interface Calls {
    void add(Runnable run);

    void add(Supplier<Integer> supplier);
}

compile bien.

Donc bizarrement;

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

Donc je me dis que quelque chose est cassé ici.

J'ai envoyé un rapport de bug pour oracle.

5voto

Oleksandr Points 7545

Tout d'abord:

Le point clé est que les méthodes de surcharge ou de constructeurs avec différentes interfaces fonctionnelles dans la même position argument causes de la confusion. Donc, ne pas surcharger les méthodes à prendre différentes interfaces fonctionnelles dans l'argument même position.

Joshua Bloch, Efficace Java.

Sinon, vous aurez besoin d'un plâtre pour indiquer le bon surcharge:

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 le cas illustré ci-dessus, le corps de lambda ne se termine jamais normalement, ce qui ajoute à la confusion: ce qui surcharge de choisir (vide-compatible ou de la valeur-compatible) si le lambda est implicitement tapé? Parce que dans cette situation, les deux méthodes ne sont pas 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 cette réponse - le plus spécifique de la méthode 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 de lambda se termine normalement (et est nul-compatible uniquement):

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

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

Comme indiqué dans le JLS §15.12.2.1, lorsqu'un corps de lambda est à la fois vide-compatible et valeur-compatible, la définition du potentiel d'application va au-delà d'un simple arité de vérifier de prendre également en compte la présence et la forme de l'interface fonctionnelle types de cibles.

2voto

David Conrad Points 3529

J'ai considéré à tort-ce 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

Ce qui se passe avec jdk-11-ea+24, jdk-10.0.1, et jdk1.8u181.

zhh réponse m'a conduit à trouver, c'est même plus simple de cas de test:

import java.util.function.Supplier;

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

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

Un bloc de corps de lambda est de la valeur compatible si elle ne peut pas se terminer normalement (§14.21) et chaque instruction de retour dans le bloc a la forme de retour de l'Expression;.

Ainsi, un bloc lambda est trivialement valeur compatible, même si elle ne contient pas d'instruction return à tous. J'aurais pensé que, parce que le compilateur doit déduire de son type, qu'il aurait besoin d'au moins un retour d' Expression;. Holgar et d'autres ont fait remarquer que ce n'est pas nécessaire avec les méthodes ordinaires tels que:

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

Mais dans ce cas, le compilateur doit seulement veiller à ne pas se retourner à l'encontre de l'explicite le type de retour; il n'a pas besoin d'en déduire un type. Cependant, la règle de la JLS est écrit pour permettre la liberté même avec bloc lambdas comme avec les méthodes ordinaires. Peut-être que j'aurais vu cela plus tôt, mais je n'ai pas.

J'ai déposé un bug avec Oracle , et il a envoyé une mise à jour de référencement §15.27.2 et en disant que je crois que mon premier rapport à être dans l'erreur.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X