Keyboard shortcuts

Touchez ou pour naviguer les chapitres

Touchez S ou / pour chercher dans le livre

Touchez ? pour afficher ce message

Touchez Esc pour masquer ce message

Génie informatique

Ce cours de génie informatique est un deuxième cours axé sur les outils, les pratiques et les connaissances de base en génie informatique. Il approfondie les connaissances et les compétences dévéloppées dans le cours ICS3U.

Ce cours utilise Java comme langage d’étude pour sa clarté conceptuelle.

📋 Lien vers le plan du cours

Unités

Revue

  • Révision des bases du Java et des compétences de génie informatique vues dans le cours ICS3U

Classes et objets

  • Une introduction aux classes et aux objets, les structures fondamentales de tout programme Java

Données et algorithmes

  • Approfondissement des connaissances des structures de données et des algorithmes utilisés avec chacun, algorithmes récursifs, et analyse d’algorithmes de recherche et de tri

Gestion de projet

  • Comment travailler en équipe incluant les rôles des membres d’une équipe et l’intégration des contributions de chacun avec les outils standard du développement logiciel

Programmation orientée objet

  • Principes de la programmation orientée objet et outils de conception de logiciels structurés avec des objets

Accueil >

Revue de ICS3U

En bref

Définitions
Algorithme
En informatique, un processus qui traite de l’information en suivant une collection bien ordonnée d’étapes réalisables et sans ambiguïté qui produit un résultat dans un temps fini. De façon générale, les algorithmes peuvent être exprimés sous forme de diagrammes de flux ou de pseudocode. Ils peuvent aussi être implémentés dans un langage de programmation donné.
Environnement de développement
Logiciel qui intègre un éditeur de texte, une console et des outils spécifiques pour la creation de projets, la compilation, le déboggage et l’exécution de programmes dans un langage de programmation donné.
Documentation interne
Les commentaires laissés dans le code pour les autres développeurs. Ceci inclut les commentaires d’en-tête et les javadoc qui sont descriptifs ainsi que les commentaires de ligne qui sont explicatifs. La documentation interne est plus facile quand les noms de variables, de fonctions et de classes sont descriptifs.
Projet Java minimal
Un fichier .java avec le même nom que la classe principale qui contient une méthode avec la signature public static void main(String[] args).
Types de données
Mots-clés ou classes reconnus par Java qui permettent au compilateur de réserver l’espace mémoire approprié et d’utiliser le protocole d’interprétation des bits appropriés pour les valeurs stockées (booléens, nombres entiers, nombres décimaux, caractères). Quelques types de base sont int, double, boolean, char et String. Il y a aussi des types représentant des collections comme des tableaux, p. ex. int[].
Structures de contrôle
Instructions qui permettent de contrôler l’exécution d’un programme. Les structures de contrôle les plus communes sont les structures de sélection (if, else, switch) et d’itération (for, while, do-while). Les structures de contrôles sont déclarées à l’aide de conditions qui s’évaluent à vrai ou faux, le résultat exacte déterminant le bloc de code à exécuter.
Entrée et sortie à la console avec Java
Instructions pour afficher des messages et obtenir les réponses tapées de l’utilisateur via la console. On utilise les méthodes print, println et printf de l’objet System.out pour afficher des messages et une instance de la classe Scanner qui observe l’objet System.in pour obtenir des réponses tapées de l’utilisateur.
Diagramme de dépendances
Diagramme représentant la chaîne d’appels de méthodes d’un programme. Chaque méthode est représentée par une boîte avec son nom, ses paramètres et son type de retour. Une flèche pointe de la méthode appelante à la méthode appelée. Ceci indique que la méthode appelante dépend de l’existance et du bon fonctionnement de la méthode appelée. Ce diagramme sert à guider l’ordre d’implémentation des méthodes (celles qui n’ont aucune dépendances d’abord, puis celles qui dépendent des premières, etc.). NOTE IMPORTANTE : ce diagramme ne représente aucunement la séquence d’exécution du programme, ce qui est le but d’un diagramme de flux.

Objectifs d’apprentissage

À la fin de cette leçon, vous devrez être en mesure de :

  • créer et lire des algorithmes au format de diagrammes de flux, de pseudocode et de code Java;
  • utiliser un environnement de développement pour écrire, compiler, exécuter et débogguer un programme Java;

Critères de succès

  • Je peux implémenter des algorithmes simples et communs en Java et les communiquer au moyen d’un diagramme de flux;
  • Je peux expliquer mes algorithmes en utilisant la documentation interne et des diagrammes de dépendances.

Ressources pour la révision

Bases de Java

  • Le site d’ICS3U > section programmation
    • Installation et configuration de VS code, Git et Java
    • Utiliser Java en ligne de commande (jshell, javac, java)
    • Les bases de Java : types de données, opérateurs, structures de contrôle, fonctions
  • Le tutoriel Java de W3Schools
    • Exemples interactifs et quiz sur les bases de Java

Pseudocode et de diagrammes de flux

Pseudocode
1.  Début
2.  Appeller GetNumber
3.  Afficher la valeur retournée
4.  Fin
5.  Définir GetNumber
6.    Assigner à num la valeur -1
7.    Afficher "Entrez un nombre entre 1 et 10 > "
8.    Saisir la réponse texte et la garder dans temp
9.    Si temp est un format valide de nombre, faire l'étape 10
10.     Convertir temp en nombre et l'assigner à num
11.   Sinon, faire l'étape 12
12.     Afficher "Ce n'est pas un nombre valide"
13.   Pendant que num < 1 ou num > 10, répéter les étapes 7 à 12
14.   Retourner num
Diagramme de flux

Diagramme de flux de GetNumber

Voir ce document pour les détails des éléments et des structures dans un diagramme de flux.

Conseils pour les outils d’assistance en programmation

Règle d’or : Si la suggestion ne correspond pas directement à votre intention, le l’acceptez pas.

Cette règle s’applique autant pour les suggestions de correction rapide (fourni par l’environnement de développement) que pour les suggestions des intelligences artificielles génératives (ChatGPT, Copilot, etc.).

Pourquoi?

Généralement, ces outils augmente votre productivité tant que vous comprenez l’impact qu’ils ont sur votre code.

L’inverse est aussi vrai, et souvent l’impact est plus grand! Sans comprendre l’impact de ces suggestions, vous insérez des erreurs ou du code que vous n’êtes pas en mesure de modifier qui peuvent vous causer des problèmes difficiles à trouver et corriger plus tard. Le temps perdu dépasse largement le temps gagné dans ces situations.

Démonstrations

En copiant le code de ces démonstrations, assurez-vous de respecter les noms des fichiers et la structure des fichiers avant de les exécuter. Ils s’exécutent tous avec la commande java <NomDuFichier>.java à partir de la racine du projet.

2024

Le contexte de cette démonstration est un questionnaire où les informations pour chaque question sont structurées dans des fichiers textes.

Durant la démonstration, le code a passé par plusieurs itérations des algorithmes et a finalement été remanié pour être plus modulaire et réutilisable. Le diagramme de dépendances ci-dessous montre la structure finale du code dans Questionnaire.java.

DDD de Questionnaire.java

Les fichiers sont organisés comme suit dans le projet revue :

