Publié par
Il y a 6 années · 8 minutes · Java / JEE

Mais pourquoi n’y a-t-il pas de multidispatch en Java ?

« Voilà une conception dont je peux être fier ! Elle n’est pas forcément extraordinaire, mais elle va simplifier drastiquement le code qui a été écrit. Bon, mettons la en pratique… Mais !… Que se passe-t-il ?… Noooon ! Pas de multidispatch, pas de multiméthode. Je vais devoir mettre du class cast et du instanceof partout. Java, je te hais ! »

Dans cet article, je vous propose de découvrir ce qu’il s’est passé.

Partons d’un exemple.

Vous souhaitez bénéficier d’une classe comportant des méthodes permettant de convertir des objets quelconques en chaîne de caractères indiquant la classe de l’objet et sa valeur. Afin d’éviter l’immonde instanceof et son fidèle compagnon class cast, vous allez naturellement vous tourner vers la surcharge de méthode.

public class Stringfier {

    public static String stringFrom(Object o) {
        return "Object: " + o.toString();
    }

    public static String stringFrom(String o) {
        return "String: " + o;
    }

    public static String stringFrom(Integer o) {
        return "Integer: " + o.toString();
    }

}

Jusque là, rien d’anormal. Vous faites quelques tests simples, qui passent bien

int anInteger = 1;
assertThat(Stringfier.stringFrom(anInteger)).isEqualTo("Integer: 1");

String aString = "string";
assertThat(Stringfier.stringFrom(aString)).isEqualTo("String: string");

Sauf que vous décidez de corser l’affaire : sans changer leur initialisation, vous décidez de typer toutes vos variables en java.lang.Object (la généricité ultime ;-) ). Et là, ça se passe différemment

Object anIntegerAsObject = anInteger;
assertThat(Stringfier.stringFrom(anIntegerAsObject)).isEqualTo("Object: 1");    // expected: "Integer: 1"

Object aStringAsObject = aString;
assertThat(Stringfier.stringFrom(aStringAsObject)).isEqualTo("Object: string"); // expected: "String: string"

wat?!

Java est apparemment partisan du moindre effort, mais il faut savoir que la détermination de la méthode à appeler se fait à la compilation (ce qui n’est plus tout à fait vrai depuis Java 7 avec l’instruction invokedynamic, voir plus loin). Et dans le cadre d’une surcharge de méthode, Java n’ira jamais voir au-delà de la façon dont vous aurez typé vos variables pour savoir quelle méthode appeler. Pour être plus précis, lorsque vous utilisez des variables dont le type est aussi spécialisé que possible, Java n’aura aucun problème pour appeler la méthode voulue. Mais si vous vous amusez à utiliser des types plus globaux pour déclarer vos variables, à la compilation, Java n’a pas la possibilité de deviner ce que vous avez mis dedans.

En fait, il existe un unique moyen de déterminer la bonne méthode à appeler même si vous utilisez des variables typées java.lang.Object : c’est de le faire au runtime.

Multidispatch

Le multidispatch est apparu initialement dans Common Lisp via le Common Lisp Object System (CLOS) et est repris dans Clojure. Souvent représenté sous forme de multiméthode, le multidispatch permet de déclarer des méthodes de même nom mais dont le type des paramètres change. Rien de nouveau jusque là. Java représente cette capacité par la surcharge. Cependant, dans le Common Lisp, l’appel de la bonne méthode se base sur le type intrinsèque des instances passées en paramètre. En Java, ce n’est pas le cas, car Java se base sur le type déclaré.

La souplesse qu’offre du coup le multidispatch fait que nous pouvons très bien ne pas limiter le multidispatch au type des paramètres. Il est même possible d’élargir le concept et baser la vérification sur l’ordre d’apparition des paramètres, leur quantité, leur valeur, etc. Pour le dernier cas, on parle de predicate dispatch. Enfin, le multidispatch est applicable sur des méthodes ayant plus d’un paramètre.

Multidispatch en Java

Afin de forcer Java à choisir la bonne méthode, il faut passer par la réflexion :

  1. on récupère l’appel à la méthode,
  2. on effectue une recherche de la bonne méthode, en utilisant notamment des critères sur les paramètres,
  3. on transmet l’appel à la méthode trouvée.

Tel qu’il est présenté ici, le multidispatch est une forme d’édition dynamique des liens (dynamic linkage) . C’est-à-dire que la méthode à appeler n’est pas déterminée à la compilation mais plutôt au runtime.

Approche simple

L’implémentation ci-dessous permet d’émuler le multidispatch en Java sur la base du type intrinsèque exact des paramètres. C’est-à-dire que si nous déclarons une implémentation de méthode qui prend en paramètre un java.util.List, elle ne fonctionnera pas avec des paramètres de type java.util.ArrayList.

public abstract class MultiMethod {

    protected Object invoke(String methodName, Object... params) {
        Class[] paramTypes = new Class[params.length];
        for (int i = 0; i < params.length; i++) {
            paramTypes[i] = params[i].getClass();
        }

        try {
            Method method = getClass().getDeclaredMethod(methodName, paramTypes);
            return method.invoke(this, params);
        } catch (Exception e) {
            throw new UnsupportedOperationException(e);
        }
    }

}

