jeudi 21 mai 2015

Scala : Value Class & Universal Trait

Avant de commencer, voyons la hiérarchie des classes en Scala :


En Scala (comme en Java), toute classe écrite par un utilisateur hérite directement ou indirectement de la classe AnyRef (Object pour Java). A la racine de toutes les classes Scala, on retrouve Any. La classe AnyRef hérite donc de la classe Any. La classe Any dispose de deux sous-classes :

  • AnyVal : c’est la classe mère des types dits "types valeurs" : Int, Char, Bool... Ils ont la particularité qu'on ne peut dans aucun cas utiliser "new" pour les créer, on peut juste utiliser des valeurs prédéfinies comme 1, ‘A’, false ;
  • AnyRef : c’est la classe mère des types dits « types références ». Toutes les classes de l’API standard héritent de cette classe ainsi que toutes les classes créées par les utilisateurs.

Toutes les Value Classes héritent de la classe AnyVal

Une Value Class est similaire à une classe « normale » mais fonctionne comme un type simple. Elle permet de créer des wrappers des types simples (Int, String, …) qui ne résultent pas en la création d'un objet à l'exécutionL'objectif est de permettre la création de types de bas niveau pouvant se substituer aux types de bases de Scala permettant ainsi un meilleur typage. Une Value class doit hériter de la classe AnyVal. Les Values classes ont plusieurs restrictions, notamment, elles ne peuvent pas avoir qu’un seul constructeur avec un seul type simple (le type wrappé) ; elles ne peuvent voir d’autres attributs (un état interne) ni définir de méthodes equals ou hashcode ….

Un exemple simple est la création d’un wrapper du type simple String. Créons une classe simple sans passer par les values class et analysons le résultat :


 class StringWrapper(val param: String) {
     def up() = param.toUpperCase
     def isNullOrEmpty = param == null || param.length==0
     def emptyToNull = if(isNullOrEmpty) null else param
  }

Testons la classe :


 //Main
  def main(args: Array[String]) {

    //création de 4 objets du type StringWrapper.
    //Au niveau JVM, aucun objet n'est crée, tous sont reprsentés par de String
    val w0 = new StringWrapper("sw0")
    val w1 = new StringWrapper("sw1")
    val w2 = new StringWrapper("sw2")
    val w3 = new StringWrapper("sw3")

    println(w0.up())
    println(w1.up())
    println(w2.isNullOrEmpty)
    println(w3.up())
}

Inspectons les objets crées par VisualVM. VisualVM est un outil de monitoring graphique de JVM locale ou distante. Il est intégré au JDK depuis la version 6 update 7. Vous le trouverez dans le répertoire JAVA_HOME/bin/jvisualvm. Il permet notamment de connaître l’occupation CPU, la taille de la heap, etc. Pour monitorer une JVM locale, il suffit de lancer l’outil qui détectera les JVM en cours d’exécution. 

Nous constatons la création de 4 instances de la classe StringWrapper. Passons maintenant par les values class (extends AnyVal) et analysons le résultat :

 class StringWrapper(val param: String) extends AnyVal {
     def up() = param.toUpperCase
     def isNullOrEmpty = param == null || param.length==0
     def emptyToNull = if(isNullOrEmpty) null else param
  }

Nous pouvons remarquer la création d’une seule instance de la classe StringWrapper. Cette instance permet d’appeler les fonctions définies dans la classe StringWrapper sur le type simple (String) de la Value Class (StringWrapper). La décompilation de la classe StringWrapper  nous donne :


 public static class StringWrapper
  {
    private final String param;
    
    public String param()
    {
      return this.param;
    }
    
    public String up()
    {
      return param().toUpperCase();
    }
    
    public boolean isNullOrEmpty()
    {
      return (param() == null) || (param().length() == 0);
    }
    
    public String emptyToNull()
    {
      null;return isNullOrEmpty() ? null : param();
    }
    
    public StringWrapper(String param) {}
  }

Pour finir, on peut juste noter qu’une Value Class peut hériter (plutôt être mixée) d’un trait universel. La documentation scala définit un trait universel comme : A universal trait is a trait that extends Any, only has defs as members, and does no initialization. 

Un exemple d'une Value Class avec l'utilisation d'un Trait Universel :
  def main(args: Array[String]) {

    //création de 4 objets du type StringWrapper.
    //Au niveau JVM, aucun objet n'est crée, tous sont reprsentés par de String
    val w0 = new StringWrapper("sw0")
    val w1 = new StringWrapper("sw1")
    val w2 = new StringWrapper("sw2")
    val w3 = new StringWrapper("sw3")

    println(w0.up())
    w0.toPrint()
    println(w1.up())
    w1.toPrint()
    println(w2.isNullOrEmpty)
    w2.toPrint()
    println(w3.up())
    w3.toPrint()
}

   class StringWrapper(val param: String) extends AnyVal with Printable {
     def up() = param.toUpperCase
     def isNullOrEmpty = param == null || param.length==0
     def emptyToNull = if(isNullOrEmpty) null else param
  }

  trait  Printable extends Any{
    def toPrint() = print(this)
  }