Le carnet On est
Chapitre 3 · encre bordeaux

Programmation orientée objet

Classes, objects, companion, héritage, traits, case classes. L'objectif : lire et comprendre un design Scala, pas réciter la syntaxe.

~ 50 min Source : Scala_03 — Classes (Zaouche, Dujol) 14 flashcards · 8 QCM

📄 Ressources du cours

📥 Télécharger les slides  ·  🔗 Ouvrir dans un nouvel onglet  ·  📝 Ouvrir le TD

🎥 Vidéos recommandées

1. Class — la brique de base

Définition (similaire à Java) Une classe décrit tous les membres définis pour toute instance :
  • Valeurs / variables (avec val ou var)
  • Méthodes : fonctions dont l'instance est implicitement un paramètre via this
class ClassName[ParameterTypes*] (typedParameter*) {
   classBody
}
  • ClassName commence par une majuscule
  • classBody contient des déclarations val/var/def
  • Typiquement dans ClassName.scala (sauf sealed)
  • Instanciation : new (comme Java)

Déclarations spéciales

Mot-cléComportement
final classComme Java : ne peut pas avoir de sous-types
sealed classTous les sous-types doivent être dans la même unité de compilation (même fichier). Permet de vérifier l'exhaustivité d'un pattern matching.
case classClasse conçue pour le pattern matching. Voir section 5.
abstract classComme Java : non instanciable

Héritage : extends et with

Règles
  • extends : première superclasse (ou trait implémenté)
  • with : pour chaque autre trait implémenté
  • Comme Java, une classe peut étendre au plus une classe.

Visibilité & immutabilité des membres

Visibilité par défaut : public. Préfixer la déclaration avec private la rend privée.

scala> class A {
         val x : Int = 2
         private val ans : Int = 42
       }
scala> (new A()).x        // 2  - accessible
scala> (new A()).ans      // error: value ans cannot be accessed

2. Object — singleton immutable

Un object est une classe avec une seule instance immutable Conséquences directes :
  • Un objet ne peut pas être type-dépendant (pas de [T])
  • Un objet ne peut pas être parameter-dépendant (pas de paramètres de constructeur)
  • Cette instance est traitée comme une lazy value
object ObjectName { objectBody }

Companion object — le compagnon

📌 Définition
Object avec le même nom qu'une classe (typiquement dans le même fichier).
  • La classe peut accéder aux membres privés de son companion object
  • Les membres du companion ne dépendent pas d'une instance
Équivalent fonctionnel du static en Java.

Variantes

  • case object — case class + object (ex : None)
  • extends / with : héritage comme une classe

3. Appel de méthode

Notation Java-like :

instance.methodName(parameters)

Les paramètres peuvent être appelés dans un autre ordre s'ils sont nommés :

(new A()).f(2, 3)         // 5
(new A()).f(y = 3, x = 2) // 5

Notation infixe :

instance methodName parameter
≡ instance.methodName(parameter)

Seulement pour méthode à un seul paramètre. Utile pour les DSL (ex : sbt).

class A(x : Int) {
   def +(y : Int) = x + y
}
(new A(2)) + 3   // 5

4. Constructeurs

Constructeur principal

Le constructeur principal est implicitement défini dans la déclaration de la classe.
  • Paramètres : la liste de paramètres de la classe
  • Corps : tout le corps de la classe
⚠ Piège à connaître Par défaut, les paramètres d'une classe sont privés et immuables. Pour les rendre publics ou mutables, ajouter un modificateur :
ModificateurVisibilitéMutabilité
(rien)privéimmuable
valpublicimmuable
varpublicmutable
private valprivéimmuable
private varprivémutable
// x est private par defaut : impossible d'y acceder de l'exterieur
scala> class A(x : Int) { def test = println(this.x) }
scala> (new A(2)).test  // 2 (acces interne OK)
scala> (new A(2)).x     // error: value x is not a member of A

// avec val : public immuable
scala> class A(val x : Int)
scala> (new A(2)).x     // 2 ✓

// avec var : public mutable
scala> class A(var x : Int)
scala> val a = new A(2) ; a.x = 3  // ok