Voici l’exemple que nous avons vu précédemment, mais cette fois en utilisant la classe MultiMethod.

public class Stringifier extends MultiMethod {
    public String stringFrom(Object o) {
        return (String) super.invoke("_stringFrom", o);
    }

    private String _stringFrom(String s) {
        return "String: " + s;
    }

    private String _stringFrom(Integer i) {
        return "Integer: " + i;
    }
}

Ici, les cas couverts par le code présenté sont simples. Nous supposons qu’à chaque type correspond un traitement spécifique. Comme précisé précédemment, ce code ne permet cependant pas de gérer l’héritage. Par exemple, j’ai une méthode qui traite spécifiquement les java.lang.Number. Si je passe un java.lang.Integer, la méthode ne sera pas appelée. Autre cas non géré : nous traduisons le cas où aucune méthode n’est trouvée par l’envoi d’une exception. Il pourraît être intéressant d’appeler dans ce cas une méthode qui récupère toutes les tentatives d’appel qui ont échoué. Une sorte de method_missing comme en Ruby.

Des approches plus complètes

Autres approches

La principale approche concurrente au multidispatch est le pattern matching. Le pattern matching est une sorte de switch-case beaucoup plus souple et plus puissant. Cette structure peut être utilisée pour réaliser des comparaisons sur des valeurs scalaires ou non, sur des types ou même sur de vrais motifs lorsque le langage intègre la notion de type algébrique. La principale différence avec les multiméthodes, c’est que dans le cadre du pattern matching, le code est colocalisé dans la même fonction. Alors que pour les multiméthodes, le code est réparti entre plusieurs méthodes généralement du même nom, mais avec des signatures différentes.

Scala, Haskell et OCaml sont des exemples de langage proposant le pattern matching. Ci-dessous, nous avons réécrit l’exemple précédant en Scala en se basant sur le pattern matching.

def stringFrom(value: Any): String = value match {
  case s:String => "String: " + s
  case i:Int => "Integer: " + i
  case o => "Object: " + o
}

Une autre approche consiste à utiliser le pattern Visiteur pour émuler le double dispatch. Le double dispatch est une version simplifiée du multidispatch, puisqu’il est limité à deux objets. Le principe de l’émulation consiste à mettre en place dans toutes les classes visitables une méthode accept qui prend en paramètre un visiteur et l’utilise en passant this en paramètre. Ainsi, même si nous exécutons le dispatch avec une variable déclarée avec une classe parente, le type véritable de l’instance qu’elle contient se dévoile par this. Du côté visiteur, il faut définir une méthode visit par type de classe de l’arborescence des visitables. Voici un exemple :

public interface ShapeVisitor {
    String visit(Shape shape);
    String visit(Circle circle);
    String visit(Square square);
}

public class Shape {
    public String accept(ShapeVisitor visitor) {
        return visitor.visit(this);
    }
}

public class Circle extends Shape {
    public String accept(ShapeVisitor visitor) {
        return visitor.visit(this);
    }
}

public class Square extends Shape {
    public String accept(ShapeVisitor visitor) {
        return visitor.visit(this);
    }
}

public class GetClassShapeVisitor implements ShapeVisitor {
    public String visit(Shape shape) {
        return "Shape: " + shape.getClass().getSimpleName();
    }

    public String visit(Circle circle) {
        return "Circle: " + circle.getClass().getSimpleName();
    }

    public String visit(Square square) {
        return "Square: " + square.getClass().getSimpleName();
    }
}

Voici un exemple d’utilisation :

GetClassShapeVisitor visitor = new GetClassShapeVisitor();

Shape shape = new Shape();
assertThat(shape.accept(visitor)).isEqualTo("Shape: Shape");

Shape circle = new Circle();
assertThat(circle.accept(visitor)).isEqualTo("Circle: Circle");

Shape square = new Square();
assertThat(square.accept(visitor)).isEqualTo("Square: Square");

Nous voyons très bien que vous ne pourrez utiliser le double dispatch que sur du code que vous pouvez modifier pour pouvoir ajouter la méthode accept. Avec des classes comme String, Object, Integer ou provenant de bibliothèques extérieures, nous sommes bloqués. Autre point, la solution est plus lourde à implémenter.

Enfin, le multidispatch n’est pas sans rappeler le fonctionnement de l’instruction de la JVM invokedynamic (JSR292). L’instruction invokedynamic est générée à la compilation avec en paramètre le nom de la méthode à appeler, le type de sortie et les types d’entrée, ainsi qu’un lien vers une méthode appelée bootstrap. Le bootstrap est utilisé par invokedynamic au runtime pour récupérer un lien vers une méthode cible. Il est possible de réaliser le multidispatch sur la base de l’instruction invokedynamic (voir invokedynamic newbie question). Néanmoins, cette instruction n’est pas directement disponible depuis le langage Java.