revue
|-- better_questions.txt
|-- Questionnaire.java
`-- questions.txt

Note : il faut inclure une ligne vide à la fin des fichiers .txt

Voici les fichiers individuels :

Accueil >

Classes et objets

Le pont entre la programmation de base (séquentielle et procédurale) et la programmation orientée objet (POO) est la notion de classe et d’objet. Les classes sont des modèles pour les objets, et les objets sont des instances de classes. Les classes définissent les attributs et les méthodes que les objets peuvent avoir.

L’idée fondamentale des objets est de modéliser des entités du monde réel qui ont des caractéristiques (les attributs) et des comportements (les méthodes).

Légende : 🛠️ 📚
  • 🛠️Compétences en génie informatique

    Savoir-faire en lien avec le génie informatique. Les compétences sont évaluées au moyen de projets concrets.

    Développez ces compétences avec les exercices pratiques dans chaque leçon.

  • 📚 Concepts

    Nouveau concept. Le plus de concepts que vous maîtrisez, le plus que vous alimenter vos compétences pour produire des choses intéressantes. Les concepts sont évalués au moyen de quiz sur papier et d’entrevues.

    Validez votre compréhension avec les mini quiz dans chaque leçon.

Séquence

Accueil > Classes et objets >

📚 Les objets

Survol et attentes

Si vos projets Java jusqu’à présent tenaient dans une seule classe, ces projets n’utilisaient pas le plein potentiel, ni même l’objectif, du langage Java. Ce langage a été développé quand l’Internet est devenu accessible au public pour créer des projets orientés-objet plus sécures et plus facilement distribuable que ce qui existaient déjà.

Mais pour des projets orienté-objet, il faut définir des objets, ce que vous n’avez pas fait intentionnellement jusqu’à présent.

Définitions
Objet
Une abstraction - un modèle informatique - pour quelque chose qui existe dans le monde réel (ou dans notre imagination). Un objet est défini dans une classe qui contient des attributs et des méthodes. On crée des instances de l’objet en déclarant des variables du type de l’objet. Par exemple, Scanner input = new Scanner(System.in); crée une instance de Scanner nommée input.
Encapsulation
Le regroupement des données et des méthodes qui les manipulent dans une seule entité.1 Les classes Java sont un exemple d’encapsulation, notamment les classes qui définissent des objets.
Classe objet
Une classe définissant un modèle pour un objet en déclarant des attributs pour ses informations et des méthodes pour ses comportements. Ces classes encapsulent dans une seule entité les données avec les méthodes qui les manipulent afin de créer un modèle adéquatement complet de l’objet.2
Instance
Un objet en mémoire avec sa propre copie de chaque attribut de la classe objet et qui peut utiliser toutes les méthodes de la classe.
Classe pilote
Une classe qui contient la méthode main pour la logique globale du programme. C’est la classe qui est lancée pour exécuter le programme. Elle crée et utilise des instances d’autres classes objets.
Mot-clé new
Utilisé pour créer une instance d’un objet correspondant au modèle défini dans une classe. Par exemple, new Scanner(System.in) crée une instance de la classe Scanner qui lit l’entrée standard.
Mot-clé this
Utilisé dans les méthodes d’une classe objet pour faire référence à l’instance spécifique qui a fait l’appel de la méthode. C’est utile pour différencier les attributs de l’instance de variables locales dans la méthode comme les paramètres.
Attribut
Une variable définie à la racine d’une classe objet.
Méthode
Une fonction définie dans une classe objet. Ces fonctions opèrent directement sur les attributs de la classe.
Commande javac
Commande nécessaire pour compiler un projet Java comportant plus qu’un fichier .java. Si tous les fichiers sont dans le dossier racine directement, la commande javac *.java compile tous les fichiers .java dans le dossier, générant les fichiers .class correspondants. On lance ensuite le programme avec java NomDeLaClassePilote (sans l’extension .class ou .java).

Objectifs d’apprentissage

À la fin de cette leçon vous devrez être en mesure de :

  • Décrire la structure d’un projet Java orienté-objet, notamment le rôle de la classe pilote et des classes objets.
  • Définir les termes attribut, méthode et instance.
  • Expliquer le rôle des mot-clés new et this.

Critères de succès

  • Je peux écrire une classe pilote qui crée et utilise des instances d’une ou plusieurs classes objets que j’ai également écrites.
  • Je peux lancer un programme Java comportant plusieurs fichiers.

Structure de projet orienté-objet Java

Un projet orienté objet tient dans plusieurs classes - typiquement une classe par fichier, notamment :

  • Une classe qui contient la méthode main pour la logique globale du programme -> on l’appelle la classe pilote. Voici quelques exemples de noms pour la classe pilote :
    • Driver.java
    • Main.java
    • App.java
  • Une ou plusieurs classes qui définissent des objets. On crée des instances de ces objets dans le pilote ou dans d’autres objets.
  • Classes pour les tests unitaires afin de valider le comportement des méthodes dans les classes objets un par un.

Les outils Java standard créent des structures de projets avec des dossiers pour les sources, les tests et les ressources. Les fichiers sources (comme le pilote et les objets) sont dans le dossier src et les fichiers tests dans le dossier test.

Pour le moment, nous créerons des projets simples sans structure spécifique : tous les fichiers .java seront dans le même dossier. La structure des fichiers pour un projet simple pourrait ressembler à ceci :

.
├── App.java
├── Person.java
└── TestPerson.java

Pour compiler ce projet, on ouvre le terminal dans le dossier racine du projet et on tape :

javac *.java

Ce qui ajoute les fichiers .class au dossier s’il n’y a pas d’erreurs de compilation. En cas d’erreurs de compilation, les messages d’erreur seront affichés à la console. Il faut faire cette étape de compilation à chaque modification du projet, sinon les fichiers .class utilisés pour lancer le programme ne seront pas à jour.

Finalement, pour lancer le programme, on tape :

java App

On ignore le fichier TestPerson.java ici parce que ce fichier est souvent généré et utilisé par des outils de test comme JUnit accessibles dans votre EDI. Pour plus d’information sur les tests unitaires, voir les notes du cours ICS3U

Les objets

Un objet est une abstraction - un modèle informatique - pour quelque chose qui existe dans le monde réelle (ou dans notre imagination). Le modèle pour un objet est défini dans une classe qui contient des :

  • informations -> ce qu’on appelle les attributs de l’objet. Ce sont des variables définies à la racine de la classe.
  • comportements -> ce qu’on appelle les méthodes de l’objet.

Voici quelques exemples d’objets qu’on utilise déjà :

  • String : un modèle pour un texte
  • Scanner : un modèle pour un lecteur de texte
  • File : un modèle pour un élément du système de fichiers.

Ces objets sont définies dans une classe du même nom dans les fichiers String.java, Scanner.java et File.java à divers endroits dans la trousse de développement de Java (JDK).

Définir un objet

Pour définir un objet, il suffit de créer une nouvelle classe dans un fichier .java du même nom, de déclarer ses attributs et de définir des méthodes.

Par exemple, dans Person.java :

class Person {
    // attributs
    String name;
    int age;

    // méthode
    void speak() {
        System.out.println(name + "dit 'Allô!'");
    }
}

Notez qu’il n’y a pas de méthode main dans cette classe, seulement les informations qui représentent notre modèle simple d’une personne (name et age) et d’un comportement associé (speak).

Utiliser un objet, partie 1 - créer des instances

Pour utiliser un objet, on déclare une variable du type de l’objet dans une méthode ou une classe. Chaque objet ainsi nommé s’appelle une instance de l’objet.

Exemple 1 : input est une instance de Scanner

import java.util.Scanner;

class App {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
    }
}

Exemple 2 : jenny et andrew sont des instances de Person

class Main {
    public static void main(String[] args) {
        Person jenny = new Person();
        Person andrew = new Person();
    }
}
  • Chaque instance a son propre bloc en mémoire pour sa propre copie des attributs. Par exemple, l’instance jenny a son propre bloc en mémoire pour name et age. De même, l’instance andrew a son propre bloc en mémoire pour name et age.
  • Chaque instance peut utiliser toutes les méthodes de la classe.

Utiliser un objet, partie 2 - utiliser les attributs et les méthodes

Pour utiliser les attributs et les méthodes d’une instance, on utilise l’opérateur . après le nom de l’instance suivi par le nom de l’attribut ou de la méthode.

class Main {
    public static void main(String[] args) {
        Person jenny = new Person();
        // Modifier les attributs
        jenny.name = "Jenny";
        jenny.age = 16;
        // Utiliser les méthodes
        jenny.speak();
        // Afficher les attributs
        System.out.println(jenny.name + " a " + jenny.age + " ans.");
    }
}

Tentez de noter chaque fois que vous avez vu l’opérateur . dans les programmes que vous avez produits en ICS3U et en ICS4U jusqu’à présent (comme System.out.println()). Chaque fois, c’était pour accéder à un attribut ou une méthode d’un objet.

Par exemple, System.out.println() : out est un attribut de type PrintStream dans la classe System, et println() est une méthode de la classe PrintStream accessible par l’instance out.

Mot-clé this

Reprenons l’exemple de l’objet Person définit plus haut et ajoutons les méthodes changeName() et happyBirthday().

Person.java

class Person {
    String name;
    int age;

    void speak() {
        System.out.println(name + "dit 'Allô!'");
    }

    void changeName(String name) {
        this.name = name;
    }

    void happyBirthday(){
        age += 1;
    }
}

Main.java

class Main {
    public static void main(String[] args) {
        Person jenny = new Person();
        jenny.name = "Jenny";
        jenny.age = 16;

        Person andrew = new Person();
        andrew.name = "Andrew";
        andrew.age = 17;

        andrew.happyBirthday();
        // andrew.age est maintenant 18

        jenny.changeName("Jen");
        // jenny.name est maintenant "Jen"
    }
}

Le mot-clé this dans les méthodes de la classe objet est utilisé pour faire référence à l’instance spécifique qui fait appel à la méthode.

Dans l’exemple ci-dessus, on le voit à un seul endroit, dans la méthode changeName(). On utilise this.name pour différencier la variable de l’instance name de la variable locale name passée en paramètre. Si vous copiez cette exemple dans votre IDE, en cliquant sur chaque name dans la méthode changeName(), vous verrez que celui précédé par this. surligne également la déclaration de l’attribut name de la classe tandis que l’autre surligne la déclaration du paramètre name. Si vous enlevez le this., le compilateur ne saura pas quelle variable name vous voulez modifier et vous donnera une erreur (surlignée en rouge).

Notez que dans les méthodes speak() et happyBirthday(), on n’a pas besoin de this. parce qu’il n’y a pas de confusion possible. En l’absence de variables locales, les références à name et age peuvent seulement être celles de l’instance. Dans de tels cas, vous pouvez tout de même utiliser this. pour plus de clarté, mais ce n’est pas nécessaire.

Exercices

📚 Tester la compréhension

Quiz de vérification sur les objets

🛠️ Pratique

Produire une classe pilote et une classe qui définit un objet. Pour l’objet, modélisez quelque chose de votre vie qui existe en plusieurs instances. p. ex.: films, jeux, vélos, véhicules, artistes/athlètes/politiciens préférés, etc. Créer une ou deux instances de votre objet dans la méthode main() du pilote, assigner des valeurs à chaque attribut et tester les différentes méthodes. Afficher les valeurs au besoin avec des System.out.println().


  1. Plusieurs définitions de l’encapsulation incluent aussi l’idée de cacher les détails d’implémentation pour protéger les données, généralement en rendant les attributs privés et en fournissant une interface publique de méthodes pour les manipuler. Voir la leçon sur le masquage de l’information pour plus de détails.

  2. Dans les langages multi-paradigmes (comme Python) ou fonctionnelles (comme JavaScript, Rust), il y a d’autres façons d’encapsuler des données et des comportements à part les classes, notamment les fermetures / clôtures (closures en anglais). Par contre, la programmation orientée-objet utilise presque toujours les classes pour l’encapsulation.

Accueil > Classes et objets >

🛠️ Diagrammes de classe UML : structure interne d’une classe

Survol et attentes

version imprimable

En commençant à concevoir nos programmes en termes d’objets, c’est souvent plus facile et naturel de nommer les objets, leurs informations et leurs comportements principaux bien avant de commencer à écrire le code pour implémenter l’idée. Parfois on a aucune idée quel code écrire, mais on sait ce qu’on veut que le programme fasse.

Les diagrammes de classe UML sont utiles à ce stade de planification.

En bref

Un diagramme de classe UML décrit avant tout la structure interne d’une classe :

  • la visibilité, le type et le nom de chaque attribut
  • la visibilité, le type de retour, le nom et les types de paramètres des méthodes

Exemple

Prenons une classe qui applique le concept d’encapsulation, la classe Square.

On peut faire le diagramme de classe au format texte comme ceci :

class Square
------------
-double side
-double area
-double perimeter
-----------------
+void setSide(double)
+double getSide()
+double getArea()
+double getPerimeter()
-void calculateArea()
-void calculatePerimeter()

…ou on peut utiliser un langage spécialisé pour les diagrammes UML, comme PlantUML :

@startuml Square
class Square {
    -double side
    -double area
    -double perimeter
    +void setSide(double)
    +double getSide()
    +double getArea()
    +double getPerimeter()
    -void calculateArea()
    -void calculatePerimeter()
}
@enduml

Les avantages de la 2e option sont que :

  • le code peut générer une image équivalente
  • on peut spécifier des relations entre plusieurs classes (ce qui est le sujet d’une future leçon)

Voici d’ailleurs l’image générée avec le code précédent :

Square

Important

Objectifs d’apprentissage

À la fin de cette leçon vous devrez être en mesure de :

  • Produire un diagramme de classe UML au format texte ou au format PlantUML incluant la visibilité des membres de la classe.
  • Utiliser la vue “Outline” de votre environnement de développement intégré (p. ex. VS Code, Eclipse) pour comparer le contenu actuel de la classe avec son diagramme UML.

Critères de succès

  • Je sais quel symbole utiliser pour indiquer un membre privé ou public.
  • J’inclus toutes les informations requises (visibilité, nom, types) pour chaque membre de la classe.

Accueil > Classes et objets >

📚 Méthodes typiques d’une classe Java

Survol et attentes

Toutes les classes Java héritent automatiquement tout ce qui est défini dans la classe Object, même les classes que vous écrivez. Cette classe contient plusieurs méthodes utiles que les développeurs tendent à réécrire, ce qu’on appelle supplanter une méthode, pour les adapter aux nouveaux objets.

Définitions
Surcharger une méthode
Définir plusieurs fois une méthode avec le même nom mais avec des paramètres différents. Java choisit la version de la méthode à appeler en fonction des paramètres passés à la méthode.
Supplanter une méthode
Redéfinir une méthode déjà définie dans une classe parente, souvent dans la classe Object (parent de toutes les classes Java). On utilise le décorateur @Override pour signaler explicitement qu’on tente de supplanter une méthode déjà définie. Des synonymes pour “supplanter” sont “remplacer” ou “surclasser”.
Constructeur
Méthode spéciale qui initialise les objets d’une manière spécifique. Le nom d’un constructeur est le même que le nom de la classe. C’est commun de surcharger le constructeur : un constructeur par défaut (sans paramètres) qui appelle le constructeur plus général en lui passant des valeurs par défaut comme arguments. On utilise l’appel this([paramètres]) pour éviter de dupliquer le code écrit dans le constructeur plus général.
Méthode String toString()
Méthode qui produit une représentation textuelle de l’objet. Cette méthode est appellé automatiquement quand on tente d’afficher un objet, mais la définition par défaut (dans Object) affiche le nom de la classe suivi par un code hexadécimal. Il est souvent préférable de supplanter cette méthode pour afficher des informations plus utiles.
Méthodes boolean equals(Object) et int hashCode()
La méthode equals compare le contenu de deux objets pour déterminer s’ils sont égaux. La méthode hashCode retourne un entier qui identifie l’objet. La classe Object spécifie un contrat entre ces deux méthodes : si deux objets retournent vrai avec equals, ils doivent aussi retourner le même entier avec hashCode. Il est donc important de supplanter ces deux méthodes ensemble. Les EDI modernes peuvent générer ces méthodes automatiquement pour vous.

Objectifs d’apprentissage

À la fin de cette leçon vous devrez être en mesure de :

  • Décrire le rôle d’un constructeur et de la méthode toString.
  • Expliquer pourquoi il faut définir les méthodes hashCode et equals ensemble.
  • Définir et distinguer surcharger une méthode et supplanter une méthode.

Critères de succès

  • Je suis capable de surcharger des constructeurs sans dupliquer le code, notamment en utilisant l’appel this().
  • Je peux produire un toString qui affiche des informations utiles sur mes objets.
  • Je suis capable de générer les méthodes hashCode et equals dans mes propres objets avec l’aide de l’EDI.

Constructeurs

On définit une méthode spéciale qui s’appelle un constructeur afin d’initialiser les objets d’une manière spécifique. La signature du constructeur est simplement le nom de la classe suivi par des parenthèses, avec ou sans paramètres.

class ShinyObject {
    boolean isShiny;

    /** Constructeur */
    ShinyObject() {
        isShiny = true; // initialise avec une valeur littérale
    }
}

Les constructeurs sont généralement surchargés, c’est-à-dire définis plusieurs fois avec différents paramètres. On définit souvent une version sans paramètres et une version avec un paramètre par attribut afin d’initialiser chaque attribut dès la création d’une instance de l’objet.

Pour éviter de dupliquer le code dans les constructeurs, c’est une bonne pratique d’appeler le constructeur le plus complet dans les autres constructeurs. On le fait avec la méthode this([paramètres,...]).

class ShinyObject {
    boolean isShiny;

    /** Constructer avec paramètre */
    ShinyObject(boolean isShiny) {
        this.isShiny = isShiny; // initialise avec le paramètre
    }

    /** Constructeur sans paramètres */
    ShinyObject() {
        this(true); // passe la valeur par défaut comme argument
    }
}

Le constructueur par défaut (définit dans la classe Object) est un constructeur sans paramètres qui assigne la valeur par défaut aux attributs :

  • 0 pour les nombres,
  • '' pour les char,
  • false pour les boolean et
  • null pour les objets, comme les String.

Méthode String toString()

La méthode toString produit une représentation textuelle de l’objet, normalement en affichant de manière conviviale les valeurs des attributs. La définition par défaut affiche le nom de la classe suivi par un code hexadécimal.

ShinyObject@2c7b84de

C’est définitivement mieux de supplanter (remplacer, surclasser) cette définition!

Reprenant la classe ShinyObject ci-dessus, on pourrait lui ajouter la méthode toString suivante :

@Override
public String toString(){
    String validationTxt = isShiny ? "est" : "n'est pas"; // opérateur ternaire = if-else compact
    return String.format("Cet objet %S reluisant", validationTxt);
}

Notez que la méthode retourne un String mais ne l’affiche pas (pas de System.out.println() ni de IO.println()).

Notez aussi qu’on précède la signature de la méthode avec le décorateur @Override afin de signaler plus explicitement qu’on tente de supplanter une méthode déjà définie dans la classe Object. Le décorateur est optionnel mais force le compilateur à vérifier que vous avez bien redéfinie une méthode existante, p. ex.: vous n’avez pas tapez tostring par accident ou mégarde au lieu de toString.

Pour les valeurs décimales, c’est souvent une bonne idée d’utiliser un texte formaté pour limiter le nombre de places après la virgule à une précision spécifique. Prenons l’exemple suivant :

class Student {
    String name;
    double average;

    @Override
    public String toString() {
        return String.format("%s a une moyenne de %.2f", name, average);
    }
}

On remarque plusieurs choses ici :

  • On utilise la méthode String String.format(String, [valeurs]) pour produire le texte et non la concaténation de texte et de valeurs.
  • Le String intègre des spécifications de format directement dans le texte - le %s et le %0.2f. Ces spécifications font trois choses :
    • la lettre à la fin correspond à un type de données : s ou S pour les String, d pour les int, f pour les double ou float, etc.
    • les caractères entre le % et la lettre donne un format optionnel. Dans le cas du %.2f, le .2 signale d’afficher seulement deux places après la virgule.
    • la présence de chaque spécification détermine combien et quels types de valeurs doivent suivre le String dans la méthode : il faut une valeur du bon type pour chaque spécification de format. Ces valeurs doivent suivre dans le même ordre que l’ordre d’apparence des spécifications dans le String.

Pour plus de détails sur les spécifications et options de format possibles, voir la documentation pour la classe Formatter.

Méthodes boolean equals(Object) et int hashCode()

La comparaison == donne vrai si les deux objets occupent le même bloc en mémoire. Ça marche pour les valeurs primitives (int, double, char, etc.) parce que chaque nouvelle valeur primitive possible prend une nouvelle place en mémoire.

Pour les objets, par contre, chaque instance occupe une nouvelle place en mémoire, peu importe si les valeurs des attributs sont identiques. C’est pourquoi on utilise la méthode equals pour comparer le texte de deux Strings. En fait, il faut utiliser cette méthode pour comparer le contenu de tous les types d’objets, pas juste les String.

La classe Object spécifie un contrat entre la méthode equals et une autre méthode, hashCode : il faut que deux instances qui retournent vrai avec le méthode equals retournent aussi le même int avec la méthode hashCode. Ça devient donc important de définir les deux méthodes ensemble.

Voici un exemple (attachez vos tuques!) :

class Student {
    String name;
    double average;

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        long temp;
        temp = Double.doubleToLongBits(average);
        result = prime * result + (int) (temp ^ (temp >>> 32));
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        // clauses de garde
        if (this == obj) // l'autre pointe au même bloc de mémoire
            return true;
        if (obj == null) // l'autre pointe à rien
            return false;
        if (getClass() != obj.getClass()) // l'autre n'est pas du même type
            return false;

        // créer une instance du bon type avec l'autre Object
        Square other = (Square) obj;
        // comparaison des attributs des objets
        if (Double.doubleToLongBits(average) != Double.doubleToLongBits(other.average))
            return false;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        return true;
    }
}

Première chose à dire : ces deux méthodes ont été générées automatiquement par l’EDI VS Code. D’autres EDI, comme Eclipse et IntelliJ, font ça aussi. C’est la façon la plus simple de les produire, surtout que c’est seulement la section “Comparaison des attributs des objets” dans la méthode equals qui pourrait (peut-être) nécessiter un ajustement si vous aviez envi de faire une comparaison particulière. Le reste dans equals et dans hashCode doit être là. Et, dans ce cours, ce n’est pas nécessaire de comprendre tous les détails.

Alors voici comment générer ces méthodes automatiquement avec VS Code (avec le “Extension Pack for Java” développé par Microsoft, vscjava.vscode-java-pack) :

  1. Cliquez sur le nom de la classe dans sa signature.
  2. Ici vous avez deux choix :
    1. Faites un clic droit sur le nom et cliquez ensuite sur l’option “Source Action…”
    2. Faites la combinaison Ctrl + . et consultez la section “More Actions”
  3. Cliquez sur “Generate hashCode() and equals()…”
  4. Sélectionnez les attributs à inclure dans leurs algorithmes et cliquez “OK”.
  5. Validez le code en faisant quelque tests avec différentes instances via la classe pilote ou une classe test.
  6. Optionnellement, ajustez le code dans equals() si vous avez une autre comparaison que vous préférez implémenter. Refaire les tests après toute modification.

Exercices

📚 Tester la compréhension

Quiz de vérification sur les méthodes typiques d’Objects Java

🛠️ Pratique

Écrivez deux constructeurs pour votre objet, un sans paramètres qui utilise l’appel this([paramètres]) pour appeler le deuxième qui est défini avec les paramètres nécessaires pour initialiser les attributs de l’objet.

Supplanter les méthodes hashCode, equals et toString pour votre objet.

Testez ces nouvelles méthodes dans la classe pilote.

Accueil > Classes et objets >

📚 Masquer les détails internes

Survol et attentes

Un des principes de la programmation orientée objet et de protéger l’accès aux attributs. On ne lis pas directement leurs valeurs et on ne les modifie pas directement non plus. Plutôt on écrit des méthodes qui retournent la valeur des attributs et des méthodes qui modifient la valeur des attributs. Ainsi, les attributs sont cachés via une interface (les méthodes); cette couche forme la capsule protectrice autour des données.

Définitions
Masquage de l’information
Selon certaines définitions de l’encapsulation, l’élément clé n’est pas nécessairement l’emballage des données avec les méthodes associées mais le fait de cacher les détails d’implémentation aux éléments externes à la classe. Cela permet de changer l’implémentation (le code dans la classe) sans changer l’interface (les appels de méthodes de l’extérieure de la classe). Un effet important de l’encapsulation est que les attributs ne sont pas accessibles directement de l’extérieur de la classe.
Mot-clés de visibilité
public et private sont les deux mots-clés de visibilité les plus importants en Java. public rend les membres de la classe visibles à l’extérieur de la classe. private les rend invisibles à l’extérieur de la classe. Par défaut (sans mot-clé explicite), la visibilité est public aux autres classes dans le même dossier (package) et private à l’extérieur du dossier.
Accesseurs et mutateurs
Méthodes public qui donnent accès aux attributs private d’une classe. Les accesseurs (get[Attribut]) retournent la valeur de l’attribut. Les mutateurs (set[Attribut]) modifient la valeur de l’attribut selon la valeur du paramètre. Autre que simplement assigner ou retourner la valeur, ces méthodes peuvent aussi appliquer différents traitements (validation de la valeur, mise à jour d’autres attributs, etc.). Cela fait partie des “détails d’implémentation” qui sont cachés à l’extérieur de la classe mais qui permettent à la classe de fonctionner comme prévu.

Objectifs d’apprentissage

À la fin de cette leçon vous devrez être en mesure de :

  • Expliquer le concept d’encapsulation.
  • Définir et distinguer les mot-clé private et public.
  • Définir et distinguer les termes accesseur et mutateur.

Critères de succès

  • Je suis capable d’encapsuler mes propres objets avec l’aide de l’EDI.
  • Je suis capable de produire des mutateurs sur mesure pour respecter le comportement attendu de mon objet.

Problèmes potentiels sans encapsulation (masquage) des données

Voici un exemple pour illustrer l’utilité du masquage de l’information. Il présente un cas où l’absence de ce type d’encapsulation peut mener à un objet qui ne respecte plus les règles de sa définition (un carré qui n’est plus un carré).

/**
 * Classe pour représenter un carré
 * Version sans encapsulation
 */
class Square {
    double side;
    double area;
    double perimeter;

    Square(double side) {
        update(side);
    }

    Square() {
        this(1); // valeur par défaut
    }

    /**
     * Méthode pour valider et mettre à jour les attributs
     * @param side la longueur de côté
     */
    void update(double side) {
        if (side < 0) {
            System.out.println("Longueur invalide");
            return;
        }
        this.side = side;
        // area et perimeter dépendent de side
        this.area = side*side;
        this.perimeter = 4*side;
    }

    @Override
    public String toString() {
        return String.format("Côté = %.2f, A = %.2f, P = %.2f",
                side, area, perimeter);
    }
}

Cette classe a trois attributs mais deux, area et perimeter, dépendent de la valeur du dernier, side.

Regardons maintenant la classe pilote qui inclut le code valide suivant :

class Main {
    public static void main(String[] args) {
        Square sq = new Square(2);
        System.out.println(sq);
        sq.area = -27; // instruction valide qui brise le concept de carré
        System.out.println(sq);
    }
}

qui produit la sortie suivante :

Côté = 2.00, A = 4.00, P = 8.00
Côté = 2.00, A = -27.00, P = 8.00

Cette sortie montre un carré qui ne respecte plus la définition d’un carré. Notre objet n’est plus un modèle valide pour ce qu’il essaye de représenter. Le problème est la 3e ligne dans main où on modifie directement l’attribut area.

Encapsulation pour protéger les données

L’encapsulation prévient ce genre d’incohérence en nous permettant de spécifier la visibilité (voire l’accès) des attributs et des méthodes. Pour le moment, seulement deux mots-clés sont importants 1 :

  • public qui rend les membres de la classe visibles dans toutes les autres classes
  • private qui rend les membres de la classe visibles seulement à l’intérieur de la classe (pour les autres membres)

L’absence d’un mot-clé de visibilité donne au membre la visibilité par défaut qui est public.2 Donc, dans l’exemple de la classe Square ci-dessus, la classe et tout ses membres sont public.

Attributs privés -> méthodes accesseurs et méthodes mutateurs

Une classe encapsulée déclare ses données (les attributs) private et inclut des méthodes public pour lire les données et pour modifier les données.

Ces méthodes s’appellent :

  • accesseurs (“getters” en anglais) pour retourner la valeur d’un attribut privé;

    Les accesseurs sont du même type que la donnée demandée et ne prennent aucun paramètre.

  • mutateurs (“setters” en anglais) pour modifier la valeur d’un attribut privé.

    Les mutateurs sont de type void et prennent un paramètre du même type que la donnée à modifier.

Modèle de carré préservé dans la classe Square avec l’encapsulation

Voici une version encapsulée de la classe Square :

/**
 * Classe pour représenter un carré
 * Version appliquant encapsulation
 */
class Square {
    // attributs masqués à l'extérieur de la classe
    private double side;
    private double area;
    private double perimeter;

    Square(double side) {
        update(side);
    }

    Square() {
        this(1); // valeur par défaut
    }

    // update() est le seul mutateur pour la classe puisqu'il
    // modifie tous les attributs de manière contrôlée.

    /**
     * Méthode pour valider et mettre à jour les valeurs.
     * @param side la longueur de côté
    */
    public void update(double side) {
        if (side < 0) {
            System.out.println("Longueur invalide");
            return;
        }
        this.side = side;
        // area et perimeter dépendent de side
        this.area = side*side;
        this.perimeter = 4*side;
    }

    // trois accesseurs pour lire les valeurs à l'extérieur de la classe

    public double getSide() {
        return this.side;
    }

    public double getArea() {
        return this.area;
    }

    public double getPerimeter() {
        return this.perimeter;
    }

    @Override
    public String toString() {
        return String.format("Côté = %.2f, A = %.2f, P = %.2f",
                side, area, perimeter);
    }
}

Maintenant la ligne

sq.area = -27;

dans le pilote produit une erreur parce que l’accès à area est rendu private. On ne peut plus accidentellement (ou intentionellement) briser notre objet dans son objectif de représenter un carré.

Accesseurs et mutateurs idiomatiques / formulaïques

Dans l’exemple de la classe Square, le mutateur était taillé sur mesure pour l’objet, ce qui est souvent requis. Mais il y a aussi plein de cas où des mutateurs plus génériques sont suffisants. La classe Point ci-dessous est un bon exemple.

class Point {
    private int x;
    private int y;

    public void setX(int x){
        this.x = x;
    }

    public void setY(int y){
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }
}

Dans cette classe partiellement implémentée (il manque : constructeurs, toString, etc.) mais qui est entièrement encapsulée, on voit que les accesseurs (get[Attribut]) et les mutateurs (set[Attribut]) sont presque identiques. Ils suivent l’idiome pour ces types de méthodes :

Idiome pour les accesseurs

Une méthode nommé “get” + le nom de l’attribut qui est sans paramètre et qui retourne la valeur de l’attribut.

public [type] get[Attribut](){
    return [attribut];
}

comme

public int getX() {
    return x;
}

Idiome pour les mutateurs

Une méthode nommé “set” + le nom de l’attribut qui prend un paramètre du même type que l’attribut et qui assigne la valeur du paramètre à l’attribut.

public void set[Attribut]([type] [attribut]){
    this.[attribut] = [attribut];
}

comme

public void setX(int x){
    this.x = x;
}

Générer les accesseurs et les mutateurs avec les outils de l’EDI

En fait, le code pour les mutateurs et les accesseurs dans l’exemple de la classe Point a été copié-collé-modifié manuellement, ce qui est une tâche répétitive qui ne nécessite pas beaucoup d’engagement intellectuel de la part du développeur et qui peut mener à des erreurs d’inattention, surtout quand il y a plusieurs attributs dans la classe.

La bonne nouvelle et que si les mutateurs et les accesseurs sont du genre répétitifs comme dans la classe Point, les outils de votre EDI (VS Code, Eclipse, etc.) peuvent les générer automatiquement.

C’est quand même important de s’assurer que ces méthodes reflètent réellement le comportement attendu et de modifier ou de remplacer les méthodes au besoin. Par exemple, dans la classe Square il fallait définir un mutateur spécialisé pour respecter le concept d’un carré.

Dans VS Code :

  1. Déclarez chacun des attributs avec le mot-clé private.
  2. Cliquez sur le nom de la classe dans sa signature.
  3. Ici vous avez deux choix :
    1. Faites un clic droit sur le nom et cliquez ensuite sur l’option “Source Action…”
    2. Faites la combinaison Ctrl + . et consultez la section “More Actions”
  4. Selon votre jugement, vous avez les options suivantes :
    1. “Generate Getters and Setters…” qui produit les deux méthodes pour les attributs que vous sélectionnez par la suite
    2. “Generate Getters…” qui produit seulement les accesseurs pour les attributs que vous sélectionnez par la suite
    3. “Generate Setters…” qui produit seulement les mutateurs pour les attributs que vous sélectionnez par la suite
  5. Une version standard de ces méthodes sera générée, mais il faut :
    1. Les déplacer afin de garder les déclarations d’attributs ensemble au début de la classe et les définitions de méthodes plus bas
    2. Modifier le code interne des méthodes pour produire le comportement voulue, au besoin.
    3. Probablement reformatter le code (clic droit -> “Format Document”) afin de corriger les indentations brisées suite aux déplacements de code.

Indiquer la visibilité des membres dans un diagramme de classe UML

Dans un diagramme de classe UML, on indique la visibilité des membres avec des symboles spécifiques :

  • + pour public
  • - pour private

Voici les diagrammes de classe UML pour l’exemple Point ci-dessus.

@startuml Point
!theme toy

class Point {
    - x : int
    - y : int
    + setX(int) : void
    + setY(int) : void
    + getX() : int
    + getY() : int
}

@enduml

Point

Exercices

Changez la visibilité des tous les attributs de votre objet et générez des accesseurs et mutateurs appropriés.

Testez ces nouvelles méthodes dans la classe pilote. Vérifiez que l’accès direct à chacun des attributs via la classe pilote produit une erreur.


  1. Il y a aussi le mot-clé protected qui s’applique lorsqu’on a des classes qui héritent explicitement le contenu d’une classe parent. On en discutera lors de la leçon sur l’héritage dans une prochaine unité sur l’orienté objet.

  2. Voir la référence suivante pour les détails exacts.

Accueil > Classes et objets >

📚 Membres statiques

Survol et attentes

Plusieurs mot-clés Java dans la signature classique public static void main(String[] args) ont maintenant été démystifiés, D’abord on a les types void et String[]. Plus récemment on a vue public et private qui déterminent la visibilité d’une classe ou de ses membres à l’extérieur de la classe. Il ne reste que static à expliquer.

Définitions
Membre dynamique
Un membre d’une classe (attribut ou méthode) qui est associé à une instance de la classe, soit aux objets créés et détruits durant l’exécution du programme.
Membre statique
Un membre d’une classe (attribut ou méthode) qui est associé à la classe elle-même et non aux objets créés à partir de la classe. Les membres statiques sont disponibles même si aucun objet n’est créé. Ces membres sont déclarés avec le mot-clé static.
Classe utilitaire
Une classe qui contient exclusivement des membres statiques pour effectuer des tâches spécifiques. Les membres statiques sont accessibles directement en utilisant le nom de la classe, comme avec les classes Math et Integer de Java.
Éléments statiques et dynamiques
En général, les parties d’un programme qui sont connues explicitement au moment de la compilation sont dites “statiques”. Les autres éléments sont “dynamiques”, connus seulement au moment de l’exécution.
  • Les déclarations de classes sont statiques.
  • Les objets et les variables locales sont dynamiques.
  • Dans certains langages où les définitions de fonctions et des variables peuvent exister en dehors des classes (la plupart des langages à part Java), ces fonctions et variables globales sont aussi statiques.
  • Les langages de programmation qui exigent la déclaration de types pour les variables et fonctions sont dits “statiquement typés”; les autres sont dits “dynamiquement typés” car le type doit être déterminé au moment de l’exécution.

Objectifs d’apprentissage

À la fin de cette leçon vous devrez être en mesure de :

  • Décrire la différence entre les membres statiques et dynamiques d’une classe.
  • Distinguer l’utilisation d’un membre statique et l’utilisation d’un membre dynamique dans divers extraits de code, comme la déclaration de membres d’une classe ou l’utilisation de ces membres dans un algorithme.

Critères de succès

  • Je suis capable d’utiliser des membres statiques dans mes objets ou les membres statiques de plusieurs classes utilitaires de Java.

Dynamique versus statique

Dans la programmation, le terme “statique” fait référence au stade de compilation/interprétation du projet. Si les types sont connus au moment de la compilation, un langage de programmation est dit “statiquement typé”. Java et C++ sont des langages statiquement typés parce qu’on doit déclarer les types des variables et fonctions dans le code. De même, si des variables ou des méthodes opèrent directement sur la classe (et non sur une instance de la classe), elles sont dites “statiques” parce que la définition de la classe est connue au moment de la compilation, contrairement aux instances de la classe (les objets) qui sont crées durant l’exécution du programme.

De l’autre côté, tout ce qui est créé ou déterminé au moment de l’exécution est dite “dynamique”. Par exemple, les variables locales sont dynamiques parce qu’elles sont créées et détruites à chaque fois que la méthode qui les contient est appelée. Les objets sont aussi dynamiques parce qu’ils sont créés et détruits durant l’exécution du programme. Dans certains langages, la détermination du type est aussi dynamique parce que le type n’est pas déclaré dans la syntaxe du langage mais seulement déterminé au moment de l’exécution en fonction de l’information contenue dans la variable. Python et Javascript sont des langages dynamiquement typés.

Le mot-clé static

En Java, le mot-clé static s’applique à un membre d’une classe pour l’associer à la classe elle-même et non aux instances de la classe. Cela signifie que les variables et les méthodes statiques sont disponibles même si aucun objet n’est créé à partir de la classe.

On connaît déjà quelques exemples notables dans la bibliothèque standard de Java :

  • La classe Math contient des méthodes et constantes (variables final) statiques pour effectuer des opérations mathématiques. On les utilise en appelant Math.PI ou par exemple, Math.sqrt(16).

  • Les classes qui emballent les types primitifs (comme Integer, Double, etc.) contiennent des méthodes statiques pour convertir des chaînes de caractères en nombres comme Integer.parseInt("42") ou pour obtenir des informations fixes en lien avec le type, comme Double.MAX_VALUE.

  • La méthode main qui est le point d’entrée de tout programme Java. On ne déclare aucun objet pour utiliser notre programme, on passe seulement le nom de notre classe, p. ex. avec la commande java App. La machine virtuelle Java peut alors appeler directement la méthode statique avec le nom de la classe, p. ex. App.main(args).

Classes utilitaires

Les exemples comme Math et Integer sont des classes contenant exclusivement des membres publiques statiques. Ce type de classe est souvent appelé une “classe utilitaire” ou “classe de service” parce qu’elle fournit des méthodes et des constantes pour effectuer des tâches spécifiques.

Dépendances statiques

Tout ce qu’on ajoutait comme membre dans nos classes en ICS3U - Scanner et autres variables globales, méthodes pour la décomposition - devaient être déclarés static pour satisfaire au compilateur. C’est parce que :

  1. la méthode main est statique et
  2. on utilisait tout directement dans main sans déclarer d’objet pour notre propre classe.

La méthode main étant static signifie qu’elle ne peut pas accéder à des membres non statiques de la classe car ils ne sont pas créés en même temps que la méthode main lors de la compilation. Donc tout ce qu’on voulait utiliser dans main devait aussi être static pour être accessible.

Classe ou tout est statique à cause de main

Cas typique de programme Java avant d’apprendre les objets. Tout est statique parce qu’on l’utilise directement dans main sans créer d’objet pour la classe.

public class Main {

    static int x = 42;

    static void doStuff() {
        System.out.println("On bosse");
    }

    public static void main(String[] args) {
        // utilise directement les attributs et méthodes statiques
        System.out.println(x);
        doStuff();
    }

}

Classe avec main et un objet pour la classe

Cas hybride où les fonctions “définition d’un objet” et “pilote du programme” sont accomplies par la même classe : on a une méthode main mais aussi des attributs et méthodes d’objet. Ce type de classe n’est pas souvent utilisé non plus. On utilise plutôt une classe pour déclarer des objets et une autre pour le pilote.

public class Main {

    int x = 42;

    void doStuff() {
        System.out.println("On bosse");
    }

    public static void main(String[] args) {
        // Crée un objet de la classe Main
        Main m = new Main();
        // utilise les attributs et méthodes de l'objet
        System.out.println(m.x);
        m.doStuff();
    }

}

Mélanger des membres statiques et dynamiques

Il est possible qu’une classe objet déclare quelques attributs ou méthodes statiques en plus de déclarer des attributs et méthodes pour les instances de la classe.

L’exemple le plus courant est un attribut qui compte le nombre d’objets créés à partir de la classe, ou qui assure l’assignation d’un numéro d’identité unique à chaque objet, où les deux.

  1. Cet attribut est déclaré static pour exister même s’il n’y a aucun objet.
  2. On l’initialise à 0 dans la déclaration.
  3. On l’incrémente dans le constructeur de la classe qui est appelé chaque fois qu’un objet est créé.
  4. Si on le cache (visibilité private), on ajoute un accesseur statique pour le lire.

Classe objet

public class Thing {
    // variable de classe
    private static int count = 0;

    // variables d'instance
    private String name;
    private int id;

    public Thing(String name) {
        this.name = name;
        this.id = count; // assigne un nombre unique à chaque objet
        count++; // incrémente la variable de classe
    }

    // accesseur statique
    public static int count() {
        return count;
    }

    // ... accesseurs, mutateurs, etc. pour les variables d'instance

    @Override
    public String toString() {
        return String.format("Thing %d s'appelle %s\n", id, name);
    }

}

Classe pilote

public class Main {

    public static void main(String[] args) {
        Thing t1 = new Thing("Antoinette");
        Thing t2 = new Thing("Berenice");
        Thing t3 = new Thing("Constantin");

        // appels de méthodes d'instance, sur les objets individuels
        String rollCall  = t1.toString() + t2.toString() + t3.toString();
        System.out.println(rollCall);

        // appelle statique, sur la classe Thing
        System.out.printf("Il existe %d Thing.\n", Thing.count());
    }

}

Voici le diagramme UML pour la classe Thing. Notez que le mot-clé static est utilisé dans le texte entre accolades pour indiquer les membres statiques et que dans l’image, ces membres sont soulignés.

@startuml Thing
!theme amiga

class Thing {
    - count = 0 : {static} int
    - name : String
    - id : int
    + Thing(String name)
    + count() : {static} int
    + toString() : String
}

@enduml

Thing

Exercices

Pratique

Ajustez le code de votre objet simple pour ajouter un compteur statique privé ainsi qu’un accesseur statique public pour ce compteur. Modifiez aussi le code dans la classe pilote pour afficher le nombre d’objets créés.

Accueil >

Structures de données et algorithmes

Lien avec l’éthique, la société ou les carrières

💭 Quelques pistes de réflexion :

  • Ordinateurs quantiques et la cryptographie : comment l’architecture quantique peut ouvrir la porte à des algorithmes impossibles avec les architectures binaires actuelles et les risques associés à la sécurité des données
Légende : 🛠️ 📚
  • 🛠️Compétences en génie informatique

    Savoir-faire en lien avec le génie informatique. Les compétences sont évaluées au moyen de projets concrets.

    Développez ces compétences avec les exercices pratiques dans chaque leçon.

  • 📚 Concepts

    Nouveau concept. Le plus de concepts que vous maîtrisez, le plus que vous alimenter vos compétences pour produire des choses intéressantes. Les concepts sont évalués au moyen de quiz sur papier et d’entrevues.

    Validez votre compréhension avec les mini quiz dans chaque leçon.

Séquence

Accueil > Structures de données et algorithmes >

📚 Quelques structures de données et des classes Java qui s’en servent

Survol et attentes

Peut-être vous avez trouvé la taille fixe d’un tableau limitant. Ou possiblement vous aurez préférez liez deux données ensemble au lieu de créer deux tableaux parallèles.

La bonne nouvelle est que des structures de données autres que les tableaux existent et, entre autres, résolvent ces deux problèmes.

Définitions

Ceci est une courte introduction aux structures de données via deux classes utiles dans Java : ArrayList et HashMap. Plusieurs autres structures existent et vous pouvez, avec vos propres objets, en définir de nouvelles.

Choisir une bonne structure de données pour votre application peut simplifier énormément l’algorithme nécessaire pour traiter les informations. Les deux - structures de données et algorithmes - sont étroitement liés.

Représentations internes

Un tableau est une séquence continue de cellules en mémoire pour le même type de donnée :

tableau

Pour accéder aux éléments du tableau, on spécifie le nom de la variable (qui donne l’adresse du tableau) puis l’index (le nombre de pas à partir du début du tableau). Cela nous donne un accès direct à chaque élément via son index.

Une liste est une structure de données où l’information vient avec un ou deux pointeurs. Avec un pointeur, le pointeur donne l’adresse du prochain élément dans la liste. Avec deux pointeurs, un des pointeurs donne aussi l’adresse de l’élément précédent.

liste

Pour accéder aux éléments d’une liste, on doit faire une recherche ;(. Par contre c’est très simple et rapide d’ajouter ou de supprimer des valeurs : on modifie les pointeurs des éléments précédents et subséquents. Les informations peuvent aussi se trouver n’importe où en mémoire, pas nécessairement en cellules collées.

Une table de hachage associe une clé à une valeur. Toutes les valeurs se trouvent dans un tableau et la clé, qui peut être de n’importe quel type, est utilisée dans une fonction de hachage pour donner l’adresse en mémoire de la valeur.

table de hachage

On accède aux valeurs en donnant la clé, ce qui est pratique si on a plusieurs informations à associer et que l’ordre (l’index) dans le tableau n’est pas important. L’accès est direct, comme avec les tableaux.

Utilité

StructureUtilité typiqueObjets Java correspondant
TableauAccès direct à une collection qui ne change pas de taillela notation [] sur un type
ListeModification fréquente d’une série de valeurs, comme l’historique de navigationLinkedList
Table de hachageAccès direct à une valeur au moyen d’une autre valeur associée (la clé)HashMap, TreeMap

La classe ArrayList offre un tableau qui au une taille modifiable, ce qui surmonte la plus grande limitation des tableaux de base.

Déclarations des objets

Les classes Java définissent la structure des données et leur comportements, mais n’indique pas le type de valeurs qu’elles contiennent, contrairement à la notation pour les tableaux qui s’ajoutent directement au type.

Il faut donc spécifier le type d’Object entre des crochets pointus suivant le nom de l’objet. Le format est :

Structure<Type> nomDeVariable = new Structure<>();

Et voici quelques exemples :

ArrayList<Integer> values = new ArrayList<>();
LinkedList<String> pages = new LinkedList<>();

// associer le nom au numéro de téléphone
HashMap<String,Long> phoneBook = new HashMap<>();

Notez que values contient des nombres entiers, mais on a utilisé Integer comme type et non int. C’est parce qu’il faut passer une classe. Pour chaque type primitif, il y a une classe “enveloppe” associée (p. ex. int -> Integer, long -> Long) qui lui offre certaines valeurs et comportements additionnels tout en augmentant son intégration dans le reste du code Java.

Notez aussi que pour la déclaration du HashMap, il fallait passer deux types : le premier pour la clé et le deuxième pour la valeur.

PrimitifClasse enveloppe
booleanBoolean
byteByte
charCharacter
shortShort
intInteger
longLong
floatFloat
doubleDouble

Objectifs d’apprentissage

À la fin de cette leçon vous devrez être en mesure de :

  • Décrire la structure interne d’un tableau, d’une liste et d’une table de hachage, notamment en identifiant les avantages et les cas d’utilisation typiques de chacune.
  • Reconnaître les différentes méthodes pour ajouter, enlever et accéder aux éléments d’un ArrayList et d’un HashMap et de traverser les valeurs de chacun.

Critères de succès

  • Je suis capable d’utiliser des ArrayList et HashMap dans mes programmes pour simplifier la gestion de collections de données ou optimiser les performances.

Notes

Notes additionnelles sur les structures de données Java

Exercices

ArrayList

HashMap

Accueil > Structures de données et algorithmes >

📚 Algorithmes de recherche classiques

Survol et attentes

Les développeurs sont appelés à trouver des solutions à de nouveaux problèmes, souvent en créant un algorithme sur mesure pour la tâche.

Rechercher une valeur dans une liste est un problème qui connaît déjà plusieurs solutions. Étudier certaines solutions de base vous aide, comme développeur en herbe, à comprendre comment les algorithmes sont structurés et à voir le lien entre un algorithme et la structure de données sous-jacente.

Pour ces types de problèmes, au quotidien, vous utiliserez généralement des solutions optimisées comme les méthodes contains ou binarySearch des différents objets de votre langage de programmation.

Définitions

Vous devrez comprendre deux algorithmes de recherche : la recherche séquentielle et la recherche binaire.

Recherche séquentielle (ou linéaire)
Une algorithme qui traverse la collection jusqu’à ce qu’il atteigne la valeur cherchée ou la fin de la collection. Des variantes incluent des algorithmes qui traversent toujours la collection entière pour trouver la position de toutes les correspondances avec la valeur cherchée.
Recherche binaire
Un algorithme qui divise une collection triée en 2 à chaque comparaison, arrêtant quand la valeur cherchée se trouve au point milieu de la partie restante ou quand il ne reste plus d’items à vérifier. C’est important de noter que la collection doit être triée pour que cet algorithme fonctionne.

Objectifs d’apprentissage

À la fin de cette leçon vous devrez être en mesure de :

  • Différencier la recherche séquentielle avec la recherche binaire selon des descriptions, du pseudocode, des diagrammes ou des exemples d’implémentation.

Critères de succès

  • Je suis capable d’expliquer un algorithme de recherche séquentielle ou binaire pour des tableaux.
  • Je suis capable d’utiliser la méthode contains (et indexOf quand elle est disponible) de collections comme les String, ArrayList, HashMap, etc. pour faire une recherche appliquant les algorithmes optimisés dans la bibliothèque standard Java.

Notes

Recherche séquentielle / linéaire

Description

La recherche séquentielle est un algorithme qui traverse une collection jusqu’à ce qu’il atteigne la valeur cherchée ou la fin de la collection. Des variantes incluent des algorithmes qui traversent toujours la collection entière pour trouver la position de toutes les correspondances avec la valeur cherchée.

Pseudocode

RechercheSequentielle(liste, cible)
    pour chaque élément dans la liste
        si élément = cible
            retourner vrai
    retourner faux

Diagramme de flux

recherche séquentielle

Exemple d’implémentation Java

public static boolean sequentialSearch(int[] elements, int target) {
    for (int i = 0; i < elements.length; i++) {
        if (elements[i] == target) {
            return true;
        }
    }
    return false;
}

Recherche binaire

Description

La recherche binaire est un algorithme qui divise une collection triée en deux à chaque comparaison, arrêtant quand la valeur cherchée se trouve au point milieu de la partie restante ou quand il ne reste plus d’items à vérifier. C’est important de noter que la collection doit être triée pour que cet algorithme fonctionne.

Pseudocode

RechercheBinaire(liste, cible)
    gauche = 0
    droite = taille(liste) - 1
    tant que gauche <= droite
        milieu = (gauche + droite) / 2
        si liste[milieu] = cible
            retourner vrai
        sinon si liste[milieu] < cible
            gauche = milieu + 1
        sinon 
            droite = milieu - 1
    retourner faux

Diagramme de flux

recherche binaire

Exemple d’implémentation Java

public static boolean binarySearch(int[] elements, int target) {
    int left = 0;
    int right = elements.length - 1;
    while (left <= right) {
        int mid = (left + right) / 2;
        if (target == elements[mid]) {
            return true;
        } else if (target < elements[mid]) {
            right = mid - 1;
        } else { // target > elements[mid]
            left = mid + 1;
        }
    }
    return false;
}

Exercices

Revue interactive - CSAwesome

anglais

Vous pouvez trouvez des vidéos explicatives, des exemples d’implémentations Java et des exercices interactifs en lien avec ce sujet sur le site CS Awesome, notamment la section suivante :

Une recherche séquentielle/linéaire adaptée

Créer un algorithme de recherche séquentielle qui retourne TOUS les index des éléments identiques à la valeur recherchée. Si l’élément n’est pas trouvé, la liste retournée devrait être vide. Vous pouvez utiliser un ArrayList pour stocker les index trouvés.

La signature de la méthode serait alors :

public static ArrayList<Integer> sequentialSearch(int[] elements, int target)

Créez une classe Algos.java pour y mettre cette méthode.

Lancer les tests dans AlgosTest.java (clic-droit et ‘Enregistrer le lien sous’ pour enregistrer le fichier dans votre dossier de projet) pour vérifier votre implémentation. Suivre les étapes 1 à 4 pour ajouter JUnit à votre projet avant de lancer les tests.

Une méthode pour vérifier si un tableau est trié

Créer une autre méthode dans la classe Algos qui vérifie si un tableau d’entiers est trié en ordre croissant. Cette condition est un prérequis pour les algorithmes de recherche binaire.

La signature de la méthode serait :

public static boolean isSorted(int[] elements)

Il devrait retourner true si le tableau est trié et false sinon.

Ajoutez ces tests à votre fichier AlgoTest.java (voir la tâche précédente), à l’intérieur de la classe existante. Mettre le document en forme après le copier-coller pour vérifier que la structure a été maintenue durant l’opération.

    @Test
    public void testIsSortedWithUnsorted() {
        int[] a = { 2, 1, 3, 4, 5 };
        assertEquals(false, Algos.isSorted(a));
    }

    @Test
    public void testIsSortedWithEqual() {
        int[] a = { 1, 1, 1, 1, 1 };
        assertEquals(true, Algos.isSorted(a));
    }

    @Test
    public void testIsSortedWithSorted() {
        int[] a = { 1, 2, 3, 4, 5 };
        assertEquals(true, Algos.isSorted(a));
    }

Accueil > Structures de données et algorithmes >

🛠️ Décomposition du problème et tests unitaires

Ce sujet est enseigné en ICS3U. Vous trouverez les références pertinentes ci-dessous.

Décomposition

Tests unitaires

Accueil > Structures de données et algorithmes >

📚 Algorithmes de tri classiques

Survol et attentes

Comme avec les algorithmes de recherche, les algorithmes de tri sont des bons exercices pour développer la capacité à analyser une tâche de traitement de données et d’arriver à une solution valide.

Les algorithmes de tri sont généralement plus complexes que les algorithmes de recherche parce que le nombre de comparaisons à faire est plus grand et il faut déplacer des éléments pour produire l’ordre voulu.

Pour ces types de problèmes, au quotidien, vous utiliserez généralement des solutions optimisées comme la méthode sort des différents objets de votre langage de programmation.

Définitions

Vous devrez comprendre deux algorithmes de recherche : le tri par sélection et le tri par insertion.

Exemple et solution

Dans les exemples ci dessous, la partie triée de la collection se trouve à la gauche et la partie non triée se trouve à la droite.

DescriptionPartie triéePartie non triée
CôtéGaucheDroit
Index0 … ii+1 … length - 1

Le tri par sélection trouve l’index du plus petit élément dans la partie non triée de la collection et échange l’élément à cette position avec le premier élément de la partie non triée. En répétant cette action, la partie triée grandie à mesure que la partie non triée diminue jusqu’à ce qu’on atteigne la fin de la collection. Voici le portrait entrée/traitement/sortie (ETS) pour l’algorithme :

EntréeTraitementSortie
Une liste ou un tableau de valeurs1. Identifier l’index min comme l’index de la première valeur dans la partie non-triée du tableau. 2. Comparer la valeur à l’index min avec toutes les valeurs restantes dans la partie non triée : si l’autre valeur est plus petite, changer l’index min pour l’index de la valeur plus petite. 3. Échanger la valeur du premier élément de la partie non-triée avec la valeur à l’index min. 4. Avancer d’une position dans la collection et répéter les étapes 1 à 3 jusqu’à ce qu’on arrive à l’avant dernier index.Retourner rien (le tableau ou la liste original est trié sur place en mémoire)

Points clés sur le tri par sélection :

  • La “sélection” fait référence à l’idée que cette algorithme sélectionne la valeur minimale parmis toutes les valeurs non triées pour ensuite l’ajouter à la fin de la partie triée.
  • Il faut toujours comparer toutes les valeurs de la collection jusqu’à l’avant-dernière position même si la collection est déjà triée.

Le tri par insertion fonctionne différemment. Il compare la prochaine valeur non-triée avec les valeurs déjà triées, afin d’insérer cette valeur au bon endroit. Voici le portrait entrée/traitement/sortie (ETS) pour l’algorithme :

EntréeTraitementSortie
Une liste ou un tableau de valeurs1. Définir l’index actuel à 1. 2. Pendant que actuel est moins grand que la taille de la collection - 1, répéter les étapes 3 à 7. 3. Assigner à temp la valeur à l’index actuel. 4. Définir l’index final comme (actuel - 1). 5. Pendant que final est plus grand que 0 ET temp et plus petit que la valeur à l’index final, diminuer la valeur de final par 1. 6. Échanger les valeurs aux index actuel et final. 7. Augmenter actuel par 1.Retourner rien (le tableau ou la liste original est trié sur place en mémoire)

Points clés sur le tri par insertion :

  • Le nom “insertion” fait référence à l’idée que la prochaine valeur non triée est insérée au bon endroit dans la partie triée.
  • Si la collection est déjà triée, cet algorithme fera un minimum de comparaisons et aucun échange de positions. Il est donc plus efficace que le tri par sélection dans cette situation.
  • Si la collection est triée avec l’ordre opposé (p.ex du plus grand au plus petit), le pire cas pour cet algorithme, le tri par insertion fera le même nombre de comparaisons que le tri par sélection.

Objectifs d’apprentissage

À la fin de cette leçon vous devrez être en mesure de :

  • Différencier le tri par sélection du tri par insertion selon des descriptions ou des exemples d’implémentation.
  • Identifier les meilleurs et les pires cas de l’état initial du tableau sur la performance de chacun de ces algorithmes de tri (p. ex. si le tableau est déjà ou presque trié, si le tableau est trié dans l’ordre opposé, etc.)

Critères de succès

  • Je suis capable d’utiliser la méthode sort d’un ArrayList, d’un LinkedList ou d’Arrays pour faire une recherche dans différents types de collections Java.
  • Je suis capable d’implémenter et d’expliquer un algorithme de tri par insertion pour des tableaux.

Notes et exercices

Pratique avec les algorithmes de base

Vous pouvez trouvez des vidéos explicatives, des exemples d’implémentations Java utilisant des tableaux et des ArrayList et des exercices interactifs en lien avec ce sujet sur le site CS Awesome, notamment la section suivante :

Accueil > Structures de données et algorithmes >

📚 Algorithmes récursifs

Survol et attentes

Définitions

Objectifs d’apprentissage

À la fin de cette leçon vous devrez être en mesure de :

  • Distinguer les cas récursifs et les cas de base dans une méthode récursive.
  • Distinguer une méthode récursive d’une méthode qui utilise l’itération.
  • Lire et préparer des diagrammes d’appels pour des méthodes récursives.

Critères de succès

  • Je peux écrire des méthodes récursives, notamment en incluant des cas de base et des cas récursifs.
  • Tracer des méthodes récursives avec des diagrammes d’appel

Notes et exercices

Vous pouvez trouvez des vidéos explicatives, des exemples d’implémentations Java appliquant les concepts de la récursivité et des exercices interactifs en lien avec ce sujet sur le site CS Awesome, notamment la section suivante :

Accueil > Structures de données et algorithmes >

📚 Analyse de la complexité algorithmique

Survol et attentes

Quelle option préférerez-vous?

  • Faire des allers-retours à vélo plusieurs fois à l’épicerie avec un seul sac ou une seule fois mais en traînant une grande remorque à vélo
  • Trouver votre chemin vers la maison d’un nouvel ami en prenant des virages au hasard aux intersections de la route jusqu’à ce que vous arriviez (peut-être des années plus tard) ou en prenant le virage qui vous mène dans la direction générale et en revenant sur vos pas si nécessaire jusqu’à ce que vous arriviez
Définitions
Analyse algorithmique
Décrire la complexité des algorithmes - combien de temps ou combien d’espace ils prennent pour accomplir la tâche. Certains algorithmes sont beaucoup plus efficaces avec les opérations ou la mémoire que d’autres. Certains sont tellement inefficaces qu’ils ne sont pas du tout pratiques!
Ordre de grandeur
Nombre d’étapes ou de la quantité de mémoire requise en fonction de la taille des données à traiter, décrit selon le terme le plus important dans la relation. L’ordre de grandeur n’est donc pas une relation exacte entre l’effort et le nombre de données à traiter mais donne un portrait de la progression globale de l’effort selon ce nombre.
Notation “grand O”
Représentation de l’ordre de grandeur au format O(f(n))f(n) est une fonction qui décrit la complexité de l’algorithme en fonction de la taille des données à traiter. Par exemple, O(n) signifie que l’effort est proportionnel à la taille des données, n, tandis que O(n²) signifie que l’effort est proportionnel au carré de la taille des données.

Objectifs d’apprentissage

À la fin de cette leçon vous devrez être en mesure de :

  • Définir la complexité algorithmique, l’efficacité temporelle, l’efficacité spatiale et les problèmes P/NP
  • Définir les niveaux de complexité algorithmique courants dans la notation “grand O” : constant, logarithmique, linéaire, log-linéaire, quadratique et exponentiel
  • Identifier les strutures algorithmiques qui conduisent à une complexité constante, linéaire, logarithmique, quadratique ou exponentielle

Critères de succès

  • Je peux reconnaître la complexité “grand O” d’un algorithme en fonction de son graphique d’opérations (ou d’utilisation de la mémoire) en fonction de la taille de l’entrée
  • Je peux identifier la complexité de la recherche séquentielle et binaire ainsi que celle de quelques algorithmes de tri courants
  • Je peux analyser un algorithme inconnu pour déterminer sa complexité algorithmique en fonction des structures observées dans l’algorithme

Notes

Exercices

Fiche de travail

Accueil > Collaboration et gestion de projet

Collaboration et gestion de projet

Lien avec l’éthique, la société ou les carrières

💭 Quelques pistes de réflexion :

  • Code de déontologie
  • Culture de travail
Légende : 🛠️ 📚
  • 🛠️Compétences en génie informatique

    Savoir-faire en lien avec le génie informatique. Les compétences sont évaluées au moyen de projets concrets.

    Développez ces compétences avec les exercices pratiques dans chaque leçon.

  • 📚 Concepts

    Nouveau concept. Le plus de concepts que vous maîtrisez, le plus que vous alimenter vos compétences pour produire des choses intéressantes. Les concepts sont évalués au moyen de quiz sur papier et d’entrevues.

    Validez votre compréhension avec les mini quiz dans chaque leçon.

Séquence

  • 🛠️ Gestion de projet

    Pour s’organiser, c’est pratique de connaître les étapes globales d’un projet et d’avoir un rôle clairement défini.

  • 📚 Packages et modules

    L’unité fondamentale en orientée objet (et en Java) est la classe. Mais c’est la relation entre les classes qui forme des projets. Un projet Java d’envergure organise les classes en packages et exportent les packages comme un (ou plusieurs) modules. Avant d’utiliser d’autres outils Java pour les projets, il faut comprendre ces structures.

  • 🛠️ Outils de construction du projet

    Si vous travaillez en équipe, c’est pratique d’avoir la même structure de projet et les mêmes versions des packages internes et externes même si vous n’utilisez pas les mêmes environnements de développement. On utilisera Maven pour y arriver.

  • 🛠️ Réviser le code produit par un collègue

    C’est une compétence essentielle au travail collaboratif : lire et critiquer le code de quelqu’un d’autre et recevoir la rétroaction des autres pour améliorer son code. Intégrer à tout ça, il y a les techniques associées à la gestion de branches (créer une branche de fonctionnalité, la mettre à jour avec les nouveautés sur main, etc.) et les demandes de tirage (intégrer votre fonctionnalité sur la branche main).

  • 📚 JavaFX pour les applications graphiques

    Les applications graphiques sont toutes indiquées pour le développement collaboratif car la séparation de l’interface utilisateur (la vue), de la logique (le contrôleur) et des données (le modèle) devient plus importante et on peut travailleur plus efficacement en développant des compétences dans seulement un des domaines à la fois à l’aide de membres d’équipe avec des compétences complémentaires.

Accueil > Collaboration et gestion de projet >

🛠️ Gestion de projet - rôles et processus

Survol et attentes

Définitions

Objectifs d’apprentissage

À la fin de cette leçon vous devrez être en mesure de :

  • Liste de 2-3 objectifs / compétences

Critères de succès

  • Liste des critères d’auto-évaluation

Notes

Exercices

📚 Vérification de la compréhension

🛠️ Pratique

Accueil > Collaboration et gestion de projet >

📚 Packages et modules Java

Survol et attentes

Il y a trois niveaux d’emballages dans Java : les classes, les packages et - depuis la version 9 - les modules. Les packages sont des collections de classes et de sous-packages. Les modules sont des collections de packages et de sous-modules. Les packages et les modules sont utilisés pour organiser les classes et pour contrôler l’accès aux classes.

Définitions

Un package est un dossier qui contient des classes et des sous-packages. Les packages sont déclarés au début d’un fichier de classe avec le mot-clé package. En créant des packages manuellement, comme dans un explorateur de fichiers ou un éditeur de code comme VS Code, on doit respecter la structure de dossiers. Certains EDI spécialisés, comme Eclipse et IntelliJ, représentent des packages comme une abstraction et on ne voit pas directement la structure des dossiers, mais elle existe toujours.

Les classes qui ne sont pas dans le même package doivent toujours être importées avec le mot-clé import. Les classes qui sont dans le même package n’ont pas besoin d’être importées.

Un module est un ensemble de packages et de sous-modules. Les modules sont déclarés dans un fichier module-info.java avec le mot-clé module. On ne verra pas beaucoup de détails en lien avec les modules dans ce cours, mais l’accès aux packages et aux modules spécifiques est déclaré dans ce fichier avec les mots-clés requires, opens et exports.

Package par défaut

Si aucune déclaration de package n’est fait au début d’un fichier de classe, la classe est placée dans le package par défaut. Le package par défaut n’a pas de nom et ne peut pas être importé. Si toutes les classes du projet sont dans le même dossier et il n’y aucune déclaration de package, elles sont toutes dans le package par défaut et elles peuvent toutes s’appeler les unes les autres.

Mais ces classes ne peuvent pas être importées dans d’autres packages (parce que le package par défaut n’a pas de nom).

Packages nommés

Le nom pour le package de base d’un projet Java suit souvent la convention d’un nom de domaine inversé. Par exemple, les packages faits par Google sont nommés com.google et les packages faits par Oracle sont nommés com.oracle. Des sous-packages (sous-dossiers), comme com.google.maps et com.oracle.jdbc, sont utilisés pour organiser les classes selon leur fonctionnalité.

Pour ce cours, je vous recommande le nom de package de base edu.ics4u.[prénom] où vous insérez votre prénom en minuscules. Par exemple, si votre prénom est Jenna, votre nom de package de base serait edu.ics4u.jenna. Vous aurez alors la déclaration au format suivant comme première ligne de chaque fichier de classe :

package [nom.du.package];

Si toutes vos classes sont dans le package de base, elles peuvent s’appeler les unes les autres sans les importer. Mais si vous organisez vos classes dans différents packages et/ou sous-packages, chaque classe qui n’est pas dans le même package doit être importée avec une déclaration au format :

import [nom.de.l'autre.package].[nomDeLaClasse];

Pour qu’une classe soit visible dans un autre package, elle doit être déclarée public. Si une classe n’est pas déclarée public, elle ne peut être utilisée que dans le même package - la visibilité est privée au package par défaut. Ainsi, on peut étendre le concept d’encapsulation au niveau des packages.

Depuis Java 9, on peut aussi encapsuler un ensemble de packages dans des modules. Le JDK est lui-même divisé en modules depuis cette version, mais on ne verra pas les détails dans ce cours.

Dossier racine

Le dossier racine de votre projet et le dossier racine de vos packages ne seront généralement pas les mêmes avec les outils standard de développement de projets Java.

Par exemple, avec Maven (dans n’importe quel EDI incluant VS Code), le dossier racine du projet contient un fichier pom.xml qui décrit la configuration du projet et le dossier racine de vos packages est, par défaut, src/main/java. Si votre package de base est edu.ics4u.nathan, alors le fichier App.java serait dans le dossier src/main/java/edu/ics4u/nathan. Notez que par défaut Maven définit aussi un dossier src/test/java pour les tests unitaires, p. ex. dans un fichier AppTest.java.

Avec des EDI comme Eclipse et IntelliJ, la structure du dossier de projet est configuré par l’EDI même si ce n’est pas un projet Maven. Par exemple, avec Eclipse le dossier racine du projet contient des fichiers de configuration qui sont souvent masqués et le dossier racine de vos packages est src. Parce que ces EDI utilisent une abstraction pour les packages Java, un package, p. ex. edu.ics4u.mohammed, sera visible comme un objet dans le dossier src et on ajoute une nouvelle classe, p. ex. App, directement dans le package.

Objectifs d’apprentissage

À la fin de cette leçon vous devrez être en mesure de :

  • Expliquer le lien entre les packages Java et les dossiers sur le disque dur.
  • Déclarer un package au début d’un fichier de classes utilisant le format de nom conventionnel.
  • Importer des classes de vos propre sous-packages.

Critères de succès

  • Je peux déclarer et utiliser des packages nommés dans mes projets Java.
  • Je peux distinguer le dossier racine du projet et le dossier racine des packages dans un projet Java configuré avec Maven.

Exercices

📚 Vérification de la compréhension

🛠️ Pratique

Accueil > Collaboration et gestion de projet >

🛠️ Outils de construction : Maven

Survol et attentes

Définitions

Objectifs d’apprentissage

À la fin de cette leçon vous devrez être en mesure de :

  • Liste de 2-3 objectifs / compétences

Critères de succès

  • Liste des critères d’auto-évaluation

Notes

Exercices

📚 Vérification de la compréhension

🛠️ Pratique

Accueil > Collaboration et gestion de projet >

🛠️ Revues de code

Survol et attentes

Définitions

Objectifs d’apprentissage

À la fin de cette leçon vous devrez être en mesure de :

  • Liste de 2-3 objectifs / compétences

Critères de succès

  • Liste des critères d’auto-évaluation

Notes

Exercices

📚 Vérification de la compréhension

🛠️ Pratique

Accueil > Collaboration et gestion de projet >

📚 JavaFX - framework Java pour les applications fenêtrées (GUI)

Survol et attentes

Définitions

Objectifs d’apprentissage

À la fin de cette leçon vous devrez être en mesure de :

  • Liste de 2-3 objectifs / compétences

Critères de succès

  • Liste des critères d’auto-évaluation

Notes

Exercices

📚 Vérification de la compréhension

🛠️ Pratique

Accueil >

Programmation orientée objet

Lien avec l’éthique, la société ou les carrières

💭 Quelques pistes de réflexion :

Légende : 🛠️ 📚
  • 🛠️Compétences en génie informatique

    Savoir-faire en lien avec le génie informatique. Les compétences sont évaluées au moyen de projets concrets.

    Développez ces compétences avec les exercices pratiques dans chaque leçon.

  • 📚 Concepts

    Nouveau concept. Le plus de concepts que vous maîtrisez, le plus que vous alimenter vos compétences pour produire des choses intéressantes. Les concepts sont évalués au moyen de quiz sur papier et d’entrevues.

    Validez votre compréhension avec les mini quiz dans chaque leçon.

Séquence

  • 🛠️ Diagrammes UML - liens entres les classes

    C’est important de planifier la structure du projet et une façon puissante est le diagramme UML

  • 📚 Héritage et polymorphisme

    Réutiliser le code d’une classe dans un autre et établissement une relation “est un” entre les classes. Ceci permet aux classes “enfant” d’être substituer partout où on s’attend de voir la classe “parent”, phénomène nommé polymorphisme.

  • 📚 Interfaces et classes abstraites

    Façon généralement plus flexible et pratique de réutiliser du code dans les classes enfant, établissant simplement des contrats pour la présence de certains comportements sans les implémenter. Cela réduit le code inutile dans les classes parents.

  • 📚 Composition : le patron Stratégie et d’autres structures utiles

    Inclure une interface comme attribut au lieu d’hériter une méthode permet une plus grande flexibilité pour la modification structurelle éventuelle d’un projet ainsi que la modification dynamique (lors de l’exécution) du comportement d’une classe enfant.

Accueil > Programmation orientée objet >

🛠️ Diagrammes de classe UML : relations entre les classes

Survol et attentes

Définitions

Objectifs d’apprentissage

À la fin de cette leçon vous devrez être en mesure de :

  • Liste de 2-3 objectifs / compétences

Critères de succès

  • Liste des critères d’auto-évaluation

Notes

Exercices

📚 Vérification de la compréhension

🛠️ Pratique

Accueil > Programmation orientée objet >

📚 Héritage et polymorphisme

Survol et attentes

En créant des logiciels plus complexes, plusieurs objets peuvent avoir des caractéristiques communes. Par exemple, un jeu vidéo peut avoir plusieurs types de personnages, mais tous les personnages ont des caractéristiques communes comme la position, la vitesse, la direction, etc. Dans ce cas, il est utile de créer une classe de base qui contient les caractéristiques communes, puis de créer des classes dérivées qui contiennent les caractéristiques spécifiques à chaque type de personnage.

Définitions

L’héritage nous perment de déclarer une classe parente qui contient des attributs et des méthodes communes à plusieurs classes enfants.

Les classes enfants héritent des attributs et des méthodes de la classe parente. Les classes enfants peuvent supplanter la définition d’une méthode de la classe parente, comme on l’a déjà vu avec les méthodes toString(), equals()ethashCode()`. Elles peuvent aussi avoir leurs propres attributs et méthodes.

Seulement les attributs de visibilité protected ou public d’une classe parent seront hérités par les enfants. La nouvelle visibilité protected est comme private sauf pour les membres de la même famille. Donc, les attributs private de la classe parent appartiennent uniquement au parent tandis que les attributs protected seront hérités.

Notez que la visibilité par défaut (sans déclaration explicite) est package-private qui est comme public à l’intérieur du package et comme private à l’extérieur. Vous ne pouvez pas explicitement déclarer quelque chose package-private… simplement n’utilisez pas de mot-clé si vous voulez ce niveau de visibilité.

Exemples et solutions

Une classe enfant indique son lien de parenté en utilisant le mot-clé extends dans sa déclaration. Par exemple, la classe Warrior peut être déclarée comme suit :

public class Warrior extends Player {
    // ...
}

Une classe enfant peut réutiliser le constructeur de sa classe parente en utilisant la méthode super() dans son constructeur. Par exemple, la classe Warrior peut être déclarée comme suit :

public class Warrior extends Player {
    public Warrior(String name, int health, int strength) {
        super(name, health, strength);
    }
}

présumant que la classe Player a déclarée un constructeur comme suit :

public class Player {
    protected String name;
    protected int health;
    protected int strength;

    public Player(String name, int health, int strength) {
        this.name = name;
        this.health = health;
        this.strength = strength;
    }
}

Le mot-clé super() s’applique aux autres méthodes de la classe parente également. S’il est utilisé, ça doit toujours être la première instruction dans la méthode de la classe enfant qui supplante la méthode originale.

Dans un diagramme de classe UML, on indique la relation d’héritage avec une ligne solide et une tête de flèche en triangle pointant vers la classe parente. Par exemple, les classes Warrior, Wizard et Healer héritent toutes de la classe Player :

heritage1

Relation “est un”

L’héritage est une relation “est un”. Par exemple, un Warrior est un Player. On peut donc utiliser un objet de type Warrior partout où un objet de type Player est attendu. Par exemple, on peut déclarer une variable de type Player et lui assigner un objet de type Warrior ou à d’autres types qui sont des extensions de Player :

ArrayList<Player> players = new ArrayList<>();
players.add(new Warrior("Conan", 90, 10));
players.add(new Wizard("Gandalf", 50, 5));
players.add(new Healer("Rowena", 100, 2));

La liste est déclarée comme une liste de Player, mais on peut y ajouter des objets de type Warrior, Wizard et Healer parce que ces classes “sont des” Player via l’héritage. Quand les objets de type parent pointent à des instances de type enfant, cela s’appelle le polymorphisme. Le polymorphisme fait spécifiquement référence à la capacité d’appeler la même méthode sur des objets de types différents et d’obtenir des résultats différents : cette méthode a plusieurs formes, littéralement la définition de polymorphisme.