Constructeurs auxiliaires

Même principe qu'en Java :

  • Méthode dont le nom est this
  • Signature différente des autres constructeurs
  • Tout appel interne à un constructeur se fait par le keyword this
class A(x : Int, y : Int) {
   def this(x : Int) = this(x, 0)  // y vaut 0 par defaut
}
new A(1, 2)  // principal
new A(1)     // auxiliaire

5. Case class — la star du chapitre

Une case class est une classe « + ... » Quatre cadeaux par rapport à une classe normale :
  1. Modificateur val implicitement appliqué à tout paramètre → public immuable
  2. == compare le contenu, pas la référence
  3. Méthode toString automatiquement bien rédigée
  4. Companion object implicite avec :
    • apply aux mêmes paramètres → plus besoin de new : Person(...)Person.apply(...)
    • unapply qui permet l'utilisation dans une clause case (d'où le nom)

Exemple intégral à comprendre

scala> case class Person(last : String, first : String)

// 1) Pas de new — apply implicite
scala> val rdl = Person("Dujol", "Romain")
rdl: Person = Person(Dujol,Romain)         // toString bien rédigé

// 2) == compare le contenu
scala> rdl == Person("Dujol", "Romain")
res0: Boolean = true                       // !

// 3) Accès public aux fields
scala> rdl.first + " " + rdl.last
res1: String = Romain Dujol

// 4) Pattern matching natif
scala> Person("Romain", "Dujol") match {
         case Person("Dujol", "Romain") => "bibi"
         case Person("Dujol", _       ) => "famille"
         case Person(l, f) if l == f  => "Ca existe ?"
         case _                       => "autre"
       }
res2: String = autre

Combo gagnant : sealed + case classes

🔑 Pattern courant
Classe abstraite sealed + sous-types tous en case class (ou case object).
Avantage : le compilateur vérifie l'exhaustivité du pattern matching.
scala> sealed abstract class IntOption
scala> case class IntSome(x : Int) extends IntOption
scala> case object IntNone extends IntOption

scala> val test : IntOption = IntNone
scala> test match { case IntNone => 0 }
// warning: match may not be exhaustive.
// It would fail on the following input: IntSome(_)
res1: Int = 0

Le warning n'apparaît pas si IntOption n'est pas sealed — c'est exactement ce que sealed apporte.

6. Traits — interfaces enrichies

