Scala 2.12 : Unification interface et trait

Interface et trait

Depuis Java 8, il est possible d’ajouter du code dans les interfaces. Ceci est possible grâce aux default methods qui permettent d’ajouter un comportement par défaut aux méthodes dans les interfaces. Prenons l’exemple d’une interface définissant un message, avec une méthode pour récupérer son contenu et une méthode pour l’afficher.

interface Message {
   String message();
   default void display() { System.out.println(message()); }
}

Cette particularité des interfaces apparue avec Java 8 correspond tout à fait à celui des trait dans Scala. Les traits de Scala correspondent à la définition des traits en sciences informatiques. Notre interface Message peut être traduite en Scala de la manière suivante :

trait Message {
   def message: String
   def display = println(message)
}

Les deux implémentations sont très proches en terme d’écriture. D’une manière générale, l’écriture d’un trait en Scala correspond à la manière d’écrire des interfaces en Java 8, à quelques exception près. Il était donc légitime d’unifier ces deux approches.

Compilation des traits Scala

Avant Java 8, Scala générait pour les trait deux fichiers .class. Par exemple, pour le trait Message, le compilateur Scala générait un fichier Message.class (contenant l’interface Java et la signature des méthodes) et un fichier Message$class.class (contenant une classe abstraite avec l’implémentation en static des méthodes). Actuellement, il ne reste plus qu’un seul fichier généré de cette compilation : Message.class.

Cette unification au niveau Scala permet de gagner un peu d’espace, mais surtout d’avoir une équivalence directe entre Java et Scala entre les interfaces et les traits. Ceci facilite l’utilisation des traits Scala en Java.

Divergence sur la résolution du diamant

Néanmoins, même si cette unification interface / trait est un avantage certain sur le plan de l’interopérabilité, la comparaison s’arrête lors de la résolution de certains problèmes d’héritage, connus sous le nom de problème du diamant. Pour illustrer ce problème, nous allons imaginer trois interfaces / trait : A, ainsi que B et C qui héritent tous les deux de A. Nous allons y ajouter deux classes : D1 qui implémente dans l’ordre B puis C, ainsi que D2 qui implémente C puis B.

En Scala, cela nous donne l’implémentation suivante :

trait A { def message: String }

trait B extends A { override def message = "Message from B" }
trait C extends A { override def message = "Message from C" }

object D1 extends B with C
object D2 extends C with B

println(D1.message)
println(D2.message)
> Message from C
> Message from B

Il n’y a aucune erreur compilation. Scala met implicitement la priorité au dernier trait intégré. Ce qui suit le sens de la lecture.

En Java, nous obtenons l’implémentation suivante :

interface A { String message(); }

interface B extends A {
   default String message() { return "Message from B"; }
}

interface C extends A {
   default String message() { return "Message from C"; }
}

class D1 implements B, C {}
class D2 implements C, B {}

Le compilateur Java produit des erreurs :

> Error: java: D1 inherits unrelated defaults for message() from types B and C
> Error: java: D2 inherits unrelated defaults for message() from types C and B

Contrairement à Scala, Java considère que c’est au développeur de résoudre le problème du diamant, en indiquant explicitement la branche qu’il veut suivre.

Conclusion

Ainsi, si unification il y a entre interface Java 8+ et trait Scala 2.12, il ne faut pas oublier qu’il reste des différences de comportement à la compilation.

Nous verrons dans un prochain article qu’il est possible en Scala 2.12 de faire correspondre les lambdas expressions avec n’importe quel type d’interface fonctionnelle, tout comme en Java 8.

Laisser un commentaire

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

*