La vidéo suivante présente un bon portrait du polymorphisme possible via l’héritage, soit celui qui est possible en prenant des types enfants pour le type parent : 📺 Upcasting et downcasting

Objectifs d’apprentissage

À la fin de cette leçon vous devrez être en mesure de :

  • Comprendre et expliquer le concept d’héritage, notamment en connaissant le sens des termes classe parente, classe enfant, et des mots-clé extends et super en Java.
  • Comprendre et expliquer le concept de polymorphisme, notamment en connaissant la relation “est un”.
  • Identifier la classe parente et la classe enfant dans un diagramme de classe UML.

Critères de succès

  • Je peux déclarer une classe enfant qui hérite d’une classe parente
  • Je peux réutiliser le code pertienent de la classe parente avec le mot-clé super et supplanter les méthodes de la classe parente avec du code plus approprié.
  • Je peux déclarer une variable de type parent et lui assigner un objet de type enfant.

Exercices

Quiz de validation des connaissances

Travail pratique :

  1. Créer les fichiers appropriés pour les classes suivantes et remarquez ce qui se passe en lançant la classe App :

    public class Parent {
        String name;
    
        Parent(String name) {
            this.name = name;
        }
    
        void role() {
            System.out.print("I'm the ");
        }
    
        void sayName() {
            System.out.println("My name is " + name);
        }
    
    }
    
    public class Dad extends Parent {
    
        Dad(String name) {
            super(name);
        }
    
        @Override
        void role() {
            super.role(); // toujours en premier... réutilise le code de la classe parente
            System.out.println("dad.");
        }
    
        @Override // supplante (prend priorité sur) la version dans la classe parente
        void sayName() {
            System.out.println("My name is Mr. " + name + ".");
        }
    }
    
    public class App {
        public static void main(String[] args) {
            Dad dad = new Dad("John");
            dad.role();
            dad.sayName();
        }
    }
    
  2. Créer deux classes Mom et LegalGuardian qui héritent de Parent et qui modifient le code hérité de manière appropriée. Ajouter des déclarations dans la classe App pour tester votre code.

  3. Préparer un diagramme de classe UML pour les classes Parent, Dad, Mom et LegalGuardian et vérifier votre diagramme avec votre enseignant(e).