François Sarradin
Consultant Java et λ développeur. Blog personnel : http://kerflyn.wordpress.com/ Twitter : @fsarradin

11 thoughts on “Mais pourquoi n’y a-t-il pas de multidispatch en Java ?”

  1. Publié par Guillaume Laforge, Il y a 6 années

    Groovy également supporte les multiméthodes.
    C’est souvent le propre des langages dynamiques.

  2. Publié par chris, Il y a 6 années

    Tout ca pour éviter de faire une map d’interface (Map<Class, InnerStringifier>) ou même (en jdk 1.7) un switch/case sur la classe du paramètre (il me semble que c’est possible, mais j’ai pas testé) ou encore des simples closure (en jdk1.8) ?
    Je trouve que cela n’en vaut pas la peine.

    Dans mon expérience, le multidispatch est aussi dangereux qu’il est bénéfique.

    Par exemple, avec un ORM on récupère parfois des classes auxquelles on ne s’attend pas et la mauvaise méthode est appelée (mais on ne le voit pas dans les tests unitaires car on n’utilise pas l’ORM à cet endroit !)

    Autre exemple, quand on fait de l’autoboxing et qu’on a mis 2 méthodes (une avec int l’autre avec Integer), quelle méthode serait appelée ?

    Je pense que les cas de multidispatch sont suffisamment rares pour ne pas être pris en charge par le langage et qu’il vaut mieux les expliciter (à l’aide d’une map, d’un switch/case, d’un visitor ou autre) pour éviter les problèmes en maintenance

  3. Publié par chris, Il y a 6 années

    note pour mon commentaire : le parser du blog a mangé mes generics dans InnerStringifier

  4. Publié par Alexandre Bertails, Il y a 6 années

    L’exemple Scala n’utilise pas le Pattern Matching, juste du Type Matching.

  5. Publié par François, Il y a 6 années

    Si ça intéresse du monde d’avoir du multidispatch, j’ai créé un ptit framework gérant ça, il est disponible sur github : https://github.com/larochef/multidispatch

    En gros, il suffit de rajouter une annotation @MultiDispatched sur la méthode prenant en paramètre « Object » et d’ajouter le plugin dans son build maven et le tour est joué.

    Le module « test » montre un exemple d’utilisation et de configuration maven pour le build.

  6. Publié par Alexandre de Pellegrin, Il y a 6 années

    Je suis d’accord avec Chris. Pourquoi passer son temps à vouloir faire de Java un langage dynamique? J’aimerais bien voir en face de ce genre de billet des questions sur l’exploitation en production de ce genre de code. A mon sens, un point fort de Java est de ne pas avoir de surprise après la phase de compilation. D’ailleurs, n’avez-vous pas déjà été témoin des dégâts faits par des frameworks avec trop d’intelligents et abusant du java.land.reflect? Bref, pour moi, dynamiser Java de la sorte, c’est diminuer la maintenabilité des gros projets et ouvrir la porte aux ennuis de production. En lisant l’introduction du billet, j’ai tout de suite pensé au pattern visiteur; ce qui me conforte dans le fait qu’il ne me manque pas fonctionnalité dans mon Java actuel. Cela écrit, pour les petits projets « one shot », pourquoi pas… Mais à quoi bon les faire en Java dans ce cas?

  7. Publié par Anas, Il y a 6 années

    Il faut arrêter avec la généricité absolue et le tout paramétrable. J’ai déjà vu des projets avec des framework trop paramétrables au point que modifier le paramétrage revient finalement plus chère que modifier quelques lignes de code.
    Il faut garder à l’esprit les contraintes d’industrialisation et le coût global de la phase de dev à la phase de mise en prod.

  8. Publié par Jon, Il y a 6 années

    Encore du bullshit soit-disant pragmatique.

    « Un des points forts de Java est de ne pas avoir de surprise après la phase de compilation. » Pour Haskell, je n’aurais pas dit non, mais pour Java c’est vraiment faire preuve de malhonnêteté. Tout le monde en Java fait de l’injection dynamique au runtime via machin ou truc pour combler les lacunes du langage.

    « Il faut arrêter avec la généricité absolue et le tout paramétrable » Hooo la neige, elle est trop molle. Le coût global de la phase de dev, de mise en prod et d’indus est à mettre en face de l’équipe qui va faire ces phases. Parfois, ce n’est pas une équipe de développeurs Java médians.

  9. Publié par Anas, Il y a 6 années

    « Encore du bullshit soit-disant pragmatique. »

    Encore du techos qui ne tien pas compte du pognons qui paye tout ça. Il ne faut pas oublier la contrainte la plus importante qui est celle fixé par le bailleur de fond, celui qui paye. Pas d’tune, pas d’code … ni de généricité ni rien de tout ce tralala. Il faudra un jour bien se le mettre dans la tête avant d’être définitivement doublé par les indiens et les chinois.

  10. Publié par Al, Il y a 6 années

    Aie aie aie. Ca fait mal de lire tout cela…

  11. Publié par Julien, Il y a 3 années

    Bonjour,
    Merci beaucoup pour cet article très intéressant et pour les références !

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *