Un aperçu du langage
Introduction
Dans ce chapitre, la totalité des exemples de code présentés peut se retrouver dans le fichier chapitre2.sc du projet scala-book. Comme il s’agit de morceaux de code non reliés, ils sont rassemblés dans un fichier de script.
Pour rappel, les exemples de code sont séparés en deux parties :
-
le code lui-même ;
-
le retour de la console REPL qui commence avec //.
Si on lance le script Scala, la deuxième partie ne s’affiche pas, mais on peut copier-coller le code dans la console REPL pour l’afficher.
Variables
1. Définition
En Scala, il existe deux types de variables :
-
val pour définir des variables immuables, comme les variables finales en Java.
-
var pour définir des variables mutables.
La plupart des variables en Scala sont définies avec le mot-clé val ; ainsi, lorsqu’on parle de variable, on parle de variable immuable. Le mot-clé var est utilisé ponctuellement pour des occasions spécifiques et on parle de variable mutable.
Prenons comme exemple la variable jour.
val jour = "Lundi"
// jour: String = Lundi
Si on essaie de modifier sa valeur, on obtient un message d’erreur.
jour = "Mardi"
// Error: reassignment to val
Avec une variable mutable jourMutable, cette modification est possible.
var jourMutable = "Lundi"
// jourMutable: String = Lundi
jourMutable = "Mardi"
// jourMutable: String = Mardi
2. Variable lazy
Lorsqu’une variable est définie, son contenu est évalué, comme on l’a vu dans la partie précédente. On peut empêcher ce comportement en utilisant le mot-clé lazy val au lieu de val. Ainsi, elle ne sera évaluée que lorsqu’elle sera appelée.
lazy val mot = "Aujourd'hui"
// mot: String = <lazy>
mot
// res0: String = Aujourd'hui
Cela peut être utile dans le cas où on veut définir une variable et l’utiliser uniquement dans un cas précis.
Prenons par exemple une variable lazy qui contient les millisecondes actuelles. On veut créer une chaîne de caractères qui donne la date actuelle selon la langue du système.
lazy val dateCourante = System.currentTimeMillis()
// dateCourante: Long = <lazy>
val langueLocale = Locale.getDefault.getLanguage
// langueLocale: String = en
val dateActuelle = if (langueLocale == "fr") {
"Temps actuel : " + dateCourante
} else if (langueLocale == "en") {
"Current time: " + dateCourante
} else {
"Langage inconnu"
}
// dateActuelle: String = Current time: 1622375853667
Avec cette méthode, la variable dateCourante n’est évaluée qu’au moment où...
Entrée/Sortie
1. Écrire dans la console
Toutes les méthodes concernant l’écriture dans la console se trouvent dans la classe System présente dans le paquet java.lang.
Pour écrire dans la console, on utilise la méthode print pour imprimer ou println pour ajouter en plus un saut à la ligne à la fin du texte.
Par défaut, ces méthodes écrivent dans la console standard et correspondent aux méthodes présentes dans le paquet out. Pour écrire dans la console d’erreur, on utilise les méthodes du paquet err.
System.out.println("Message d'information")
// Message d'information
System.err.println("Message d'erreur")
// Message d'erreur
On peut mettre en forme le texte de sortie grâce aux éléments présents dans la classe Console :
-
BOLD pour rendre le texte gras.
-
UNDERLINED pour souligner le texte.
-
BLUE pour colorer le texte en bleu.
-
BLUE_B pour colorer le fond en bleu.
Les couleurs suivantes sont disponibles pour colorer le texte et le fond :
-
BLACK
-
RED
-
GREEN
-
YELLOW
-
BLUE
-
MAGENTA
-
CYAN
-
WHITE
Une fois un élément de style appliqué, il s’ajoute aux éléments précédents et est actif jusqu’à l’appel de l’élément RESET.

2. Lire depuis la console
Pour lire depuis la console, on utilise...
Les structures de contrôle
1. Structure if/else
Comme en Java, on définit un bloc if / else en décrivant la première condition dans une clause if, les suivantes dans des clauses else if et la dernière dans une clause else.
if (bonneReponse && !mauvaiseReponse) {
"GAGNÉ"
} else if (bonneReponse && mauvaiseReponse) {
"PRESQUE"
} else {
"PERDU"
}
// res213: String = GAGNÉ
Contrairement à Java, le bloc if/else renvoie toujours une valeur et on peut donc assigner son résultat dans une variable. De plus, dans le cas où le bloc ne comprend qu’une seule ligne, on peut supprimer les accolades pour réduire le bloc pour plus de lisibilité.
val resultat = if (bonneReponse) "GAGNÉ" else "PERDU"
// resultat: String = GAGNÉ
Cette syntaxe est semblable à l’opération ternaire disponible en Java :
(bonneReponse) ? "GAGNÉ" : "PERDU";
2. Structure for
En Scala, il existe deux types de boucles for : celle qui ne renvoie pas de valeur, comme en Java, et celle qui renvoie une valeur, utilisable pour assigner son résultat dans une variable.
a. Boucle for classique
La première boucle for se présente de cette façon : for (element <- iterateur) resultat où iterateur correspond à une liste ou une plage de nombres. Le résultat peut se présenter sur une seule ligne ou un bloc de code.
Prenons comme...
Méthodes
1. Définition
Une méthode est une expression définie dans une classe ou un objet et qui prend en entrée des arguments et renvoie un résultat. Dans notre exemple, toutes les méthodes seront définies dans le script.
Pour définir une méthode, on utilise la syntaxe suivante : def nom(t: T) = ???.
La partie gauche correspond au nom et aux arguments de la méthode et la partie droite aux opérations effectuées sur ces paramètres.
Une méthode peut prendre aucun, un ou plusieurs arguments.
def unArg(i: Int) = i * 1.2
// unArg: (i: Int)Double
def deuxArg(i: Int, j: Int): Double = i * j
// deuxArg: (i: Int, j: Int)Double
def sansArg() = ()
// sansArg: ()Unit
a. Type de retour d’une méthode
Toutes ces méthodes possèdent un type de retour, même la méthode qui ne fait rien. Ce type de retour, Unit, correspond au void en Java. On peut spécifier ce type de retour, auquel cas il prévaudra sur le type inféré par le compilateur, comme dans le cas de notre méthode deuxArg qui force la conversion du type Int vers le type Double.
Dans le cas de la méthode sans argument, on peut enlever les parenthèses pour plus de lisibilité.
Contrairement au Java, on n’a pas besoin d’utiliser le mot return pour spécifier le résultat....
Classes
1. Création d’une classe
Pour définir une classe, on utilise le mot-clé class suivi du nom de cette classe et de ses éventuels paramètres, comme en Java.
Prenons comme exemple la classe Vetement qui prend comme arguments un nom.
class Vetement(nom: String)
// defined class Vetement
Pour définir une instance de cette classe, on utilise le mot-clé new suivi du nom de la classe et des arguments.
val short = new Vetement("short")
// short: Vetement = Vetement@63ed4f58
Dans une classe, on peut définir des variables supplémentaires, des méthodes, fonctions ou autres constructeurs comme on le verra dans les sections suivantes.
2. Accès aux champs
En Java, pour accéder aux paramètres de la classe, on doit créer des accesseurs.
En Scala, on peut également le faire mais il existe une méthode plus élégante. On ajoute le mot-clé var ou val, selon si on souhaite modifier ou pas ce paramètre, devant le nom de ce dernier. Pour accéder à un paramètre x d’un objet a, il suffit alors d’utiliser x.a.
Prenons comme exemple la classe Vetements qui prend comme arguments un nom immuable et un nombre mutable.
class Vetements(val nom: String, var nombre: Int)
// defined class Vetements
Ainsi, pour une instance de la classe Vetements, on peut accéder à son nom et son nombre.
val shorts = new Vetements("short", 3)
// shorts: Vetements = Vetements@57e0f329
shorts.nom
// res231: String = short
shorts.nombre
// res232: Int = 5
On peut également modifier son paramètre nombre mais on obtient une erreur à la compilation si on essaye de modifier son nom.
shorts.nom = "SHORT"
// reassignment to val
shorts.nombre += 2 ...
Héritage
1. Définition
Comme en Java, on peut étendre une classe unique et hériter de ses variables et méthodes. Pour ce faire, il suffit d’ajouter le mot-clé extends suivi de la classe parent et de ses arguments.
Prenons comme exemple une classe Appartement qui étend la classe Maison définie plus tôt. Un Appartement a toujours une chambre et prend comme argument un lieu.
class Appartement(lieu: String) extends Maison(1, lieu)
// defined class Appartement
Une instance de cette classe aura accès au champ lieu défini comme argument de la classe enfant, mais aussi à tous les champs définis dans la classe parent, que ce soit le nombre de chambres ou la description.
val appartement = new Appartement("Lyon")
// appartement: Appartement = Appartement@75f3e7b1
appartement.lieu
// res249: String = Lyon
appartement.description
// res250: String = Maison à Lyon avec 1 chambres
appartement.chambres
// res251: Int = 1
L’objet obtenu est une instance à la fois de la classe Appartement et de la classe Maison.
appartement.isInstanceOf[Appartement]
// res252: Boolean = true
appartement.isInstanceOf[Maison]
// res253: Boolean = true
On peut créer un objet Maison à partir de cet appartement en forçant son type lors de l’instanciation, mais l’inverse n’est pas possible. Lorsqu’on essaye de typer un objet Maison en tant qu’objet Appartement, une erreur se produit à la compilation.
val appartementMaison: Maison = appartement
// appartementMaison: Maison = Appartement@42f51320
val maisonAppartement:...
Objets singletons
1. Définition
Un objet singleton est une valeur qu’on définit de la même façon qu’une classe avec le mot-clé object. C’est à la fois une classe et une instance de cette même classe qui est créée lors du premier appel.
Cela permet de définir des méthodes ou des constantes accessibles depuis tout le projet sans devoir créer une instance de classe ou étendre une classe.
Prenons comme exemple l’objet Compteur qui contient :
-
une variable nom de type String,
-
une variable privée mutable total de type Int,
-
une méthode decompte qui récupère la valeur de la variable total,
-
une méthode ajouter qui incrémente la variable total.
object Compteur {
val nom = "COMPTEUR"
private var total = 0
def decompte = total
def ajouter = total += 1
}
Depuis une autre classe ou un autre objet, on peut appeler les méthodes et récupérer les variables accessibles de cet objet en indiquant son nom devant les éléments appelés.
Dans notre exemple, on peut incrémenter le compteur avec la méthode ajouter, récupérer le total avec la méthode decompte et la variable nom mais pas la variable total qui est privée.
Compteur.ajouter
Compteur.decompte
// res259: Int = 1
Compteur.nom
// res260: String = COMPTEUR
Compteur.total
// Error: variable total in object Compteur cannot be accessed in
object Compteur
Si l’objet se trouve dans un paquet différent, il suffit d’importer l’objet pour pouvoir l’utiliser. Si on veut utiliser un des éléments sans spécifier son nom, il suffit d’utiliser le mot-clé ._ pour importer tout le contenu de l’objet.
Prenons comme exemple l’objet Random. Il permet de générer...
Exemple complet
Dans cet exemple final, nous allons créer une application qui pose des questions en bleu, lit des chaînes de caractères en entrée et imprime des résultats en vert ou rouge selon la réussite. Cet exemple est disponible dans la classe ChapitreDeux présent dans le paquet eni.
Tout d’abord, on demande à l’utilisateur son âge avec la méthode readInt.
print(s"${BLUE}Quel est votre âge ? $RESET")
val age = readInt()
Ensuite, on demande le nombre de dates que l’utilisateur souhaite renseigner avec la méthode readInt.
print(s"${BLUE}Combien de dates voulez-vous renseigner ? $RESET")
val nombre = readInt()
Le but est que l’utilisateur renseigne des dates au format dd/MM/yyyy et de comparer l’âge obtenu avec l’âge de l’utilisateur. Si un des âges correspond à l’âge de l’utilisateur, on indique le numéro de la personne en vert. Sinon, on affiche une phrase d’échec en rouge. On donnera également à la fin du programme l’âge moyen des personnes avec un chiffre significatif.
Pour cela, on crée un formateur pour transformer la chaîne de caractères en élément de type LocalDate et on instancie un élément correspondant à la LocalDate actuelle pour avoir un point...