Accueil > Programmation orientée objet >

📚 Interfaces et classes abstraites

Survol et attentes

L’héritage via un lien de parenté est bon pour des objets similaires qui accomplissent une tâche semblable. Mais que faire si l’objet parent ne sera jamais utilisé pour déclarer un objet? Ou si on veut un nouvel type d’objet qui n’utilise qu’une partie de la fonctionnalité d’une autre classe ou qui veut combiner la fonctionnalité de plusieurs classes?

Les réponses sont les classes abstraites et les interfaces.

Définitions

Une classe abstraite ne peut jamais être instanciée. Elle sert de modèle pour d’autres classes qui héritent de ses attributs et méthodes. Une classe abstraite peut contenir des méthodes abstraites, qui sont des méthodes qui n’ont pas de corps. Les classes qui héritent d’une classe abstraite doivent implémenter les méthodes abstraites. Une classe qui extends une classe abstraite respecte toujours la relation “est un” avec la classe abstraite, pareil que pour l’héritage normal.

Une interface est un type de classe qui ne contient que des méthodes abstraites (et parfois des constantes privées). Une classe peut implémenter plusieurs interfaces, mais ne peut hériter que d’une seule classe abstraite.

La vidéo suivante explique bien le portrait : 📺 Classes et méthodes abstraites