Un trait est comme une interface Java…
  • Pas de paramètres (jusqu'à Scala 3)
  • Abstrait : pas d'instanciation directe avec new
  • Pas de constructeur
  • Une classe/un trait peut implémenter plusieurs traits
… mais en plus, un trait peut :
  • Contenir des valeurs / variables
  • Contenir des implémentations partielles
  • Être implémenté par une seule instance (pas toute la classe)

Exemple : trait Comparable — Java style vs Scala style

Java style — toutes les méthodes abstraites, la classe doit toutes les définir :

trait Comparable[T] {
   def == (that : T)
   def != (that : T)
   def <  (that : T)
   def <= (that : T)
   def >  (that : T)
   def >= (that : T)
}

Scala style — implémentations partielles dérivées, la classe ne définit que == et < :

trait Comparable[T] {
   def == (that : T)
   def != (that : T) = !(this == that)
   def <  (that : T)
   def <= (that : T) = (this < that) || (this == that)
   def >  (that : T) = !(this <= that)
   def >= (that : T) = !(this <  that)
}

Trait avec val/var et implémentation par une instance

scala> trait Person { val last : String;  val first : String }
scala> case class Employee(last : String, first : String, department : String) extends Person

scala> val rdl : Person = Employee("Dujol", "Romain", "Mathematics")
scala> rdl.last
res0: String = Dujol

// Implementation sur une instance unique (sans toucher la classe)
scala> trait Employable { val department : String }
scala> case class P(last : String, first : String)
scala> val rdl = P("Dujol", "Romain") with Employable {
         override val department = "Mathematics"
       }
scala> rdl.department
res0: String = Mathematics

7. Génériques — bornes de type

Comme pour les méthodes, le type peut être inféré à l'usage. On peut borner ce que T peut être :

BorneSyntaxeSignification
Upper boundclass A[T <: T1]T doit étendre (ou implémenter) T1
Lower boundclass A[T >: T0]T0 doit étendre (ou implémenter) T
Les deuxclass A[T <: T1 >: T0]Combinaison des deux
🔑 Pourquoi borner ?
  • Upper bound : assurer que tout élément de type T possède certaines propriétés (celles de T1). Ex : T <: Comparable[T] garantit qu'on peut comparer.
  • Lower bound : pour les paramètres contravariants — typiquement définition de méthodes sur collections.

Réviser le chapitre

🃏 Flashcards — axées comportement

Différence class vs object ?
class = modèle d'instances multiples (avec new). object = une seule instance immuable, lazy. L'object remplace le static Java.
À quoi sert un companion object ?
Object de même nom qu'une classe (même fichier). Sert à ranger les méthodes "statiques" (factory, constants), avec accès aux membres privés de la classe.
Par défaut, un paramètre de classe class A(x : Int) est…
Privé immuable. Ajouter val → public immuable. var → public mutable. private val / private var idem privés.
Quels 4 cadeaux apporte une case class ?
val implicit sur tout paramètre · ② == compare le contenu · ③ toString bien rédigé · ④ companion avec apply (plus de new) et unapply (pattern matching).
Quand utiliser sealed + case class/object ?
Pour modéliser un type somme (ex : Option = Some | None). Le compilateur vérifie l'exhaustivité des patterns matchés.
Comment appeler une méthode def +(y : Int) en notation infixe ?
a + b au lieu de a.+(b). Seulement pour méthode à un seul paramètre. C'est ce qui rend les DSL Scala (sbt) si fluides.
Différence trait Java-style vs trait Scala-style ?
Java-style : que des signatures (interfaces strictes). Scala-style : implémentations partielles dérivées — la classe ne définit que les méthodes essentielles, le reste est calculé.
Comment hériter de plusieurs traits ?
extends Trait1 with Trait2 with Trait3 .... Une classe étend au plus une classe, mais autant de traits qu'elle veut.
Upper bound vs Lower bound ?
T <: T1 (upper) : T étend T1 → T a au moins les capacités de T1. T >: T0 (lower) : T0 étend T → T est plus général que T0.
Que fait Person("Dujol", "Romain") == Person("Dujol", "Romain") avec case class Person ?
true — la case class redéfinit == pour comparer le contenu. Sans case, ce serait false (comparaison de références).
Constructeur auxiliaire en Scala : quel nom ?
La méthode s'appelle this. Sa signature doit être différente des autres. Doit appeler this(...) en interne.
Différence sealed vs final ?
final : pas de sous-types du tout. sealed : sous-types autorisés, mais dans le même fichier seulement. Sealed reste extensible localement.
Que veut dire « un object est lazy » ?
L'instance unique de l'object est créée au premier usage, pas au démarrage du programme. Conséquence directe de la propriété « lazy val ».
À quoi sert un trait avec un seul with sur une instance ?
Ajouter des capacités à une instance spécifique sans modifier la classe. Pratique pour des "mixins" ad-hoc, ou pour des tests/mocks.

✎ Quiz éclair — comprendre ce que fait le code

1.class P(x : Int). Que vaut (new P(5)).x ?
  • 5
  • Erreur : x n'est pas membre de P
  • null
  • 0
Voir la section du chapitre pour la justification détaillée.
2.case class P(x : Int) ; val a = P(1) ; val b = P(1) ; a == b
  • true
  • false
  • Erreur de compilation
  • Dépend de la JVM
Voir la section du chapitre pour la justification détaillée.
3.object Singleton { var n = 0 ; def inc() = { n += 1 } }. Si on appelle Singleton.inc() 3 fois, Singleton.n vaut :
  • 1 (chaque appel crée une nouvelle instance)
  • 0 (le compteur reste à zéro car immutable)
  • 3 (instance unique, état partagé)
  • undefined
Voir la section du chapitre pour la justification détaillée.
4.sealed abstract class S ; case class A(x : Int) extends S ; case object B extends S. Le code val s : S = A(1) ; s match { case B => "B" } donne :
  • "B"
  • null
  • "A"
  • Warning à la compilation + MatchError à l'exécution
Voir la section du chapitre pour la justification détaillée.
5.trait T { val name : String ; def hello = s"Hi $name" } est :
  • Une classe abstraite
  • Une interface Java pure (que des signatures)
  • Un trait avec une valeur abstraite et une méthode implémentée
  • Erreur : un trait ne peut pas avoir de val
Voir la section du chapitre pour la justification détaillée.
6.Une classe étend combien d'autres classes en Scala ?
  • Au plus une (comme Java)
  • Au plus deux
  • Aucune limite
  • Une classe + un trait
Voir la section du chapitre pour la justification détaillée.
7.class A[T <: Comparable[T]] signifie :
  • T doit être l'inverse de Comparable[T]
  • T doit étendre Comparable[T] (on peut donc comparer)
  • T doit être supertype de Comparable[T]
  • Erreur de syntaxe
Voir la section du chapitre pour la justification détaillée.
8.case class P(x : Int) ; P(5) match { case P(n) => n * 2 } donne :
  • 5
  • Erreur : pattern matching pas autorisé sur case class
  • 10 (grâce à unapply implicite)
  • P(10)
Voir la section du chapitre pour la justification détaillée.

Score : 0 / 8 ·

❓ Q/R — décisions de design

Class normale ou case class : comment choisir ?
  • case class si l'objet représente une valeur de donnée (un point, une transaction, un message Akka). On veut comparer le contenu, le pattern-matcher, le sérialiser facilement.
  • class normale si l'objet porte un comportement / un état mutable / des invariants complexes. Ex : un Account avec deposit / withdraw.
Règle de pouce : si tu commences à écrire un constructeur de copie ou un equals/hashCode, tu veux probablement une case class.
Trait ou classe abstraite : quelle différence pratique ?
  • Classe abstraite : on n'en hérite que d'une seule. Peut avoir un constructeur avec paramètres.
  • Trait : on peut en hériter de plusieurs (with). Pas de constructeur avec paramètres (Scala 2). Peut être mixé sur une instance unique.
En pratique : préférer trait sauf si on a vraiment besoin de paramètres au constructeur ou d'une seule hiérarchie verticale.
Pourquoi le combo sealed + case class est si présent dans le code Scala ?
Parce qu'il modélise parfaitement un type somme (ADT — algebraic data type). Exemple :
sealed abstract class Tree
case class Leaf(value : Int) extends Tree
case class Node(left : Tree, right : Tree) extends Tree
  • sealed garantit qu'on liste toutes les variantes dans le même fichier
  • Pattern matching exhaustif vérifié à la compilation
  • Construction sans new (apply implicite)
  • Décomposition propre via case class
C'est le pattern central de la programmation Scala.
Quelle différence entre une case class et un case object ?
  • case class avec paramètres : sert à porter des données. Une instance par valeur (ex : Some(42)Some(43)).
  • case object : pour les variants sans donnée, instance unique. Ex : None — il n'y a qu'un None dans l'univers, pas besoin de paramètres.
Penser case object pour les feuilles d'un type somme qui n'ont besoin d'aucune valeur.
À l'examen, on me donne du code. Que regarder en premier ?
Méthode systématique pour comprendre du code Scala :
  1. Le type de chaque expression — Scala l'affiche dans le REPL, mais à la main il faut savoir le déduire.
  2. Le pattern matching — quelle branche match ? Y a-t-il un if guard ?
  3. Mutable vs immutablevar vs val, case class partage les champs ?
  4. Effet de bord ? — y a-t-il un println, une variable mutée, un Thread.sleep ?
  5. L'ordre d'évaluation — by value (immédiat) vs by name (=>) vs lazy (lazy val).
Si on hésite : traduire mentalement le code en pseudo-code mathématique. Une fonction pure peut littéralement se substituer par son corps.