Classes abstraites

La classe Animal est un bon candidat pour une classe abstraite parce qu’aucun animal n’est juste un animal. Il y a des chiens, des chats, des oiseaux, etc. Pour déclarer que la classe Animal est abstraite, on utilise le mot-clé abstract :

public abstract class Animal { // abstract ici prévient l'instanciation de la classe
    protected String name;
    //...

    abstract void family(); // ; au lieu de {}
        // abstract ici force les classes enfants à implémenter cette méthode
    //...
}

Une classe enfant, p. ex. Dog, doit implémenter toutes les méthodes abstraites de la classe parente :

public class Dog extends Animal {
    //...
    @Override
    void family() {
        System.out.println("Canine");
    }
    //...
}

Souvent, les outils de votre EDI vous signalent une erreur en déclarant l’héritage d’une classe abstraite et vous proposent d’implémenter les méthodes abstraites. Dans VS Code, ce serait une suggestion parmis les “Quick Fix (Ctrl+.)”.

Voici le diagramme de classe UML simplifié (ne montrant pas le contenu des classes) pour la relation entre le parent abstrait Animal et l’enfant Dog :

abstract

Notez que c’est le même type de flèche parce que la relation est toujours de l’héritage, soit Dog est un Animal. La différence est l’apparence de la classe parente, qui est en italique pour indiquer qu’elle est abstraite.

Interfaces

Pour les interfaces, on remplace le mot-clé class par interface :

public interface Eating {
    void eat(); // méthode abstraite par défaut : pas besoin du mot-clé abstract
}
public interface Moving {
    void move();
}

Une classe peut implémenter plusieurs interfaces. Pour indiquer que la classe implémente une interface, on utilise le mot-clé implements. S’il y a plusieurs interfaces, on les sépare par des virgules :

public class Dog extends Animal implements Eating, Moving {
    //...
    @Override
    void family() {
        System.out.println("Canine");
    }
    @Override
    void eat() {
        System.out.println("I eat kibble!");
    }
    @Override
    void move() {
        System.out.println("Running and rolling and wagging!");
    }
    //...
}

Les interfaces peuvent être utilisées dans une variété de classes peut importe leur lien de parenté :

public class Car implements Moving {
    //...
    @Override
    void move() {
        System.out.println("Driving!");
    }
    //...
}

L’implémentation d’une interface signale une relation “a un” entre la classe et l’interface. La classe Car a une méthode move() qui est définie dans l’interface Moving, mais on ne dirait pas que Car est un Moving parce qu’il peut être d’autres choses aussi, notamment une autre classe parente (p. ex. Vehicle).

Voici le diagramme de classe UML simplifié pour les relations d’implémentation dans ces exemples.

interface

Notez que la flèche est différente pour l’implémentation : la tête est pleine mais la ligne est pointillée.

Le but des interfaces et de fournir une interface de programmation (API) pour toutes les classes qui implémentent une fonctionnalité commune. Par exemple, dans la bibliothèque standard de Java toutes les collections - comme les listes, les ensembles et les dictionnaires - implémentent l’interface Collection qui définit les méthodes add(), remove(), contains(), etc. Ainsi toutes les classes peuvent implémenter à leur façon ces méthodes, mais les utilisateurs de ces classes accèdent à la fonctionnalité avec les mêmes appels de méthodes. Cela rend les grands projets plus prévisibles, extensibles et faciles à utiliser.

Objectifs d’apprentissage

À la fin de cette leçon vous devrez être en mesure de :

  • Comprendre et expliquer les structures abstraites : les classes abstraites, les méthodes abstraites et les interfaces et leurs mot-clés respectifs
  • Savoir dans quels contextes il est préférable d’étendre une classe (l’enfant “est un”?) au lieu d’implémenter une interface (l’enfant “as un”?)
  • Distinguer les relations “est un” et “as un” dans un diagramme de classe UML.

Critères de succès

  • Je peux identifier quand une classe abstraite ou une interface est plus appropriée que l’héritage simple.
  • Je peux déclarer une classe abstraite et une interface et implémenter leurs méthodes abstraites dans une classe enfant.

Exercices

Quiz de consolidation

Travail pratique:

  1. Pour une catégorie d’objets nommée GuiWindow (pour les fenêtres qui s’ouvrent sur un ordinateur), pensez à une courte liste (soit une liste partielle) d’interfaces qui pourraient être implémentées par les classes de cette catégorie. Idées : on peut fermer, redimensionner, réduire, etc. les fenêtres. Pour la classe GuiWindow et les interfaces indentifiées, créez les déclarations de base dans des fichiers Java. L’utilité de chaque interface devrait être évidente par son nom et par le nom de sa ou ses méthodes abstraites.
  2. Créez le diagramme de classe UML simplifié (sans contenu interne) pour montrer les relations entre les classes et les interfaces que vous avez déclarées dans la question précédente.

Accueil > Programmation orientée objet >

📚 Composition

Survol et attentes

Quand vous avez un peu d’expérience avec l’héritage, le polymorphisme, les classes abstraites et les interfaces, vous aurez probablement envie de développer des projets plus complexes. La composition est un concept important que vous devrez comprendre afin de planifier les relations entres les classes pour les rendre facile à utiliser et à modifier.

C’est un sujet avancé en programmation orientée-objet. Cette leçon est donc optionnelle. Elle est destinée aux étudiants qui veulent aller plus loin dans leur apprentissage de la programmation orientée-objet. Elle n’est pas nécessaire pour la réussite du cours ICS4U.

Définitions

La composition est une alternative à l’héritage pour la réutilisation de comportements communs. Au lieu d’étendre ou d’implémenter une classe ou une interface, une classe applique la composition en déclarant des attributs (variables) du type de la classe ou de l’interface qu’elle veut réutiliser. Cette classe “se compose” alors en intégrant ces attributs spécifiques (et les méthodes qu’elles définissent).

La composition se fait généralement avec des interfaces parce que chaque interface est généralement très spécifique. Et on peut composer quelque chose avec peu ou avec beaucoup de composants, soit selon les besoins de l’objet.

Pourquoi pas simplement implémenter directement toutes ces interfaces? Dans l’implémentation, la classe définit elle-même l’implémentation de chaque interface. Elle fixe alors son comportement. Dans la composition, la classe délègue l’implémentation à l’objet qu’elle contient. C’est-à-dire que son comportement peut changer selon l’objet qu’elle assigne à son attribut.

Avec l’héritage et l’implmentation directe

Voici un exemple de code pour un personnage de jeu vidéo qui peut se déplacer et attaquer. Il implémente directement les interfaces Moves et Skill.

public interface Moves {
    void move();
}
public interface Skill {
    void applySkill();
}
public class Warrior implements Moves, Skill {
    private int health;
    private int skillLevel;
    // ...autres attributs, constructeurs, etc.

    @Override
    void move(){
        System.out.println("Marching and yelling");
    }

    @Override
    void applySkill(){
        System.out.println("Fighting with sword");
    }
}
public class Wizard implements Moves, Skill {
    private int health;
    private int skillLevel;
    // ...autres attributs

    @Override
    void move(){
        System.out.println("Floating on an energy ball");
    }

    @Override
    void applySkill(){
        System.out.println("Lightning bolt from a staff");
    }
}

basic players

Tout ça est acceptable à la base, mais qu’est-ce qui se passe si le Warrior se bat avec une arme différente ou se déplace à cheval? Ou si le Wizard change également ses comportements durant le jeu. Il faudrait créer des classes pour chaque variante possible d’un Warrior et d’un Wizard, p.ex : MountedWarriorWithSpear, MountedWarriorWithSword, etc. C’est beaucoup de classes à créer et à maintenir! Et si ces personnages devaient éventuellement incorporer d’autres comportements, il faudrait encore plus de classes!!!

Avec la composition

La composition nous offre une autre solution. Au lieu d’implémenter directement les interfaces dans les classes des différents types de personnages, on peut utiliser les interfaces comme attributs dans une classe parente. Si on implémente les interfaces dans des petites classes à part, les différents types de personnages ont simplement à s’assigner une des ces classes comme valeur de leur attribut. Et si dans le futur on ajoute de nouveaux comportments pour les interfaces existantes, rien ne change dans le code du personnage. En fait, même s’il y a aucun nouveau code mais le personnage évolue durant le jeu et obtient un nouveau comportement, c’est aussi simple que de lui assigner la bonne implémentation : on peut changer les comportements durant l’exécution du programme! Finalement, si on ajoute un nouveau comportement dans une nouvelle version du programme, on ajoute juste un nouvel attribut et les méthodes nécessaires pour l’utiliser (accesseurs, mutateurs, etc.). Le code existant n’est pas affecté.

Voici à quoi ça ressemble, présumant que les interfaces Moves et Skill sont inchangées de l’exemple précédent :

public class MarchingAndYelling implements Moves {
    @Override
    void move(){
        System.out.println("Marching and yelling");
    }
}
public class FightingWithSword implements Skill {
    @Override
    void applySkill(){
        System.out.println("Fighting with sword");
    }
}

..etc. Il y a plusieurs petites classes comme celles-ci, chacune implémentant une interface de façon spécifique.

public abstract class Player {
    private int health;
    private int skillLevel;
    private Moves move; // voici la composition : inclut l'interface comme attribut
    private Skill skill;
    // ...autres attributs, constructeurs, etc.

    public void move(){ // parce que c'est une interface on peut définir l'utilisation publique ici
        move.move();
    }
    public void applySkill(){
        skill.applySkill();
    }
}
public class Warrior extends Player{
    public Warrior(){
        super();
        this.move = new MarchingAndYelling(); // l'enfant choisit l'implémentation spécifique
        this.skill = new FightingWithSword();
    }
}
public class Wizard extends Player{
    public Wizard(){
        super();
        this.move = new FloatingOnEnergyBall();
        this.skill = new LightningBoltFromStaff();
    }
}

strategy

Dans le cas de la composition, une classe parente abstraite définit la structure d’un personnage. Cette structure inclut des attributs qui sont des interfaces (on le représente dans un diagramme UML avec un diamant sur la classe qui “se compose de” l’autre classe). Les constructeurs des classes enfants définissent les valeurs spécifiques à insérer dans la structure.

L’architecture présentée dans cet exemple avec les personnages et leur comportements s’appelle le patron de conception Strategy. Il y a plusieurs autres patrons de conception qui offrent des solutions pratiques à des problèmes courants en développement logiciel, mais cela dépasse le cadre de ce cours.

En utilisant une classe parente abstraite avec la composition, on applique plusieurs bonnes pratiques en programmation orientée-objet :

  • La composition produit un couplage faible entre les classes. Les classes sont indépendantes les unes des autres. On peut changer une classe sans affecter les autres ou sans exiger la production d’une quantité énorme de code pour acceuillir le changement.
  • La composition réduit aussi le nombre de fois qu’il faut supplanter les méthodes abstraites, notamment le nombre de fois qu’on écrit une implémentation qui sera immédiatement supplantée par la classe enfant spécifique que le programme utilise.
  • L’héritage des attributs d’une classe parente évite d’avoir à copier la structure de la classe pour chaque type. C’est le principe DRY : “don’t repeat yourself”. Et si on voulait augmenter la capacité de Player, en y ajoutant un nouvel attribut par exemple, on n’aurait qu’à le faire dans la classe parente. Les classes enfants n’auraient qu’à spécifier la partie qui leur est unique : le choix d’une implémentation pour le nouvel attribut.

Objectifs d’apprentissage

À la fin de cette leçon vous devrez être en mesure de :

  • Explorer des architectures plus complexes qui appliquent la composition afin de voir comment utiliser la programmation orientée-objet dans un projet de développement logiciel plus réaliste.

Critères de succès

  • Je peux incorporer la composition dans mes propres classes.

Exercices

Recherche :

  • Faire une recherche sur un patron de conception qui vous intéresse.
    • Tentez d’identifier les concepts de la programmation orientée-objet qui sont appliquées : héritage, interfaces, composition, etc.
    • Identifier des cas où ce patron de conception pourrait être utile dans un projet de développement logiciel.

© 2022-2025 David Crowley