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

Introduction au génie informatique

Ce cours est une introduction au génie informatique axé sur les outils, les pratiques et les connaissances de base en génie informatique.

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

📋 Lien vers le plan du cours

Types d’apprentissage

🛠️ Compétences en génie informatique

Dans chaque unité, vous développerez un savoir-faire en lien avec le génie informatique. Ces compétences sont évaluées au moyen de projets concrets.

Les leçons sur une compétence sont dénotées par le symbole : 🛠️

📚 Concepts

Dans chaque unité, vous apprendrez aussi des nouveaux concepts. 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

Les leçons sur un concept sont dénotées par le symbole : 📚

Unités

Partie A : Fonctionnement de l’ordinateur

Algorithmes

Définitions fondamentales pour le reste du cours

Matériel informatique

Comment les machines exécutent des algorithmes

Logiciels du système

Comment les utilisateurs et les applications utilisent le matériel informatique

Partie B : Programmation

Préparer l’environnement de développement

Installer, configurer et tester les logiciels pour la programmation

Les bases de Java

Les éléments de base du langage Java

Décomposition et modularité

Découper un problème en sous-problèmes et comment l’implémenter dans un programme

Structures de contrôle

Comment contrôler l’exécution d’un programme - le rendre intelligent et puissant

Structures de données

Mieux gérer et manipuler les données dans un programme

Domaines d’application variés

Se familiariser avec les technologies utilisées pour programmer des applications dans une variété de domaines

Accueil >

Partie A - Fonctionnement de l’ordinateur

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.

Algorithmes

Sommaire

Comment dire à une machine comment afficher une image et l’animer, comme dans une vidéo ou un jeu? Est-ce qu’il y a une façon de communiquer qui est assez simple et claire que même une machine à base de 1 et de 0 est en mesure de le comprendre?

La réponse est “oui” : c’est le domaine des algorithmes et du génie informatique. C’est la première étape à maîtriser : un nouveau niveau et type de communication formelle.

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

💭 Quelques pistes de réflexion :

  • Quel pourcentage des informations que vous consommez de façon volontaire (vidéos, annonces, articles, etc.) sont suggérées par un algorithme? Savez-vous comment ces algorithmes prennent leurs décisions?
  • Durant le développement d’un produit ou service technologique, qui a la chance de décider des valeurs et des priorités qui sont incorporées dans le produit? Comment est-ce que ces décisions affectent les utilisateurs?

Leçons

  1. 📚 Définition - c’est quoi un algorithme?

  2. 📚 Niveaux d’abstraction - masquer ou plonger dans les détails

  3. 🛠️ Cas d’utilisation - comprendre le produit et les besoins des parties prenantes

Matériel

Sommaire

L’unité précédente a introduit un langage formel pour décrire une série d’étapes : l’algorithme. Dans cette unité, nous allons voir comment ces algorithmes sont exécutés par un ordinateur. Vous ne deviendrez pas experte en électronique mais vous saurez comment un simple état binaire (haute/basse tension) peut-être manipulé une couche de complexité à la fois dans des circuits pour donner toute la logique et la mémoire nécessaire pour exécuter n’importe quel algorithme valide.

Du côté plus pratique et concret, vous apprendrez comment ces circuits se traduisent en composants communs, comme le processeur et la mémoire, et comment ces composants sont assemblés pour former un ordinateur. Vous apprendrez comment la performance de ces composants et celle de divers périphériques se mesure et comment ces informations sont utilisées pour comparer les ordinateurs entre eux.

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

💭 Quelques pistes de réflexion :

  • Combien d’énergie est nécessaire pour exécuter des algorithmes importants dans la société moderne, comme une recherche sur Google ou pour développer et utiliser un modèle comme ChatGPT?
  • Comment est-ce que les matériaux utilisés dans les composants électroniques sont extraits et traités? Quels sont les impacts environnementaux de ces activités? Comment est-ce que les gens qui travaillent dans ces industries sont traités?

Leçons

  1. 📚 Portes logiques - Implémentations électroniques des opérations booléennes

  2. 📚 Architecture - Organisation des composants électroniques dans un ordinateur

  3. 📚 Périphériques - Détails pratiques en lien avec les périphériques connectés à un ordinateur

Logiciels du système

Sommaire

Les deux premières unités traitent de la nature d’un algorithme et de la circuiterie physique utilisée pour créer une machine qui peut exécuter ces algorithmes.

Cette unité fait le pont entre l’ordinateur et les utilisateurs. D’un côté du pont, il y a la machine, l’ordinateur. Ici, nous voyons comment le système binaire (1/0) est utilisé pour représenter différents types de données et pour représenter des instructions. Cela devient la première communication humain-machine possible : le langage machine. Sachant que c’est possible d’encoder des instructions et des informations dans un langage machine, on peut imaginer l’encodage d’algorithmes complets, et même de logiciels complets. De l’autre côté du pont, nous voici, des utilisateur humains réguliers qui veulent utiliser l’ordinateur pour faire des choses utiles. Nous avons besoin d’un moyen convivial de communiquer avec l’ordinateur, et c’est là que les systèmes d’exploitation et les interfaces utilisateur entrent en jeu. Ces logiciels du système traduisent des gestes humains en langage machine sans qu’on le remarque. Il nous suffit de savoir comment utiliser les logiciels qui sont disponibles pour faire ce qu’on veut faire.

Comme ingénieurs informatiques en herbe, vous aurez à explorer les deux côtés du pont et d’aller plus loin dans le type de logiciel que vous utilisez. Vous apprendrez comment installer des logiciels, comment les utiliser, et comment les configurer pour vos besoins.

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

💭 Quelques pistes de réflexion :

  • La plupart des systèmes d’exploitation associent des applications critiques, comme les navigateurs web et la messagerie, au système d’exploitation lui-même. Quels sont les avantages et les inconvénients de cette approche pour l’utilisateur? Qui profite de ces inconvenients?
  • Plusieurs logiciels sont développés selon le modèle de logiciel libre et open source. Quels sont les avantages et les inconvénients de ce modèle :
    • pour les utilisateurs?
    • pour les développeurs?
    • pour les entreprises?

Leçons

  1. 📚 Représentation interne des données - Quelques standards pour interpréter les bits comme informations variées

  2. 📚 Langages de bas niveau - Les bits peuvent aussi représenter des instructions

  3. 📚 Machine virtuelle - une interface qui masque les détails de la machine réelle

  4. 🛠️ Organisation des fichiers - structure hiérarchique et chemins

Accueil > 1-Algorithmes >

📚 Algorithmes

Survol et attentes

Est-ce que vous parlez de la même façon à vos amis, à un très jeune membre de votre famille, à vos parents ou grands-parents quand vous leur demandez quelque chose? Probablement pas, entre autres, parce que le vocabulaire et les connaissances de chacun sont différents. La même chose s’applique pour demander quelque chose à un ordinateur.

Définitions

Algorithme : En informatique, un algorithme est un processus qui décrit ou qui transforme de l’information. Les algorithmes valides sont des collections bien ordonnées d’étapes réalisables et sans ambiguïté et qui produisent un résultat dans un temps fini.

Objectifs d’apprentissage

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

  • de décrire les différents domaines associés aux études informatiques;
  • d’identifier les critères d’un algorithme valide.

Critères de succès

  • Je peux écrire un algorithme valide en langage naturel (aussi appelé “pseudocode”).
  • Je peux juger si une séquence d’étapes est un algorothme valide ou non.

Notes

Introduction aux études informatiques

Définition d’un algorithme

Exercices

📚 Tester la compréhension

aucun quiz de vérification des concepts ici encore

🛠️ Pratique

  1. Selon les listes des domaines d’études informatiques et des domaines associés, faites une courte recherche sur deux domaines que vous n’avez pas considérez jusqu’à présent mais qui vous semblent intéressants. Une courte recherche inclut, p. ex. :
    • Une brève description
    • Si on peut commencer les études/le travail dans le domaine spécifique directement après le secondaire ou si on fait des études dans un programme général avant de se spécialiser dans ce domaine
    • Des exemples récents d’innovations dans le domaine
  2. Écrivez un algorithme pour laver la vaisselle qui correspond entièrement à la définition d’un algorithme (bien ordonnée, sans ambiguïté, réalisable, produit un résultat, s’arrête).

Accueil > Algorithmes >

🛠️ Abstraction

Survol et attentes

Définitions

Selon ce qui est important pour notre analyse, on tend juste à regarder un niveau de détail, le niveau le plus superficiel possible, et d’ignorer tous les détails internes. Le niveau de détail observé s’appelle un niveau d’abstraction. Chaque fois qu’on cache des détails en formant un modèle simplifié, on a ajouté une couche d’abstraction.

Abstraction : Une abstraction est une simplification d’un concept ou d’un objet. L’abstraction devient un modèle emballant et masquant les détails complexes du fonctionnement interne. En informatique, il y a plusieurs couches d’abstraction, tant pour les programmes (les algorithmes) que pour le matériel qui implémentent les algorithmes.

Objectifs d’apprentissage

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

  • Définir le terme abstraction
  • Relativiser deux niveaux d’abstraction d’un concept : identifier le plus bas niveau et le plus haut niveau

Critères de succès

  • Je peux décrire le niveau d’abstraction directement inférieur au mécanisme que j’étudie, p. ex.: les transistors si j’étudie les portes logiques ou le système d’exploitation si j’écris un programme Java.

Notes

Les couches d’abstraction sont fondamentale pour permettre aux gens de travailler efficacement sur différents problèmes.

Un exemple sont les domaines de la science : la physique qui traite des interactions un-à-un entre les particules, la chimie qui traite des interactions entre les atomes et les molécules et la biologie qui traite des interactions entre les cellules et les organismes. Si on veut étudier le comportement des chauves-souris, la physique n’est pas le bon niveau d’abstraction : l’analyse des particules serait trop complexe, sans nous donner facilement de l’information utile. La biologie est le bon niveau d’abstraction car elle s’intréresse aux systèmes directement.

Voici les couches d’abstraction que nous aborderons en lien avec la structure d’un ordinateur dans ce cours.

abstraction ordinateur

Voici les couches d’abstraction que nous aborderons en lien avec les structures algorithmiques dans ce cours.

abstraction instruction

Exercices

📚 Tester la compréhension

aucun quiz de vérification des concepts ici encore

🛠️ Pratique

  1. En développant une habilité - par exemple, en sports, en arts ou en artisanat - vous devez souvent passer par des cycles d’abstraction : découper l’habilité en morceaux, intégrer les éléments de base pour arriver à une technique fluide, puis intégrer la nouvelle technique dans un contexte ou séquence plus grand. Ultimement vous êtes en mesure de réaliser la technique sans réfléchir aux détails, ce qui correspond à notre concept d’abstraction. Décrivez un exemple de ce processus d’abstraction dans votre vie.

Accueil > 1-Algorithmes >

🛠️ Cas d’utilisation

Survol et attentes

Définitions

Cas d’usage : Un cas d’usage est un document qui analyse comment un produit ou un service sera utilisé. Ce type de document doit être compréhensible par toutes les parties prenantes. Il sert alors de point de connexion entre les différentes parties prenantes et guide le développement technique du produit ou service.

Partie prenante / Acteur : Une personne qui est affectée par le développement du produit ou service. Les parties prenantes peuvent être des utilisateurs, des gérants, des développeurs, des investisseurs, etc.

Public cible : Les personnes qui utiliseront le produit ou service. Le public cible est souvent divisé en plusieurs groupes, chacun ayant des besoins spécifiques.

Flux : séquence d’événements

Objectifs d’apprentissage

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

  • énumerer les éléments d’un cas d’usage, notamment le public cible, les acteurs, les objectifs, les conditions préalables, les flux et les conditions de sortie;
  • distinguer les flux de base, alternatifs et exceptionnels d’un cas d’usage

Critères de succès

  • Je peux rédiger un cas d’usage pour une technologie ou un processus que je développe.
  • Je peux adapter les flux aux besoins des acteurs.

But des cas d’usage

Les cas d’usage se concentrent sur les utilisateurs du système plutôt que sur le système lui-même. Un cas d’usage doit être compréhensible par toutes les parties prenantes, pas seulement les développeurs et les testeurs. Cela inclut les clients, les utilisateurs et les dirigeants. Ainsi le texte d’un cas d’usage est plutôt narratif que technique.

Une des meilleures justifications pour la création de cas d’utilisation est qu’ils servent de véritables points de connexion. Si bien rédigés, ils peuvent être compris par toutes les parties prenantes qui peuvent alors ajouter leurs commentaires et suggestions avant que le développement ne commence. Cela permet de s’assurer que le produit ou service répondra aux besoins de tous les utilisateurs.

Comment rédiger un cas d’usage

Pour rédiger un cas d’usage, procédez comme suit :

  1. Déterminer le public cible du produit et le décrire en détail
  2. Nommer toutes les parties prenantes et sélectionnez un membre de cette liste comme acteur principal. Pour des projets d’informatique du point de vue des développeurs, l’acteur principal est souvent un utilisateur du système.
  3. Déterminez exactement ce que l’utilisateur souhaite faire avec le produit (son objectif). On devrait créer un cas d’usage pour chaque objectif.
  4. Décrivez les conditions préalables à l’utilisation du produit ou du service. Par exemple, est-ce qu’un équipement spécial est nécessaire? Est-ce que l’utilisateur doit être connecté à Internet?
  5. Déterminer le flux typique d’événements pour une session ou l’acteur principal atteint son objectif. Cela ressemble généralement, pour les produits informatiques, à plusieurs cycles d’une action de l’utilisateur suivie par une réponse du système.
  6. Envisager d’autres usages normaux possibles et les décrire comme flux alternatifs.
  7. Envisager également des situations anormales (p. ex.: des bris ou des erreurs) et les décrire comme flux exceptionnels.
  8. Finalement, décrivez les conditions de sortie qui suivent l’usage normal par l’utilisateur, p. ex.: l’enregistrement des données.

Idéalement, après la rédaction d’un cas d’usage, on aurait une discussion avec les parties prenantes pour obtenir des commentaires et des suggestions pour ajuster d’avantage les flux et les conditions afin de mieux répondre aux besoins de chacun.

Exemples

Exemple d'un cas d'usage non informatique

Le but de ce cas d’usage est de vérifier si un service de buanderie externe a la capacité requise pour nettoyer et remplacer, au besoin, le linge d’un restaurant une fois par semaine.

Public cible

Employés et clients d’un restaurant (qui utilisent le linge propre)

Acteurs

Employés de cuisine, serveurs, gérants, clients, service de buanderie

Acteur principal — service de buanderie

Objectif

Nettoyer les uniformes et le linge divers du restaurant chaque semaine

Conditions préalables

C’est un vendredi et il y a du linge sale dans le bac de la buanderie

Flux

Le flux de base pour cet exemple de cas d’usage est le suivant :

  • Le service de buanderie vient au restaurant le vendredi et collecte le linge sale.
  • De retour à la buanderie, le service trie le linge disponible.
  • Le service nettoie et sèche chaque charge.
  • Le service repasse les uniformes, les nappes et les serviettes de table.
  • Le service plie les articles qui doivent être pliés et accroche les autres sur des cintres.
  • Le service retourne le linge propre au restaurant le vendredi pour le service du soir.

Flux alternatifs :

  1. Basse inventaire : Si un gérant signale une basse inventaire d’un item spécifique lors de la collecte, le service de buanderie ajoute la quantité d’items manquante aux items propres avant de retourner le linge au restaurant.
  2. Vêtements encore sales : Le service de buanderie relave tout ce qu’elle trouve encore sale.

Flux exceptionnels :

Voici quelques exemples de flux exceptionnels :

  • Vêtement abîmé : Le service de buanderie trouve un item déchiré. Il le signale à un gérant qui décide de le réparer ou de le remplacer.
  • Machine en panne : La machine à laver est en panne. Le service de buanderie signale le bris (et le retard du service) au gérant du restaurant et initie la procédure de réparation.
  • Manque de linge propre avant le vendredi : Le gérant tente d’organiser un service additionnel pour le linge sale un autre jour de la semaine.

Conditions de sortie :

  • Le nombre d’items propres plus les nouveaux items est enregistré dans un registre avant que le linge quitte la buanderie.
  • Le gérant du restaurant signe le registre pour confirmer la réception du linge propre.
Exemple d'un projet informatique (un jeu d'aventure textuel)

Le programme est un jeu d’aventure textuel dans lequel vous vous promenez à la découverte d’objets. Les descriptions de chaque zone du jeu sont légèrement humoristiques, tout comme celles des objets que vous ramassez. Vous pouvez sauvegarder votre progression afin de ne pas avoir à collecter tous les objets depuis le début à chaque fois que vous jouez.

Public cible

Ce jeu pourrait intéresser les personnes qui n’aiment pas les jeux violents ou qui sont anxieuses car il n’y a pas de conflit dans le jeu, juste de la découverte.

La tranche d’âge est probablement celle des préadolescents et plus car l’interface textuelle nécessite beaucoup de lecture.

L’intégration de texte en couleur pourrait contribuer à rendre le programme plus lisible.

La version initiale est conçue uniquement en français, mais des versions futures pourraient être développées dans d’autres langues, comme l’anglais, car la plupart des informations textuelles sont stockées dans des fichiers extérieurs à la logique qui seraient faciles à traduire.

L’humour est une chose très personnelle et est lié à nos références culturelles, il peut donc ne pas être aussi efficace avec tout le monde. Encore une fois, il est susceptible de fonctionner mieux avec les préadolescents et les adolescents. Il doit plaire aux utilisateurs masculins et féminins, donc ne pas avoir de styles d’humour qui ne soient offensants pour aucun sexe.

Acteurs

Ceci peut être un jeu indépendant, alors les acteurs impliqués pour le réaliser sont juste le(s) développeur(s)/entrepreneur(s) et les utilisateurs.

Notre acteur principal est un adolescent de la 10e à la 12e année qui s’intéresse aux ordinateurs.

Objectif

L’utilisateur joue à un nouveau jeu.

Conditions préalables

L’utilisateur devra avoir accès à un ordinateur et à un interpréteur Python, soit localement, soit en ligne.

Flux de base

L’utilisateur lance le jeu → Les fichiers de données du jeu sont lus dans les structures de données de la mémoire du programme → Un message de bienvenue s’affiche à l’utilisateur et lui demande de charger une partie, de démarrer une nouvelle partie ou de quitter → l’utilisateur choisit une nouvelle partie → le nom du premier emplacement, la description, la navigation et d’autres options s’affichent avec une invite de saisie → l’utilisateur tape son choix → le jeu analyse le choix pour déterminer la nature de l’option et met à jour l’état du jeu en conséquence → … cela continue jusqu’à ce que l’utilisateur atteigne la fin du jeu → un message de sortie s’affiche à l’utilisateur → le programme se termine.

Flux alternatifs : sauvegarde et sortie

Il existe un emplacement dans le jeu où l’utilisateur peut aller pour sauvegarder et quitter le jeu avant de terminer.

S’il y va et choisit cette option → le jeu enregistre son inventaire dans un fichier → il ajoute également ce fichier à une liste de parties sauvegardées dans un fichier journal.

L’utilisateur relance le jeu et choisit l’option de chargement → les fichiers de données du jeu sont lus dans les structures de données de la mémoire du programme → le fichier de données utilisateur est lu en mémoire et l’état du jeu est mis à jour en conséquence → le joueur commence plus loin dans le jeu.

Flux alternatifs : quitter

Il existe un emplacement dans le jeu où l’utilisateur peut se rendre pour quitter le jeu avant de le terminer.

Il s’y rend et choisit l’option quitter → le jeu affiche simplement l’écran de fin et se ferme.

Flux d’exception : saisie utilisateur non valide

L’utilisateur tape un choix incorrect ou incompréhensible → le programme revient en arrière et demande à nouveau une réponse valide → ce cycle continue jusqu’à ce que l’utilisateur donne une réponse reconnaissable.

Flux d’exception : données de jeu non valides

Le fichier de données du jeu peut contenir des séquences de navigation incompatibles qui n’ont pas été découvertes lors des tests → l’utilisateur choisit l’une de ces options de navigation → le jeu génère une erreur et se ferme.

Conditions de sortie

Les données utilisateur sont enregistrées dans un fichier, si l’utilisateur le souhaite, et son fichier est ajouté à un fichier journal de jeu.

Références additionnelles

Exercices

📚 Tester la compréhension

aucun quiz de vérification des concepts ici encore

🛠️ Pratique

  1. Il y a un projet formatif en lien avec cette leçon. Voir les instructions dans le Classroom.

Accueil > Matériel >

📚 Portes logiques

Survol et attentes

Les algorithmes sont une séquence d’étapes ou d’instructions pour résoudre un problème spécifique. Il nous faut maintenant une machine pour exécuter ces instructions afin de rendre l’exécution des algorithmes plus automatique et rapide. Le mécanisme de base utilisé pour implémenter les données, la logique et les opérations s’appelle une porte logique.

Définitions
Binaire
système où il existe seulement deux valeurs, comme haute/basse tension, 0/1, vrai/faux.
Logique booléenne
branche de la mathématique (nommée pour son inventeur Georges Boole) qui traite les équations de vérité, donnant toujours un résultat binaire : vrai ou faux.
Transistor
composant électronique qui agit comme un interrupteur actionné par un courant de contrôle. Les transistors sont maintenant fabriqués à l’échelle de quelques centaines d’atomes de large, plaçant plusieurs milliards de transistors sur une même puce électronique.
Bit
chiffre binaire (“binary digit” en anglais), soit 1, soit 0. Les chiffres utilisés pour représenter l’état d’un circuit avec les correspondances 1 = haute tension et 0 = basse tension.
Tableau de vérité
tableau indiquant l’état vrai/faux (ou 1/0) pour chaque combinaison possible des valeurs d’entrée. Les opérations booléennes, comme et, ou et non, sont définies dans des tableaux de vérité.
Porte logique
composant électronique qui combine des transistors de manière à effectuer des opérations booléennes sur les bits d’entrée.

Objectifs d’apprentissage

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

  • décrire le lien entre le système binaire implémenté dans les appareils électroniques et la logique booléenne;
  • reconnaître les symboles des portes logiques de base et déterminer leur sortie.

Critères de succès

  • Je peux décrire pourquoi les ordinateurs utilisent la logique booléenne comme principe fondamental.
  • Je peux analyser des circuits de portes logiques et déterminer leur sortie en fonction d’une entrée spécifique.

Notes

Ressources additionnelles

La liste de lecture Youtube Crash Course : Computer Science par PBS Digital Studios présente d’excellents survols visuels de ces concepts. Notamment, en lien avec cette leçon, les épisodes 2 à 6 sont pertinentes:

Exercices

📚 Tester la compréhension

Quiz de vérification sur les portes logiques

🛠️ Pratique

Exercices

Accueil > Matériel >

📚 Architecture von Neumann

Survol et attentes

La logique, les données, les instructions et la gestion des entrées et sorties d’un ordinateur sont organisées de manière assez uniforme dans la plupart des cas, peu importe le type d’ordinateur : portables, tablettes, cellulaires, serveurs, systèmes embarqués, etc. Cette organisation est appelée l’architecture von Neumann.

Définitions

La structure von Neumann est composée de quatres systèmes principaux. Les systèmes sont la mémoire (qui stockent les données), l’unité de contrôle (qui gère la prochaine commande à exécuter), l’unité arithmétique et logique (qui fait les opérations sur les données) et les entrées/sorties (E/S) (qui reçoivent et envoient des informations de/vers l’unité de contrôle). Les quatre systèmes sont liés par un bus de communication.

Objectifs d’apprentissage

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

  • Reconnaître et de nommer les quatres systèmes de l’architecture von Neumann
  • Classer différents composants matériels dans le système approprié

Critères de succès

  • Je peux nommer et décrire la fonction de la mémoire, de l’unité de contrôle, de l’unité arithmétique et logique et des entrées/sorties
  • Je peux classer des composants matériels communs dans l’un des quatre catégories de l’architecture von Neumann

Notes

Présentation rapide des composants d’un ordinateur

Composants d’un ordinateurParties de la carte mère
Source: Computer Info BitsSource: Computer Info Bits

Les images ci-dessus présentent un portrait assez complet de la structure d’un ordinateur.

Du point de vue d’un programmeur, par contre, les composants principaux à considérer sont les suivants car ils influencent la performance des logiciels (mémoire, stockage, processeur) et la communication avec l’utilisateur (périphériques d’entrée et de sortie).

  • Mémoire : stocke les données et les instructions en cours d’utilisation
  • Processeur : exécute les instructions et coordonne les opérations des autres composants; il y a généralement un processeur tout usage (CPU) qui délègue certaines tâches à un processeur graphique (GPU) dans les ordinateurs modernes.

    Les processeurs sont composés de plusieurs unités, notamment :

    • Unité de contrôle : coordonne les opérations des autres unités
    • Unité arithmétique et logique : effectue les opérations mathématiques et logiques
    • Mémoire cache : stocke temporairement les données et les instructions les plus utilisées pour réduire le temps d’attente de lecture de la mémoire principale
  • Stockage : stocke les données et les instructions de façon permanente (p. ex. disque dur, carte SD, clé USB). Le stockage est considéré comme un périphérique d’entrée/sortie même si le disque dur est souvent intégré plus étroitement à la carte mère.

    Une bonne illustration du fait que le stockage est un périphérique est le Raspberry Pi qui utilise une carte SD amovible comme disque principal.

  • Périphériques d’entrée et de sortie : matériel qui envoie ou reçoit de l’information du processeur (comme le stockage, mais aussi les écrans, souris, et les routeurs de réseau). Ils permettent à l’utilisateur (ou d’autres ordinateurs) de communiquer avec l’ordinateur.

Explication de la coordination de ce composants dans l’architecture von Neumann

Exemple de traçage du flux (de la séquence) d’opérations matérielles dans un ordinateur

Plusieurs détails liés aux logiciels du système d’exploitation ne sont pas inclus dans cette séquence. Ils font partie de la prochaine leçon.

Qu’est-ce qui se passe quand on lance une application?

  1. L’utilisateur envoie l’instruction de lancer l’application avec un périphérique d’entrée comme un clavier ou une souris.
  2. Le contrôleur des entrées/sorties reçoit ce signal d’interruption et passe l’instruction au contrôleur de l’UTC.
  3. Plusieurs logiciels du système chargés en mémoire s’occupe de l’instruction - toute une danse de communication entre la mémoire, et l’UTC (mémoire cache, unité arithmétique et logique, contrôleur) - pour finalement arriver à l’instruction de charger l’application en mémoire.
  4. L’unité de contrôle demande au contrôleur des entrées/sorties de lire le disque dur et charger l’application dans la mémoire.
  5. Le contrôleur des entrées/sorties envoie une instruction à l’UTC quand la lecture est terminée.
  6. L’UTC commence alors à exécuter l’application - une autre danse entre la mémoire et l’UTC incluant la gestion des signaux vers les périphériques de sortie (écran, haut-parleurs, imprimante, Internet, etc.) et reçus des périphériques d’entrée (souris, clavier, microphone, Internet, etc.).

Exercices

📚 Tester la compréhension

aucun quiz de vérification des concepts ici encore

🛠️ Pratique

Exercices

Accueil > Matériel >

📚 Périphériques externes

Survol et attentes

En bref

Vous n’avez pas besoin de comprendre la structure interne d’un ordinateur pour commencer à programmer, mais vous avez besoin d’un ordinateur fonctionnel qui répond à vos besoins! On regarde quelques options réelles pour différents composants matériels et on apprend comment comparer leurs performances.

Objectifs d’apprentissage

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

  • Reconnaître des termes et des unités communs pour décrire la performance ou la capacité des composants, comme GHz, MHz, Go et ips.
  • Déterminer quel composant entre deux options possède la meilleure performance.

Critères de succès

  • Je peux trouver les données de performance du matériel installé sur son ordinateur et sur des périphériques
  • Je peux identifier le composant avec la meilleure performance en se servant des données disponibles.

Notes

Exemple

Vous trouvez deux annonces pour des ordinateurs portables à 600$, les suivantes :

Option A

Portable de 15,6 po Vivobook 15 d’ASUS - Bleu calme (Core i5-1235U d’Intel/SSD 512 Go/RAM 8 Go/Windows 11) voir l’offre)

Option B

Portable de 15,6 po de HP - Argenté naturel (Core i5-1135G7 d’Intel/SSD 512 Go/RAM 16 Go/Windows 11 Home) voir l’offre)

Quels sont les avantages de A comparé à B?

Solution

C’est dur de le savoir en regardant simplement le nom et les spécifications succinctes du produit qui sont presque identiques.

À première vue, l’option B semble plus intéréssante car elle a le double de la mémoire vive (16Go de RAM contre 8Go). Mais en lisant les spécifications détaillées et en faisant un peu de recherche on trouve que le processeur de l’option A a 10 coeurs ou lieu de 4 et un mémoire cache de 12Mo au lieu de 8Mo. Donc A a le meilleur processeur.

Finalement, il y a aussi des différences sur le nombre et le type de ports, la vie de la batterie et la qualité de l’écran. Bref, ce sont des produits semblables et le meilleur dépend de ce qui est le plus important pour vous.

Exercices

📚 Tester la compréhension

Quiz de vérification sur les unités de performance des périphériques

Accueil > Logiciels du système >

📚 Représentation interne des données : les encodages binaires

Survol et attentes

Imaginez que tous les caractères dans votre livre, magazine ou blogue préféré sont remplacés par des codes numériques, un code unique par caractère. Ce serait assez dur à lire!

Imaginez maintenant que peut-être ces codes ne représentent PAS des lettres mais peut-être les données de couleur pour un pixel ou peut-être une valeur numérique ou peut-être une adresse en mémoire pour un fichier. C’est encore pire!

Comment faire sens de tout ça?

La mémoire et le stockage d’un ordinateur contiennent juste ces types de codes, mais les codes sont - en plus - en binaire! Plusieurs standards sont mis en place pour structurer ces informations et pour offrir des formats d’information communs. Il y a des opérations intégrées dans l’UAL ou disponibles comme algorithmes dans le système d’exploitation pour décoder ces informations correctement.

Définitions
Encodage
La conversion d’une valeur d’un type de données à un autre, p. ex. d’un caractère à un nombre ou d’un nombre à une couleur. Plusieurs encodages sont utilisés pour interpréter du binaire en données significatives.
ASCII
Acronyme pour American Standard Code for Information Interchange, un standard pour représenter des caractères en binaire avec un code de 7 bits (128 caractères). Au-delà du standard ASCII, il y a des encodages plus larges pour représenter des caractères de plusieurs langues, divers symboles et des émoticônes. Le standard Unicode est le plus universel, mais les standards ANSI et ISO sont aussi utilisés, notamment sur les systèmes Windows.
Nombre à virgule flottante
Un standard pour représenter des nombres décimaux en binaire, avec un bit pour le signe, un nombre de bits pour l’exposant et un nombre de bits pour la mantisse. Le standard IEEE 754 est le plus commun. Ce standard est implémenté directement dans l’unité d’arithmétique du processeur.
Hexadécimal
Une base numérique qui utilise 16 symboles (0-9 et a-f) pour représenter des valeurs en binaire de manière plus compacte et plus lisible pour les humains. Chaque symbole hexadécimal représente 4 bits. L’hexadécimal est souvent utilisé pour représenter des adresses mémoire, des couleurs RVB et des valeurs de configuration.
RVB
Acronyme pour Rouge, Vert, Bleu, un standard pour représenter des couleurs en binaire avec 3 octets (24 bits) pour chaque pixel. Chaque octet représente un niveau de couleur (0 à 255) pour chaque couleur primaire. Les couleurs RVB sont utilisées dans les écrans, les images et les vidéos.

Objectifs d’apprentissage

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

  • reconnaître des valeurs en différentes bases numériques : binaire, décimal et hexadécimal
  • décrire la structure interne de types de données comme les nombres entiers et les caractères

Critères de succès

  • Je peux identifier si une valeur est binaire ou hexadécimal connaissant la valeur décimal représentée.
  • Je peux décrire la représentation interne de différents types de données comme les nombres entiers, les caractères et les couleurs RVB

Les bases numériques

D’abord il faut réaliser que les valeurs dans un ordinateur sont toutes représentées en base 2 (avec seulement les nombres 0 et 1).

Toutes les bases numériques - décimal, binaire, hexadécimal, etc. - sont des systèmes de numération positionnels, où chaque position représente une puissance de la base.

  • La première position (à la droite) est la puissance 0, donnant toujours les unités.
  • La valeur à chaque position est un facteur à multiplier par la puissance de la base pour obtenir la valeur de la position. Par exemple, en décimal, la valeur 23 est décomposée comme suit :
23 
= 2 * 10^1 + 3 * 10^0 
= 2 * 10   + 3 * 1 
= 20       + 3

Comme les valeurs à base 10 (où chaque position représente la prochaine puissance de 10), les positions des bits dans les valeurs à base 2 représentent la prochaine puissance de 2.

Une autre base utile, rendant le binaire plus facile à digérer pour les humains, est l’hexadécimal, la base 16, où chaque position représente une puissance de 16. L’hexadécimal utilise un seul nombre pour chaque 4 bits. Par exemple, la valeur 1101 en binaire est équivalente à d (13) en hexadécimal.

Décimal (base 10)
Chiffres

0123456789

Préfixe conventionnel

Aucun. L’absence de préfixe indique que la valeur est en base 10.

Structure
Positions3210
Puissance103102101100
Valeur1000100101
NomMilliersCentainesDizainesUnités
Exemple

4628 =

  • 4 milles + 6 cents + 2 dizaines + 8 unités
  • 4000 + 600 + 20 + 8
Binaire (base 2)
Chiffres

01

Préfixe conventionnel

0b ou 0B devant la valeur pour indiquer que la valeur est en base 2.

On peut aussi placer le souscript 2 après le chiffre sans préfixe, p. ex. 11012 est équivalent à 0b1101

Structure
Positions3210
Puissance23222120
Valeur8421
NomHuitsQuatresDeuxUnités
Exemple

0b1101 =

  • 1 huit + 1 quatre + 0 deux + 1 unité
  • 8 + 4 + 0 + 1
  • 1310
Hexadécimal (base 16)
Chiffres

0123456789abcdef

Préfixe conventionnel
  • 0x ou 0X
  • # devant les couleurs RVB en hexadécimal
  • souscript 16, p. ex. 10016 est équivalent à 0x100 (et représente la valeur décimale 25610).
Structure
Positions3210
Puissance163162161160
Valeur4096256161
NomSeize cubeSeize carréSeizesUnités
Exemple

0x1f =

  • 1 seize + 15 unités
  • 16 + 15
  • 3110

Encodage binaire pour différents types de données

Type de donnéeExempleReprésentation interne (Encodage)
Nombre entier32, -12, 1000000Base 2 : 8 à 32 bits (1 à 4 octets) pour la valeur numérique en binaire
Caractères97 (‘a’), 50 (‘2’)ASCII : 7 bits faisant référence au caractère dans le tableau ASCII qui sont identifiés de 0 à 127; Unicode : plus universellement, un code de 8 à 32 bits (1 à 4 octets) pour représenter les caractères dans le tableau Unicode. Différents encodages sont possibles : UTF-8 (1 à 4 octets), UTF-16 (2 à 4 octets), UTF-32 (4 octets)
Nombres décimaux3.14, 2.5, -45900.1134IEEE 754 : 32 ou 64 bits pour spécifier un nombre à virgule flottante, avec 1 bit représentant le signe (-/+), 8 ou 11 bits représentant l’exposant et 23 ou 52 bits représentant la mantisse
Couleurs 24 bits RVB)rgb(255, 0, 0), #00ff00RVB : 1 octet (8 bits) représentant 256 niveaux de rouge, 1 octet pour l’intensité du vert et 1 octet pour l’intensité du bleu. Avec 8 bits, la gamme d’intensités est de 0 à 255 en décimal ou de #00 à #ff en hexadécimal; RVBA : en ajoutant un quatrième octet (nommé “alpha”), on peut spécifier la transparence d’une couleur.

Exercices

📚 Tester la compréhension

Quiz de vérification sur la représentation interne des données

🛠️ Pratique

Encodages de base

Enrichissement - algorithmes pour convertir les bases

Accueil > Logiciels du système >

📚 Langages de bas niveau

Survol et attentes

On sait qu’on peut créer des circuits et composants pour performer diverses opérations logiques et arithmétiques. On sait aussi que les bits peuvent être interprétés de différentes façons pour représenter différents types d’informations (p. ex. nombres, texte, couleurs).

Maintenant on va combiner les deux, fusionner le matériel et les données, pour savoir comment on peut programmer nos propres algorithmes.

Définitions

Ceci est une vue de très bas niveau et nous ne ferons pas de programmation comme ça… on va utiliser des abstractions puissantes - les langages de haut niveau - pour rendre la tâche plus facile. Mais si vous comprenez comment ça marche “sur le métal nu” vous comprendrez mieux pourquoi certains éléments du langage de haut niveau existent.

Langage de bas niveau
Langage de programmation où les instructions (codes d’opératipn) correspondent directement aux opérations des circuits de l’ordinateur et les données sont des références directes à des registres du CPU ou des adresses en mémoire vive. Le langage machine (binaire) et le langage assembleur (codes machines en mots/nombres lisibles par les humains) sont des exemples de langages de bas niveau. Le langage machine peut-être envoyé directement au processeur. Le langage assembleur a simplement besoin d’être traduit en langage machine (substituant les codes humains pour les codes binaires associés) avant d’être exécuté.
Langage de haut niveau
Langage de programmation qui doit d’abord être interprété ou compilé en langage machine par un logiciel spécialisé. Cette étape intermédiaire d’analyse du programme permet aux langages de haut niveau d’être plus naturels pour les humains, d’inclure des instructions puissantes (fonctions, boucles, lecture/écriture de fichiers, etc.) et des abstractions pour les données (objets, variables). Python, Java, C++, et JavaScript sont des exemples de langages de haut niveau.

Objectifs d’apprentissage

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

  • Nommer deux langages de bas niveau, le langage machine et le langage assembleur, et décrire leurs différences.
  • Décrire si les instructions de bas niveau font référence à la valeur ou à l’adresse des données

Critères de succès

  • Je peux donner une définition et deux exemples de langage de bas niveau.
  • Je peux décrire la structure des instructions de bas niveau et le rôle du compteur du programme.

Comment les langages de programmation fonctionnent à bas niveau

Démonstration (vidéo - du début à 3:30)

Une excellente explication des bases du fonctionnement se trouve dans la vidéo de Computerphile, notamment les trois premières minutes et demi.

Explication des instructions de bas niveau

Quelques points clés :

  • chaque donnée, incluant les instructions, se trouve à une adresse spécifique en mémoire vive
  • les instructions font toujours référence à l’adresse en mémoire et non à la valeur qu’elle contient; certaines instructions demandent de lire la valeur à cet endroit et d’autres demandent d’écrire une nouvelle valeur à cet endroit
  • il y a des adresses spécialisées sur le processeur pour stocker des valeurs à court terme durant les opérations - les registres de l’unité d’arithmétique et de logique
  • il y a un registre spécial qui stocke toujours l’adresse de la prochaine instruction à exécuter - le compteur du programme; certaines opérations modifient directement cette valeur, sinon l’adresse avance par un nombre de bits spécifique correspondant à la largeur des cellules en mémoire.

Ce qui n’est pas mentionné dans la vidéo mais qui est également important :

  • S’il y a des données que vous voulez utilisez en référence durant le programme ou conserver par la suite, il doit avoir des adresses en mémoire pour ces valeurs au delà des registres de l’UAL (qui seront réutilisés pour d’autres opérations)
  • On montre les instructions utilisant des mots qu’on peut lire et des nombres décimaux : Cela correspond au langage assembleur ou à un autre langage de bas niveau. Ces instructions seraient traduits en binaire (langage machine) pour l’exécution :
    • un code unique par mot-clé, chaque code correspondant à un circuit qui existe dans le processeur
    • toutes les valeurs sont en binaire
    • un format standard pour l’instruction, p. ex. 1 octet pour le code d’instruction et, selon l’instruction, des nombres spécifiques d’octets pour la 1e opérande et pour la 2e opérande.

Langages de bas niveau

Quand tout est en binaire (les codes d’opération et les opérandes), le code s’appelle le langage machine. Les 1 et 0 opèrent directement sur les circuits de l’ordinateur (les portes logiques et composants vus précédement).

Quand on remplace le code d’opération avec un mot-clé et on écrit les adresses avec des caractères (p.ex.: r1, 0x0004), le code s’appelle un langage assembleur. Il est équivalent au langage machine, mais lisible par les humains.

Ce sont les deux langages de bas niveau. Les opérations sont contraintes à représenter directement les circuits de la machine. Les langages de haut niveau n’ont pas cette contrainte.

Langages de haut niveau

Avec les langagages de haut niveau, on peut se permettre d’utiliser des mots-clés et des structures plus naturelles pour les humains sachant que ces instructions seront analysés par un logiciel (l’interpréteur ou le compilateur) qui les traduira en langage machine (ou produira des messages d’erreur si les instructions ne sont pas valides).

On trouve dans la plupart des langages de haut niveau des mot-clés comme : if-else (si, sinon), for (pour les éléments ou valeurs suivants), while (tant que la condition suivante est vraie), etc. Chacun de ces mots-clés seraient remplacé par toute une série de codes d’opération en langage machine.

Comparaison des différents types de programmes

Voici une comparaison de programmes écrites en trois version présumant les informations suivantes :

  • le code 0b0001 = 0x1 = 1 est l’opération d’enregistrement d’une valeur en mémoire dans le registre r1
  • le code 0b0010 = 0x2 = 2 est l’opération d’ajout d’une valeur à celle déjà dans le registre r1
  • le code 0b0011 = 0x3 = 3 est l’opération de stockage d’une valeur à une adresse en mémoire vive
  • la valeur 0b00011011 = 0x1b = 27 est une adresse en mémoire vive qui contient la valeur 2 (0b10 = 0x2)
  • la valeur 0b00011100 = 0x1c = 28 est une adresse en mémoire vive qui contient la valeur 3 (0b11 = 0x3)

Le but du programme est d’ajouter les valeurs 2 et 3 et le stocker en mémoire à l’adresse 0x1d.

Langage machine

Codes d’opération en binaire Adresses mémoire en binaire

0b0001 0b00011011
0b0010 0b00011100
0b0011 0b00011101

Langage assembleur

Mêmes codes d’opération en mots Mêmes adresses en hexadécimal

Load r1, 0x1b
Add r1, 0x1c
Store r1, 0x1d

Langage de haut niveau (Python)

n est une abstraction pour un endroit en mémoire L’addition de valeurs est représentée naturellement L’interpréteur de Python traduit le tout en langage machine.

n = 2 + 3

Accueil > Logiciels du système >

📚 Machines virtuelles

Survol et attentes

À part les périphériques comme le clavier, la souris, le microphone, etc. vous n’avez jamais manipuler directement les composants d’un ordinateur pour traiter les valeurs binaires.

Définitions

Entre vous et la machine réelle se trouve une ou plusieurs couches qui s’appellent la machine virtuelle. Elle est plus intuitive à utiliser. C’est sa tâche de traduire toutes vos interactions avec l’ordinateur en binaire pour la machine réelle.

Les parties de la machine virtuelle que vous manipulez s’appellent l’interface utilisateur. Il y a d’autres parties de la machine virtuelle qui sont utilisées par les logiciels afin qu’elles aient accès aux ressources de l’ordinateur. Cette partie s’appelle l’interface de programmation.

Un autre nom pour la machine virtuelle principale sur un appareil est le système d’exploitation, comme Windows, macOS, iOS, Android, Ubuntu.

Objectifs d’apprentissage

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

  • décrire le système d’exploitation et les grandes catégories de logiciels du système;
  • différencier la machine réelle de la machine virtuelle et l’interface utilisateur de l’interface de programmation;
  • différencier les types d’interfaces utilisateurs : ligne de commande, graphique et réalité virtuelle.

Critères de succès

  • Je peux identifier les types de logiciels du système et décrire ce que chacun fait.
  • Je peux décrire les différences entre une machine virtuelle et réelle et entre une interface utilisateur et une interface de programmation.
  • Je peux identifier le type d’interface utilisateur à partir d’une description ou d’une image.

Notes

On ne manipule pas des bits ni des octets en travaillant avec un ordinateur. On utilise un clavier, une souris, etc. On travaille alors avec une machine virtuelle qui traduit nos gestes en informations et instructions binaires pour la machine réelle. C’est encore un niveau d’abstraction plus élevée, comme les portes logiques, les composants et les systèmes de l’architecture von Neumann : les détails du fonctionnement de la machine réelle sont cachés.

Pour rendre un ordinateur pratique pour un utilisateur du grand public qui n’est pas nécessairement expert en informatique, les systèmes d’exploitation ont été développés, p. ex. MS-DOS, Apple DOS (des “Disk Operating Systems”), Unix, Windows, macOS, iOS, Android, Linux/GNU, ChromeOS. Un système d’exploitation est une suite de logiciels qui présente une interface entre l’utilisateur et le matériel physique. Cette interface est une machine virtuelle ou environnement virtuel.

La machine virtuelle fait les quatre choses suivantes :

  1. masquer les détails complexes du matériel physique de l’ordinateur en le gérant pour les utilisateurs et les applications;
  2. présenter l’information d’une façon naturelle (caractères, nombres décimaux, éléments graphiques, etc.) qui ne demande aucune connaissance de la structure interne des données;
  3. permettre un accès facile aux ressources disponibles sur l’ordinateur;
  4. prévenir les dommages accidentels ou intentionnels du matériel, des programmes ou des données.

Interface utilisateur, interface de programmation et interface interne

Les utilisateurs ne manipulent pas directement le matériel de l’ordinateur. Ils utilisent une interface utilisateur pour demander des ressources ou des services. Par exemple, un utilisateur peut cliquer sur une icône pour ouvrir un fichier. L’interface utilisateur, au moyen d’une suite de logiciels du système, traduit ce geste en une série d’instructions binaires pour la machine réelle.

interface utilisateur

De même, toutes les applications installées sur un système d’exploitation n’accède pas directement au matériel. Elles utilisent une interface de programmation pour demander des ressources ou des services au système d’exploitation. Par exemple, une application de traitement de texte n’écrit pas directement sur le disque dur. Elle demande au système d’exploitation de le faire pour elle.

interface de programmation

Dans les deux cas, les interfaces font le pont entre des demandes externes (d’une personne ou d’un programme) en les passant à une interface interne - aussi appelé le noyau - qui est en mesure d’exploiter les ressources matérielles directement via des données et des instructions en format binaire correspondant directement à l’architecture matérielle installée.

Services du système

Le noyau (interface interne) : pour gérer le matériel directement

  1. Gestion de la mémoire : allouer et libérer de l’espace mémoire pour les applications;
  2. Gestion des processus ou Programmateur : démarrer, arrêter, suspendre et reprendre des applications; bref, gérer la prochaine instruction à exécuter;
  3. Pilotes de périphériques : traduire les signaux des périphériques d’entrée en instructions utilisables par le système d’exploitation et les autres applications et vice versa pour les périphériques de sortie;
  4. Gestion des fichiers : organiser les fichiers sur le disque dur, les lire, les écrire, les effacer, etc.
  5. Gestion des autorisations : déterminer qui a le droit de faire quoi sur l’ordinateur; p. ex. si un utilisateur ou une application a le droit de faire certaines opérations normalement réservées au système d’exploitation.

Le shell (interface utilisateur)

  1. Interface en ligne de commande : une fenêtre de texte où l’utilisateur tape des commandes pour demander des services;
  2. Interface graphique : une fenêtre avec des icônes, des menus et des boutons pour demander des services;
  3. Utilitaires : des applications qui offrent des services spécifiques, comme un explorateur de fichiers, un éditeur de texte, un gestionnaire des tâches/performances, un navigateur web, un gestionnaire d’installation/désinstallation d’applications, etc. Ces utilitaires peuvent exister à la fois en versions ligne de commande et graphique, comme les gestionnaires de fichiers.
  4. Services de langages : des applications qui offrent des services de programmation, comme un assembleur, un compilateur, un débogueur, un éditeur de code, etc.

L’interface de programmation d’applications (API)

Les fonctions, structures de données et protocoles qui permettent aux applications de tous les niveaux (interne/noyau, utilisateur/shell) de demander des services aux autres applications. Par exemple, une application de traitement de texte peut demander à une application de navigateur web de lui afficher une page web via l’API de l’application de navigateur web.

Exercices

📚 Tester la compréhension

aucun quiz de vérification des concepts ici encore

🛠️ Pratique

  1. Énumérez les systèmes d’exploitation que vous avez déjà utilisés ou que vous utilisez encore.
  2. Identifiez 3 utilitaires que vous avez déjà utilisés sur un cellulaire ou un ordinateur. Pour chacun indiquez :
    1. son nom et sa fonction;
    2. s’il est un interface graphique ou en ligne de commande;
    3. s’il est directement lié à un service du noyau (oui/non - si oui, lequel).
  3. Énumérer 2 à 5 applications que vous avez installées sur votre cellulaire ou ordinateur.
  4. Activez vos connaissances existantes de l’architecture matérielle de l’ordinateur et les joindre aux nouvelles connaissances sur les services du système pour décrire une séquence d’opérations raisonnable1 pour :
    1. ouvrir un fichier;
    2. lancer le navigateur web;

    Vous devrez faire le geste pour voir au moins les étapes visibles de l’interface utilisateur (les différentes vues/fenêtres et interactions que vous faites). Ensuite, tentez d’associer vos gestes aux réponses du système via la machine virtuelle (services du système) et la machine réelle (architecture Von Neumann).


  1. Raisonnable signifie que la séquence devrait être cohérente avec les différents services du système et les différents composants matériels mais pas nécessairement complète ou exacte (ce qui nécessite plus de connaissances et plus de détails sur le contexte… ce sera un sujet pour un cours spécialisé sur les systèmes d’exploitation à l’université).

Accueil > Logiciels du système >

🛠️ Organisation des fichiers dans un système d’exploitation

Survol et attentes

Comment est-ce que votre système d’exploitation sait où trouver vos fichiers? ses propres logiciels du système? vos applications?

Définitions

Le système garde un fichier spécial dans les premières positions de tout disque qui est un tableau avec les adresses mémoire du début de tous les dossiers et fichiers sur le disque.

Les fichiers sont organisés dans une hiérarchie qui s’appelle une arborescence. Chaque dossier sur un disque représente une branche de l’arborescence et chaque fichier dans un dossier représente les feuilles sur cette branche.

Pour identifier un fichier ou un dossier précis, on écrit son chemin. Le chemin est une séquence de noms de dossiers séparés par des barres obliques (/ ou \) qui indique la route à suivre à partir de la racine du disque pour trouver le fichier ou le dossier.

Objectifs d’apprentissage

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

  • Lire et interpréter les chemins sur un système Windows ou Linux
  • Savoir comment naviguer à travers le système de fichiers avec un interface graphique (l’explorateur de fichiers) ou en ligne de commande (avec les commandes cd, ls, tree)

Critères de succès

  • Je suis capable d’écrire le chemin pour un dossier ou un fichier spécifique sur mon ordinateur.
  • Je suis capable de trouver des dossiers et des fichiers spécifiques sur mon ordinateur.

Structure hiérarchique (arborescence) des fichiers et dossiers

Comment sont organisés les fichiers dans votre dossier utilisateur? Il y a plusieurs façons de le voir avec les logiciels du système. On va en utiliser deux ici.

logo_explorateur Explorateur de fichiers (graphique)logo_terminal Terminal (ligne de commande)
window_explorateurwindow_terminal
TâcheExplorateur de fichiers (clics)Terminal (commandes)
Ce qui se trouve à la base de votre dossier utilisateurExplorateur de fichiers > Ce PC > Disque local (C:) > Utilisateurs > votre compte image ↗cd ~ ensuite ls image ↗
L’arborescence de votre dossier utilisateurOuvrir tous les sous dossiers manuellement en cliquant les flèches image ↗tree image ↗
Obtenir le cheminCliquer sur la barre de navigation image ↗pwd image ↗

Chemin d’accès (path) d’un fichier ou d’un dossier

L’adresse d’un fichier ou d’un dossier en format lisible par un humain s’appelle un chemin. Le chemin représente la séquence d’embranchements (dossiers) à suivre à partir de la racine du disque pour se rendre au dossier ou au fichier voulu.

Exercices

📚 Tester la compréhension

aucun quiz de vérification des concepts ici encore

🛠️ Pratique

Exercices

Accueil >

Partie B - Programmation

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.

Sommaire

Les unités précédentes vous donnent un grand portrait de comment un ordinateur fonctionne, tant du point de vue formel (algorithmes) que du point de vue matériel (circuits) et logiciel (la machine virtuelle).

Cette unité vous donne une première expérience pratique de la programmation, soit du développement de vos propres logiciels. Vous apprendrez un langage de programmation, Java, et vous apprendrez comment utiliser un environnement de développement intégré (EDI) pour unifier plusieurs des logiciels du système nécessaires pour écrire, compiler et exécuter un programme à partir du code source. Vous apprendrez aussi comment partager votre code avec d’autres personnes en utilisant Git et GitHub.

À la fin de cette unité, vous aurez une base suffisante en programmation pour :

  • implémenter n’importe quel algorithme;
  • apprendre n’importe quel autre langage de programmation selon vos besoins et vos intérêts.

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

💭 Quelques pistes de réflexion :

  • Dans le milieu du travail, les gens ne sont pas tous traités de façon équitable. Cela va pour la relation employeur-employé, pour les relations entre les employés et même pour la relation entre les utilisateurs et les fournisseurs de logiciels. Énumérez des exemples de situations où les gens ne sont pas traités de façon équitable pour chacune de ces relations.
  • Il existe des codes d’éthique pour les ingénieurs logiciels qui tentent de décrire un standard acceptable de comportement pour un professionnel dans le domaine. Parcourez le code d’éthique de l’IEEE-CS ou celui de l’ACM et notez quelques points que vous trouvez particulièrement frappants.
  • Sachant comment ça peut être difficile de produire même un simple projet logiciel complet, fonctionnel et à la hauteur de vos standards, discutez de l’importance de la propriété intellectuelle, des droits d’auteur et des licences dans le domaine du logiciel. Notamment, si l’auteur attend une compensation pour son travail (en lui attribuant une licence commerciale, p. ex.), est-ce que c’est acceptable de copier - pirater - son travail sans sa permission et sans le rémunérer?
  • Dans quelles situations est-ce qu’une licence commerciale peut-être considérée abusive?

Domaines d’application

Sommaire

Cette section, lorsqu’on a le temps de la faire, vous donne la chance d’explorer les possibitilités du génie informatique. L’unité précédente vous a donné une base en programmation, mais aucun travail ou projet qui ressemble aux logiciels que vous utilisez quotidiennement comme simple utilisateur.

Cette unité vous montre différents domaines d’application pour les solutions logicielles, vous permet d’explorer les piles technologies utilisées pour créer ces solutions, et vous donne la chance de créer un projet concret pour démontrer vos compétences.

À la fin de cette unité, vous serez capable de :

  • Inclure des bibliothèques et des frameworks dans vos projets Java afin d’incorporer des fonctionnalités avancées dans votre propre code;
  • Connaître quelles piles technologiques sont généralement utilisées pour différents types d’applications afin de mieux cibler un langage de programmation et des outils de développement pour un projet donné.
Lien avec l'éthique la société ou les carrières

💭 Quelques pistes de réflexion :

  • Explorez deux ou trois cheminements professionnels considérablement différents si votre objectif était de devenir :
    • développeur web;
    • développeur de jeux vidéo;
    • développeur de solutions de sécurité informatique
  • À l’école, votre rendement est plus étroitement lié à votre mérite qu’il le sera pour le reste de votre vie. Malheuruesement, le monde n’est pas juste ni méritocratique. Heureusement, il y a beaucoup de variété dans la qualité des situations. Quels sont des signes clairs que vous êtes dans une situation injuste? Que feriez-vous dans une telle situation? Quels sont des indicateurs fiables qu’une sitation est plus équitable?
  • Deux ressources importantes pour les communautés sous-représentées dans divers domaines technologoqiues sont le mentorat et les groupes de soutien. Trouvez un groupe de soutien ou un service de mentorat pour professionnels en informatique qui vise spécifiquement votre communauté. Notez comment y avoir accès et la nature des services offerts.

Ces leçons restent à développer… les sujets sont actuellement traités via des démonstrations et des projets concrets en classe.

  1. 🛠️ Diverses piles technologiques - des langages et des outils adaptés à la tâche
  2. 🛠️ Gérer des projets Java de groupe ou de plus grande taille avec Maven
  3. 🛠️ Utiliser le framework JavaFX pour créer des interfaces graphiques dans Java

Accueil > Programmer avec Java > Préprarer l’environnement de développement >

🛠️ Installation de logiciels

Survol et attentes

version imprimable

Définitions
Éditeur de texte
Un logiciel qui modifie des fichiers au format texte, soit qui ne contiennent que des caractères imprimables (comme les lettres, chiffres, ponctuation, espaces, etc.). Tout système d’exploitation vient avec un éditeur de texte simple, comme le Bloc-notes de Windows ou TextEdit de macOS. Les logiciels comme Word ou Docs ne sont pas des éditeurs de texte mais des traitements de texte, car ils ajoutent des informations de mise en forme dans le fichier qui ne peuvent pas être interprétées comme des caractères imprimables.
Langage de programmation
Une suite de logiciels qui traduisent des fichiers de texte (code source) en instructions pour l’ordinateur. Certains langages interprètent le code directement en binaire (p. ex. python) et d’autres compilent d’abord le code source (texte) en code objet (binaire) (p. ex. C++). Java est un langage compilé, mais le code objet n’est pas en binaire spécifique à un système d’exploitation, mais en bytecode. Ces fichiers .class sont ensuite interprétés par la machine virtuelle Java selon la machine spécifique.
Console ou terminal
Une interface en ligne de commande qui permet (entre autres) de lancer l’interpréteur ou le compilateur de votre langage de programmation sur votre code source, code objet ou bytecode. C’est ici qu’on lance les programmes pour les tester.
Éditeur de code
Pour la programmation, les éditeurs de code sont des éditeurs de texte qui viennent aussi avec des fonctionnalités supplémentaires pour faciliter la programmation, comme la coloration syntaxique, l’auto-complétion, la vérification de syntaxe, etc. Visual Studio Code, Sublime Text et Vim sont des exemples d’éditeurs de code. Comme les éditeurs de texte, les éditeurs de code sont flexibles et peuvent servir pour la programmation dans une variété de langages.
Environnement de développement intégré (EDI)
Un logiciel qui combine un éditeur de texte, un compilateur ou interpréteur (selon le langage), un débogueur et d’autres outils pour faciliter la programmation. Chaque EDI est conçu pour un langage de programmation spécifique contrairement aux éditeurs de code qui sont plus polyvalents. IntelliJ IDEA, Eclipse et NetBeans sont des exemples d’EDI pour Java.
Gestion du code source
Un système qui permet de suivre les modifications apportées à un projet de programmation. Git est le logiciel le plus commun pour ceci et vous permet de sychroniser votre code source avec un serveur distant (comme GitHub) pour le partager avec d’autres développeurs (incluant vous-même sur un autre ordinateur et votre enseignant).

Objectifs d’apprentissage

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

  • Installer vos logiciels de développement avec un installateur fourni par le développeur ou manuellement en extrayant les fichiers et en mettant à jour la variable Path de votre compte.
  • Configurer les applications via leur interfaces graphiques ou en ligne de commande

Critères de succès

  • Les programmes installés se lancent correctement.

Une des compétences le plus fondamentales pour un développeur est la capacité à installer ses outils de travail et à configurer son système pour le rendre fonctionnel.

Instructions d’installation

Liste des logiciels à installer localement (sur votre compte) et à configurer pour ce cours :

Langage de programmation

Trousse de développement Java (JDK) avec JavaFX -> par extraction d’un .zip et modification de la variable Path

Éditeur de code

Visual Studio Code -> installation avec une interface graphique + ajout d’extensions spécifiques à Java

Gestion des versions

Logiciel : Git pour Windows -> installation avec une interface graphique + configuration initiale au console

Serveur distant : GitHub -> création d’un compte + intégration avec VS Code

Accueil > Programmer avec Java > Préprarer l’environnement de développement >

🛠️ Environnements de développement intégrés (IDE) - VS Code et Codespaces

Fenêtre VS Code

Survol et attentes

Définitions
Palette de commandes
façon le plus efficace de lancer des commandes dans VS Code. Tapez Ctrl + Shift + P pour ouvrir la palette de commandes et ensuite tapez le début d’une commande pour la trouver. Les commandes utilisées récemment sont affichées en premier.
Projet
Dans VS Code, un projet est un dossier. VS Code tout seul peut ouvrir des fichiers de nombreux dossiers à la fois, mais les outils de gestion de projet, p. ex. ceux de Java, seront confus : ils présument que le dossier ouvert est la racine du projet et peuvent se comporter de façon inattendue si vous ne respectez pas cette convention.
Explorateur de fichiers
permet de naviguer dans les fichiers et dossiers de votre projet. Cliquez sur un fichier pour l’ouvrir dans l’éditeur. L’explorateur s’affiche à gauche de l’éditeur et montre uniquement le dossier ouvert. Vous pouvez sauter directement à l’explorateur en tapant Ctrl + Shift + E.
Terminal intégré
permet d’exécuter des commandes dans le terminal sans quitter VS Code. Tapez Ctrl + ` pour ouvrir le terminal intégré ou naviguer dans le menu “Terminal” puis choisir “New Terminal”.
Paramètres
permet de configurer VS Code. Tapez Ctrl + , pour ouvrir les paramètres ou naviguer dans le menu “File” puis choisir “Preferences” puis “Settings”.
Extensions
ajoutent des fonctionnalités à VS Code, le transformant d’éditeur de code en environnement de développement intégré. Tapez Ctrl + Shift + X pour ouvrir la vue des extensions ou cliquez sur l’icône dans la barre latérale gauche.

Objectifs d’apprentissage

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

  • Savoir ouvrir un projet dans VS Code
  • Savoir comment configurer les paramètres de VS Code
  • Savoir comment ajouter les extensions à VS Code pour le développement Java

Critères de succès

  • Je peux utiliser VS Code comme un environnement de développement intégré pour Java et passer d’un projet à un autre avec aisance.

Ouvrir un projet

Pour ouvrir un projet, il faut ouvrir un dossier dans VS Code. Voici comment faire :

  • Ouvrez VS Code.
  • Cliquez sur “File” dans la barre de menu.
  • Cliquez sur “Open Folder…”.
  • Trouvez le dossier que vous voulez ouvrir et cliquez sur “Select Folder”.

Vous pouvez aussi ouvrir un dossier à partir de votre Explorateur de fichiers Windows :

  • Naviguer au dossier voulu
  • Faites un clic droit sur le dossier, choisissez “Plus d’options” et choisissez “Ouvrir avec Code”.

Le nom du dossier que vous avez ouvert devient une section de la barre latérale gauche de VS Code. Cliquez sur le nom du dossier pour voir le contenu de ce dossier ou tapez Ctrl + Shift + E (le “E” est pour “Explorateur de fichiers”) pour le même effet.

Testez votre compréhension

Dans VS Code, ouvrez le dossier qui contient le fichier Demo.java que vous avez créé dans une leçon précédente. Si vous n’avez pas encore créé de fichier Java, simplement ouvrir le dossier ~/Documents dans VS Code.

Créer un fichier Java dans votre projet

Pour ajouter un fichier Java à votre projet, il suffit de créer un fichier avec l’extension .java dans le dossier de votre projet. Voici comment faire :

  • Cliquez sur le bouton “New File” dans l’Explorateur de fichiers de VS Code.

    Nouveau fichier

  • Tapez le nom du fichier suivi de l’extension .java, p. ex. HelloWorld.java.

  • Le fichier sera créé et ouvert dans l’éditeur de code.

C’est TRÈS IMPORTANT d’ajouter l’extension .java au nom du fichier, sinon les outils de Java ne fonctionneront pas correctement (même si VS Code peut reconnaître le code Java sans l’extension).

Testez votre compréhension

Créez le fichier HelloWorld.java dans votre projet.

Ajouter le code Java brisé suivant1. Ne le réparez pas pour l’instant. On verra très bientôt comment les outils Java nous aident à trouver les erreurs.

void main() {
   system.out.prinln("Hello, World!")
}

Enregistrement automatique - un paramètre utile

Avec les outils de travail dans le nuage, comme la suite Google ou la suite Microsoft Office, nous sommes habitués à ce que nos documents soient enregistrés automatiquements pour nous, mais ce n’est pas le comportement par défaut dans VS Code.

Pour activer l’enregistrement automatique, la façon la plus simple est :

  • d’ouvrir la palette de commandes avec Ctrl + Shift + P
  • taper Auto Save pour trouver la commande Files: Toggle Auto Save. S’il n’était pas activé, il le sera maintenant. S’il était activé, il ne le sera plus.

Vous pouvez savoir si l’enregistrement automatique n’est pas activé si le X à côté du nom du fichier dans l’éditeur n’est plus un X mais un cercle . Cela signale que les derniers changements dans l’éditeur n’ont pas été enregistrés sur le disque.

Attention : si vous tentez de lancer un programme à partir d’un fichier qui n’a pas été enregistré, c’est la dernière version enregistrée qui se lance, pas ce que vous voyez dans l’éditeur. Croyez-moi, vous pouvez perdre beaucoup de temps à chercher une erreur qui n’existe plus simplement parce qu’il y a cet écart entre ce que vous voyez et ce qui est enregistré.

Testez votre compréhension

  1. Regardez le nom du fichier HelloWorld.java dans l’éditeur. Est-ce que le X est un X ou un cercle? Le indique un fichier qui n’est pas enregistré.
  2. Basculer l’activation de l’enregistrement automatique et vérifier si le cercle devient un X.
  3. Ajoutez une ligne vide à la fin du code. Est-ce que le Xdevient un cercle ou reste un X?
  4. Répétez les deux dernières étapes une autre fois et notez la différence.
  5. Si l’enregistrement automatique n’est pas activé en ce moment, l’activer avant de continuer.

Extensions utiles pour programmer avec Java

C’est important d’avoir un peu d’équilibre dans la vie. Je vous suggère d’installer l’extension VS Code Pets en plus de l’extension de productivité ci-dessous. Choisissez un animal (ou un autre entité) de compagnie pour vous tenir compagnie pendant que vous travaillez.

On peut rendre VS Code plus puissant pour la programmation dans le langage de notre choix en ajoutant des extensions adaptées. Pour Java, ces extensions sont emballés dans le Java Extension Pack.

Ces outils vous aident à :

  • Trouver des erreurs dans votre code avant de l’exécuter
  • Exécuter votre code avec un bouton au lieu d’une commande
  • Trouver des suggestions pour améliorer votre code
  • Trouver des informations sur les classes et les méthodes que vous utilisez (si vous leur ajouter des commentaires de documentation)
  • Passer à travers le code pas-à-pas avec un débogueur pour voir comment il fonctionne ou pour trouver des erreurs de logique

Installez et configurer le Java Extension Pack

  1. Ouvrez la vue des extensions en tapant Ctrl + Shift + X (“X” pour “eXtensions”) ou en cliquant sur l’icône dans la barre latérale gauche.
  2. Tapez Java Extension Pack dans la barre de recherche.
  3. Trouvez la version de Microsoft et cliquez sur le bouton “Install”.

    Si vous avez une erreur indiquant qu’il faut installer un JDK, votre installation de Java est incomplète. Voir la leçon sur l’installation de Java pour résoudre ce problème (notamment la section sur les variables d’environnement).

  4. Attendre que l’installation soit complète… vous le saurez quand l’indicateur Java dans la barre d’état en bas de l’écran arrête son animation.
  5. Ouvrez les paramètres de VS Code avec Ctrl + ,.
  6. Changez le paramètre “inlay hints” :
    • Tapez java inlay dans la barre de recherche.
    • Trouvez le paramètre “Java: Inlay Hints”.
    • Changez la valeur de “literal” -> “none”. Cela élimine des notations dans le code qui n’existent pas dans le texte que vous avez écrit.
  7. Changez le paramètre du “mode de lancement” :
    • Tapez java launch dans la barre de recherche.
    • Trouvez le paramètre “Java: Server Launch Mode”.
    • Changez la valeur de “hybrid” -> “standard”. Cela assure que les outils d’analyse du code démarrent immédiatement quand vous ouvrez vos projets.
  8. Redémarrez VS Code.

Testez votre compréhension

Ouvrez le fichier HelloWorld.java dans VS Code. Vous devriez voir des lignes rouges apparaître dans le code. Ces lignes indiquent des erreurs dans le code. Cliquez sur une ligne rouge pour voir le message d’erreur.

Cliquez sur le bouton “Run” en haut à droite de l’éditeur pour exécuter le code. Vous devriez voir les mêmes messages d’erreur dans la console que ceux que vous avez vus en passant votre curseur sur les lignes rouges.

Si vous parvenez à décoder ces messages d’erreur (ils ne sont pas toujours très clairs), vous pouvez essayer de corriger le code et le lancer de nouveau. Comment interpréter ces messages est un art qui se développe avec l’expérience et la pratique.

Terminal intégré

En lançant votre programme Java avec le bouton “Run” dans l’éditeur, vous avez déjà utilisé le terminal intégré : c’est là que la commande derrière le bouton “Run” est exécutée et c’est aussi là que la sortie de votre programme est affichée.

Vous pouvez utiliser le terminal intégré dans VS Code exactement comme vous utilisez le programme Terminal. Les deux utilisent le même shell (bash, PowerShell, etc.) et les mêmes commandes.

Pour ouvrir le terminal intégré, tapez Ctrl + ` (le ` est la touche à gauche de Entrée sur un clavier francophone et à la gauche du 1 sur un clavier anglophone) ou naviguer dans le menu “Terminal” puis choisir “New Terminal”.

Testez votre compréhension

  1. Ouvrez le terminal intégré.
  2. Tapez ls pour voir la liste des fichiers dans le dossier de votre projet. Assurez-vous que HelloWorld.java est dans la liste.
  3. Tapez java HelloWorld.java pour lancer le programme dans le code source HelloWorld.java.
  4. Tapez clear pour effacer la console.
  5. Utilisez la flèche du haut pour charger la commande java HelloWorld.java (les flèches du haut et du bas permettent de naviguer dans l’historique des commandes). Tapez Enter pour lancer le programme une autre fois.
  6. Tapez exit pour fermer le terminal.

  1. Le code Java publié sur ce site applique le JEP 463 (mise à jour aux fonctionnalités de Java - notamment déclaration de classe explicite et méthode main d’instance au lieu de l’idiome psvm) qui est en deuxième révision depuis Java22 mais pleinement intégré dans l’extension Java de VS Code pour ce même Java22. Si vous utilisez un autre IDE, vous pourriez rencontrer des erreurs de compilation. Le but ultime est d’incorporer le JEP 477 dès que Java23 soit incorporé aux outils Java. Cette 3e révision inclut aussi l’importation automatique de méthodes statiques de la nouvelle classe java.io.IO et l’utiliation de print et readln similaire à Python (print/input) ou C++ (cout/cin).

Accueil > Programmer avec Java > Préprarer l’environnement de développement >

📚 Utiliser Java

Survol et attentes

Définitions
REPL
une interface de ligne de commande pour un langage de programmation qui attend une entrée tapée par l’utilisateur (Read), évalue l’expression tapée (Evaluate), affiche le résultat (Print) et recommence (Loop). Le REPL Java s’appelle jshell. Les REPL sont des moyens efficaces d’exécuter des petits morceaux de code pour voir comment ils fonctionnent.
Compilateur
un programme qui traduit le code source (texte) en code objet (binaire) et l’enregistre dans un fichier. Pour exécuter le programme, il faut utiliser le code objet. Le compilateur Java s’appelle javac - dit “Java C” - mais il ne produit pas de code objet. Il produit plutôt du bytecode Java et des fichiers .class. L’avantage du bytecode est qu’il peut être exécuté sur n’importe quelle machine où la machine virtuelle Java (JVM) est installée tandis que le code objet est spécifique à une machine.
Interpréteur
un programme qui traduit le code source en code objet et l’exécute directement. L’interpréteur Java - le Java Virtual Machine (JVM) - s’appelle java mais il ne prend généralement pas le code source en entrée. Il prend plutôt le bytecode Java produit par son compilateur. Par contre, depuis Java 11 le JVM peut interpréter directement un programme qui se tient à un seul fichier de code source .java.

Objectifs d’apprentissage

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

  • Distinguer un compilateur et un interpréteur.
  • Associer des extensions de fichiers (.java, .class, .exe) au type de code qu’ils contiennent : code source, code objet ou bytecode.
  • Décrire la fonction des commandes Java suivantes : javac, java, et jshell.

Critères de succès

  • Je peux utiliser les outils Java pour tester les exemples de code dans les notes de cours et pour lancer les programmes dans mon code source.

Travailler avec des extraits de code

Pour tester des extraits de code avec Java 9 ou plus récent, on peut utiliser un REPL Java.

Le REPL Java s’appelle jshell. Pour lancer jshell, tapez simplement jshell au terminal. Vous devriez voir un prompt jshell> qui attend votre entrée.

PS ~\Documents> jshell
|  Welcome to JShell -- Version 20.0.1
|  For an introduction type: /help intro

jshell>

Cela signifie que le terminal attend des instructions Java qu’il peut évaluer. Essayez d’entrer une expression simple comme 2 + 2 et appuyez sur Enter. Vous devriez voir le résultat de l’expression affiché à l’écran.

jshell> 2 + 2
$1 ==> 4

Le $1 est un numéro de référence pour le résultat de l’expression. Si vous entrez une autre expression, elle sera référencée par $2, et ainsi de suite. Vous pouvez utiler ces références plus tard pour accéder à la valeur.

jshell> $1
$1 ==> 4

Pour quitter jshell, tapez la commande /exit.

jshell> /exit
|  Goodbye

Travailler avec le code source

Le code source Java se trouve dans des fichiers avec l’extension .java. Il y a trois façons de lancer un programme Java à partir de son code source :

  • l’interpréter directement via une commande au terminal
  • compiler le code source en bytecode Java et interpréter le bytecode avec des commandes au terminal
  • avec les outils d’un EDI qui compilent et interprètent le code source pour vous

L’interpréter directement avec java

Pour interpréter votre code source directement, vous avez besoin de la version Java 11 ou plus récent. Au terminal, tapez la commande suivante :

java chemin/au/fichier/MonProgramme.java

où vous remplacez chemin/au/fichier/MonProgramme.java par le chemin complet vers votre fichier .java. Notez que vous devez inclure l’extension .java dans le nom du fichier pour l’interpréter directement.

Note : avec Windows, le séparateur de dossiers est le \. La commande java peut reconnaître les / mais si vous tentez d’utiliser la fonction de complétion automatique de Powershell, il remplacera les / par des \.

Quelques points importants :

  • Si vous êtes dans le dossier du fichier au terminal, le chemin complet est simplement le nom du fichier. Si vous avez ouvert le dossier du projet dans votre EDI, ce sera le comportement par défaut : l’EDI ouvre le terminal dans le dossier du projet.

    java MonProgramme.java
    
  • Si vous êtes dans un dossier différent au terminal, vous devez spécifier le chemin complet ou vous diriger dans le bon dossier au terminal avec la commande cd suivi par le chemin du dossier.

    java .\Documents\MonProgramme.java
    
    cd Documents
    java MonProgramme.java
    

Compiler et interpréter avec javac et java

Ici la même approche générale s’applique sauf qu’on lance le programme en deux étapes. L’avantage est que si le code source ne change plus, lancer le programme est plus rapide : le temps pour la compilation est déjà fait et on peut en profiter en utilisant le fichier .class déjà compilé.

Aussi, dans des programmes plus complexes qu’un seul fichier de code source, cette démarche est nécessaire pour lier les fichiers ensemble.

Vous êtes dans le bon dossier

Première fois :

javac MonProgramme.java
java MonProgramme

Après la première fois : on lance le bytecode déjà compilé

java MonProgramme

Notez que la commande java prend le nom de la classe, pas le nom du fichier .java.

Vous êtes dans un dossier différent

Première fois :

javac .\Documents\MonProgramme.java
java -cp .\Documents MonProgramme

Comme avec l’interprétation direct, il faut spécifier le chemin complet pour la première commande, dans ce cas javac. Pour la deuxième commande - java, on utilise l’option -cp (pour classpath) pour spécifier le dossier où se trouve le fichier .class. Finalement, on donne le nom de la classe.

Après la première fois : on lance le bytecode déjà compilé

java -cp .\Documents MonProgramme

Avec le bouton Run de l’EDI

Le bouton Run fait les mêmes choses que la section suivante mais le fait automatiquement en tentant de suivre votre structure de dossier.

Ces outils s’attendent à ce que vos programmes se trouvent directement à la racine du dossier du projet. Si vous avez des sous-dossiers pour organiser vos fichiers, le bouton Run peut ne pas fonctionner correctement et d’autres outils de l’EDI peuvent vous donner des avertissements ou des messages d’erreur. C’est le problème avec un outil qui tente d’automatiser : il ne peut pas toujours deviner ce que vous voulez faire.

Exercices

📚 Tester la compréhension

Quiz de vérification sur les outils Java

🛠️ Pratique

Ces instructions présument que vous avez fait le travail précédent (dans la leçon sur l’EDI) : le dossier “Documents” est ouvert dans VS Code et il contient le programme HelloWorld.java.

  1. Lancez le programme HelloWorld.java avec la commande java HelloWorld.java dans le terminal intégré de votre EDI.
  2. Compilez le programme HelloWorld.java avec la commande javac HelloWorld.java dans le terminal intégré de votre EDI. Rien ne devrait se produire sauf la création du fichier HelloWorld.class.
  3. Lancez le programme HelloWorld.class avec la commande java HelloWorld dans le terminal intégré de votre EDI. Notez que c’est seulement le nom de la classe qui est donné à la commande java pour utiliser le bytecode déjà compilé.
  4. Lancez le programme avec le bouton Run de votre EDI. Regardez attentivement la commande complète qui s’affiche à la console. Cette commande commence par la version longue de java, inclut quelques options que nous n’utilisons pas quand on lance la commande manuellement et se termine avec quoi : le nom d’un fichier ou le nom de la classe? Selon votre observation, est-ce que cette commande interprète directement le code source ou le compile d’abord avant de lancer le bytecode?
  5. Modifier et enregistrez le code dans HelloWorld.java, p.ex. pour afficher un message différent.
    • Interprétez directement le programme modifié avec la commande java HelloWorld.java.
    • Lancez la version compilée manuellement avec la commande java HelloWorld. Est-ce que la version compilée a changé? Si oui, qu’avez-vous fait pour que ça change? Si non, pourquoi pas?
    • Lancez le programme avec le bouton Run. Est-ce que le programme a changé? Les outils de l’EDI s’occupent de compiler automatiquement les fichiers de vos projets.

Accueil > Programmer avec Java > Préprarer l’environnement de développement >

🛠️ Partager le code avec Git et GitHub

Survol et attentes

Définitions
Dépôts
(“repos” en anglais) Les projets gérés avec Git sont appelés dépôts. Un dépôt est un dossier qui contient tous les fichiers de votre projet, y compris les fichiers de configuration de Git. Les dépôts sont stockés sur votre ordinateur et peuvent être partagés avec d’autres personnes sur GitHub. Les dépôts sur votre ordinateur s’appellent des dépôts locaux, tandis que ceux sur GitHub sont des dépôts distants.
Clôner
Pour copier un dépôt distant sur votre ordinateur, vous devez le clôner. Cela crée une copie locale du dépôt sur votre ordinateur. Vous pouvez ensuite travailler sur le projet localement et synchroniser les changements avec le dépôt distant.
Modifier
selon Git un fichier modifié a été soit édité, ajouté ou supprimé depuis la dernière version validée. Ces modifications existent sur votre disque dur, mais ne sont pas encore enregistrées dans Git. Git vous permet de supprimer les modifications depuis la dernière validation, au besoin, ce qui affectent les fichiers sur votre disque dur.
Indexer
(“stage” en anglais) indexer les modifications est comme un photographe qui choisit son sujet. Vous sélectionnez les fichiers modifiés que vous voulez inclure dans votre prochain instantané. Vous ne pouvez pas prendre une photo sans un sujet, et vous ne pouvez pas faire une validation sans avoir indexé au moins une modification.
Valider
(“commit” en anglais) toujours dans le contexte de la photographie, valider est comme prendre une photo. Vous créez un instantané de votre projet à un moment donné. Vous pouvez revenir à cet instantané à tout moment pour voir ce à quoi ressemblait votre projet à ce moment-là.

Il faut toujours identifier un commit avec un court message; il n’est pas possible de compléter l’opération sans ce message.

Synchroniser
(“sync” en anglais) synchroniser un dépôt veut dire combiner et mettre à jour les validations du dépôt local et du dépôt distant. Toutes les validations qui ne sont pas communes seront partagées. Ainsi, une synchronisation poussera les validations locales vers le dépôt distant et tirera les validations distantes dans votre dépôt local.

Cette opération peut générer des conflits de fusion des validations. Si cela se produit, demandez de l’aide à votre enseignant.

Objectifs d’apprentissage

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

  • Clôner un projet partagé via GitHub
  • Modifier, indexer et valider des fichiers dans un dépôt local
  • Synchroniser les changements avec le dépôt distant

Critères de succès

  • Je suis capable d’utiliser Git (dans VS Code) et GitHub (dans un navigateur web) pour archiver mon code et le partager avec mes enseignants.

Clôner un projet partagé via GitHub

Votre enseignant vous prépare des gabarits de projets, parfois juste des dossiers vides, pour organiser vos programmes dans le cours. Pour les utiliser, vous devez les clôner dans votre ordinateur. Voici comment faire :

  • Acceptez d’abord la tâche dans GitHub Classroom, ce qui vous créera une copie personnelle du projet sur GitHub.

  • Ouvrez VS Code.

  • Ouvrez la palette de commandes avec Ctrl + Shift + P.

  • Tapez “clone” pour trouver la commande Git: Clone et cliquez dessus. Si vous l’avez déjà utilisée, elle apparaîtra dans la liste des commandes récentes sans avoir à la chercher.

    Si c’est la première connexion, VS Code vous demandera de vous connecter à GitHub. Suivez ATTENTIVEMENT les instructions pour vous connecter. Si on vous propose une option “toujours…”, cochez-la pour éviter des problèmes de connexion à l’avenir.

    À SURVEILLER : Parfois la fenêtre pour une des étapes d’authentification apparaît en arrière-plan, bloquant la connexion. Regarder la barre des tâches pour voir s’il y a des nouvelles fenêtres ouvertes.

  • Cliquez ensuite sur Clone from GitHub et attendre que la liste des dépôts apparaisse. Puisque vous n’en avez pas beaucoup, votre dépôt sera facile à trouver dans la liste.

  • Cliquez sur le dépôt que vous voulez clôner et choisissez un dossier où le clôner dans la fenêtre qui s’ouvre. Je suggère fortement d’utiliser votre dossier “Documents” pour tous les projets.

  • VS Code créera un dossier avec le nom du dépôt à cet endroit et copiera le projet dedans.

  • VS Code vous proposera d’ouvrir le dossier clôné avec les options suivantes :

    • Open : ferme le projet ouvert actuellement et ouvre le projet clôné
    • Open in New Window : ouvre le projet clôné dans une nouvelle fenêtre de VS Code (gardant le projet actuellement ouvert dans la fenêtre originale)
    • Add to Workspace : ajoute le projet clôné à la liste des dossiers actuellement ouverts dans VS Code (utile si vous avez plusieurs projets ouverts en même temps)

      En général, les options Open ou Open in New Window sont les plus simples à gérer.

Clônez une seule fois, synchronisez par la suite

Si vous avez déjà clôné un projet, vous n’avez pas besoin de le clôner à nouveau pour continuez à travailler dedans. Vous pouvez simplement ouvrir le dossier du projet dans VS Code. Il y a même une section “Open Recent” dans le menu “File” pour vous aider à retrouver les projets que vous avez ouverts récemment.

Testez votre compréhension

Clônez le projet “pratique” dans votre dossier “Documents” et ouvrez-le dans VS Code. Le lien pour le gabarit de ce projet est partagé avec vous dans Classroom.

Modifier, indexer et valider des fichiers dans un dépôt local

Cette section vous montre comment apporter des changements dans votre dépôt local, les conserver avec Git et les partager avec le dépôt distant, sur GitHub.

Modifier

Cette étape est la partie normale de la production de code : vous créez, modifiez et supprimez des fichiers en faisant vos exercices ou en développant vos programmes. Assurez-vous de travailler dans un dossier de projet et non sur des fichiers isolés. Seulement les fichiers dans un dépôt local - comme ceux que vous clônés de GitHub - peuvent être indexés et validés. Les dépôts locaux sont tous des dossiers.

Vous savez que vous faites des modifications dans un dépôt local si vous voyez un “M” à côté des noms des fichiers modifiés ou un “U” (pour “Untracked”) pour les fichiers que vous venez de créer. Si vous faites des modifications et vous ne voyez pas ces indicateurs, il y a deux possibilités : vous n’avez pas enregistré les fichiers sur le disque (activez l’enregistrement automatique) ou les fichiers ne sont pas dans un dépôt (utiliser l’Explorateur de fichiers Windows pour les déplacer dans votre dépôt).

De plus, vous devrez utiliser les outils de travail (les extensions Java, le terminal intégré, etc.) pour vous aider à écrire du code Java valide et fonctionnel. La leçon précédente sur VS Code vous explique davantage ces outils. En d’autres mots, vous devrez tester votre code avant de passer aux prochaines étapes.

Indexer et valider

En plus des indicateurs de modification mentionnés plus haut, vous pouvez voir le nombre de fichiers modifiés sur l’icône de l’outil Git dans la barre latérale de VS Code. Vous pouvez seulement indexer et valider des fichiers modifiés, alors vous pouvez seulement passer à cette étape quand il y a un nombre affiché.

Ces étapes peuvent se faire au terminal - et vous avez beaucoup plus de possibilités au terminal - mais VS Code vous offre une interface graphique pour les faire, ce qui est plus convivial. Voici comment l’utiliser :

  • Cliquez sur l’icône “Source Control” dans la barre latérale de VS Code ou en tapant Ctrl + Shift + G (“G” pour “Git”).
  • Les fichiers modifiés apparaîtront dans la liste “Changes”. Ce sont tout les fichiers qu’il est possible d’indexer.

    Vous pouvez cliquer sur un fichier pour voir les modifications qu’il contient. Les lignes en vert sont les ajouts, les lignes en rouge sont les suppressions.

  • Pour indexer un fichier, cliquez sur le + à côté du nom du fichier. Cela ajoutera le fichier à la liste des fichiers indexés. Vous pouvez indexer tous les fichiers en cliquant sur le + en haut de la liste (à côté de “Changes”). Les fichiers indexés sont les sujets de l’instantané que vous allez prendre en validant les modifications.
  • Écrire un message qui décrit les modifications indexées. Il est impossible de compléter la validation sans ce message. Il y a une zone de texte en haut du panneau de contrôle pour taper ce message. Voici quelques exemples de messages courts et descriptif :
    • Salut.java et capture pour l’exercice 1
    • ajout de commentaires pour l’exercice 2
    • progession sur Projet.java
    • correction d’erreurs dans Main.java
  • Valider les modifications indexées en cliquant sur le bouton “Commit” (le bouton avec la coche). Cela prend un instantané des modifications indexées. Ces modifications sont maintenant enregistrées dans Git, mais seulement sur votre ordinateur dans le dépôt local.

    Si vous oubliez d’écrire un message, VS Code bloque l’opération et ouvre un éditeur de message à la droite. Écrivez votre message sur une ligne qui ne commence pas par # (qui est un commentaire) et fermez l’éditeur (en l’enregistrant au besoin). La validation se complétera automatiquement.

Testez votre compréhension

Modifiez le fichier “README.md” dans le projet “pratique” : ajouter une ligne vide ou supprimez quelque chose. Indexez et validez le fichier avec un message descriptif.

Visitez votre dépôt sur GitHub pour voir que les modification n’ont pas été partagées. Rafraîchissez la page pour être certain. Ces étapes ont seulement créé une version qui peut être partagée avec un dépôt distant. Il faut faire la synchronisation pour partager cette version.

Synchroniser les changements avec le dépôt distant

Valider et synchroniser du code est comme la photographie. Un cliché se trouve d’abord juste sur l’appareil du photographe. Pour les partager avec le monde, le photographe doit les téléverser sur une plateforme de partage, comme un réseau social.

Partager des instantanés de code est pareil. La validation enregistre l’instantané sur votre ordinateur mais vous devez encore le téléverser sur le dépôt distant (GitHub), soit le synchroniser avec ce dépôt. Voici comment faire :

  • Après la validation, le bouton “Commit” devient “Sync Changes” avec le nombre de validations à pousser. S’il y a aussi des validations sur le dépôt distant qui ne sont pas sur votre ordinateur, le bouton affichera le nombre de validations à tirer.
  • Pour synchroniser, il suffit de cliquer sur le bouton.

Bonnes habitudes pour éviter les conflits de fusion

Pour éviter d’avoir à fusionner les validations à tirer avec les validations à pousser, vous devrez suivre ces règles d’or : toujours synchroniser (tirer) avant de commencer à travailler sur un projet et toujours synchroniser avant de fermer l’ordinateur. De cette façon, tout ce qui est à tirer arrive seul au début d’une session de travail, laissant seulement les modifications à pousser à la fin de la session de travail. De plus, vous commencerez toujours votre session avec la version la plus à jour du projet.

Après votre première synchronisation, VS Code vous demandera si vous voulez tirer périodiquement les modifications distantes. Je vous recommande de le faire (ce qui inclut une synchronisation chaque fois que vous ouvrez le projet), mais vous pouvez choisir de le faire manuellement si vous préférez afin de développer la discipline de synchroniser avant de travailler.

Vous devrez tout de même développer la discipline de synchroniser à la fin des périodes de travail.

Quand vous travaillerez en équipes (l’année prochaine), il y a des stratégies plus avancées pour éviter les conflits de fusion - notamment l’utilisation de branches, mais pour vos projets personnels, cette règle est suffisante et efficace.

Testez votre compréhension

Synchronisez les modifications du projet “pratique” avec le dépôt distant. Visitez votre dépôt sur GitHub pour voir que les modifications ont été partagées. Rafraîchissez la page au besoin.

Accueil > Programmer avec Java > Les bases de Java >

📚 Structure d’un projet Java

Survol et attentes

Définitions

Dans les programmes que nous écrirons, nous verrons des éléments de structure entre ‘expression’ et ‘méthode main’ de façon régulière. Occasionnellement nous aurons à utiliser une classe, un package ou un framework plus explicitement. Mais cette liste couvre les éléments d’un programme Java de la plus petite à la plus grande structure possible.

Expression
combinaison minimale de texte dans le code source qui respecte la syntaxe du langage de programmation. D’autre texte produit des erreurs de syntaxe. Les expressions Java peuvent être des valeurs littérales, des variables, des opérations sur les données, des appels de méthodes, etc.
Instruction
une expression ou combinaison d’expressions qui effectue une action. Les instructions Java se terminent toujours avec un point-virgule ;. Ils peuvent être des déclarations de variables, des appels de méthodes ou des opérations sur les données.
Bloc de code
un ensemble d’instructions associé à la même partie d’un programme. En Java, les blocs de code sont délimités par des accolades {}. Pour améliorer la lisibilité du code, les blocs de code sont souvent indentés. Les outils de votre éditeur de code peuvent vous aider à indenter automatiquement le code.
Méthode
un bloc de code nommé qui a un type (comme les variables) et qui peut recevoir des informations affectant son résultat. En programmation plus généralement, cette structure s’appelle une fonction. Dans Java, toutes les fonctions sont définies dans des classes et le nom officielle pour ce type de fonction est une méthode.
Méthode main
le point d’entrée du programme1 et le seul bloc de code qui doit absolument être déclaré dans tout programme Java. C’est la méthode que Java exécute automatiquement lorsqu’on lance le programme. Au plus simple2, elle a la signature void main() - ce que nous utiliserons dans nos programmes, mais cette signature peut se développer jusqu’à la signature complète, traditionnelle public static void main(String[] args). Ce cours n’expliquera pas tous les détails de la signature traditionnelle.
Classe

Une structure qui définit un objet, soit une unité de programmation qui a des données (nommées attributs) et des comportements (nommés méthodes). Java étant un langage orienté-objet, la classe est la plus petite structure autonome possible. Tous les programmes Java se tiennent à l’intérieur d’une ou de plusieurs classes.

Dans nos programmes, la classe unique sera déclarée automatiquement2 la plupart du temps, nous laissant définir les méthodes et attributs directement dans le fichier .java. Parfois, et de manière plus commune dans les grands projets Java, les classes sont des blocs de code déclarées avec le mot-clé class.

Package
un conteneur pour des classes Java, associé à un dossier sur le disque dur. Les packages sont utilisés pour organiser les classes en groupes logiques et gérer l’accès aux classes. Dans tous nos programmes, le package de notre code est implicite et ne sera pas déclaré dans le code source.
Module
un conteneur pour des packages Java (depuis Java9) associé à un fichier module-info.java. Les modules sont utilisés pour organiser les packages en groupes logiques pour créer des applications modulaires, n’utilisant que les packages nécessaires pour l’application. Dans tous nos programmes, le module de notre code est implicite et ne sera pas déclaré dans le code source.
Framework
une collection de modules Java qui fournissent des fonctionnalités prêtes à l’emploi pour des applications spécifiques. Les frameworks Java les plus connus sont JUnit (pour les tests unitaires), Spring (pour le développement web), Hibernate (pour la persistance des données) et JavaFX (pour les interfaces graphiques).
Application
une collection de modules Java - ceux du langage, ceux que vous avez écrits et ceux de divers frameworks - qui forment un programme complet.

Objectifs d’apprentissage

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

  • Nommer la structure essentielle à tout programme Java.
  • Comprendre le rôle des accolades et des points-virgules pour séparer les parties d’un programme Java.
  • Expliquer l’utilité d’une bonne indentation.

Critères de succès

  • Je peux créer un fichier .java et écrire le code de base pour un programme Java.
  • Je peux lancer un programme Java au terminal et résoudre des erreurs de syntaxe, au besoin, pour le faire fonctionner.

Exemple - un programme qui affiche “Bonjour” à la console

Voici les étapes pour produire ce programme :

  1. Créer un fichier qui s’appelle Bonjour.java - l’extension .java est important. D’autres éléments du nom seront expliqués dans la prochaine leçon.

    C’est possible que les outils de votre EDI suggèrent l’insertion d’une classe du même nom, p. ex. public class Bonjour { }, dès que le fichier s’ouvre dans l’éditeur. Vous devrez effacer ce code afin de maintenir la structure minimale pour ce programme.2

  2. Définir une méthode main. Les deux accolades {} représentent les limites du contenu de la méthode.
    void main() {}
    

    Si vos outils Java sont activés, vous devrez voir deux liens : Run | Debug apparaître au-dessus de la méthode main. Cela signifie que le compilateur Java a reconnu la méthode main comme le point d’entrée du programme.

  3. C’est utile de taper Entrée entre les deux accolades pour créer un espace pour les instructions. Vous verrez que l’accolade fermante est placée directement sous le début de la ligne void main() tandis que le curseur est indenté vers la droite. Cela est une convention de format pour nous aider à voir que les accolades sont bien fermées pour les différents blocs de code.
    void main() {
        
    }
    
  4. Ajouter des instructions à l’intérieur de la méthode main, soit entre ces deux accolades. Dans ce cas-ci, on inclut une seule instruction qui affiche le texte “Bonjour!” à la sortie du système (la console)
    void main() {
         System.out.println("Bonjour!");
    }
    

    Notez que l’instruction se termine avec un point-virgule ;. C’est nécessaire pour toutes les instructions Java.

  5. Lancez le programme avec le bouton Run - soit celui au-dessus de la méthode main dans l’éditeur, soit le bouton Play en haut à droite de VS Code.

    Normalement, je préfère lancer les programmes au terminal avec ma propre commande soit java NomDuFichier.java (ici ce serait java Bonjour.java) mais la structure simplifiée présentée ici exige une commande un peu plus complexe en date de Java23 java --enable-preview NomDuFichier.java.

  6. Vous devriez voir le message “Bonjour!” affiché à la console, sinon un message d’erreur s’il y a un problème avec le code. Les messages d’erreur incluent plusieurs informations pour vous aider à trouver le problème, notamment le numéro de ligne où le programme a planté.

Indentation

L’indentation dans l’exemple ci-dessus est très important pour la lisibilité du code et pour vérifier que les accolades sont bien fermées. Notez que l’accolade fermante est visible directement en dessous de la définition de la structure qu’elle ferme (ici la méthode, mais il y en aura d’autres). C’est une convention de style très utile parce que c’est facile d’oublier ou d’effacer une accolade par accident.

VS Code devrait créer cette indentation automatiquement, mais c’est facile de le briser en modifiant ou en déplaçant du code. Utiliser régulièrement la commande “Mettre le document en forme” (dans VS Code : Alt+Shift+F ou clic droit > “Mettre le document en forme”) pour maintenir une indentation correcte. Voici des moments quand vous devriez utiliser cette commande :

  • après avoir copié-collé un exemple de code
  • après avoir déplacé des instructions ou un bloc de code que vous avez écrit
  • si vous ne voyez plus les accolades fermantes sous la signature du bloc de code (comme void main()) parce qu’il y a d’autre code dans le chemin
  • si toutes vos instructions dans un même bloc de code ne sont pas alignées verticalement
  • avant de rendre un programme lors d’une évaluation

Erreurs de syntaxe communes

Notez que “Mettre le document en forme” ne fonctionne pas s’il y a des erreurs de syntaxe, soit du code qui n’est pas compris par le compilateur Java. Si les outils Java sont activés, ces erreurs seront signalés pendant que vous tapez votre code : l’endroit de l’erreur est souligné en rouge. Si vous passez votre curseur sur cet endroit, vous pouvez lire dans une info-bulle un message décrivant l’erreur. Si les outils Java ne sont pas activés, vous verrez le même message d’erreur et des informations sur son endroit dans le code quand vous tentez de lancer le programme.

Voici quelques exemples d’erreurs communes :

  • des {, (, ; ou des " manquants ou de surplus dans le code
  • mauvaise capitalisation d’un mot, p. ex. string au lieu de String
  • mot mal écrit, p. ex. print1n (le chiffre 1) au lieu de println (la lettre L minuscule)
  • du texte qui n’est pas emballé dans des guillemets (") -> ce texte est alors interprété comme le nom de quelque chose qui devrait exister dans le code mais qu’on ne trouve pas.

Exercices

📚 Tester la compréhension

Quiz de vérification sur la structure d’un programme Java

🛠️ Pratique

Travaillez dans le répertoire GitHub partagé par votre enseignant pour la pratique et les exercices

  1. Créez un fichier Salut.java et écrivez le code pour afficher le message “Salut gang!” à la console. Assurez-vous que le code fonctionne.
  2. Créez un dossier captures dans votre répertoire de travail.
  3. Prenez une capture d’écran du lancement réussi de votre programme au terminal et l’enregistrez dans “captures”. Nommez le fichier 4-1-Structure.png.

    Sur Windows, l’outil de capture d’écran se lance avec la touche Windows + Shift + S. Une fois l’image capturé, cliquez sur l’icône de notification pour ouvrir l’éditeur des captures. Enregistrez l’image dans le dossier “captures” de ce dossier.

  4. Faites un commit et synchronisez vos changements avec GitHub.

À la fin de cet exercice, votre répertoire devrait avoir la structure et le contenu suivants :

pratique-[votre nom]
|---captures
|   `---4-1-Structure.png
|---README.md
`---Salut.java

  1. main n’est pas juste le point d’entrée pour les programmes Java. C’est une convention qui se trouve implicitement ou explicitement dans un grand nombre de langages, comme Python, C++, C#, Go, etc.

  2. La déclaration implicite d’une classe (ne pas avoir à déclarer une classe dans le code source) est une simplification permise pour les petits projets depuis Java22 et le JEP463. Cette simplification permet aussi une méthode void main() comme point de départ pour le programme. ↩2 ↩3

Accueil > Programmer avec Java >

🛠️ Communication pour les développeurs

Cette page est une référence, même une liste de vérification, pour l’ensemble de vos travaux. Vous pouvez revenir à cette page pour vous assurer que vous avez tout ce qu’il vous faut en termes de communication à l’intérieur de vos programmes.

Notez qu’il y a souvent des éléments de communication externes (comme le nom des fichiers, le format de divers documents de planification ou preuves, la façon de rendre vos travaux, etc.) qui ne sont pas inclus ici.

Survol et attentes

Créer des algorithmes est difficile. Combiner un paquet d’algorithmes faits par différentes personnes ou par la même personne à différents moments est encore plus difficile!

C’est donc important de rendre le code le plus clair possible en choisissant des noms appropriés et en laissant des commentaires descriptifs et explicatifs dans le code là ou nécessaire.

Définitions
Identifiant

un identifiant est un mot avec une syntaxe valide en Java qui représente une classe, une méthode, une variable ou une constante. Les caractéristiques principales d’un identifiant sont qu’il doit être unique dans son contexte et qu’il doit être descriptif.

La leçon précédente a déjà introduit les classes et les méthode, mais nous verrons dans des leçons ultérieures exactement c’est quoi une variable et une constante.

Convention de nommage

pratiques communes qui rendent le code plus lisible et plus facile à comprendre. Si ces règles ne sont pas respectées mais la syntaxe est encore valide, le code fonctionnera mais sera difficile à lire et à maintenir.

Commentaires de ligne ou de bloc

des annotations dans le code qui expliquent le code, notamment la logique des algorithmes, mais qui sont ignorées lors de la conversion du code en langage machine. Les commentaires de ligne débutent par // et les commentaires de bloc sont emballés entre /* et */.

Documentation interne

commentaires descriptifs qui aident les autres développeurs à comprendre le code. Avec Java, cette documentation se fait avec le javadoc. Les outils de votre EDI reconnaissent généralement le javadoc et présentent des info-bulles avec le contenu du javadoc quand votre curseur se trouve sur le nom d’un élément documenté. Le javadoc précède immédiatement la déclaration (méthode ou variable globale) qu’il documente; il est emballé entre /** et */ (notez le deuxième astérisque au début).

Objectifs d’apprentissage

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

  • Reconnaître si un identifiant respecte ou non les règles strictes de Java.
  • Distinguer les classes, les méthodes, les variables et les constantes en appliquant les conventions de nommage de Java.
  • Distinguer les trois types de commentaires en Java et décrire la fonction de chacun.

Critères de succès

  • Je peux nommer descriptivement les classes, les méthodes et les variables dans le code en applicant les règles et les conventions de Java.
  • Je peux ajouter des commentaires explicatifs dans le code, notamment pour la déclaration de variables et pour les structures de contrôle (sélection, itération).
  • Je peux écrire le javadoc pour la classe et ses méthodes.

Règles Java pour les identifiants

Les règles suivantes s’appliquent quand vous choisissiez des noms pour les classes, les méthodes, les variables et les constantes dans Java. Le programme plantera si ces règles ne sont pas respectées:

  • Caractères permis : L’identifiant est composé uniquement des caractères a-z, A-Z, 0-9, _ et $. Notez qu’il n’y a pas d’espaces dans cette liste. Les lettres avec accents sont aussi acceptés, mais à cause de la complexité des différents encodages de caractères, il est préférable de les éviter.
  • Les majuscules et les minuscules font une différence : Les identifiants sont sensibles à la casse. Le même mot écrit avec des majuscules ou des minuscules fait référence à deux identifiants distincts.
  • Aucun chiffre au début : Les identifiants ne peuvent pas commencer par un chiffre.
  • Pas un mot clé : Les identifiants ne peuvent pas être des mots-clés Java, p. ex. public, class, void, int, for, if, etc. Ces mots sont déjà définis par Java et ne peuvent pas être utilisés pour autre chose.

Conventions Java pour les identifiants

Différents langages encouragent différents styles de noms pour les identifiants pour les différents élements du programme. Cela aide les développeurs à associer une fonction à un identifiant simplement en observant sa forme.

Voici les conventions de Java.

Casse chameau pour les méthodes et les variables

Tous les noms de variables et de méthodes respectent la casse chameau (“camel case” en anglais) : toutes les lettres sont minuscules et chaque mot suivant commence par une majuscule. Par exemple : nomDeVariable, nomDeMethode().

Pourquoi le nom chameau? Parce que les majuscules au milieu des mots ressemblent aux bosses sur le dos d’un chameau.

Casse pascal pour les classes et les noms de fichiers

La casse pascal et comme la casse chameau, mais la première lettre est aussi une majuscule. Par exemple : NomDeClasse.

C’est vraiment important d’écrire les noms de classe avec une majuscule initiale pour les distinguer des variables et des méthodes. C’est une convention de nommage très répandue même au-delà de Java.

Pourquoi le nom pascal? C’est un hommage à Pascal, un langage de programmation historiquement important qui utilisait cette convention.

En Java, le nom du fichier doit avoir le même nom que la classe qu’il contient. Si la classe s’appelle NomDeClasse, le fichier doit s’appeler NomDeClasse.java. Ainsi, tous les noms de fichiers seront également en casse pascal. Dans notre cas, où la déclaration de la classe sera implicite, il faut tout de même nommer le fichier avec la casse pascal parce que le fichier et la classe sont liés.

Casse serpent avec majuscules pour les constantes

Pour indiquer qu’une variable ne change jamais de valeur, soit qu’elle est constante, on utilise une casse spéciale. Les constantes sont écrites en majuscules avec des soulignements pour séparer les mots. Par exemple : NOM_DE_CONSTANTE.

Pourquoi le nom serpent? Possiblement parce qu’en chaînant les mots avec des soulignements, on imagine les segments du corps d’un serpent. Possiblement parce que les soulignement ressemblent à des serpents au sol. Le nom n’est pas officiel, mais il est communément utilisé depuis au moins 2004.

Résumé des conventions

ÉlémentConventionExemple
Classe, FichierCasse pascalString, System
MéthodeCasse chameaumain(), calculateAge(), testMainWithInvalidData()
VariableCasse chameauage, totalAmount, userInput
ConstanteCasse serpentMAX_VALUE, PI, DEFAULT_NAME

Identifiants descriptifs

Si les conventions aident les développeurs à reconnaître la fonction générale d’un identifiant, les mots utilisés dans l’identifiant les aident à comprendre plus spécifiquement ce que l’identifiant représente dans le programme.

C’est extrêmement important de choisir des noms descriptifs pour tous les identifiants tant pour vous aider à comprendre le sens de ce que vous tentez d’accomplir que pour communiquer ce sens aux autres développeurs.

Cela s’applique pour tous les langages de programmation.

Exemple

Que veux dire l’expression suivante?

a = b * c;

À part savoir qu’on assigne le produit de deux valeurs à une troisième valeur, c’est impossible de le savoir.

Maintenant, que veux dire l’expression suivante?

areaOfRectangle = length * width;

ou

weeklyPay = hoursWorked * hourlyRate;

Ces deux expressions on exactement la même forme que le premier exemple, mais on peut savoir pourquoi on l’a utilisé dans chaque cas. C’est parce que les identifiants ont été nommés de manière descriptive.

Note : même si on travaille en français, il est préférable d’utiliser l’anglais pour les identifiants parce que les mots-clés du langage sont en anglais. Ainsi, le code est plus facile à lire et on évite les problèmes de caractères spéciaux (p. ex. é, ç, à, etc.).

Commentaires explicatifs - commentaires de ligne ou de bloc

Parfois simplement respecter les conventions de nommage et la pratique de noms descriptifs, il ne reste pas grande chose à expliquer dans le code. Mais il y a des moments où un commentaire est nécessaire pour expliquer un choix de variable ou un algorithme particulier. C’est là que les commentaires explicatifs ont leur place.

Les commentaires sont ignorés par le compilateur, donc ils n’ont pas d’impact sur le fonctionnement du programme. Ainsi, on devrait les écrire en français dans un langage naturel qui nous convient.

C’est un art de trouver à quel point les commentaires explicatifs passent d’utile à redondant. Lorsqu’il y a trop de commentaires ou les commentaires dupliquent l’information dans les identifiants descriptifs, ils nuisent à la clarté du code. En contrepartie, s’il n’y a pas assez de commentaires, la logique de certaines parties du code peut être difficile à suivre. EN CAS DE DOUTE, surtout comme novice, il vaut mieux en mettre un peu trop que pas assez.

Syntaxe des commentaires

Il y a deux façons de laisser des commentaires dans le code Java :

  • Les commentaires de ligne suivent deux barres obliques // et se limitent à la ligne sur laquelle ils sont écrits.
  • Les commentaires de bloc s’étendent entre les délimiteurs /* et */ ce qui leur permet de couvrir plusieurs lignes, au besoin.

Exemple de commentaire de ligne

On suit souvent la déclaration d’une variable avec un commentaire de ligne pour expliquer pourquoi on a choisi une valeur initiale particulière ou pour indiquer comment il sera utilisé plus loin, si ce n’est pas déjà assez clair par le nom de la variable.

int age; // conservera l'âge fourni par l'utilisateur

Si on arrive à une structure de contrôle (comme une sélection ou une boucle), on précède souvent la structure avec un commentaire pour expliquer le but de la structure. Si l’explication est courte, ce commentaire peut-être un commentaire de ligne.

// Décider si l'utilisateur peut voter
if (age > 18) {
    System.out.println("Vous pouvez voter!");
} else {
    System.out.println("Vous ne pouvez pas voter.");
}

Finalement, certaines opérations complexes peuvent être expliquées avec un commentaire de ligne.

// La distance entre deux points (x1, y1) et (x2, y2)
int dx = x2 - x1;
int dy = y2 - y1;
double distance = Math.sqrt(dx * dx + dy * dy);

Exemple de commentaire de bloc

Le commentaire de bloc est utilisé comme en-tête d’un fichier, pour présenter des informations comme la description, comment l’utiliser, qui l’a fait, s’il y a une licence spécifique attachée au code, etc.

/*
Nom de la classe : Salut
Description : Un programme simple qui affiche un message de salutation.
Auteur : David Crowley
Date de création : 2023-09-01
*/

On utilise aussi un commentaire de bloc si la logique d’un algorithme ne peut pas s’expliquer sur une seule ligne.

/*
On gagne si on atteint 100 points ou plus.
On perd si on atteint 20 tentatives.
On quitte en milieu du jeu si l'utilisateur a tapé "q".
*/
if (points >= 100) {
    System.out.println("Vous avez gagné!");
} else if (attempts >= 20) {
    System.out.println("Vous avez perdu.");
} else if (userInput.equals("q")) {
    System.out.println("Vous avez quitté le jeu.");
}

Notez que vous pouvez aussi utiliser plusieurs commentaires de ligne une après l’autre au lieu d’utiliser un commentaire de bloc. C’est ce qui arrive souvent, parce qu’on ne réalise pas la longueur du commentaire avant de l’écrire.

Documentation interne - Javadoc

Ceci est un sujet qui est plus avancé. Nous l’utiliserons pas dès le début, contrairement aux autres sujets de cette leçon. La section demeure ici pour référence future.

Les commentaires explicatifs sont utiles pour présenter notre logique lorsqu’un développeur lit les détails internes de notre code. Mais c’est aussi pratique de connaître le contenu d’une classe ou la structure d’une méthode même sans lire le code. C’est là que la documentation interne entre en jeu.

La documentation interne, appelé javadoc en Java, est un format de commentaire spécial qui est reconnu par les outils Java pour générer de la documentation officielle. Les outils Java vous affichent le contenu de ces commentaires dans l’auto-complétion (lorsque vous tapez votre code) et dans les info-bulles (lorsque votre souris passe sur un identifiant).

Pour créer un commentaire javadoc :

  • on utilise un commentaire de bloc avec un * supplémentaire au début. Le bloc commence alors par /** (deux astérisques) et se termine par */.
  • Le javadoc doit être placé immédiatement devant la déclaration de la classe ou de la méthode qu’il documente. Sinon, les outils Java ne sauront pas à quoi il se réfère : il sera considéré comme un simple commentaire de bloc.
  • C’est mieux d’écrire le javadoc après avoir écrit le code. Les outils de votre EDI peuvent alors ajouter plusieurs détails automatiquement avec le bon format, vous laissant juste la tâche d’ajouter les descriptions.
  • Votre EDI ajoutera probablement des * supplémentaires pour chaque ligne du javadoc. C’est une convention pour rendre le commentaire plus lisible, mais ces astérisques internes ne sont pas nécessaires pour que le javadoc fonctionne.

Format général pour le javadoc d’une méthode

Les paramètres (les @param) et la valeur de retour (le @return) sont des sujets plus avancés qui seront traités plus tard dans le cours. Pour l’instant, si on demande le javadoc d’une méthode, on s’attend à voir juste sa description.

/**
 * Description de la méthode.
 *
 * @param nomParamètre Description du paramètre
 * @param nomAutreParamètre Description de l'autre paramètre
 * @return Description de la valeur de retour
 */

Exercices

📚 Tester la compréhension

Quiz de vérification sur les identifiants et les commentaires

🛠️ Pratique

Travaillez dans le dépôt “pratique” partagé par votre enseignant. Si vous l’avez déjà clôné, simplement l’ouvrir dans VS Code.

Votre répertoire devrait avoir la structure et le contenu suivants avant de commencer les exercices :

pratique-[votre nom]
|---captures
|   `---4-1-Structure.png
|---README.md
`---Salut.java

Dans le programme Salut.java que vous avez fait précédemment :

  1. Ajouter un commentaire de bloc comme en-tête du fichier pour expliquer ce que le programme fait.
  2. Ajoutez le javadoc sur la méthode main. Notez dans la description quelles parties du commentaire ont été ajoutées automatiquement par votre EDI.
  3. Ajoutez un commentaire de ligne pour expliquer ce que l’instruction System.out.println("Salut gang!"); fait.
  4. Prendre une capture d’écran du code quand votre curseur est sur le nom de la méthode main (et l’info-bulle est affiché) et l’enregistrer dans “captures”. Nommez le fichier 4-3-Communication.png.

Accueil > Programmer avec Java > Les bases de Java >

Afficher des messages à la console - partie 1

Survol et attentes

Tout programme doit communiquer avec l’utilisateur. Les programmes lancés via la console affichent des messages texte. Cette leçon vous montre comment le faire de différentes façons en Java.

Définitions
System.out
objet Java qui représente la sortie par défaut de l’ordinateur, soit où les messages de votre programme seront affichées. Par défaut, c’est la console (comme le terminal PowerShell).
print()
méthode de l’objet System.out qui affiche un message sans passer à la ligne suivante. Elle prend un seul argument, le message à afficher.
println()
méthode de l’objet System.out qui affiche un message et passe à la ligne suivante. On peut aussi l’utiliser sans message juste pour passer à la ligne suivante à la console.
caractère d’échappement
caractère spécial dans un message texte qui indique à l’ordinateur de faire quelque chose de spécial, comme un retour à la ligne (\n) ou insérer une double guillemet (\").

Objectifs d’apprentissage

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

  • Distinguer les deux méthodes d’affichage de base dans Java : print et println
  • Reconnaître le caractère d’échappement \n l’utiliser dans des Strings pour gérer l’affichage.

Critères de succès

  • Je peux utiliser les méthodes print et println pour afficher des messages texte efficacement à la console.

Voici le contenu d’un fichier nommé SimplePrinting.java :

void main() {
    System.out.print("Bonjour ");
    System.out.println("tout le monde!");
    System.out.println("Comment ça va?");
}

Et voici sa sortie :

Bonjour tout le monde!
Comment ça va?

Notez que la première instruction, System.out.print("Bonjour ");, n’a pas de retour à la ligne parce qu’on a utilisé la méthode print(). Alors le texte de la deuxième instruction, System.out.println("tout le monde!");, continue sur la même ligne. Puisque cette instruction utilise la méthode println() le texte à afficher avec la troisième instruction, System.out.println("Comment ça va?"); est sur une nouvelle ligne. Cette dernière instruction utilise aussi la méthode println(), alors le curseur se trouve à la ligne suivante après l’affichage du message.

Insérer un retour de ligne manuellement : les caractères d’échappement

On peut insérer des retours de ligne manuellement dans les messages à afficher en utilisant le caractère d’échappement \n. Voici un exemple dans le fichier LineReturns.java :

void main() {
    System.out.println("Bonjour\n tout\n  le\n   monde!");
}

Et voici sa sortie :

Bonjour
 tout
  le
   monde!

Notez que le caractère \n est remplacé par un retour à la ligne dans le message affiché, alors chaque mot dans le message est sur une ligne séparée. Les mots sont aussi décalés de plus en plus vers la droite parce qu’on a inclut plus d’espaces après chaque caractère \n.

Il y a d’autres caractères d’échappement comme \" pour insérer une double guillemet dans un message entre guillemets. Voir cette page pour une liste complète.

Exercices

📚 Tester la compréhension

Quiz de vérification sur print, println et \n

🛠️ Pratique

Travaillez dans le répertoire GitHub partagé par votre enseignant pour la pratique et les exercices

But : produire la sortie suivante à la console :

MESSAGE 1
Ceci est une phrase simple

MESSAGE 2
Ceci
est
une phrase
simple

MESSAGE 3
Ceci est une
phrase simple

  1. Créez un fichier Print1.java et y ajouter la structure de base (méthode main). Utilisez trois fois la méthode println pour afficher ces trois lignes : “MESSAGE 1”, “MESSAGE 2” et “MESSAGE 3”.
  2. En dessous de “MESSAGE 1”, afficher le message “Ceci est une phrase simple” en utilisant une fois la méthode print et une fois la méthode println.
  3. En dessous de “MESSAGE 2”, afficher le message “Ceci est une phrase simple” sur quatre lignes une utilisant une seule fois la méthode println et en utilisant le caractère d’échappement \n.
  4. En dessous de “MESSAGE 3”, afficher le message “Ceci est une phrase simple” sur deux lignes en utilisant une seule fois la méthode print et en utilisant le caractère d’échappement \n.
  5. Assurez-vous de laisser une ligne vide après chaque message pour séparer les messages. Pour y arriver, utilisez un println vide ou un caractère d’échappement \n additionnel à la fin de chaque message.
  6. Prenez une capture d’écran du terminal incluant les commandes pour lancer le programme et la sortie complète. Nommez la capture “4-1-3-print.png” et ajoutez-la au dossier “captures” de votre projet.

Accueil > Programmer avec Java > Les bases de Java >

📚 Données (int, double, boolean, char, String)

Survol et attentes

Les programmes informatiques sont créés pour manipuler et gérer des données. On connaît la représentation interne de différents types de données, mais les langages de haut niveau ont leurs propres représentations.

Définitions
type
format de donnée spécifique, représenté par un mot-clé ou le nom d’une classe en Java. Les types de base sont : int, double, boolean, char et String. Les types sont directement liés à la leçon sur la représentation interne des données.
valeur littérale
une valeur écrite directement dans le code, comme 0, 3.14159, true, 'A', "David". En Java, on doit entourer une valeur littérale de type char avec des apostrophes ' et une valeur littérale de type String avec des guillemets ".
variable
nom représentant une adresse en mémoire qui stocke une donnée. En Java, les variables sont fortement typées elles doivent être déclarées avec un type spécifique et ne peuvent pas changer de type dans le code. En Java, les variables sont aussi statiquement typées : le type de chaque variable est déterminé à la compilation du code et ne peut pas changer durant l’exécution du programme. Ces restrictions réduisent énormément les erreurs dans le code liées aux types de données.
déclaration de variable
indiquer le type de donnée et le nom de la variable. Par exemple : int count;, double pi;, boolean isRaining;, char letter;, String name;. Les noms doivent respecter les règles et la convention de nommage des identifiants.
assignation de valeur
donner une valeur à une variable. L’opérateur d’assignation dans Java est le =. Par exemple : count = 0;, pi = 3.14159;, isRaining = true;, letter = 'A';, name = "David";.
initialisation d’une variable
L’assignation d’une première valeur à une variable.
réassignation
donner une nouvelle valeur à une variable déjà initialisée, remplaçant la valeur précédente. Par exemple : count = 1;, pi = 3.14;, isRaining = false;, letter = 'B';, name = "Alice";.
référence
utiliser le nom de la variable (sans guillemets) pour accéder à sa valeur. Par exemple : System.out.println(name);. Le nom doit être sans guillemets pour que Java le cherche parmis les variables déclarées dans votre programme. La variable doit être initialisée avant d’y faire référence dans le code.

Objectifs d’apprentissage

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

  • Identifier les types de données les plus communs en Java.
  • Reconnaître les valeurs littérales pour les types de données les plus communs.
  • Reconnaître le format de déclaration et d’initialisation d’une variable.

Critères de succès

  • Déclarer et initialiser des variables de type int, double, boolean, char et String.
  • Assigner de nouvelles valeurs aux variables déclarées et faire référence à ces variables dans le code.

Types de données de base

Les types Java de base sont :

TypeDescriptionValeurs littéralesExemples de déclarations
intnombre entier0, 1, -1, 2, -2, …int count; int age = 16;
doublenombre décimal0.0, 1.0, -1.0, 2.0, -2.0, …double pi; double temperature = 20.5;
booleanvaleur logiquetrue ou falseboolean isRaining; boolean isSunny = true;
charcaractère unique‘a’, ‘b’, ‘c’, …char letter; char grade = 'A';
Stringtexte (aussi appelé une “chaîne de caractères” d’où le nom en anglais)“Bonjour”, “Au revoir”, “A”, “B”, …String name; String message = "Bonjour";

Déclaration de variables

Le format pour la déclaration d’une variable est le suivant :

type nom;

par exemple :

int count;
double pi;
boolean isRaining;
char letter;
String name;

Initialisation de variables

Pour initialiser une variable, on doit lui assigner une première valeur. Le symbole = est utilisé en Java pour signifier que la variable à la gauche stocke maintenant la valeur à la droite.

Si la variable est déjà déclarée on utilise seulement son nom du côté gauche :

nom = valeur; // initialiser ou assigner une valeur à une variable qui existe déjà

On peut aussi donner une valeur initiale lors de la déclaration de variable, donc la déclaration complète serait du côté gauche :

type nom = valeur; // déclaration et initialisation en une seule ligne

Voici quelques exemples :

// ces variables ont été déclarées dans l'exemple précédent
count = 0;
pi = 3.14159;
isRaining = true;
letter = 'A';
name = "David";

// ces nouvelles variables sont initialisées durant la déclaration
double sum = 2.0 + 3.1; // assigner la somme de deux nombres
String answer = ""; // assigner le texte vide comme valeur initiale
int sum = 0;

Ordre des opérations dans une assignation de valeur

L’opération à droite du symbole = est effectuée avant l’assignation de la valeur à la variable à gauche. Par exemple, dans l’exemple double sum = 2.0 + 3.1;, la somme de 2.0 et 3.1 est calculée avant d’être assignée à la variable sum.

Ceci est très utile si on veut changer la valeur d’une variable par un montant fixe. Par exemple, count = count + 1; ajoute 1 à la valeur actuelle de count. Cette expression n’est pas une équation mathématique, mais plutôt une opération spécifique (l’assignation) qui est effectuée en deux étapes : d’abord, le côté droit est évalué, puis le résultat est assigné au côté gauche.

Exemple concret

Voici un court exemple dans le contexte d’un programme Java complet :

void main() {
    int age = 16;
    String name = "Myriam";
    System.out.print(name);
    System.out.print(" a ");
    System.out.print(age);
    System.out.println(" ans.");
}

et sa sortie serait :

Myriam a 16 ans.

Exercices

📚 Tester la compréhension

aucun quiz de vérification des concepts ici encore

🛠️ Pratique

Travaillez dans le répertoire GitHub partagé par votre enseignant pour la pratique et les exercices

  1. Créer un fichier Variables.java dans votre répertoire de travail. Assurez-vous d’y ajouter la structure de base (méthode main).
  2. Initialiser une variable de type approprié pour chacune des valeurs suivantes et affichez chacun individuellement avec System.out.println().
    1. 3
    2. -8.2
    3. true
    4. "texte"
    5. '@'
  3. Réassigner une nouvelle valeur à chaque variable du numéro précédent et affichez chacun de nouveau avec System.out.println().
  4. Prenez une capture d’écran du lancement réussi de votre programme au terminal et l’enregistrez dans le dossier “captures” avec le nom 4-2data.png

Indice: N’entourez pas le nom de votre variable avec des guillemets " dans System.out.println(). Ceci affichera le nom de la variable au lieu de sa valeur.

Accueil > Programmer avec Java > Les bases de Java >

Afficher des messages à la console - partie 2

Survol et attentes

Parfois on a besoin de plus de contrôle sur le format de messages ou simplement une façon plus efficace d’afficher plusieurs informations à la console. Cette leçon vous montre comment utiliser la méthode printf pour afficher des messages formatés en Java.

Définitions
printf()

méthode de l’objet System.out qui affiche un message formaté. Il prend un nombre variable d’arguments : le premier est le message formaté, les autres sont les valeurs à insérer dans le message :

System.out.printf("message avec drapeaux pour les valeurs", valeur1, valeur2, ...);
drapeaux de formatage

caractères spéciaux dans un message formaté qui gardent une place pour une valeur de type spécifique. Par exemple, %s pour une valeur de type String, %d pour une valeur de type int et %f pour une valeur de type double.

concaténation

coller du texte ensemble. En Java, l’opérateur + agit comme opérateur de concaténation si au moins un des opérandes est un String. L’autre opérande sera converti automatiquement en String pour l’opération. La concaténation se propage de gauche à droite, alors l’expression "a" + 1 + 2 donne "a12"; pour obtenir "a3", il faut utiliser des parenthèses autour de l’addition : "a" + (1 + 2).

Objectifs d’apprentissage

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

  • Comprendre la méthode printf et ses arguments
  • Reconnaître les drapeaux de formatage les plus communs pour la méthode printf, notamment %d, %f et %s.

Critères de succès

  • Je peux utiliser la méthode printf pour afficher des données efficacement à la console.

Messages formatés : printf

Vous pouvez utiliser la méthode printf() pour afficher des messages formatés. C’est utile quand vous :

  • essayez de coller plusieurs valeurs ensemble dans une phrase.
  • voulez contrôler le nombre de chiffres après la virgule pour un nombre décimal.
  • voulez afficher des valeurs en colonnes de largeur fixe.

Vous verrez que pour la plupart des situations, vous pouvez utiliser des print, des println et de la concaténation pour afficher ces messages au lieu d’utiliser printf : dans ces cas, c’est une préférence personnelle. Par contre, pour l’affichage uniforme des valeurs décimales ou pour l’alignement du texte, printf est la seule solution raisonnable.

Exemple - Coller des valeurs dans une phrase

Reproduire cet exemple dans un fichier nommé PrintGlue.java et tester la sortie.

Rappel : c’est toujours mieux de taper les exemples vous-même (et découvrir ce que vos outils vous aident à compléter, où se trouvent les symboles sur le clavier, etc.) que de copier-coller l’exemple. Vous gagnerez plus de confiance avec votre capacité à produire du code. C’est la différence entre CRÉER (taper) et UTILISER (copier-coller) du code, même si le code final est le même.

void main() {
    String name = "Malcolm";
    int age = 16;

    // AVEC PLUSIEURS PRINT ET PRINTLN (sans concaténation des valeurs)
    System.out.print("Mon nom est ");
    System.out.print(name);
    System.out.print(" et j'ai ");
    System.out.print(age);
    System.out.println(" ans.");

    // AVEC PRINTLN (et concaténation des valeurs)
    System.out.println("Mon nom est " + name + " et j'ai " + age + " ans.");

    // AVEC PRINTF
    System.out.printf("Mon nom est %s et j'ai %d ans.\n", name, age);
}

et la sortie identique pour les trois approches :

Mon nom est Malcolm et j'ai 16 ans.
Mon nom est Malcolm et j'ai 16 ans.
Mon nom est Malcolm et j'ai 16 ans.

Structure de la méthode printf

La méthode printf accepte un nombre variable d’arguments. Le premier argument est toujours le String qui définit le format du message à afficher, incluant le texte de base et des drapeaux de formatage pour les valeurs à insérer dans le message. Les prochains arguments sont les valeurs à insérer dans le message, remplaçant chaque drapeau.

System.out.printf("message avec drapeaux pour les valeurs", valeur1, valeur2, ...);

Dans dans l’exemple System.out.printf("Mon nom est %s et j'ai %d ans.\n", name, age); :

  • Le String de formatage est "Mon nom est %s et j'ai %d ans.\n".
    • il y a deux drapeaux de formatage : %s pour une valeur de type String et %d pour une valeur de type int.
    • Il y a aussi le caractère d’échappement pour le retour de ligne, \n, parce que printf ne l’inclut pas; c’est comme print.
  • Le prochain argument est name (de type String qui s’évalue à “Malcolm”). Sa valeur remplacera le %s, le premier drapeau de formatage.
  • Le dernier argument est age (de type int qui s’évalue à 16). Sa valeur remplacera le %d, soit le dernier drapeau de formatage.

Voici un tableau de quelques drapeaux de formatage communs :

DrapeauType de valeurExemple
%sString"Bonjour %s"
%SString en majuscules"Bonjour %S"
%dint en décimal"La valeur du rouge est %d"
%xint en hexadécimal"La valeur du rouge est #%x"
%fdouble"Le prix est de %f"

Exemple - Contrôler le nombre de chiffres après la virgule pour un nombre décimal

Voici quelques exemples que vous pouvez tester dans une session jshell. Lancez la commande jshell dans un terminal pour ouvrir une session jshell. Quittez la sesssion avec la commande \exit.

double PI = 3.141592653589793; // constante pour la valeur de pi
System.out.printf("La valeur de pi est %f.\n", PI);
System.out.printf("La valeur de pi est %.2f.\n", PI);

sortie :

La valeur de pi est 3.141593.
La valeur de pi est 3.14.

Comme le premier exemple, le premier argument est le String qui définit le format du message. Dans ce cas on utilise le drapeau %f pour une valeur de type double. La valeur à insérer est 3.141592653589793. Par défaut, printf affiche 6 chiffres après la virgule pour les nombres décimaux. Pour contrôler le nombre de chiffres après la virgule, on peut ajouter un point et un nombre entre le % et le f.

Dans le deuxième exemple, on a ajouté .2 pour afficher seulement 2 chiffres après la virgule. Le .2 est un élément optionnel dans le drapeau de formatage pour les valeurs décimales. On peut remplacer 2 par n’importe quel nombre pour afficher ce nombre de chiffres après la virgule.

Voici un tableau présentant quelques options pour le formatage des valeurs décimales :

DrapeauExempleSortie
%f"La valeur de pi est %f"3.141593
%.2f"La valeur de pi est %.2f"3.14
%.0f"La valeur de pi est %.0f"3

Exemple - Afficher des colonnes de valeurs alignées

// Afficher des valeurs en colonnes de largeur fixe
System.out.printf("%-10s %10s\n", "NOM", "ÂGE"); // en-têtes
System.out.printf("%-10s %10d\n", "Malcolm", 16);
System.out.printf("%-10s %10d\n", "Myriam", 17);

sortie :

NOM               ÂGE
Malcolm            16
Myriam             17

Ici on a plusieurs instructions printf, une pour chaque ligne d’un tableau. La première instruction donne les en-têtes, donc les drapeaux de formatage dans le premier argument sont %s pour une valeur de type String. Pour spécifier la largeur de colonne, on peut ajouter des éléments optionnels entre % et le s. Dans ce cas :

  • on a ajouté -10 pour spécifier une largeur de 10 caractères et un alignement à gauche.
  • on a ajouté 10 pour spécifier une largeur de 10 caractères avec l’alignement par défaut (à droite).
  • comme avec les valeurs décimales, on peut remplacer 10 par n’importe quel nombre pour spécifier la largeur de colonne voulue.
  • Notez que les valeurs substituées seront tronquées (coupées) si elles sont plus longues que la largeur de colonne spécifiée. Soit, le format spécifié est respecté même s’il faut couper le texte pour le faire.

Les arguments suivants sont les valeurs à insérer dans le message, dans l’ordre où ils apparaissent dans le message. Donc "NOM" est inséré à la place de %-10s et "ÂGE" est inséré à la place de %10s.

Pour les lignes suivantes, les valeurs pour “Âge” sont de type int, alors on utilise le drapeau %d au lieu de %s, mais on maintient les mêmes spécifications pour la largeur et l’alignement, donc %10d au lieu de %10s. Les valeurs à insérer sont 16 pour la première ligne et 17 pour la deuxième.

Voici un tableau présentant quelques options pour le formatage des colonnes :

DrapeauExempleEffet
%-10s"Le nom est %-10s"colonne de 10 char de large, aligné à gauche
%10d"L'âge est %10d"colonne de 10 char de large, aligné à droite
%-10.2f"Le prix est %-10.2f"colonne de 10 char de large, aligné à gauche avec 2 places après la virgule

Exercices

📚 Tester la compréhension

Quiz quiz de vérification sur printf

🛠️ Pratique

Travaillez dans le répertoire GitHub partagé par votre enseignant pour la pratique et les exercices

  1. Créez le fichier Print2.java et y ajouter la structure de base (méthode main).
  2. Dans la méthode main, reproduire les trois instructions printf ci-dessus pour produire un tableau formaté pour les noms et les âges.
  3. Changez la largeur de colonne pour les noms à 5 caractères.
    • Lancez le programme et notez ce qui se passe à la sortie.
    • Écrire un commentaire de bloc pour consigner vos observations.
  4. Changez la colonne “ÂGE” à une colonne “MOYENNE”.
    • Ajouter des valeurs décimales raisonnables pour les moyennes de Malcolm et de Myriam et ajuster les drapeaux de formatage de façon appropriée.
    • Lancez le programme et vérifiez qu’il fonctionne.
    • Ajustez le drapeau de formatage pour les valeurs décimales pour afficher seulement 1 chiffre après la virgule. Lancez le programme de nouveau pour vérifier que ça fonctionne.
  5. Prenez une capture d’écran du terminal incluant les commandes pour lancer le programme et la sortie complète. Nommez la capture “4-1-5-print.png” et ajoutez-la au dossier “captures” de votre projet.

Accueil > Programmer avec Java > Les bases de Java >

Utiliser des méthodes

Survol et attentes

Définitions
Fonction

un bloc de code nommé. Certaines fonctions sont définies afin de prendre des informations en entrée (appelées arguments) ou pour retourner une valeur en sortie.

Méthode

une fonction liée à une classe ou un objet. On appelle les méthodes avec la notation pointée, soit objet.methode(), où on suit le nom de l’objet par un point et ensuite le nom de la méthode. Des exemples simples sont les méthodes println et print de l’objet System.out : on l’appel avec System.out.println().

Dans Java, parce que tout est défini dans une classe, toutes les fonctions sont des méthodes. Mêmes les programmes où on ne déclare pas une classe explicitement (comme les exemples dans ces notes de cours, Java déclare implicitement une classe pour nous qui emballe l’ensemble de notre code).

Méthode d’instance

une méthode qui est appellée sur un objet. Les méthodes pour manipuler les String sont des exemples communs, p. ex. : String s = "Bonjour"; s.length(); où la méthode d’instance length est appellée sur l’objet s.

Méthode statique

une méthode qui est appellée sur la classe directement, soit Classe.methodeStatique(). Vous pouvez les repérez si les conventions de nommage sont respectées : le nom précédant le point commence avec une majuscule. Un exemple commun est la classe Math qui fournit plusieurs méthodes utilitaires comme Math.sqrt(9).

Appel

utiliser le nom d’une méthode pour exécuter son bloc de code. Si la méthode prend des arguments, il faut fournir ces informations séparées par des virgules entre les parenthèses de l’appel. Si la méthode retourne une valeur, on peut utiliser l’appel pour représenter cette valeur dans une expression, comme un calcul ou une assignation de valeur à une variable.

Objectifs d’apprentissage

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

  • Expliquer les termes “appelle de méthode” et “argument”.
  • Distinguer les appels de méthodes statiques (sur des classes) et d’instance (sur des objets).

Critères de succès

  • Je peux utiliser des appels de méthodes statiques et d’instance, avec ou sans arguments et avec ou sans valeur de retour efficacement dans mes programmes Java.

Utilise jshell

C’est mieux de testez les exemples ici dans le REPL jshell parce que les résultats sont affichés immédiatement un-par-un. Tapez une seule ligne de code à la fois et appuyez sur Enter pour voir le résultat.

Méthodes d’instances de String

Voici quelques exemples de méthodes qu’on peut utiliser avec des variables de type String :

String name = "David";
name.length(); // le nombre de caractères
name.charAt(0); // le premier caractère
name.toUpperCase();
// conserver le résultat dans une variable
String lowerName = name.toLowerCase();

Méthodes statiques de la classe Math

Voici quelques exemples de méthodes statiques de la classe Math :

Math.sqrt(9); // racine carrée ("square root") -> sqrt
Math.abs(-5); // valeur absolue (ignore le signe)
Math.pow(10, 3); // puissance ("power") -> pow... 10^3
// conserver le résultat dans une variable
double chance = Math.random(); // aléatoire entre 0.0 et 0.9999999
chance * 100; // changer l'échelle (0.0 à 99.9)
25 + chance * 25 // changer l'échelle et la valeur initiale (25.0 à 49.9)

Un regard sur les méthodes print

Est-ce que println est une méthode d’instance ou une méthode statique?

System.out.println("Bonjour");

C’est une méthode d’instance! La variable out dans la classe System est un objet de type PrintStream (placez votre curseur sur le mot out dans un programme Java complet pour voir son info-bulle), et les PrintStream ont une méthode println().

On verra plusieurs autres types de données dans le cours, comme des Random, des Scanner et des File, qui ont aussi des méthodes d’instance pour utiliser leurs objets.

Suggestions par votre EDI

Votre EDI devrait suggérer les méthodes d’instance ou les méthodes statique possibles dès que vous tapez le . après le nom de l’objet ou de la classe. Vous pouvez sélectionner une méthode dans la liste pour l’ajouter à votre code source. Quand les méthodes sont affichées dans la bulle de suggestion, vous voyez aussi le type des arguments et le type de retour.

VS Code suggestions Java

Par contre, ce que vous faites au terminal - notamment les sessions jshell - ne bénificie pas de cette fonctionnalité.

Les outils Java prennent une minute ou plus pour s’activer pleinement après l’ouverture d’un projet dans VS Code. Ils ne s’activent jamais pleinement si vous n’avez pas configurez l’extension pour se lancer en mode Standard au lieu d’Hybrid. Si vous ne voyez rien ou vous voyez juste le message “Loading…” quand votre curseur est sur un nom comme String, vous devrez attendre encore un peu.

Exercices

📚 Tester la compréhension

aucun quiz de vérification des concepts ici encore

🛠️ Pratique

Travaillez dans le répertoire GitHub partagé par votre enseignant pour la pratique et les exercices

  1. Prenez une capture d’écran du terminal montrant les résultats de votre session jshell après avoir lancé les commandes pour les méthodes d’instance de Stringdans les notes ci-dessus. Enregistrez l’image dans le dossier “captures” avec le nom 4-3-String.png.
  2. Prenez une capture d’écran du terminal montrant votre session jshell après avoir lancé les commandes pour les méthodes statiques de Math dans les notes ci-dessus. Enregistrez l’image dans le dossier “captures” avec le nom 4-3-Math.png.

Utiliser les méthodes dans un programme

  1. Créer un fichier Methods.java dans votre dossier “pratique” et y ajouter le contenu suivant :

    void main() {
        String name = "David"; // Remplacez "David" par votre prénom
        name.length();
        name.endsWith("d"); // remplacer "d" par la lettre à la fin de votre nom
        name.endsWith("s"); // cette lettre doit être différente à la précédente
    }
    /* Ajoutez vos commentaires sur le comportement ici :
    
    */
    
  2. Lancez le programme et décrire ce qui s’est affiché dans le commentaire de bloc suivant main.

  3. Parce qu’on n’est pas dans un REPL, pour voir le résultat des méthodes il faut les afficher explicitement :

    1. Placez votre curseur sur le nom de chaque méthode (length, endsWith) et attendre que la bulle d’information apparaisse. Vous trouverez le type de retour de chaque méthode.
    2. Au début de la ligne pour chaque méthode, ajouter la déclaration d’une variable du type approprié. P. ex., length retourne un int, donc vous pouvez ajouter int nameLength = name.length();. Faites la même chose pour les endsWith (mais faites attention! endsWith retourne un boolean).
    3. Ajoutez une ou plusieurs instructions print, println ou printf pour afficher les valeurs des variables que vous avez déclarées à l’étape précédente.
  4. Tapez “name.” et choisissez une autre méthode que vous n’avez pas encore utilisée parmis celles qui sont suggérées. Le but est simplement l’exploration et le transfert de ce que vous avez appris à un contexte ouvert. Ajouter une valeur appropriée pour le ou les arguments requis, au besoin. Ajoutez une déclaration de variable pour le type de retour de cette méthode et affichez le résultat.

  5. Prenez une capture d’écran montrant le code final et les résultats de votre programme au terminal. Enregistrez l’image dans le dossier “captures” avec le nom 4-3-Methods.png.

Accueil > Programmer avec Java > Les bases de Java >

📚 Importer des classes

Survol et attentes

Définitions
Bibliothèque standard

un ensemble de code inclut avec tout langage de programmation qui fournit des outils pour accomplir des tâches communes. La bibliothèque standard de Java est composée de classes organisées en packages et inclut tout ce qui vient avec le JDK (Java Development Kit).

package

structure dans Java qui correspond à un dossier dans le système de fichiers. Le mot-clé package est utilisé pour déclarer le package d’une classe et les outils de compilation de projet utilisent ces déclarations pour lier le code source correctement. Un projet simple dans un seul dossier n’a pas besoin de déclaration de package : Java place les classes automatiquement dans le package par défaut. Également par défaut, toutes les classes dans le même package peuvent se voir sans déclaration d’importation.

import

une déclaration qui permet d’utiliser des classes d’autres packages. La déclaration d’importation est placée au début du fichier, avant la déclaration de la classe. La structure de la déclaration est import package.Class;.

java.lang

un package qui contient des classes de base pour Java, comme les classes System, String et Math. Ces classes sont inclus automatiquement dans tous les programmes Java. On peut voir le nom complet de ces classes en plaçant le curseur sur le nom de la classe dans le code et en observant l’info-bulle qui apparaît.

java.util

un package qui contient des classes utilitaires pour Java, comme Scanner et Random et aussi des classes pour les collections de données, comme ArrayList et HashMap. On peut importer sur demande les classes de ce package avec la déclaration import java.util.*; au début du fichier de code source.

java.io

un package qui contient des classes pour les entrées et les sorties, comme File et FileWriter. On peut importer sur demande les classes de ce package avec la déclaration import java.io.*;.

Bibliothèque externe

code qui peut être ajouté à l’environnement de programmation mais qui n’est pas inclus par défaut et qui n’est souvent pas maintenu par les développeurs qui s’occupe de la bibliothèque standard. Ces bibliothèques sont souvent utilisées pour des tâches spécialisées. Parce que ces bibliothèques ne sont pas inclus dans le JDK, il faut configurer l’environnement de programmation pour les obtenir et les utiliser en plus de les importer explicitement.

On n’utilise pas de bibliothèques externes dans ce cours avant l’unité sur les applications. À ce moment, c’est possible qu’on explore une bibliothèque graphique (JavaFX ou Processing) ou une bibliothèque pour la computation physique (Phidgets).

Objectifs d’apprentissage

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

  • Expliquer la différence entre les classes dans le package java.lang et celles dans les autres packages de la bibliothèque standard de Java.
  • Décrire le format d’une déclaration d’importation.
  • Expliquer la différence entre la bibliothèque standard de Java et une bibliothèque externe.

Critères de succès

  • Vous pouvez importer des classes de la bibliothèque standard dans vos programmes.

Gabarit de programme avec importations standard

Voici un gabarit de structure de programme Java avec les importations nécessaires pour utiliser les classes dans les packages java.io et java.util. Vous pouvez copier ce gabarit pour commencer un nouveau programme.

/*
 * En-tête de fichier
 */

import java.io.*; // importe sur demande les classes du package java.io
import java.util.*; // importe sur demande les classes du package java.util


void main() {
  // code du programme
}

Note

Depuis JDK 25, les classes dans java.util et java.io sont importés implictement (sans déclaration) comme si vous aviez déclaré leur importation. Bref, ces commande import ne sont plus nécessaires depuis Java 25.

Exemples de déclarations de variables avec les types importés

import java.util.*;

void main() {
  String myString; // pas besoin d'importation parce que String est dans java.lang
  Scanner myScanner; // importation nécessaire pour utiliser java.util.Scanner
  Random myRandom; // importation nécessaire pour utiliser java.util.Random
}

Si vous oubliez la déclaration d’importation, votre IDE peut généralement ajouter les importations nécessaires automatiquement. Dans VS Code, si vous tentez de déclarer un Scanner sans avoir ajouté la ligne pour l’importer, vous verrez une ligne ondulée rouge sous le nom de la classe indiquant une erreur. Si vous cliquez sur le nom souligné et tapez la commande Ctrl + . vous verrez une option pour ajouter l’importation nécessaire.

Les exemples ici n’initialisent pas les variables et ne les utilisent pas. On verra dans des leçons futures comment ces différents objets sont créés et utilisés.

Exercices

📚 Tester la compréhension

aucun quiz de vérification des concepts ici encore

🛠️ Pratique

Travaillez dans le répertoire GitHub partagé par votre enseignant pour la pratique et les exercices.

  1. Créez un fichier nommé Imports.java et y ajouter les structures de base d’un programme Java.
  2. Déclarez une variable de chaque type suivant :
    • String
    • Scanner
    • Random
    • File
    • Locale
  3. Lancez votre programme en vous assurant d’avoir inclut les lignes pour les importations. S’il n’y aucune erreur, aucune sortie du programme ne devrait s’afficher parce qu’il n’y pas d’instruction println ou print.
  4. Écrivez un en-tête de fichier pour votre programme. Inclure des sections “Description”, “Auteur” et “Date”.
  5. Prendre une capture d’écran pour les positions de curseur suivantes et enregistrez chacun dans le dossier “captures” :
    1. sur le mot String (l’info-bulle affiche le nom complet de la classe) -> nom du fichier : 4-4-String.png
    2. sur le mot Scanner (l’info-bulle affiche le nom complet de la classe) -> nom du fichier : 4-4-Scanner.png
  6. Créez un commit et un push pour synchroniser vos changements avec GitHub.

Accueil > Programmer avec Java > Les bases de Java >

Entrées et sorties à la console

Survol et attentes

Pour ajouter de l’interaction à un programme, il faut donner la chance à l’utilisateur de nous fournir une réponse. Dans le contexte d’un programme qui se lance à la console, cela veut dire capter les réponses tapées par l’utilisateur au clavier. Dans cette leçon, nous allons voir comment faire ça en Java.

Définitions
Mot-clé new
un mot-clé qui crée une nouvelle instance d’une classe (qui crée un objet). Ce mot-clé est suivi par une méthode spéciale de la classe appelée le constructeur. Chaque classe définit son propre constructeur, alors il faut connaître les détails de la classe pour créer des objets de cette classe. Votre EDI vous aide avec ces détails. La déclaration complète d’un nouvel objet est Type nom = new Type(arguments, ...);.
Classe Scanner
une classe qui nous permet de lire une source de texte comme la console, un String ou un fichier de texte. Un objet de type Scanner a accès à plusieurs méthodes d’instance pour retourner différents types de données. Un Scanner saisie toujours du texte, donc les méthodes comme nextInt() et nextDouble() convertissent le texte en nombre et peuvent planter si le format des caractères saisis ne correspond pas au type attendu. C’est pourquoi il y a aussi des méthodes comme hasNextInt() et hasNextDouble() pour valider le format avant de tenter une conversion.
Classe Locale
une classe qui permet de spécifier un ensemble de formats régionaux, notamment celui des nombres décimaux. Par défaut, le format dépend du système d’exploitation donc le programme se comportera différemment sur différents ordinateurs. Pour éviter des erreurs de conversion, on peut spécifier un Locale pour chaque Scanner en utilisant la méthode useLocale().
Objet System.in
un objet de type InputStream qui représente l’entrée standard du système d’exploitation, généralement la console. L’avantage est qu’il est toujours défini (ça fonctionne toujours). Le désavantage est que Windows gère les caractères Unicode différemment que le reste du monde, ce qui donne parfois des comportements inattendus et difficiles à résoudre.
Invite de réponse
un message affiché à l’utilisateur pour lui indiquer ce qu’il doit saisir. C’est une bonne pratique de donner des invites de réponse claires et spécifiques pour éviter des erreurs de saisie. Des bonnes invites de réponse rendent toujours l’intéraction plus agréable pour l’utilisateur.

Liste des méthodes de Scanner les plus courantes :

  • next() : saisir le prochain mot (jusqu’à un espace, ignorant les espaces initiaux)
  • nextInt() : saisir le prochain mot et le convertir en int
  • nextDouble() : saisir le prochain mot et le convertir en double
  • hasNextInt() : vérifier si le prochain mot est un int
  • hasNextDouble() : vérifier si le prochain mot est un double
  • nextLine() : saisir la prochaine ligne de texte (jusqu’à un retour à la ligne)… il y a des nuances avec cette méthode que nous explorerons en détail ci-dessous.

Objectifs d’apprentissage

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

  • Savoir comment déclarer un Scanner pour la console et pour un String
  • Savoir utiliser les méthodes d’instance d’un Scanner pour obtenir le type de réponse désiré.
  • Expliquer l’importance d’une invite de réponse spécifique et claire avant l’instruction de saisie.
  • Expliquer c’est quoi un Locale et pourquoi il faut le spécifier si on veut saisir des valeurs décimales.

Critères de succès

  • Je peux importer la classe Scanner, déclarer un Scanner pour la console et utiliser ses méthodes next, nextInt et nextDouble pour lire des données.
  • Je peux définir un Locale approprié pour le Scanner si j’ai besoin de saisir des valeurs décimales.
  • (enrichissement) Je peux valider les réponses fournies par l’utilisateur avec les méthodes hasNextInt et hasNextDouble avant de tenter une conversion afin de réduire le nombre de plantages de mes programmes.

Structure générale d’un programme avec un Scanner

Les Scanner sont utilisés pour lire du texte à partir de différentes sources. Pour commencer, on utilisera un Scanner qui surveille System.in pour complémenter le PrintStream (System.out) qu’on utilise déjà pour afficher des messsages texte à la console. Bref, les Scanner vont nous permettre de lire des réponses de l’utilisateur à la console et d’avoir des programmes interactifs.

Si vous ne l’aviez pas deviné, System.in et System.out sont les entrées et sorties standard de votre système d’exploitation, notamment la console.

La structure globale d’un programme qui utilise un Scanner est la suivante :

import java.util.*; // importer la définition de Scanner de java.util

void main() {
    Scanner console = new Scanner(System.in); // déclarer un Scanner pour la console

    // ... le reste du programme
}

Déclarer un Scanner

Le format de la déclaration d’un Scanner est le suivant :

Scanner nom = new Scanner(source);

nom est le nom de la variable qui contiendra l’objet Scanner et source est la source de texte que le Scanner lira. Le nom est généralement un reflèt précis de la source.

Pour lire de la console

On utilise System.in, l’entrée standard, comme source.

Scanner console = new Scanner(System.in);

Pour lire un String

On utilise un String comme source.

String text = "Bonjour, le monde!";
Scanner textReader = new Scanner(text);

Fermer un Scanner

On ne ferme jamais le Scanner pour la console parce qu’on s’en sert généralement jusqu’à la fin du programme interactif. Ce Scanner sera fermé automatiquement en arrivant à la fin de la méthode main.

Par contre, c’est important de fermer les Scanner pour toutes les autres sources de texte, comme un fichier ou un String. On ferme un Scanner avec la méthode close() une fois qu’on a finit de le lire. Par exemple :

textReader.close();

Introduction aux méthodes de Scanner - extraire les données d’un String

Pour comprendre comment les méthodes de Scanner fonctionnent, on va les tester sur différents String. Les sections plus bas montrent le Scanner dans le contexte d’un programme interactif.

La méthode next() : lire le prochain mot

La méthode next() saisit le prochain mot - plus spécifiquement, le prochain ‘jeton’ - dans le texte. Un jeton est une séquence de caractères qui n’inclut pas d’espaces. Si la prochaine chose dans le texte commence par un ou plusieurs espaces, ces espaces seront ignorés en saisissant le jeton. La méthode next() retourne le jeton saisi.

String words = "\n   \tBonjour le monde!";
Scanner wordsReader = new Scanner(words); // Scanner qui utilise le texte 'words' comme source

String word; // variable pour stocker le mot lu

word = wordsReader.next(); // ignore les espaces initiaux puis saisit tout jusqu'au prochain espace, soit "Bonjour"
System.out.println(word);

word = wordsReader.next(); // saisit "le"
System.out.println(word); 

word = wordsReader.next(); // saisit "monde!"
System.out.println(word); 
wordsReader.close();

Les méthodes nextInt() et nextDouble() : convertir le texte en nombre

Peut être que la ligne de texte suivante représente des informations sur une personne, comme son âge et sa moyenne scolaire. Connaissant le format du texte, on peut utiliser les méthodes appropriées pour lire les valeurs.

Les méthodes nextInt() et nextDouble() saisissent le prochain mot et tentent de le convertir en int ou double respectivement. Si le texte ne peut pas être converti en nombre, le programme plantera, alors il faut faire attention de :

  • bien aviser l’utilisateur du format attendu dans un programme interactif
  • bien observer les formats du texte dans des fichiers qu’on veut lire, comme un fichier qui contiendrait plusieurs lignes de données semblables à l’exemple suivant.
String info = "Marie 15 85.5";
Scanner infoReader = new Scanner(info);
String nom = infoReader.next(); // saisir le nom
int age = infoReader.nextInt(); // saisir le texte pour le nombre entier
double moyenne = infoReader.nextDouble(); // saisir le nombre décimal
infoReader.close();
System.out.printf("%s a %d ans et sa moyenne est %.1f\n", nom, age, moyenne);

Si le programme fonctionne correctement, il nous donnera cette sortie :

Marie a 15 ans et sa moyenne est 85.5

Dans notre contexte spécifique - travailler en français avec des machines configurées en français - il y a quand même un bon risque de plantage parce que le format pour le décimal est différent sur un système anglais (.) que sur un système français (,). À l’interne, Java utilise toujours le . comme décimal, mais il utilisera les formats du système dans ses Scanner pour interpréter les réponses de l’utilisateur à la console.

Donc c’est possible que Java présume que le format 85,5 est le bon et lance une erreur quand il voit plutôt 85.5. Ce n’est pas un bon système, mais il y a une façon de dire dans notre programme quel format on veut utiliser.

Spécifier le format des nombres décimaux

Quand on connaît le format des nombres décimaux dans le texte à lire, ou on dit à l’utilisateur quel format utiliser dans sa réponse interactive, on devrait spécifier ce format explicitement dans le code.

On le fait en utilisant la classe Locale (aussi dans java.util) et les valeurs qu’elle définit par défaut pour différents formats régionaux. Dans notre contexte, on utilise l’une au l’autre des options suivantes :

  • Locale.CANADA : pour utiliser le . comme séparateur décimal
  • Locale.CANADA_FRENCH : pour utiliser la , comme séparateur décimal

Le code précédent devient alors :

String info = "Marie 15 85.5";
Scanner infoReader = new Scanner(info);

infoReader.useLocale(Locale.CANADA); // spécifie le format "."

String nom = infoReader.next(); 
int age = infoReader.nextInt(); 
double moyenne = infoReader.nextDouble(); 
infoReader.close();
System.out.printf("%s a %d ans et sa moyenne est %.1f\n", nom, age, moyenne);

Maintenant, le programme fonctionne correctement peu importe l’ordinateur utilisé pour le lancer.

Version interactive : lire la console

En interagissant à la console, c’est important de donner des invites de réponse claires et spécifiques à l’utilisateur afin qu’elle sache quoi taper et avec quel format, si nécessaire.

Voici un exemple en transformant le programme précédent en un programme interactif :

Scanner console = new Scanner(System.in); // source = entrée standard
console.useLocale(Locale.CANADA_FRENCH); // spécifie le format ","

System.out.print("Quel est votre prénom? "); // invite de réponse
String nom = console.next(); 

System.out.print("Quel est votre âge? "); // invite de réponse
int age = console.nextInt(); 

// invite de réponse qui reflète la déclaration du Locale plus haut dans le code
System.out.print("Quelle est votre moyenne? (utilisez le ',' comme décimal) "); 
double moyenne = console.nextDouble(); 

System.out.printf("%s a %d ans et sa moyenne est %.1f\n", nom, age, moyenne);

// notez qu'on n'a pas fermé le Scanner pour la console

Enrichissement : valider la réponse avant de la saisir

Cette section est annotée comme “avancée” parce qu’elle incorpore des éléments de programmation que nous n’avons pas encore vus avec Java, notamment les opérations logiques et les boucles. Si vous ne comprenez pas tout de suite, ne vous inquiétez pas. Vous aurez l’occasion de revoir ces concepts plus tard dans le cours.

Si vous comprenez bien comment formuler une invite de réponse précise et comment assurer la cohérence entre votre invite et ce que vous avez écrit dans votre code, peut-être que vous ne serez pas satisfait de laisser une erreur de frappe par l’utilisateur faire planter votre programme. C’est là qu’on peut commencer à rendre le code plus tolérante aux erreurs en les gérant avant qu’elles ne causent des problèmes.

Pour valider une réponse avant de la saisir, on utilise les méthodes hasNextInt() et hasNextDouble() pour vérifier si le prochain mot est convertible en int ou double respectivement. Si la réponse est valide, on la saisit, la convertit et la conserve. Sinon, on doit vider le Scanner pour préparer la saisie suivante, alors on saisit la réponse comme texte sans la conserver. On peut également afficher un message d’erreur et inviter l’utilisateur à réessayer.

Diagramme de flux pour valider une réponse

Scanner console = new Scanner(System.in);

System.out.print("Saisissez un nombre entier : ");
while (!console.hasNextInt()) {
    console.next(); // vider le Scanner du jeton incorrect
    System.out.println("Ce n'est pas un nombre entier valide.");
    System.out.print("Saisissez un nombre entier : ");
}

int number = console.nextInt();
System.out.println("Vous avez saisi " + number);

L’opérateur ! est l’inverse logique (“not”). Il transforme true en false et false en true. Dans ce contexte, !console.hasNextInt() signifie “tant que le Scanner n’a pas un nombre entier valide comme prochain jeton”.

Exercices

📚 Tester la compréhension

aucun quiz de vérification des concepts ici encore

🛠️ Pratique

Travaillez dans le répertoire GitHub partagé par votre enseignant pour la pratique et les exercices.

  • Dupliquez tous les exemples dans le fichier Scanning.java.
  • Prenez une capture d’écran de votre éditeur de code et du terminal pour les phases suivantes :
    • l’exemple “Bonjour le monde!” -> nommez l’image 4-5-Scanner1.png.
    • l’exemple “Marie 15 85.5” avec l’ajustement pour inclure le bon Local -> nommez l’image 4-5-Scanner2.png.
  • l’exemple interactif avec les invites de réponse; répondez comme vous le voulez -> nommez l’image 4-5-Scanner3.png.
  • l’exemple avec la validation de la réponse; donnez au moins une mauvaise réponse avant de donner une réponse valide -> nommez l’image 4-5-Scanner4.png.

Accueil > Programmer avec Java > Les bases de Java >

📚 Opérations mathématiques et concaténation

Survol et attentes

Les ordinateurs sont des calculatrices très puissantes. Dans cette leçon on apprend les opérations mathématiques les plus communes et une opération sur le texte.

Définitions
Opération
une action qui implique deux données pour produire un résultat. Par exemple, l’addition est une opération qui combine deux nombres pour donner la somme de ces nombres.
Opérateur
un symbole qui représente une opération sur des données. Par exemple, - est l’opérateur de soustraction de nombres et = est l’opérateur d’assignation de valeurs à des variables.
Opérande
les données de chaque côté d’un opérateur. Par exemple, dans 3 + 4, 3 et 4 sont les opérandes. Les opérandes peuvent être des valeurs littérales (comme 3 ou 4) ou une expression qui s’évalue au type approprié. Des exemples d’expressions simples sont : une variable, un appel de méthode ou une autre opération.
Ordre des opérations
les règles qui déterminent l’ordre dans lequel les opérations sont effectuées. Java utilisent la même ordre que les mathématiques : d’abord les parenthèses, ensuite la multiplication et la division, et finalement l’addition et la soustraction. Pour les opérations de même niveau, les opérations s’évaluent de gauche vers la droite. S’il y a un appel de méthode, on remplace l’appel avec sa valeur de retour avant de poursuivre les opérations. S’il y a une référence de variable, on remplace la variable avec sa valeur avant de poursuivre les opérations.

Objectifs d’apprentissage

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

  • Comprendre et évaluer des opérations mathématiques et de concaténation.
  • Comprendre le cas spécial de la division avec des int et, de manière connexe, l’utilité de l’opérateur %.

Critères de succès

  • Écrire des expressions incluant des opérations mathématiques et de concaténation.

jshell

C’est pratique d’explorer ces opérations dans une session interactive qui donne le résultat instruction par instruction (au lieu d’écrire un programme complet et l’exécuter). Pour le faire, simplement lancer la commande jshell au terminal. Ensuite tapez vos instructions une à la fois (le ; est optionnel dans jshell). Pour quitter, tapez la commande /exit ou fermez le terminal.

Opérations mathématiques

Voici la liste des opérateurs mathématiques qui fonctionnent sur deux valeurs numériques, soit de type int ou double :

  • + addition
  • - soustraction
  • / division
  • * multiplication
  • % modulo (restant)

Division entière

La division entre deux entiers (int) peut seulement donner un entier comme résultat parce qu’il n’y a pas de partie décimale pour un int.

Exemples à évaluer dans jshell :

7 / 2; // donne 3 (pas 3.5)
7 / 3; // donne 2 (pas 2.333...)
1 / 2; // donne 0 (pas 0.5)

Modulo (restant)

Le modulo (%) donne le reste de la division.

1 % 3; // restants de la division par 3
2 % 3;
3 % 3;
4 % 3;

7 % 2; // restant de la division par 2

113 % 10; // restant de la division par 10

C’est utile pour vérifier si un nombre est divisible par un autre utilisant la condition “egale 0?”, soit == 0 en Java. Par exemple :

if ((a % 17) == 0) {
    // a est divisible par 17 parce qu'il n'y a pas de restant
    // fait quelque chose ici avec a
}

Les prochaines leçons présententent les opérations logiques (comme ==) et les structures de contrôle (if, while, for, etc.) qui permettent d’utiliser des conditions comme celle-ci.

Incrémentation et décrémentation

Ces opérateurs simplifient des assignations où on modifie la valeur d’une variable de type int ou double par une quantité fixe.

OpérateurDescriptionExempleÉquivalent
++incrémenter (augmenter la valeur) par 1a++a = a + 1
+=incrémenter par une valeur spécifiquea += 3a = a + 3
--décrémenter (diminuer la valeur) par 1a--a = a - 1
-=décrémenter par une valeur spécifiquea -= 3a = a - 3
int a = 5;
a++; // a vaut maintenant 6
a--; // a vaut maintenant 5
a += 3; // a vaut maintenant 8
a -= 2; // a vaut maintenant 6

Concaténation de texte

L’opérateur + est surchargé en Java; il a différentes définitions selon le type des opérandes.

  • Si les deux opérandes sont numériques, + fait une addition
  • Si au moins une des opérandes est du texte, + fait une concaténation. C’est-à-dire qu’il colle les deux représentations texte des valeurs pour donner un seul texte combiné.

Exemples :

  • "Bon" + "jour" donne "Bonjour"
  • "Trois = " + 3 donne "Trois = 3"

    le int 3 est converti en String "3" avant d’être collé au texte

  • "4" + 3 donne "43"

    Parce que "4" est du texte, l’opérateur + colle les deux représentations texte des valeurs au lieu de faire une addition.

Classe Math

N’oubliez pas que la classe Math contient plusieurs méthodes pour faire des opérations mathématiques plus complexes. Par exemple : Math.pow(), Math.sqrt(), Math.abs(), Math.sin(), etc.

Exercices

📚 Tester la compréhension

aucun quiz de vérification des concepts ici encore

🛠️ Pratique

Travaillez dans le répertoire GitHub partagé par votre enseignant pour la pratique et les exercices.

  1. Créer un fichier Calculs.java dans votre répertoire de travail. Assurez-vous d’y ajouter la structure de base (méthode main).
  2. Déclarez et initialisez deux int et faire les 5 opérations mathématiques des bases avec ces deux variables. Affichez le résultat de chaque opération, p. ex. System.out.println("a + b = " + (a + b));
  3. Ajouter un commentaire explicatif (// ... ou /* ... */) pour un des calculs explicant ce qui est une concaténation et ce qui est une opération mathématique dans l’expression que vous affichez.
  4. Déclarez et initialisez deux double et faire les deux divisions possibles entre ces deux variables (a/b et b/a). Affichez le résultat de chaque opération.
  5. Testez votre programme en le lançant au terminal. Corrigez les erreurs s’il y en a.
  6. Faites un commit et un push de votre travail dans GitHub.

Accueil > Programmer avec Java > Les bases de Java >

📚 Diagrammes de flux

Survol et attentes

Définitions
Flux
Synonyme de séquence dans le sens d’un écoulement des étapes
Diagramme de flux
Diagramme représentant la séquence des étapes d’un algorithme. Ces diagrammes utilisent des symboles standards, des étiquettes et des flèches pour représenter clairement le déroulement d’un algorithme, comme ceux implémentés dans les programmes Java.
Début / Fin
Un ovale
Traitement / Opération mathématique
Un rectangle
Entrée / Sortie
Un parallélogramme
Flèche
Indique que l’étape à la queue de la flèche est suivie par l’étape à la tête de la flèche.

Objectifs d’apprentissage

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

  • Identifier la forme des symboles standards pour le début/fin d’un algorithme, pour les entrées et les sorties et pour les traitements de données.
  • Tracer le diagramme dans l’ordre d’exécution des étapes.

Critères de succès

  • Je peux lire et produire un diagramme de flux pour des algorithmes de base (traitement, entrée/sortie)
  • Je peux évaluer la sortie d’un algorithme de base en traçant l’exécution illustrée.

Formes

Début / finTraitementEntrée / Sortie
startoperationio

Étiquettes

Dans chaque forme il y a du texte. À part les symboles pour le début et la fin, les étiquettes commencent par un verbe suivi par une description, p. ex.“ Assigner 3 à num“.

Traitements

  • Assigner une valeur à une variable
  • Modifier une variable
  • Faire un calcul
  • Combiner du texte (concaténation)

Entrées / sorties

Les entrées représentent de l’information qui n’est pas définie dans l’algorithme mais à laquelle on peut accéder :

  • Réponses de l’utilisateur
  • Contenu de fichiers
  • Lecture d’instruments

Les sorties représentent de l’information qui est exportée de l’algorithme :

  • Valeurs affichées à l’écran
  • Valeurs envoyées à une imprimante
  • Valeurs enregistrées dans un fichier

Exemple “Bonjour le monde”

hello-world

Cet exemple utilise simplement une sortie entre les ovales de début et de fin.

Le code équivalent est :

void main() {
    System.out.println("Bonjour, le monde!");
}

Exemple avec traitement

calc

Dans cet exemple il y a deux traitements - un mis en évidence et un dans l’instruction de sortie.

Le code équivalent est :

void main() {
    int num = 3;
    System.out.println(num * 10);
}

Ce programme afficherait la valeur 30 parce que num est définie comme 3 et on affiche num * 10 soit 3 * 10.

Une version plus réaliste de ce programme serait le suivant, avec saisie de la valeur de l’utilisateur :

calc réaliste

Le code équivalent est :

import java.util.*;

void main() {
    Scanner in = new Scanner(System.in);
    // saisir la valeur, incluant l'invite de commande
    System.out.print("Entrez un nombre entier > ");
    int num = in.nextInt();
    // afficher le résultat
    System.out.println(num * 10);
}

Produire ces diagrammes

On peut produire ces diagrammes avec toute sorte de logiciels de dessin, comme :

  • Google Dessin (seul ou intégré dans Docs ou Slides)
  • Lucidchart
  • Draw.io (drawio.com, app.diagrams.net)
  • etc.

Extension VS Code

Il y a une extension VS Code pour Draw.io qui nous permet de créer des diagrammes dans l’éditeur, alors c’est souvent le plus efficace dans ce cours. C’est Draw.io Integration par Henning Deitrichs (~2 million de téléchargements). Avec cette extension (ou son équivalent sur le web) :

  • C’est bon de choisir le format .drawio.png qui produit une image affichable directement dans des documents mais également éditable dans le logiciel.
  • Toutes les formes utilisées pour le diagramme de flux se trouvent dans la section Général : ovale, rectangle, parallélogramme.
  • On trace des flèches directement de la première forme à la deuxième. On a même le choix de commencer une flèche à partir d’une forme et choisir parmi quelques formes pour la destination sans passer par le menu.
  • On clique sur un objet pour y ajouter du texte en tapant.

Séquence

La structure représentée par ces diagrammes est la structure fondamentale de tout programme qui s’exécute sur un ordinateur classique : une simple séquence. Dans cette structure, toutes les instructions se succèdent dans l’ordre du début jusqu’à la fin.

Exercices

📚 Tester la compréhension

aucun quiz de vérification des concepts ici encore

🛠️ Pratique

Travaillez dans le répertoire GitHub partagé par votre enseignant pour la pratique et les exercices.

  1. Créez un nouveau dossier diagrammes dans le dépôt de pratique pour stocker ces diagrammes.
  2. Créer un nouveau fichier nommé collation.drawio.png. Il s’ouvrira avec l’extension Draw.io si vous l’avez déjà ajoutée. Faire un diagramme de flux pour la séquence de préparation d’une collation, comme un sandwich ou du thé.
  3. Reprendre 2 vos programmes de cette unité qui incorporent des traitements et/ou des entreés et des sorties. Produire leurs diagrammes de flux. Nommez les fichiers [NomDuProgramme].drawio.png.
  4. Faire un diagramme de flux pour la réalisation d’un projet de recherche dans recherche.drawio.png. Utilisez les grandes étapes du projet comme traitements.

Accueil > Programmer avec Java >

🛠️ Décomposition des problèmes

Survol et attentes

Même sans connaissances en programmation, vous êtes capable de décomposer des grands problèmes en plus petits problèmes. C’est une compétence essentielle pour la programmation et pour la résolution de problèmes en général. En particulier, à l’ère de l’intelligence artificielle, votre capacité à décrire les morceaux individuels d’un problème complexe vous permettra de tirer le plus grand profit des outils de rédaction de code, surtout si vous commencez par la rédaction des méthodes au bas de chaque chaîne d’appels.

Définitions
Modulaire
Un programme est dit modulaire quand il est divisé en parties indépendantes qui peuvent être réutilisées. Les méthodes sont les “modules” les plus simples dans un programme Java.
Décomposition / Approche descendante
Stratégie de résolution de problèmes qui consiste à diviser un problème complexe en plus petits problèmes plus faciles à résoudre. L’approche de partir du problème global et de le diviser en plus petits problèmes est appelée approche descendante. En programmation, on peut utiliser des commentaires de ligne pour créer des sections dans la méthode main pour chaque partie de la solution mais on utilise généralement une méthode séparée pour chacun de ces sous-problèmes.
Déclarer
Définir de façon plus ou moins formelle les éléments (étapes/méthodes) à inclure dans un plan pour la réalisation d’un projet, la structure d’un programme, etc. Une déclaration informelle est souvent juste un titre ou une courte description. Quand le plan est basé sur la décomposition, les éléments sont les différents “modules” (étapes / méthodes) de la solution complète.
Diagramme de dépendances
Diagramme indiquant la relation entre les étapes/modules/méthodes de la solution à un problème . Les méthodes sont des blocs nommés. Si une méthode a une flèche sortante vers une autre méthode, elle dépend de cette autre méthode. Si une méthode n’a aucune flèche sortante, elle est indépendante.
Chaîne d’appels
Une suite de flèches dans le diagramme de dépendances, du début (souvent la méthode main) jusqu’à la fin (un bloc qui n’a pas de flèche sortante).
Refactoriser
En programmation, ce terme veut dire restructurer le code pour le rendre plus lisible, plus efficace ou plus facile à maintenir. Séparer un morceau de code plus long et complexe en méthodes distinctes (le décomposer) après l’avoir écrit est un exemple de refactorisation qui rend le code plus facile à lire, à tester, à réutiliser et à modifier.

Objectifs d’apprentissage

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

  • Décrire un programme modulaire.
  • Décire la décomposition d’un problème avec une approche descendante.
  • Représenter les étapes d’un problème décomposé avec un diagramme de dépendances.
  • Identifier les dépendances entre les modules selon les chaînes d’appels.

Critères de succès

  • Je suis capable d’utiliser une approche descendante pour planifier les étapes de mon projet.
  • Je peux créer un diagramme de dépendances pour un programme décomposé.
  • Je peux refactoriser un programme en modules appropriés pour le rendre plus facile à lire, tester et maintenir.

Un programme modulaire

Au début d’une tâche de programmation, on peut penser à notre solution comme un seul programme :

programme monolithique

Par contre, on a souvent à faire au moins les trois étapes suivantes dans chaque programme qui interagit avec un utilisateur :

programme modulaire

Si on connaît les détails de la tâche, on peut probablement diviser l’étape “Actions” encore selon les différentes tâches à réaliser :

programme plus modulaire

Ce qu’on vient de faire est une décomposition du problème selon une approche descendante : on a pris un problème complexe et on l’a divisé en plus petits problèmes. Chaque petit problème est plus facile à résoudre que le problème global.

En programmation, on peut utiliser des méthodes pour résoudre chaque petit problème.

Un projet modulaire

On peut appliquer la même approche à la planification d’un projet dans n’importe quelle domaine.

Par exemple, un projet de recherche qui commence comme ceci :

projet

Peut être décomposé en étapes comme ceci :

projet modulaire

Voyant les étapes exposées comme ça, on peut mieux prévoir le temps nécessaire ou mieux s’organiser en équipe pour répartir les tâches.

Diagramme de dépendances

Certaines étapes ou modules d’un projet ne peuvent pas se faire sans que d’autres ne soient complétées. Un diagramme de dépendances illustre ces relations entre les modules.

On a déjà vu un diagramme de flux (qui montre la séquence des étapes) dans une leçon précédente. Un diagramme de dépendances est différent sur plusieurs points :

  • Il ne montre pas ce qui se passe dans les détails mais identifie seulement les “modules” ou les “étapes” du projet.
  • Il ne montre pas l’ordre d’exécution mais plutôt quelle étape dépend de quelle autre étape.

Le projet de recherche

Ici on compare les deux types de diagrammes pour le projet de recherche :

Diagramme de flux :

flux du projet

Diagramme de dépendances :

dépendances du projet

Avez-vous remarqué que la direction des flèches est inversée entre les deux diagrammes? C’est parce que le diagramme de dépendances commence avec le projet complet et regarde ce qui est nécessaire pour le rendre… réponse : que le projet soit rédigé. Et pour rédiger le projet, on a besoin de quoi?… réponse : les notes de recherche. Et pour faire les notes, on a besoin de quoi?… réponse : les sources d’information, etc. Ici les flèches indiquent les dépendances. Si on a une flèche de A vers B, ça veut dire que A dépend de B.

Dans le diagramme de flux, c’est l’opposé : on commence avec la première étape à réaliser. Ensuite, on regarde ce qui doit se passer après. Et après, et après, jusqu’à la fin du projet. Si on a une flèche de A vers B, ça veut dire que B est l’étape qui suit A.

C’est naturel que les flèches soient inversées entre les deux diagrammes. Si A dépend de B, alors A doit attendre que B soit fait. Cela nous donne :

  • flèche de dépendance de A vers B
  • flèche de séquence de B vers A

Le programme modulaire

Maintenant on fait le même exercice pour le programme modulaire :

Diagramme de flux :

flux du programme

Les modules se suivent dans un ordre logique.

Diagramme de dépendances :

dépendances du programme

Le programme dépend des modules Accueil, Actions et Au revoir qui sont indépendants entre eux (aucune flèche). Il y a aussi Actions qui dépend des modules Tâche 1, Tâche 2, … qui sont aussi montrés comme indépendants entre eux.

Ici la relation entre les deux types de diagrammes n’est pas aussi claire. Entre autres, on ne peut pas regarder le diagramme de flux et savoir, avec certitude, si une étape dépend de l’étape précédante ou non. De l’autre côté, il n’y a aucun lien direct entre les “modules” Accueil, Actions et Au revoir dans le diagramme de dépendances alors rien ne suggère une séquence sauf notre expérience qu’un accueil vient avant un au revoir.

En général, on produit chaque type de diagramme indépendamment en appliquant les concepts de chacun :

  • le diagramme de flux montre toujours la séquence d’exécution des étapes;
  • le diagramme de dépendances montre les dépendances entre les différents modules.

Chaînes d’appels

Dans les diagrammes de dépendances, les modules sont liées dans des chaînes de dépendance. Les modules en programmation sont souvent des méthodes. Dans ce contexte on parle de chaînes d’appels, parce qu’une méthode doit appeller les autres méthodes qu’elle a besoin d’utiliser.

Si une méthode à plusieurs dépendances, il y a des branches qui se forment dans le diagramme, créant autant de chaînes d’appels que de branches. Notre exemple de programme modulaire a donc ces cinq chaînes d’appels car la méthode Actions dépend de trois méthodes, triplant le nombre de chaînes passant par elle :

chaînes d’appels

Au début d’une chaîne d’appels (le bloc avec aucune flèche entrante), on a la méthode dépendante d’une autre. À la fin d’une chaîne d’appels, on a une méthode qui n’a pas de dépendances (aucune flèche sortante), qui est indépendante.

Si on veut profiter pleinement de la décomposition (plusieurs problèmes plus simples au lieu d’un grand problème), on ne peut pas commencer par écrire les instructions pour les méthodes dépendantes car il faudra aussi immédiatement écrire les instructions pour toutes les autres méthodes plus bas dans la chaîne d’appels (la première méthode dépend d’eux)… on n’a pas vraiment simplifié notre tâche! Par contre, si on commence avec une des méthodes indépendantes, on peut écrire le code juste pour cette méthode et la tester. On a juste un petit problème à résoudre. Ensuite, on peut soit choisir une autre méthode indépendante à faire ou bien la prochaine méthode plus haut dans la chaîne d’appels. Dans chaque cas, on reste avec juste un petit problème à résoudre dans l’immédiat. Mais peu à peu, tous les modules du projet se réalisent.

Refactoriser - quand on a déjà tout écrit

Parfois on n’a pas prévu découper notre programme en plus petites méthodes, mais la taille du programme ou sa complexité ou le besoin de réutiliser plusieurs fois le même code nous pousse à le faire. Réorganiser le code sans changer son comportement s’appelle refactoriser. Il n’y a pas de planification, juste un besoin pressant pour la décomposition, pour la création de modules.

La plupart du temps, surtout en commençant à programmer, c’est dur de savoir quand quelque chose sera trop complexe avant de l’avoir écrit, alors la refactorisation est souvent ce qui motive les premières expériences avec le code modulaire.

Exercices

🛠️ Pratique

Travaillez dans le répertoire GitHub partagé par votre enseignant pour la pratique et les exercices

Assurez-vous d’avoir installé l’extension “Draw.io Integration” pour VS Code pour éditer les fichiers .drawio.

  1. Pour la grande tâche de “préparer un repas” avec les modules “préparer la table”, “cuisiner”, “servir le mets principal”, “servir le dessert”, “remplir les breuvages” et “nettoyer”, dans votre dossier diagrammes :
    1. Créez un diagramme de dépendances pour ces modules nommé modules_repas-dependancy.drawio
    2. Créez un diagramme de flux pour ces modules nommé modules_repas-flux.drawio.

Accueil > Programmer avec Java >

Séquence d’exécution avec le code modulaire

Survol et attentes

Définitions
Diagramme de séquence d’exécution
Un diagramme qui identifie les numéros de lignes des instructions selon l’ordre réelle d’exécution (et non l’ordre de déclaration). Les appels de méthode sont indiqués par une flèche de la ligne de l’appel vers la ligne de la signature de la méthode appelée. Le retour (fin) de la méthode est indiqué par une flèche de la ligne de la dernière instruction de la méthode vers la ligne de l’appel.
Appel de procédure
Dans un diagramme de flux : un rectangle avec des lignes de côté doublées qui contient le nom de la méthode. Cette forme représente l’appel de la méthode.
Définition de procédure
Dans un diagramme de flux : commence par un ovale qui contient le nom de la méthode (au lieu de “début”), continue avec les instructions de la méthode et finit par un autre ovale qui contient “fin de [nom de la méthode]” ou “retour”.

Objectifs d’apprentissage

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

  • Suivre l’exécution d’un programme qui contient des appels de méthodes que nous avons définies et le documenter dans un diagramme de séquence d’exécution.
  • Identifier des appels et des définitions de méthodes dans un diagramme de flux, un diagramme de séquence d’appels et un programme Java.
  • Produire un diagramme de flux pour un algorithme simple qui contient des procédures.

Critères de succès

  • Je peux suivre l’exécution d’un programme qui contient des méthodes et le documenter avec un diagramme de séquence d’exécution.
  • Je peux lire des diagrammes de flux qui contiennent des procédures.

Le code modulaire

Commençons avec un exemple simple1 d’un programme décomposé, d’un programme modulaire.

Fichier : MainClass.java

void main() {
    welcome();
    doStuff();
    bye();
}

void welcome() {
    System.out.println("Bienvenue dans mon programme!");
}

void doStuff() {
    System.out.println("On fait des choses utiles ici...");
}

void bye() {
    System.out.println("Bye!");
}

Dans ce programme, comme dans tout programme Java, le programme principal est dans la méthode main. Dans ce cas-ci, le programme principal est très simple. Il appelle trois autres méthodes, welcome, doStuff et bye. On comprend très bien la structure globale du programme. Par la suite, tous les détails du programme se trouvent dans les méthodes individuelles appelées par main. Même si ces détails devenaient plus complexes, la méthode main resterait simple et facile à lire.

Le diagramme de flux

Le même programme est représenté ci-dessous dans un diagramme de flux.

diagramme de flux avec modules

Notez qu’il y a plusieurs séquences d’instructions distinctes dans le diagramme de flux : une pour chaque méthode.

L’algorithme commence toujours avec l’ovale “Début” et se termine avec l’ovale “Fin”. Par contre, on a maintenant un nouveau type de bloc dans le diagramme - un appel de procédure :

appel dans un diagramme de flux

Ce bloc envoie l’exécution du programme à la séquence d’instructions qui commence par le nom inscrit dans le bloc. Quand la séquence d’instructions de la méthode appelée est terminée, l’exécution est retournée au point de l’appel et continue le long des flèches.

On voit aussi que les ovales de début et de fin sont légèrement différents pour les procédures :

  • le mot “Début” est remplacé par le nom de la méthode
  • le mot “Fin” est remplacé par “Fin de [nom de la méthode]” ou “Retourne”

Tracer la séquence d’exécution programme

Chaque appel d’une méthode envoie l’exécution du programme à la signature de la méthode appelée et à travers ses instructions. Quand la méthode appelée se termine, l’exécution est retournée au point de l’appel pour continuer avec le reste du programme.

On peut le voir et le représenter comme suit.

À partir du code

On peut utiliser l’outil Python Tutor pour visualiser l’exécution du programme étape par étape afin de mieux comprendre la mécanique d’appel et de retour de méthodes.

Notez que le code est logiquement identique à celui ci-dessus mais que la version de Java disponible (Java 8) ne permet pas les simplifications de la syntaxe qu’on a avec Java 22. On a donc la déclaration explicite d’une classe et les mots clés static et public pour les méthodes.

Sans un tel outil, on peut tracer manuellement l’éxecution avec un diagramme de séquence d’exécution, soit :

  • une liste des numéros de ligne exécutés dans l’ordre d’exécution;
  • on indente vers la droite avec une flèche pour indiquer un appel de méthode et
  • on revient à gauche avec une autre flèche pour indiquer le retour de la méthode.

Pour notre version de MainClass.java (avec la syntaxe simplifiée), le diagramme de séquence d’exécution serait :

1
2 -> 7  
     8
2 <- 9 
3 -> 11 
     12
3 <- 13 
4 -> 15 
     16
4 <- 17 
5 

Les lignes suivantes représentent des appels de méthodes :

Appel de welcome :

2 -> 7

Appel de doStuff :

3 -> 11

Appel de bye :

4 -> 15

Les lignes suivantes représentent les retours de méthodes :

Retour de welcome :

2 <- 9

Retour de doStuff :

3 <- 13

Retour de bye :

4 <- 17

Notez que l’appel et le retour reviennent à la même ligne dans le diagramme de séquence, soit 2 appel welcome et welcome retourne à 2, etc.

Dans Python Tutor, on sautait les lignes de signature durant l’appel et la ligne appelante lors du retour. C’est parce que nos procédures n’ont pas de paramètres et ne retournent pas de valeur en se terminant alors il n’y avait rien à visualiser pour ces lignes. On voit les paramètres et les valeurs de retour dans une prochaine leçon.

Notez aussi que durant l’exécution des méthodes appelées, les numéros de ligne sont identées au même niveau que la signature de la méthode afin de distinguer ces instructions des instructions de la méthode appelante.

À partir du diagramme de flux

On peut aussi tracer la séquence d’exécution à partir du diagramme de flux. On y ajoute manuellement plus de flèches!

diagramme de flux avec modules

Encore une fois, comme avec le diagramme de séquence d’exécution, l’appel et le retour se font à partir du même bloc dans le diagramme de flux.

Exercices

🛠️ Pratique

Travaillez dans le répertoire GitHub partagé par votre enseignant pour la pratique et les exercices

  1. Pour le programme ci-dessous dans un fichier nommé ModularSequence.java :
    1. Créez le diagramme de séquence d’exécution dans un commentaire de bloc à la fin du programme.
    2. Ajoutez un astérisque (*) à la fin de chaque ligne du diagramme de séquence d’exécution où il y a une instruction d’affichage.
    3. Dans un deuxième commentaire de bloc à la fin du programme, documentez ce que le programme affiche à la console durant l’exécution. Comparez cette sortie avec le diagramme de séquence d’exécution et l’ordre des lignes avec des astérisques. Est-ce que c’est cohérent? Si non, revoyez votre diagramme de séquence d’exécution pour trouver où vous avez fait une erreur.
    4. Créez le diagramme de flux dans le dossier diagrammes dans un fichier nommé modular-flux.drawio.
    5. Créez le diagramme de dépendances (voir la leçon précédente) dans un fichier nommé modular-dependancy.drawio. Utilisez main comme le bloc de départ.

ModularSequence.java

void main() {
    System.out.println("Bienvenue!");
    a();
    b();
}

void a() {
    c();
    System.out.println("bleu");
}

void b() {
    System.out.println("blanc");
    a();
}

void c() {
    System.out.println("rouge");
}

  1. Exemple applicant les classes implicites et le main d’instance de JEP 463 (pleinement intégré aux outils Java22+)

Accueil > Programmer avec Java >

Méthodes donnant une valeur de retour

Survol et attentes

Définitions
Implémenter
En programmation, ce terme veut dire “écrire le code selon le plan pour le programme”. Avec les programmes modulaire, l’implémentation se fait une méthode à la fois afin de tester que chaque morceau fonctionne correctement avant de passer au suivant.
Déclaration de méthode
signature spécifiant le type de retour, le nom, les paramètres et le bloc de code d’une fonction.
Corps de méthode
tout entre les accolades {} qui suivent directement la signatures de la méthode. Les méthodes implémentées doivent toujours avoir un corps; la signature n’est jamais terminée avec un ;.
Appel de méthode
instruction qui utilise le nom de la méthode suivi par des parenthèses. Si la méthode retourne une valeur, on l’assigne souvent à une variable ou on l’utilise directement dans le reste de l’instruction, comme un calcul ou un affichage. Si la méthode reçoit aussi de l’information, on passe ces valeurs entre les parenthèses.
Valeur de retour
valeur produit par une méthode et renvoyée au programme. L’appel de méthode représente essentiellement cette valeur dans l’expression où l’appel est insérée, p. ex. un calcul ou une assignation. Dans Java, le type de retour est spécifié dans la déclaration de la méthode devant le nom de la méthode. Les méthodes qui ne retournent rien ont le type de retour void. Les autres ont un type de retour comme les variables et incluent une instruction return [valeur]; à la fin du bloc de code de la méthode.

Objectifs d’apprentissage

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

  • Distinguer les méthodes qui retournent par défaut de celles qui retournent explicitement.
  • Décrire ce qui se passe quand une méthode retourne, tant pour la méthode que pour le programme appelant, notamment quand la méthode retourne une valeur.
  • Lire et tracer un diagramme de séquence d’exécution pour un programme qui inclut des méthodes avec valeur de retour.

Critères de succès

  • Je peux implémenter des méthodes avec valeur de retour dans mes programmes et reconnaître celles que j’utilise déjà.

Exemples connus de méthodes avec valeur de retour

Vous connaissez déjà quelques méthodes avec valeur de retour :

  • celles pour la classe Scanner - comme nextInt(), nextDouble(), nextLine()
  • celles pour la classe Math - comme Math.pow() et Math.sqrt()
  • celles pour la classe String - comme length() et toUpperCase()
Qu'est-ce que ces exemples ont tous en commun?

Réponse

Il y a quelques indices que ces méthodes retournent une valeur au programme appelant :

  1. On assigne souvent une variable du type approprié pour recevoir la valeur retournée, p. ex. String name = scanner.nextLine();
  2. Les info-bulles de notre éditeur nous montrent le type de retour de la méthode dans l’en-tête et décrivent la nature de la valeur retournée, p. ex. : info-bulle pour nextInt()

Exemples connus de méthodes sans valeur de retour

Les méthodes sans valeur de retour ont le type de retour void. Elles sont souvent utilisées pour des actions qui modifient directement l’état de la mémoire du programme ou qui font des sorties (comme afficher à l’écran ou enregistrer dans un fichier).

Ceux que vous connaissez le mieux sont les méthodes :

  • main - qui ne retourne rien au système d’exploitation : sa fin est la fin du programme
  • println, print et printf de System.out - qui affichent des messages à la console

Il y a quelques indices que ces méthodes ne retournent pas de valeur :

  1. On ne déclare aucune variable pour recevoir la valeur retourné (car il n’y en a pas)
  2. Le mot-clé void précède immédiatment le nom de la méthode dans la déclaration, p. ex. void main() ou sa version plus standard public static void main(String[] args)

Écrire nos propres méthodes avec valeur de retour

Avec la décomposition, on a vu que c’est souvent utile de diviser un programme complet et plus petits sous-problèmes. Chacun serait une (ou plusieurs) méthode(s). Il faut alors savoir comment les déclarer dans nos programmes.

Signature de méthode

L’exemple de la leçon précédente, dans MainClass.java, montre comment déclarer les méthodes sans valeur de retour - comme void main() et void welcome(). C’est la même chose pour les méthodes avec valeur de retour, sauf qu’on remplace void par le type de retour désiré. Le format général est alors :

[type de retour] [nom de la méthode]() {
    // bloc de code
}

Corps de méthode

Les corps de méthode, les blocs de code entre les accolades {}, seront différents selon si le type est void (pas de valeur de retour) ou non.

Sans valeur de retour

Type : void

void [nom de la méthode]() {
    // instructions
} // retour implicite à l'accolade fermante

ou

void [nom de la méthode]() {
    // autres instructions
    return; // retour explicite sans valeur de retour
}

Avec valeur de retour

Type : int, double, String, etc.

[type de retour] [nom de la méthode]() {
    // autres instructions
    return [valeur]; // retour explicite où valeur est du type de retour
}

Deux exemples

Le programme ci-dessous montre un exemple très simple (et banal) de deux méthodes avec valeur de retour.

Fichier : BasicReturn.java

String getName() {
    return "Dave3000";
}

int getMeaningOfLife() {
    return 42;
}

void main() {
    String name = getName();
    System.out.println("Bonjour, je m'appelle " + name);
    System.out.println("J'ai aussi un chiffre très important pour vous : " + getMeaningOfLife());
}

Remarquez qu’on a placé la méthode main à la fin du fichier. C’est possible de le faire, et souvent très naturel de le faire si vous avez planifié les différents modules de votre programme avant de commencer. Les développeurs tendent à mettre la méthode main soit au début, soit à la fin du fichier pour faciliter la lecture du programme.

String getName()

  • Type de retour : String : retourne "Dave3000", un String littéral
  • Quand on l’appelle dans main (ligne 10), on assigne la valeur retournée à la variable name, qu’on affiche ensuite à la ligne 11.

int getMeaningOfLife()

  • Type de retour : int : retourne 42, un int littéral
  • Quand on l’appelle dans main (ligne 12), on affiche directement la valeur retournée sur cette même ligne. Notez que le nom de la méthode est tout de même suivi de parenthèses.

void main()

  • Type de retour : void : ne retourne rien
  • retourne implicitement à la fin de la méthode (sans instruction return)

Diagramme de flux pour une méthode avec valeur de retour

Ce qu’on a déjà appris sur les diagrammes de flux pour les méthodes s’applique, mais il faut maintenant :

  1. Indiquer dans la définition de la méthode ce qui sera retourné
  2. Utiliser dans l’algorithme principal ce qui est retourné suite à l’appel de la méthode

Voici le diagramme de flux pour le programme BasicReturn.java :

diagramme de flux pour BasicReturn

Le diagramme de flux nous aide à mieux voir la mécanique appel-retour. On voit quelque chose là qui n’est pas aussi apparent dans le code. Dans l’algorithme principal du diagramme de flux nous devons utiliser au moins deux instructions, soit :

  • une pour l’appel de la méthode et
  • une pour utiliser la valeur de retour.

Dans le code, tout ça se fait normalement sur la même ligne de code, en combinant plusieurs expressions (p. ex. : assignation et appel, affichage et appel) dans une seule instruction.

Notez aussi que l’appel précède toujours l’autre instruction dans le diagramme (comme durant l’exécution réelle du programme). Dans le code, ce n’est pas évident que ce soit le cas, car l’appel se trouve plus à gauche que l’autre instruction.

Structure générale pour l’utilisation d’une méthode avec valeur de retour

Tracer la séquence d’exécution

Peu importe si main se trouve au début ou à la fin du fichier, on commence toujours le programme à main. À part le déplacement de main, il n’y a rien de nouveau dans le diagramme de séquence d’exécution, sauf que :

  • le fait de retourner à la même ligne que l’appel devrait faire plus de sens;
  • on n’arrive pas à la ligne de l’accolade fermante s’il y a une instruction return; l’accolade fermante est un return implicite dans les autres cas.
9
10 -> 1
10 <- 2
11
12 -> 5
12 <- 6
13

Pour getName

10 -> 1

est l’appel

10 <- 2

est l’assignation de la valeur de retour à name

Pour getMeaningOfLife

12 -> 5

est l’appel

12 <- 6

est l’affichage de la valeur de retour

Diagramme de dépendances

Encore, pour ce diagramme on doit commencer à main et regarder quelles méthodes il appelle.

La nouveauté est qu’avec des valeurs de retour, on inclut le type de retour dans le bloc de la méthode. Plusieurs styles sont possibles, mais deux qui sont cohérents avec ce que nous voyons sont :

  • [nom de la méthode]() -> [type de retour] … la flèche est semblable à ce qu’on utilise dans le diagramme de séquence d’exécution
  • [nom de la méthode]() : [type de retour] … ce format ressemble à ce que vous voyez si vous ouvrez la panneau Structure (Outline en anglais) de VS Code. structure du code dans VS Code

Voici donc une version possible de ce diagramme. L’ajout des valeurs de retour nous indique que main dépend des valeurs String et int produites par les deux méthodes qu’il appelle.

diagramme de dépendance avec valeurs de retour

Exercices

🛠️ Pratique

Travaillez dans le répertoire GitHub partagé par votre enseignant pour la pratique et les exercices

  1. Créer un fichier DeclaringMethods.java qui respecte le diagramme de dépendances ci-dessous ainsi que les détails d’implémentation suivants :
    1. showTheNumber affiche la valeur retournée de getTheNumber et ne retourne rien à main
    2. makeItCaps retourne une valeur en majuscules du texte retournée par getTheWords. Cette valeur sera affichée par main. Indice: utiliser la méthode toUpperCase() pour changer la casse de lettres.
  2. Prendre une capture d’écran de la section Structure de VS Code. Quelle est le type de retour des méthodes showTheNumber et main qui ne retournent rien au programme? Conservez la capture comme ./captures/declaringMethods_structure.png
  3. Lancer le programme et prendre une capture d’écran de la session à la console. Nomme-la ./captures/declaringMethods.png.
  4. Créer un diagramme de flux nommé ./diagrams/declaringMethods.drawio pour le programme.
  5. Tracer l’exécution avec un diagramme de séquence d’exécution que vous écrivez dans le commentaire d’en-tête du code source.

diagramme de dépendance pour la pratique

Accueil > Programmer avec Java >

Méthodes recevant des arguments

Survol et attentes

Définitions
Argument
valeur passé à une méthode lors de l’appel. Cette valeur initialise le paramètre correspondant dans la déclaration de la méthode.
Paramètre
variable déclarée dans la signature d’une méthode qui reçoit une valeur lors de l’appel de la méthode. Cette variable est utilisée dans le bloc de code de la méthode et est détruite quand la méthode se termine.

Objectifs d’apprentissage

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

  • Distinguer les méthodes qui n’ont pas de paramètres de celles qui en ont.
  • Décrire ce qui se passe quand on appelle une méthode en lui passant des arguments.
  • Lire et tracer un diagramme de séquence d’exécution pour un programme qui inclut des méthodes avec des paramètres.

Critères de succès

  • Je peux implémenter des méthodes avec des paramètres dans mes programmes et reconnaître celles que j’utilise déjà.

Rôle des paramètres

Les paramètres rendent les méthodes plus flexibles. Leur code peut fonctionner avec différentes informations comme entrées, alors au lieu d’écrire une méthode pour chaque entrée possible, on a la possibilité de fournir cette information durant l’appel.

C’est un concept très puissant.

En mathématiques, vous avez quelque chose de similaire avec les fonctions, p. ex. : f(x) = x^2. Cette fonction nous donne le carré de x. x est donc un paramètre de f. En passant différentes valeur de x à la fonction f(x) on obtient différents résultats.

Exemples connus de méthodes avec paramètres

Vous connaissez déjà quelques méthodes avec des paramètres :

  • toutes les formes de print“ :
    • System.out.print("Votre réponse > ")
    • System.out.println("Bonjour " + name + "!")
    • System.out.printf("%S\n", "david")
  • créer un nouveau Scanner avec : new Scanner(System.in)
  • spécifier le format de nombres sur un objet Scanner : .useLocale(Locale.CANADA_FRENCH)
  • faire des calculs avec la classe Math : Math.pow(2, 3), Math.sqrt(9), Math.abs(-5)
  • certaines opérations sur des objets String : comme .charAt(0)
Qu'est-ce que ces exemples ont tous en commun?

Réponse

Il y a quelques indices que ces méthodes prennent des paramètres :

  1. Il y a toujours quelque chose entre les parenthèses quand on les appelle.
  2. Les info-bulles de notre éditeur nous montrent la liste des paramètres et les décrivent quand on passe la souris sur le nom de la méthode. info-bulle pour Math.pow()

Exemples connus de méthodes sans paramètres

Les méthodes sans paramètres sont appelées avec des parenthèses vides.

Ceux que vous connaissez le mieux sont les méthodes de Scanner :

  • .next(), .nextLine(), .close()

On a aussi vu quelques méthodes de traitement des String :

  • .length(), .toUpperCase()

Notez que ces méthodes commencent avec un point . car ils s’appelent suite au nom d’un objet. Elles utilisent l’information qui est déjà dans l’objet (le Scanner, le String) pour faire ce qu’ils ont à faire et n’ont donc pas besoin de paramètres.

Écrire nos propres méthodes avec paramètres

On a déjà vus la structure générale de la déclaration d’une méthode. Ici on ajoute un dernier détail : la liste de paramètres.

Signature de méthode

Voici le gabarit complet pour la déclaration d’une méthode, incluant maintenant les paramètres :

[type de retour] [nom de la méthode] ( [liste de paramètres] ) {
    // bloc de code,
    // avec instruction `return` au besoin
}

La liste de paramètres a le format suivant, soit une liste de déclarations de variables :

type1 nom1, type2 nom2, type3 nom3, ...

où les types sont ceux qu’on connaît (int, double, String, Scanner, etc.) et les noms sont un nom de variable selon les règles et les conventions qu’on utilise déjà.

Corps de méthode

Le but de déclarer des paramètres est d’avoir des informations qu’on peut manipuler dans le code de la méthode. On ne connaît pas encore la valeur de ces informations, mais on connaît leur type (à cause de la signature). On peut alors travailler librement avec ces variables dans le code comme on le ferait avec n’importe quel autre variable du même type.

Appeller des méthodes avec paramètres

Quand on appelle une méthode qui a déclaré des paramètres dans sa signature, nous devons fournir des informations du type approprié entre les parenthèses. Sinon le programme nous signale une erreur de syntaxe.

Les informations passées durant l’appel s’appellent des arguments.

Par exemple, si nous avons déclaré cette méthode en déclarant un paramètre String name :

void greetWithName(String name) {
    System.out.println("Bonjour " + name + "!");
}

Il faut absolument placer une valeur de type String entre les parenthèses durant l’appel, p. ex. :

void main(){
    greetWithName("Jean");
    greetWithName("Lucie");
    greetWithName(); // erreur de syntaxe à cette ligne
}

Différences entre paramètres et arguments

Paramètres

  • Une déclaration fixe
  • Déclarés dans la signature de la méthode
  • Le nom du paramètre est exclusif à la méthode et prend priorité à l’intérieur de la méthode si d’autres variables utilisent le même nom à l’extérieur de la méthode1

Arguments

  • Les valeurs peuvent être différentes à chaque appel
  • Spécifiées durant l’appel de la méthode
  • Si on utilise une variable comme argument, la méthode copie sa valeur pour son exécution et ne change pas la valeur de notre variable

Comment ça marche? Le diagramme de flux

Comme le mécanisme appel-retour était plus explicite dans le diagramme de flux que dans le code, le mécanisme argument-appel l’est aussi.

Regardons donc le diagramme de flux pour l’exemple greetWithName ci-dessus.

diagramme de flux pour greetWithName

Le diagramme de flux nous aide à mieux voir la mécanique argument-paramètre. On voit quelque chose là qui n’est pas aussi apparent dans le code.

Dans l’algorithme pour la méthode :

  • la première instruction assigne la valeur reçue à une variable (dans cette exemple, name), ce qui correspond au paramètre de la méthode
  • le reste du code est définie selon ce nom de variable

Dans l’algorithme principal du diagramme :

  • l’instruction d’appel inclut une information à passer à la méthode, ce qui correspond à l’argument

Dans le code Java pour la méthode greetWithName, l’assignation de la valeur reçue est masquée / implicite.

Dans le diagramme de flux, on doit le mentionner explicitement.

Structure générale pour l’utilisation d’une méthode avec paramètre

Deux exemples

Le programme ci-dessous montre un exemple simple (et banal) de deux méthodes avec des paramètres.

Fichier : BasicParam.java

void greetWithName(String name) {
    System.out.println("Bonjour " + name + "!");
}

int product(int a, int b) {
    return a * b;
}

void main() {
    String david = "David";
    int x = 3;
    int b = 5;
    greetWithName(david);
    System.out.println(product(x, b));
}

Remarquez qu’on a encore placé la méthode main à la fin du fichier.

void greetWithName(String name)

Paramètre
String name - s’attend à recevoir un texte
Corps
utilise la variable name dans une instruction d’affichage
Retour
rien
Appel
dans main avec la variable david qui contient le texte "David"; le paramètre name prend alors la valeur "David"

int product(int a, int b)

Paramètres
int a, int b - s’attend à recevoir deux nombres entiers nommés a et b
Corps
utilise les variables a et b dans un calcul
Retour
retourne le résultat du calcul, un type int
Appel
dans main avec les variables x (contient 3) et b (contient 5); le paramètre a prend la valeur 3 et le paramètre b prend la valeur 5; la valeur de retour est utilisée directement dans une instruction d’affichage

void main()

Ne prend aucun paramètre

Tracer la séquence d’exécution

On commence toujours le programme à main. Le diagramme de séquence d’appel pour le programme dans BasicParam.java serait :

9
10
11
12
13 -> 1
      2
13 <- 3
14 -> 5
14 <- 6
15

Pour greetWithName

13 -> 1

est l’appel.

Parce qu’il y a des paramètres, c’est ici qu’on assigne au paramètre name la valeur de l’argument.

13 <- 3

est le retour à l’accolade fermante (retour implicite)

Pour product

14 -> 5

est l’appel.

Parce qu’il y a des paramètres, c’est ici qu’on assigne à a et b les valeurs des arguments.

14 <- 6

affichage dans main de la valeur de retour (retour explicite avec return)

Diagramme de dépendances

On commence à main et regarde quelles méthodes il appelle.

La nouveauté est qu’avec les paramètres, on inclut le type des chaque paramètre dans le bloc de la méthode entre des parenthèses. On peut trouvez cette information dans la signature de chaque méthode mais aussi dans la structure du document.

  • [nom de la méthode]( [liste des types de paramètres] ) : [type de retour]

    structure du code dans VS Code

Voici donc une version possible de ce diagramme. L’ajout des types des paramètres nous indique que chaque méthode dépend sur main pour leur fournir les valeurs String et int pour leurs paramètres. On voit aussi que main devra géré la valeur int retournée par la méthode product.

diagramme de dépendance avec paramètres

Exercices

🛠️ Pratique

Travaillez dans le répertoire GitHub partagé par votre enseignant pour la pratique et les exercices

  1. Ajouter le fichier BasicParam.java à votre dossier de pratique.
  2. Changer le nom de la variable david à un nom de votre choix.
  3. Modifier le programme afin d’utiliser un Scanner dans main afin d’ajouter de l’interaction pour obtenir les valeurs d’un nom et des deux nombres.
  4. Changer le type de nombre dans la méthode product à des double.
  5. Créer une nouvelle méthode double add(double, doubl) en complétant sa signature. Il devrait ajouter la valeurs des deux paramètres et retourner le résultat au programme.
  6. Mettre à jour de diagramme de dépendance (vous pouvez télécharger l’exemple dans les notes avec un clic droit > enregistrer sous) et l’ajouter à votre dossier diagrammes.
  7. Enrichissement : créer une nouvelle méthode double linear(double, double, double) qui utilisera les méthodes add et product pour calculer le résultat de y = mx + b. Les trois paramètres sont utilisés pour m, x et b repectivement. La méthode retourne la valeur de y. Tester cette méthode dans main Mettre à jour le diagramme de dépendances.

  1. On étudie ce type de priorité en plus de détails dans la prochaine leçon

Accueil > Programmer avec Java > Décomposition et modularité >

📚 Portée des variables

Survol et attentes

Définitions
Portée d’une variable
l’étendue du code source où le nom de variable est reconnue, se limitant aux accolades {} du bloc qui le contiennent. Intentionnellement, la portée des variables est limitée pour mieux contrôler l’accès aux informations et pour permettre la réutilisation des mêmes noms de variables dans différentes parties du code source sans créer des conflits de noms.
Variable globale
une variable déclarée en dehors de toutes les méthodes. Toutes les méthodes dans le fichier peuvent accéder directement à ces variables.
Variable locale
une variable déclarée dans une méthode ou, à l’intérieur des méthodes, dans une structure de contrôle (incluant leurs déclarations). C’est pourquoi les méthodes doivent recevoir des arguments et retourner des valeurs pour passer de l’information aux autres méthodes.
Constante globale
une variable globale qui est déclarée final pour indiquer qu’elle ne peut pas être modifiée. Les constantes globales sont utilisées pour des valeurs qui ne changent pas dans le programme et qui sont utilisées dans plusieurs méthodes. Par exemples : une référence au Scanner de la console, des valeurs de référence, des codes de couleur ANSI, des constantes mathématiques. Nommant ce type de valeur dans une constante globale rend le code plus lisible que d’inclure la valeur littérale partout où elle est utilisée.

Objectifs d’apprentissage

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

  • Décrire la différence entre une portée globale et une portée locale.
  • Savoir quelle variable, entre deux variables de même nom, sera préférée en considérant sa portée.
  • Identifier si une variable spécifique est accessible ou non dans un endroit donné du code source.
  • Décrire des raisons de préférer l’utilisation de variables locales à l’utilisation de variables globales.

Critères de succès

  • Je minimise la visibilité des variables en leur donnant la plus petite portée nécessaire, ce qui veut dire utiliser une majorité de variables locales.
  • J’utilise les variables globales dans mon code lorsque cela rend le code plus lisible et plus facile à maintenir.
  • J’utilise des constantes globales pour les valeurs qui ne changent pas dans mon programme et qui sont utilisées dans plusieurs méthodes.

Portée des variables

La portée d’une variable est synonyme de sa visibilité : jusqu’à quels endroits dans le code est-ce qu’on peut “voir” cette variable (la nommer sans erreur de syntaxe)?

Les variables globales sont visibles partout dans un programme1. Elles sont déclarées à l’extérieur des méthodes et on peut les lire et les modifier dans toutes les méthodes du programme.

C’est un avantage parce qu’on a pas besoin d’échanger cette information entre les méthodes via des paramètres ou des valeurs de retour.

C’est un désavantage parce n’importe quelle partie de notre code peut modifier cette information, ce qui peut rendre le code difficile à corriger, briser la modularité du code (les sous-problèmes sont maintenant fusionnés autour de l’information partagée) ou introduire des problèmes de sécurité (modifications possibles au-delà des intentions originales).

Les variables locales sont seulement visibles dans la méthode où elles sont définies. Les paramètres et les variables définies dans le corps d’une méthode sont des variables locales. Jusqu’à présent, nous avons seulement utilisé des variables locales dans nos programmes.

Les avantages des variables locales sont que : l’information existe seulement à l’endroit où elle sera directement utile et plusieurs variables dans un programme peuvent avoir le même nom sans interférence parce que leurs portées ne se croisent pas.

Le désavantage est que pour partager l’information locale à une autre méthode, il faut “passer des messages” avec des arguments/paramètres et des valeurs de retour. Cela peut parfois rendre la liste des paramètres assez longue que c’est difficile à se rappeler quel paramètre va à quelle position.

---------------fichier------------------

déclaration de variable globale


méthode() {

    déclaration de variable locale

}


méthode( les paramètres sont des variable locales ) {

}

----------------------------------------

Variables globales

C’est rarement une bonne idée d’utiliser des variables globales parce que le code devient beaucoup plus dur à déboguer. Mais il y a quelques cas où c’est une solution bien adaptée :

La valeur est constante et requise par plusieurs méthodes

On peut alors déclarer une constante globale en le préfixant avec le mot-clé final et en appliquant la convention du nom en majuscules. Les Scanner pour la console, les codes de couleur ANSI et les structures de données (à venir dans une prochaine section du cours) sont des exemples de bons candidats de constantes globales.

Dans ce cas, le fait que la valeur soit constante élimine la possibilité de modification inappropriée et la portée globale peut réduire la complexité et la répétitivité des déclarations et appels de méthodes.

La variable représente un état du programme que plusieurs méthodes doivent consulter ou peuvent modifier

Plusieurs informations dans un jeu tombent dans cette catégorie. Si ces variables n’étaient pas globales, la liste des paramètres pour la plupart des méthodes deviendrait ingérable. De plus, la restriction avec Java d’une seule valeur de retour rend l’exportation de plus qu’une modification à ces valeurs, si pas impossible, du moins très complexe. Cette complexité additionnelle rendrait le code plus dur à lire, à déboguer et à modifier. Les risques liés aux variables globales sont alors moindre que les risques avec l’autre approche.

Exemples de constantes globales

Fichier GlobalScanner.java

import java.util.*;

final Scanner CONSOLE = new Scanner(System.in); // visible globalement

void main() {
    // final Scanner CONSOLE = new Scanner(System.in); // ne serait pas visible dans getName ni getInteger
    String name = getName();
    int age = getInteger("age");

    System.out.printf("%s a %d ans.\n", name, age);
}

String getName() {
    System.out.print("Quel est votre nom? > ");
    return CONSOLE.nextLine();
}

int getInteger(String what) {
    System.out.printf("Entrez un nombre entier pour votre %s > ", what);
    int result = CONSOLE.nextInt(); // pour consommer la valeur
    CONSOLE.nextLine(); // pour consommer le retour de ligne aussi
    return result;
}

Ici le Scanner pour la console est utilisée dans plusieurs méthodes et ne change pas de valeur (réfère toujours à System.in), alors c’est un bon candidat de constant globale.

Notez que la déclaration commence avec final disant à Java que cette valeur ne peut jamais changer durant l’exécution. Notez aussi qu’on a écrit le nom en majuscules afin de mieux le repérer comme constante.

Fichier GlobalColorCodes.java

import java.util.*;

final String RED = "\033[0;31m";
final String GREEN = "\033[0;32m";
final String YELLOW = "\033[0;33m";
final String RESET = "\033[0m";

void printRed(String text) {
   System.out.println(RED + text + RESET);
}

void printGreen(String text) {
   System.out.println(GREEN + text + RESET);
}

void printYellow(String text) {
   System.out.println(YELLOW + text + RESET);
}

void main() {
    printRed("Ceci est un message d'erreur!");
    printGreen("Ceci est un message de succès!");
    printYellow("Ceci est un avertissement");

    System.out.printf("%sCe texte%s est en %ssurbrillance%s%s!!!%s", 
            GREEN, RESET, YELLOW, RESET, RED, RESET);
}

Ici on a plusieurs constantes globales pour les différents codes de couleur pour le texte à la console. Ces constantes sont utilisées dans les différentes méthodes, incluant main qui fait un usage sur mesure pour la dernière phrase.

Variables globales d’état

Voici un exemple banal pour quelques variables globales pour un jeu.

Fichier : GlobalGameState.java

int lives = 3;
int xp = 0;
boolean hasWeapon = true;
boolean hasHorse = true;

void diedInBattle() {
    xp += 5;
    lives--;
    hasWeapon = false;
    System.out.println("Vous êtes mort au combat et avez perdu votre arme!");
}

void diedWhileRiding() {
    xp += 2;
    lives--;
    hasHorse = false;
    System.out.println("Vous êtes mort en tombant de votre cheval!");
}

void showStats() {
    System.out.println("Vies : " + lives);
    System.out.println("Points d'expérience : " + xp);
    System.out.println("Vous avez une arme : " + hasWeapon);
    System.out.println("Vous avez un cheval : " + hasHorse);
}

void main() {
    showStats();
    diedInBattle();
    showStats();
    diedWhileRiding();
    showStats();
}

Priorité de nom selon la portée

Une des caractéristiques d’un programme avec différentes portées de variables est que le même nom peut être déclaré à plusieurs endroits sans conflits en autant que leurs portées soient différentes.

S’il y a deux variables du même type et du même nom, mais une est globale est l’autre est locale, la version locale sera toujours utilisée en priorité.

Voici quelques exemples courts pour illustrer la chose.

/* Globale versus locale */

String name = "Steve";

void main() {
    String name = "Angelica";
    name = "Naomi";
    System.out.println(name);
}
Quelle variable est modifiée à la ligne 7? locale ou globale?

La variable locale, déclarée à la ligne 6, car elle a priorité sur la variable globale.

Quel nom s'affiche? Naomi ou Steve?

Naomi. La variable locale dans main masque la variable globale (qui n’est plus accessible dans main à cause de ce masquage).

/* Locale versus locale */

void main() {
    String name = "Angelica";
    System.out.println(rename(name));
    System.out.println(name);
}

String rename(String name) {
    name = "Demonica";
    return name;
}
Qu'est-ce qui s'affiche à la ligne 5?

Demonica. C’est la valeur de retour de rename.

Qu'est-ce qui s'affiche à la ligne 6?

Angelica. Le paramètre name est locale à la méthode rename. Cette variable récoit la valeur de la variable dans main comme argument. Par contre, ce paramètre (locale à rename) n’existe plus après le retour de la méthode. rename retourne la nouvelle valeur, mais la variable name dans main ne la reçoit pas. La valeur de retour passe plutôt directement dans un print. Alors la variable locale dans main n’a subit aucune modification du début jusqu’à la fin.

Quelques exemples d’erreur (de conflit) de portée

Variable inaccessible

void isolated() {
    int answer = 7;
}

void main() {
    System.out.println(answer); // erreur "cannot find symbol"
}

Ici, answer est locale à la méthode isolated, alors il n’est pas visible dans main, contrairement à si cette variable avait été déclarée globalement, à l’extérieure d’une méthode.

int answer = 7; // déclaration globale

void main() {
    System.out.println(answer); // answer est visible
}

Déclaration dupliquée

String thing(String t) {
    String t = t + "2"; // erreur "duplicate local variable"
    return t;
}

void main() {
    System.out.println(thing("thing"));
}

Ici, dans la méthode thing on déclare déjà un paramètre String t. Alors quand on déclare une autre variable locale du même nom à la ligne 2, Java nous indique une erreur. Si l’intention était de simplement modifier la valeur reçue dans le paramètre alors on peut réécrire la ligne 2 comme ceci :

String thing(String t) {
    t = t + "2"; // référence valide au paramètre t déjà déclarée
    return t;
}

void main() {
    System.out.println(thing("thing"));
}

La différence est que la ligne 2 n’est plus une déclaration d’une nouvelle variable. Elle est désormais une référence à la variable déjà déclarée dans la liste de paramètres.

Exercices

📚 Tester la compréhension

Faire les deux questions de choix multiples au début de la page ici pour vérifier votre compréhension de la portée des variables. Ne faites pas les problèmes de programmation car ils intègrent d’autres concepts que nous ne verrons pas dans ce cours.

La raison que les exercices de programmation sur la page sont déconseillés est qu’ils sollicitent des concepts de la programmation orientée objet que nous n’avons pas appris. Ce sont plutôt des concepts du cours ICS4U.

Je vous suggère plutôt les exercices pratiques suivants qui sont mieux adaptés à notre contexte.

🛠️ Pratique

Travaillez dans le répertoire GitHub partagé par votre enseignant pour la pratique et les exercices.

Cette pratique vise à démontrer les différences entre une approche de variables globales et une approche de variables locales dans un cas où les deux sont des choix valides.

Le contexte est l’utilisation d’un symbole spécifique comme indicateur d’invite de réponses chaque fois qu’on pose une question à l’utilisateur. On veut que ce symbole soit facile à changer si on veut utiliser un autre symbole. On veut aussi que le symbole soit utilisé dans plusieurs méthodes.

Approche avec variable globale

Avec cette approche, on déclare une variable globale pour le symbole. Par la suite nous concatenons ce symbole à la fin de chaque question posée à l’utilisateur. Pour montrer l’utilité à travers plusieurs méthodes, chaque question se trouve dans sa propre méthode.

Le Scanner de la console est également déclaré comme variable globale pour un accès direct pour toutes les méthodes.

diagramme de méthodes pour var globale

Fichier GlobalVar.java

import java.util.*;

final Scanner CONSOLE = new Scanner(System.in);
final String PROMPT = " > ";

void main() {
    String name = getName();
    int age = getAge();
    System.out.println("Votre nom est " + name + " et vous avez " + age + " ans.");
}

String getName(){
    System.out.print("Quel est votre prénom?" + PROMPT);
    String name = CONSOLE.next();
    CONSOLE.nextLine(); //vider le reste de la ligne
    return name;
}

int getAge() {
    System.out.print("Quel est votre âge?" + PROMPT);
    int age = CONSOLE.nextInt();
    CONSOLE.nextLine();
    return age;
}
  1. Copiez ce code dans un fichier GlobalVar.java et l’exécuter pour voir comment il fonctionne.
  2. Ajouter un commentaire après chaque déclaration de variable pour indiquer s’il s’agit d’une variable locale ou globale. Les variables sont CONSOLE, PROMPT; name, age; name; age.
  3. Modifiez la valeur de PROMPT pour utiliser un autre symbole que >. Lancez le programme de nouveau pour voir le changement.
  4. Renommez les variables dans main et lancez le programme pour voir si ça change le fonctionnement. Selon vos observations, est-ce que la variable nommée name dans main est le même objet en mémoire que la variable nommée name dans getName?

Approche avec variables locales seulement

Avec cette approche, les variables à partager sont déclarées localement dans main et partagé via le mécanisme argument-paramètre. De plus, au lieu de créer une variable pour le symbole prompt, on crée une méthode. Parce que chaque méthode est visible par les autres dans le fichier, toutes les autres méthodes peuvent l’appeler.

diagramme de dépendances pour var locale

Fichier LocalVar.java

import java.util.*;

void main() {
    Scanner console = new Scanner(System.in);
    String name = getName(console);
    int age = getAge(console);
    System.out.println("Votre nom est " + name + " et vous avez " + age + " ans.");
}

void prompt(String question) {
    System.out.print(question + " > ");
}

String getName(Scanner in) {
    prompt("Quel est votre nom?");
    String name = in.next();
    in.nextLine(); // vider le reste de la ligne
    return name;
}

int getAge(Scanner in) {
    prompt("Quel est votre âge?");
    int age = in.nextInt();
    in.nextLine();
    return age;
}
  1. Copiez ce code dans un fichier LocalVar.java et l’exécuter pour voir comment il fonctionne. Confirmez qu’il fonctionne - du point de vue de l’utilisateur - de manière identique à GlobalVar.
  2. Modifiez le programme pour changer le symbole d’invite de commande dans prompt(). Est-ce que c’était plus facile ou difficile ou équivalent à ce qu’il fallait faire la dernière fois?
  3. Énumérer les éléments que vous avez préférez de la version GlobalVar et les éléments que vous avez préférez dans LocalVar. Énumérer aussi des éléments que vous n’avez pas aimé dans les deux versions. Ajoutez ces listes dans un commentaire d’en-tête dans un troisième fichier PreferredVar.java. Combiner vos éléments préférés de chaque version dans cette troisième version ou apporter des modifications pour créer votre propre approche. Résoudre les erreurs de fusion et lancer ce troisième programme pour vérifier qu’à la console l’expérience reste identique pour l’utilisateur malgré les changements.

  1. Il y a des nuances à “partout dans un programme”, mais ces nuances ne sont pas importants dans ce cours. Pour votre curiosité, voici quand même un bref survol. Nos programmes tiennent dans un seul fichier .java, alors partout dans le fichier serait plus apte. Les fichiers .java représentent implicitement une classe Java. Alors partout dans la classe serait aussi plus précis. Mais les variables Java peuvent être qualifiées de publiques ou privées. Les variables privées sont seulement “globales” dans la classe, tandis que les variables publiques sont “globales” pour tout un package de classes (à l’extérieur de la classe elle-même). Ce concept s’étend à la portée des classes (publiques ou privées) dans les packages et la portée des packages (exportées ou non) dans des modules.

Accueil > Programmer avec Java >

🛠️ Tests unitaires

Survol et attentes

Dans un programme décomposé, c’est important de tester chaque méthode avant de l’intégrer dans une méthode appelante. Ainsi, si une erreur se produit, on limite la recherche de l’erreur à une seule méthode.

Définitions
Test unitaire
vérifier si une méthode produit le résultat attendu pour un ensemble d’entrées spécifiques. Un test unitaire évalue une seule méthode, d’où le terme “unitaire”.
Cas de test
un scénario (une entrée spécifique) qui doit être testé pour une méthode. Chaque cas de test fait le lien entre les entrées et les sorties attendus. Un test unitaire inclut généralement tous les cas de test identifiées pour une méthode.
JUnit
un framework de test unitaire pour Java. Nous utiliserons la suite d’outils Java dans VS Code pour installer et exécuter les tests JUnit. De plus, ces outils créent automatiquement les fichiers de test et les signatures des méthodes pour chaque test unitaire.
Framework
un ensemble d’outils et de conventions qui facilitent le développement de logiciels. Un framework est plus vaste qu’un package ou une module de code, et la plupart des applications logicielles sont bâtis à l’aide de frameworks. JUnit est un framework pour les tests unitaires.
Test d’entrée/sortie
test unitaire qui doit tenir compte des valeurs normalement saisies via l’entrée standard et affichées à la console. Ces tests sont plus difficiles à automatiser parce qu’il faut temporairement rediriger l’entrée et/ou la sortie standard vers des sources de texte écrites à l’avance dans le test.

Objectifs d’apprentissage

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

  • décrire c’est quoi un test unitaire et pourquoi on les utilise.
  • savoir comment choisir des cas de test pour une méthode donnée.
  • savoir comment utiliser les outils dans VS Code pour créer et exécuter des tests unitaires pour des programmes Java.

Critères de succès

Évalués sommativement dans ce cours :

  • Je suis capable de rédiger une liste de cas de test pour une méthode donnée et d’intégrer ces cas dans le test unitaire.
  • Je suis capable d’adapter le gabarit de test d’égalité pour mes méthodes.

Aspirationnels - on n’a pas assez de temps dans ce cours pour devenir vraiment bons à ces compétences, mais on peut les explorer :

  • Je suis capable de créer un fichier de test et les méthodes de test pour mes programmes en utilisant les outils de JUnit dans VS Code.
  • Je suis capable d’adapter le gabarit de test d’entrée et de test de sortie pour mes méthodes.

Pourquoi faire des tests unitaires?

Un programme devient très rapidement large et complexe. On utilise déjà la décomposition pour gérer la complexité et la taille du programme : elle nous permet de résoudre une série de petits problèmes.

Par contre, l’avantage de la décomposition est largement gaspillé si on ne vérifie pas le bon fonctionnement de chaque méthode - chaque morceau du problème décomposé - avant de l’intégrer dans la méthode plus haut dans la chaîne d’appels. La raison est la suivante : s’il y a une erreur dans une méthode qu’on n’a pas testé mais on l’intègre sans connaissance dans une autre méthode, lorsque on exécute éventuellement le programme, on ne sait pas si l’erreur vient de la méthode appelante ou de la méthode appelée. Ce problème explose avec le nombre d’intégrations de méthodes qu’on fait avant de tester.

Avec les tests unitaires, on peut isoler les erreurs à une seule méthode, limitant la quantité de code à vérifier pour corriger l’erreur. Le but est de tester chaque méthode et de s’assurer de son fonctionnement avant de l’intégrer dans une autre méthode. Ainsi, si une erreur se produit, on sait qu’elle vient du plus récent code qu’on a écrit et non des autres méthodes qui existent déjà.

Un autre avantage des tests unitaires formelles, comme ceux avec le framework JUnit, est que si on modifie le programme, on peut relancer tous les tests que nous avons déjà écrits pour s’assurer que les modifications n’ont pas cassé quelque chose qui fonctionnait déjà.

Cas de tests

En faisant un test unitaire, on doit savoir quel comportement est attendu de notre méthode, soit qu’est-ce qui constitue un résultat acceptable. Les cas de tests sont simplement la description de ces comportements attendus pour une méthode donnée. C’est, en fait, la partie la plus importante de l’écriture des tests unitaires. Si on ne sait pas ce qu’on attend de notre méthode, on ne peut pas écrire de tests pour vérifier si la méthode fonctionne correctement.

Déterminer quels sont les cas de test importants est un art. Il faut trouver un équilibre entre tester tous les cas possibles et tester seulement les cas les plus importants. Voici quelques pistes pour déterminer les cas de test pour une méthode :

  • Cas de test limites : les cas de test qui couvrent les valeurs aux limites de ce qui serait normalement attendu. Par exemple, si on doit donner une note entre 0 et 100, on devrait tester avec 0 et 100. De même, si dans la zone des valeurs acceptés, il y a des limites pour différentes catégories, on devrait tester ces limites aussi. Par exemple, si la note de passage est de 50, on devrait tester avec 49 et 50.
  • Cas de test invalides : si notre programme doit gérer la qualité des entrées, il faut aussi prévoir les cas de test qui couvrent les valeurs qui ne devraient pas être acceptées. Par exemple, si on doit donner une note entre 0 et 100, on devrait tester avec -1, 101, et des lettres.

Tableau de cas tests

Pour l’exemple précédent, soit d’une méthode qui calcule la moyenne de deux notes et retourne le résultat comme valeur de retour, on peut préparer le tableau de cas de test suivant :

IntentionEntréeSortie attendue
deux notes int valides80, 9085.0
deux notes double valides80.0, 90.085.0
notes limites0, 10050.0
notes limites0, 00.0
notes limites100, 100100.0
note négative-1, 90-1.0 // une valeur drapeau pour signaler un résultat invalide
note supérieur à 10080, 101-1.0 // une valeur drapeau pour signaler un résultat invalide

On peut représenter ce même tableau comme commentaire de bloc ou comme javadoc dans la méthode de test unitaire, comme ceci :

/*
 * Test la méthode `average` pour les cas suivants :
 * - deux notes int valides : 80, 90 => 85.0
 * - deux notes double valides : 80.0, 90.0 => 85.0
 * - notes limites : 0, 100 => 50.0
 * - notes limites : 0, 0 => 0.0
 * - notes limites : 100, 100 => 100.0
 * - note négative : -1, 90 => -1.0 // une valeur drapeau pour signaler un résultat invalide
 * - note supérieur à 100 : 80, 101 => -1.0 // une valeur drapeau pour signaler un résultat invalide
 */

Approche naïve pour les tests unitaires

Sans utiliser des outils spécialisés, nous pouvons simplement lancer manuellement le programme après avoir ajouté une nouvelle méthode. Naturellement, on aura probablement appelé la méthode dans la logique globale du programme pour voir si elle fonctionne. Ça marche pour des programmes très simples mais c’est limité même dans ces cas :

  1. S’il y a plusieur cas de test, on doit les exécuter un par un et comparer les résultats manuellement, possiblement en insérant des System.out.println() pour afficher les résultats qu’on aura à retirer plus tard. Cela est une tâche lourde et sujette à des erreurs.
  2. Si plusieurs parties du programme doivent s’exécuter avant d’arriver à l’appel de la nouvelle méthode à tester, on doit passer à travers tout le programme pour chaque test. Cela est une perte de temps et d’énergie qui fait en sorte qu’on évite souvent de tester rigoureusement les méthodes.

Mieux mais sans les outils de test unitaire

Pour faire mieux que l’approche décrite ci-dessus, on peut tenter d’écrire des tests unitaires sans l’appui des outils, notamment parce que l’installation des outils peut être complexe et parce que ça nous évite d’apprendre comment utiliser les outils. En écrivant nos propres tests, on peut définitivement pallier aux deux problèmes soulevés ci-dessus :

  • on peut inclure les cas de test dans notre code de test
  • on peut inclure les comparaisons automatiques avec les résultats attendus dans notre code de test
  • on peut exécuter les tests unitaires sans passer par la logique globale du programme

Par contre, tout ça exige la rédaction de beaucoup de code additionnel (avec l’introduction presque garantie de nouvelles erreurs). Et tout ce nouveau code est probablement mieux écrit et définitivement déjà validé dans les outils de test unitaire.

De plus, le code de test “maison” est le plus facile à écrire et à lancer dans la même classe que les méthodes à tester. Cela rend le code dans cette classe plus difficile à lire, sans parler de la méthode main qui peut commencer à ressembler à quelque chose comme ceci :

Fichier: MyGreatProgram.java

void main() {
    // tests unitaires
    // testMethod1(); // masqué derrière un commentaire pour ne pas l'exécuter
    // testMethod2();
    testMethod3();


    // logique globale du programme
    // ...
}

void method1() {
    // ... code qu'on veut utiliser dans le programme
}

void testMethod1() {
    // ... code pour tester la méthode qu'on veut utiliser dans le programme
}

// ... reste des méthodes de la classe

Approche standard pour les tests unitaires avec JUnit 4

En se servant des outils existants, comme JUnit :

  • notre classe peut être écrite exactement comme on l’a décrit avec la décomposition du problème sans code additionnel,
  • les tests peuvent être écrits plus succinctement avec le code fourni par JUnit,
  • le lancement des tests est entièrement indépendant du lancement de notre programme et
  • nous n’avons pas besoin de programmer comment afficher les résultats des tests parce que JUnit le fait pour nous.

L’utilisation de JUnit nous impose pour la première fois une structure de projet Java qui dépasse un seul fichier :

  • le fichier pour notre code et
  • un autre fichier pour les tests unitaires.

La section suivante décrit la modification qu’il faut apporter à nos programmes pour les tester avec JUnit.

Structure de projet pour les tests unitaires

On a vu dans une leçon sur les bases de Java que ce langage est utilisé pour d’énormes projets logiciels et que le code peut être divisé en plusieurs types d’unités autonomes du plus grand (les logiciels et les frameworks) aux unités intermédiaires (les modules et les packages) et finalement aux unités atomiques (les classes).

Parce que tous nos programmes jusqu’à présent tenaient dans une seule classe, le compilateur Java nous permettait d’omettre une déclaration de classe et de simplement écrire des déclarations pour nos méthodes et nos variables globales.1 De plus, on n’avait pas à se préoccuper des mots-clés qui sont utilisés pour gérer la visibilité des éléments de notre classe (public, private) ni de comment les méthodes sont appelées (static ou non). On ne travaillera pas avec ces concepts dans ce cours (ils sont couverts dans le cours de 12e année), mais on doit quand même ajouter une chose à notre code pour le tester avec JUnit : une déclaration de classe.

La déclaration de classe donne un nom à notre code et permet au code dans la classe test d’y référer pour utiliser nos méthodes.

Ainsi, si on a un fichier Calculator.java avec le contenu suivant :

int add(int a, int b) {
    return a + b;
}

void main() {
    System.out.println(add(1, 2));
}

et on veut tester la méthode add, on doit ajouter une déclaration de classe autour de notre code pour le tester avec JUnit :

class Calculator {
    int add(int a, int b) {
        return a + b;
    }

    void main() {
        System.out.println(add(1, 2));
    }
}
  1. Utiliser l’outil “Mettre le document en forme” dans VS Code pour rétablir une bonne indentation après avoir ajouté la déclaration de classe (n’oubliez pas son accolade fermante à la fin du code).
  2. Le nom de la classe doit être le même que le nom du fichier et on devrait respecter les conventions Java pour les noms de classe/fichier : commencer par une majuscule et utiliser la casse chameau pour les noms composés.

    Pour renommer le fichier, au besoin, utiliser l’outil “Renommer le fichier” dans VS Code parce qu’il renommera automatiquement le nom de la classe et toutes les références à cette classe à travers le projet.

Maintenant, le code dans la classe de test pourra créer un objet de type Calculator pour tester la méthode add(), comme on ajoute des objets de type Scanner pour utiliser ses méthodes next*().

Travailler avec JUnit dans VS Code et l'Extension Pack for Java

On va utiliser pour la première fois l’option “Source Actions” dans VS Code pour créer des tests unitaires. Cette section vous montre comment le faire étape par étape.

ÉTAPE 1 : Créez votre fichier dans un projet Java et écrire au moins une de ses méthodes. Pour cet exemple, on peut utiliser le fichier Calculator.java avec le contenu suivant :

class Calculator {
    int add(int a, int b) {
        return a + b;
    }

    void main() {
        System.out.println(add(1, 2));
    }
}
ÉTAPE 2

Ouvrir le fichier dans VS Code et attendre une minute afin de laisser les outils Java s’activer.

ÉTAPE 3

Faites un clic droit n’importe où dans le fichier et choisissez “Source Action…”. Vous devriez voir une option pour Generate Tests. Cliquez dessus.

Source Action | Generate Tests

ÉTAPE 4

Si c’est la première fois qu’on fait ces étapes dans un projet, VS Code vous donnera une erreur et un bouton Enable tests. Cliquez dessus et choisir le framework JUnit. VS Code installera automatiquement les dépendances nécessaires pour JUnit dans votre projet, dans le sous-dossier lib.

Enable tests | Choisir JUnit | Installation de JUnit

Note : vous aurez à faire cette étape une seule fois par projet, mais vous aurez à la refaire si vous créez un nouveau projet.

ÉTAPE 5

Tapez Enter pour acceptez le nom proposé pour la classe de test, généralement [nom de ma classe]Test. Dans notre exemple, ce serait CalculatorTest.

ÉTAPE 6

Sélectionnez la méthode que vous voulez tester. Dans notre exemple, on veut tester add. Cliquez sur Enter pour accepter. La classe sera créée avec la signature de la méthode de test pour add. Cochez seulement la plus récente méthode, celle qui n’a pas encore de test unitaire.

choisir les tests à générer | classe teste générée

Notez qu’on ne doit pas tester main parce qu’il contient la logique globale du programme. main n’est pas une “unité” mais plutôt “l’intégration” ultime de toutes les unités de notre programme. Ça ne fait pas de sens de préparer des tests unitaires pour cette méthode.

ÉTAPE 7

Écrivez les tests unitaires pour les méthodes choisies. C’est à cet étape qu’on doit considérer les cas de test et les implémenter.

Les sections suivantes donnent des gabarits de tests unitaires que vous pouvez utiliser pour écrire vos tests.

ÉTAPE 8

Exécutez les tests unitaires en cliquant sur le bouton Run Test à côté de la méthode de test ou en cliquant sur le bouton Run All Tests en haut de la classe de test.

Exécuter les tests

Notez que s’il y a une seule classe dans votre projet qui contient des erreurs de syntaxe, vous recevrez un message d’erreur avant le lancement des tests parce que les outils Java compilent tout votre projet. Si l’erreur n’est pas dans la classe à tester ni dans la classe avec les tests, vous pouvez simplement cliquer sur le bouton Continue pour ignorer ces erreurs et lancer les tests.

ignore errors

ÉTAPE 9

Analysez les résultats des tests dans la fenêtre de sortie de JUnit. Si les tests sont tous réussis, rien ne s’affichera et vous devrez vous rendre à l’onglet “Test Results” pour voir la sortie. Si un test échoue, vous verrez un message d’erreur dans la fenêtre de sortie.

Résultats des tests

Notez que la sortie texte dans la partie gauche ne dit pas grand-chose d’utile. C’est plutôt la partie droite qui donne l’état de chacun des tests. Le crochet vert est pour un test réussi, le point rouge est pour un test échoué. Vous pouvez cliquer sur le résultat de chaque test pour plus de détails ou pour les lancer de nouveau.

ÉTAPE 10 : Corrigez les erreurs dans votre code et réexécutez les tests jusqu’à ce qu’ils passent tous. Les erreurs peuvent se trouver dans le test unitaire ou dans la méthode que vous testez… mais pas ailleurs! C’est l’avantage de faire des tests unitaires pour chaque méthode qu’on finit d’écrire.

ÉTAPE 11 : Répétez les étapes 3 à 10 pour chaque nouvelle méthode que vous écrivez dans la classe principale du projet.

Gabarits - code de démarrage pour vos tests unitaires avec JUnit

Votre responsabilité principale en lien avec les tests unitaires est la définition des cas de tests pour chaque méthode que vous écrivez.

Par contre, vous pouvez aussi apprendre comment implémenter les tests unitaires qui appliquent ces cas de test à vos méthodes. Pour vous aider, les exemples de tests unitaires ci-dessous vous donnent du bon code de démarrage pour quatre cas communs :

  • un test d’égalité,
  • un test d’entrée (avec un Scanner global),
  • un test d’entrée (avec un Scanner local) et
  • un test de sortie.

Selon la structure de vos méthodes, vous aurez probablement à combiner le code de tests différents, p. ex. d’égalité et d’entrée ou d’éntrée et de sortie pour avoir la bonne structure de test pour vos méthodes.

Structure de la classe de test

Voici un gabarit de base pour une classe de test unitaire où vous remplacerez “MyClassname” par le nom de la classe que vous testez. Vous pouvez ajouter autant de méthodes de test que nécessaire dans cette classe.

import static org.junit.Assert.*; // pour les méthodes de comparaison
import org.junit.Test; // pour l'annotation @Test et les outils associés

public class MyClassnameTest {

    // déclaration d'un objet de la classe à tester
    // p. ex. Calculator calc = new Calculator();

    @Test
    // déclaration d'une' méthode de test pour une méthode dans votre code

    @Test
    // déclaration d'une méthode de test pour une autre méthode dans votre code

}

Test d’égalité

Les tests d’égalité sont pour des méthodes qui retournent une valeur. On compare la valeur retournée par la méthode avec la valeur attendue.

Exemple de base

Considérant notre méthode add dans la classe Calculator :

class Calculator {
    // ... reste du code de la classe

    int add(int a, int b) {
        return a + b;
    }
}

La classe de test produit par les outils de JUnit dans VS Code serait le suivant, avec quelques lignes additionnelles :

import static org.junit.Assert.*; // ajoutez cette ligne pour les méthodes de comparaison

import org.junit.Test;

public class CalculatorTest {

    Calculator calc = new Calculator(); // ajoutez cette ligne pour faire référence à votre code

    @Test
    public void testAdd() {

    }
}

On peut écrire le test unitaire testAdd dans la classe CalculatorTestcomme suit à l’intérieur de la classe de test :

    @Test
    public void testAdd() {
        /*
         * Cas de tests pour int add(int, int)
         *
         * Descr.   Entrées  Sortie attendue
         * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
         * base     1, 2    3
         */

        assertEquals(3, calc.add(1,2));
    }

Deux points qui peuvent faire planter ce code :

  • La méthode assertEquals() ne sera pas reconnue si vous n’avez pas ajouté l’imporation static d’Assert au début du fichier de test. Voir le gabarit pour la classe de test ci-dessus.
  • La méthode add() ne sera pas reconnue si vous n’avez pas créé une variable calc de type Calculator dans la classe de test. Voir le gabarit pour la classe de test ci-dessus.

Notez que assertEquals est une méthode de JUnit qui compare le premier argument avec le deuxième argument et lève une exception si les deux arguments ne sont pas égaux. C’est la méthode la plus courante pour tester des valeurs de retour.

Il y a plusieurs autres détails à noter dans ce code :

  1. L’annotation @Test et la signature de la méthode ont peut-être été créées automatiquement si vous avez suivi les étapes ci-dessus. L’annotation @Test devient un point de lancement du code de test pour JUnit.
  2. On écrit les cas de test dans un commentaire de bloc au début de la méthode de test. Dans l’exemple, on a simplement inclut un cas de test, mais vous devrez considérer l’ensemble des cas à tester dans la méthode.
  3. On utilise la méthode assertEquals pour implémenter le cas de test, soit comparer le résultat attendu avec la valeur de retour de la méthode testée. Il devrait y avoir un appel à assertEquals pour chaque cas de test.
Exemple pour comparer des 'double'

L’exemple de base fonctionne pour tous les types sauf les valeurs à virgule flottante (comme les double).

Avec les double, dû à la conversion inexacte entre le binaire (dans la machine) et le décimal (dans la représentation du nombre), il y a toujours - ou presque - des erreurs d’arrondissement. Ainsi on ne peut jamais évaluer l’égalité entre deux double directement comme on le fait avec les autres types de données.

L’algorithme à utiliser se résume à :

  • valeur 1 = valeur 2 ± une marge d'erreur acceptable ou
  • valeur absolue de (valeur 1 - valeur 2) <= une marge d'erreur acceptable (la valeur absolue ignore le signe du résultat)

Par exemple, pour des notes de cours à une décimale près on pourrait note1 = note2 ± 0.1 ou note1 = note2 ± 0.05, selon votre préférence.

Pour les double, JUnit fournit une méthode assertEquals qui prend un troisième argument, la marge d’erreur acceptable. Par exemple, pour une méthode average qui retourne la moyenne de deux nombres à une décimale près, on pourrait écrire le test unitaire comme suit :

    @Test
    public void testAverage() {
        /*
         * Cas de tests pour double average(double, double) :
         *
         * Descr.   Entrées     Sortie attendue
         * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
         * base     80.0, 90.0  85.0
         */

        assertEquals(85.0, calc.average(80.0, 90.0), 0.1);
    }

Notez que nous fournissons la valeur attendue et la valeur de retour de la méthode testée comme avec l’exemple. Cependant, on ajoute aussi un troisième argument, la marge d’erreur acceptable. Dans cet exemple, on accepte une différence de 0.1 entre la valeur attendue et la valeur de retour.

Le choix de la marge acceptable dépend de la précision requise par votre programme. Comme règle de base, on devrait choisir une marge d’erreur qui est plus petite que la précision finale que nous voulons parce que les erreurs d’arrondissment s’accumulent à chaque opération.

Donc la marge de 0.1 si on veut des résultats à une place décimale est trop généreuse et on perd beaucoup de précision. Règle de base : utiliser une marge d’erreur au moins 3 chiffres de plus que la précision finale désirée. P. ex., si on veut une précision finale de 0.1 on devrait choisir une marge d’erreur d’au maximum 0.0001. le test ci-dessus serait modifié à :

assertEquals(85.0, calc.average(80.0, 90.0), 0.0001);

Test d’entrée

Pour tester une méthode qui sollicite des entrées de l’utilisateur via la console, on a deux options selon la façon dont le Scanner de la console est géré dans la classe du programme :

Scanner static et globalScanner passé comme paramètre (Scanner local)
algorithme pour Scanner globalalgorithme pour Scanner local

Dans le cas d’un Scanner global, parce que la vie de la variable est plus longue que la vie de la méthode, on doit s’assurer de rétablir sa valeur originale avant de quitter le test. Pour le faire, on doit s’assurer de copier sa valeur originale au début du test.

Pour le Scanner local, ces étapes sont éliminées, mais on doit s’assurer de passer le Scanner comme argument à la méthode à tester.

Exemple avec un Scanner global

Si on a la méthode getNameUsingGlobalScanner dans la classe Calculator :

import java.util.Scanner;

class Calculator {

    /** Scanner global pour toutes les méthodes de la classe */
    Scanner console = new Scanner(System.in);

    // ... reste du code de la classe

    String getNameUsingGlobalScanner() {
        System.out.print("Entrez votre prénom > ");
        String name = console.next(); // utilise le Scanner global
        console.nextLine(); // jeter tout après le premier mot
        return name;
    }
}

On peut écrire le test unitaire testGetNameUsingGlobalScanner dans la classe CalculatorTest comme suit :

    @Test
    public void testGetNameUsingGlobalScanner() {
        /*
         * Cas de test pour String getName() qui utilise un
         * Scanner déclaré globalement
         * 
         * Descr.           Entrée          Sortie attendue
         * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
         * normal           "david"         "david"
         * un caractère     "A"             "A"
         * plusieurs mots   "David Crowley" "David"
         */

        /* PRÉPARATION */

        // garder une référence au Scanner original de calc
        Scanner original = calc.console;

        // définir les cas de test
        String testInput = "david\n" +
                "A\n" +
                "David Crowley\n"; // les \n sont les `Entrée` de l'utilisateur

        // créer une source d'entrées qui contient nos cas de tests
        InputStream testStream = new ByteArrayInputStream(testInput.getBytes());

        // passer la nouvelle source d'entrées au Scanner de calc
        calc.console = new Scanner(testStream); 

        /* TESTS */
        
        assertEquals("david", calc.getNameUsingGlobalScanner());
        assertEquals("A", calc.getNameUsingGlobalScanner());
        assertEquals("David", calc.getNameUsingGlobalScanner());

        /* NETTOYAGE */

        // rediriger le Scanner global de calc à lire sa source originale
        calc.console = original;
    }

Note : avec l’ajout des InputStream et du Scanner, on doit ajouter import java.io.*; et import java.util.*; au début du fichier de test, p. ex. :

import static org.junit.Assert.*;

import java.io.*;   // ici
import java.util.*; // et ici

import org.junit.Test;

public class CalculatorTest {
    // ... reste du code de la classe

}

Si on oublie, parfois les outils d’autocomplétion dans VS Code peuvent ajouter ces déclarations automatiquement, mais c’est quelque chose à vérifier si vous avez des messages d’erreurs sur ces variables.

Exemple avec un Scanner local (passé en argument)

Une autre façon d’utiliser un Scanner dans un programme est de le déclarer localement, par exemple avec la méthode getName dans la classe Calculator :

import java.util.Scanner;

class Calculator {
    String getName(Scanner in) {
        System.out.print("Entrez votre prénom > ");
        String name = in.next(); // fait référence au Scanner passé en paramètre
        in.nextLine(); // jeter tout après le premier mot
        return name;
    }

    void main() {
        Scanner console = new Scanner(System.in); // déclarer un Scanner local
        //... autre code
        String name = getName(console); // passer le Scanner local comme argument
        //... autre code
    }
}

Le test unitaire testGetName dans CalculatorTest serait alors :

    @Test
    public void testGetName() {
        /*
         * Cas de test pour String getName(Scanner)
         * 
         * Descr.           Entrée          Sortie attendue
         * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
         * normal           "david"         "david"
         * un caractère     "A"             "A"
         * plusieurs mots   "David Crowley" "David"
         */

        /* PRÉPARATION */

        // définir les cas de test
        String testInput = "david\n" +
                "A\n" +
                "David Crowley\n"; // les \n sont les `Entrée` de l'utilisateur

        // créer un Scanner qui lit nos entrées de test au lieu de l'entrée standard
        InputStream testStream = new ByteArrayInputStream(testInput.getBytes());
        Scanner testScanner = new Scanner(testStream);

        /* TESTS */

        assertEquals("david", calc.getName(testScanner));
        assertEquals("A", calc.getName(testScanner));
        assertEquals("David", calc.getName(testScanner));

        /* NETTOYAGE */

        // Le nouveau Scanner est détruit automatiquement à la fin de cette méthode
    }

Note : avec l’ajout des InputStream et du Scanner, on doit ajouter import java.io.*; et import java.util.*; au début du fichier de test, p. ex. :

import static org.junit.Assert.*;

import java.io.*;   // ici
import java.util.*; // et ici

import org.junit.Test;

public class CalculatorTest {
    // ... reste du code de la classe

}

Si on oublie, parfois les outils d’autocomplétion dans VS Code peuvent ajouter ces déclarations automatiquement, mais c’est quelque chose à vérifier si vous avez des messages d’erreurs sur ces variables.

Test de sortie

Les tests de sortie sont utiles pour les méthodes void qui passent leurs résultats à la console. On peut utiliser la redirection de la sortie standard pour capturer les messages affichés par la méthode et les comparer avec les messages attendus.

Exemple de redirection de System.out pour les tests

Considérant la méthode sayName dans la classe Calculator :

    void sayName(String name) {
        System.out.println("Bonjour " + name);
    }

On peut écrire le test unitaire testSayName dans la classe CalculatorTest comme suit :

    @Test
    public void testSayName() {
        /*
         * Cas de test pour void sayName(String)
         * 
         * Descr.           Entrée          Sortie attendue
         * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
         * normal           "David"         "Bonjour David"
         * un caractère     "A"             "Bonjour A"
         * plusieurs mots   "David Crowley" "Bonjour David Crowley"
         * 
         */

        /* PRÉPARATION */

        // garder une référence à la sortie standard originale
        PrintStream original = System.out;

        // rediriger la sortie standard vers un flux de sortie temporaire
        OutputStream outContent = new ByteArrayOutputStream();
        System.setOut(new PrintStream(outContent));
    
        /* TESTS */

        calc.sayName("David");
        calc.sayName("A");
        calc.sayName("David Crowley");

        String expectedOutput = "Bonjour David\nBonjour A\nBonjour David Crowley\n";

        assertEquals(expectedOutput, outContent.toString().replace("\r\n", "\n")); // le replace() est nécessaire sur Windows (encodage de fin de ligne différent)

        /* NETTOYAGE */

        // rétablir la sortie standard originale
        System.setOut(original);
    }

Note : on applique la méthode de traitement de texte .replace("\r\n", "\n") sur la sortie pour s’assurer que les différents types de retour de ligne ne causent pas un échec de la comparaison. Windows utilise \r\n pour le retour de ligne, alors que Linux, MacOS et nos propres instructions Java utilisent seulement \n.

Note : avec l’ajout du PrintStream et des OutputStream, on doit ajouter import java.io.*; au début du fichier de test, p. ex. :

import static org.junit.Assert.*;

import java.io.*;   // ici

import org.junit.Test;

public class CalculatorTest {
    // ... reste du code de la classe

}

Si on oublie, parfois les outils d’autocomplétion dans VS Code peuvent ajouter ces déclarations automatiquement, mais c’est quelque chose à vérifier si vous avez des messages d’erreurs sur ces variables.

Les exemples complets

Vous pouvez voir le code complet pour les classes Calculator et CalculatorTest dans les fichiers Calculator.java et CalculatorTest.java.

Exercices

🛠️ Pratique

Travaillez dans le répertoire GitHub partagé par votre enseignant pour la pratique et les exercices

  1. Créez un fichier texte nommé Cas-de-tests.txt dans votre réprtoire de travail.

    1. Écrivez le tableau de cas de test pour une méthode qui retourne true si un nombre est pair et false sinon. La signature de cette méthode est boolean isEven(int number). Quels sont les cas normaux? Est-ce qu’il y a des cas limites ou invalides pour cette méthode?
    2. Écrivez le tableau de cas de test pour une méthode qui retourne une note en lettres pour une note numérique. La signature de cette méthode est char letterGrade(double average). Les lettres sont F (moins de 50), D (50 à 59), C (60 à 69), B (70 à 79) et A (80 à 100). Quels sont les cas normaux? Quels sont les cas limites pour cette méthode, se rappelant que la moyenne est une valeur décimale (il y en a beaucoup!)? Quels sont les cas invalides?
  2. Voici une implémentation de la logique de la méthode isEven décrit dans le premier exemple.

    class NumberUtils {
        boolean isEven(int number) {
            return number % 2 == 0;
        }
    }
    
    • Créez une copie de ce fichier et nommez-le NumberUtils.java.
    • Utilisez les outils de VS Code pour créer la classe de test et la squelette de test unitaire pour cette méthode.
    • Ajoutez une déclaration globale (dans la classe NumberUtilsTest) pour un objet NumberUtils : NumberUtils utils = new NumberUtils();
    • Vous devrez traduire votre tableau de cas de test (écrit plus haut) en commentaire de bloc à l’intérieur de votre méthode de test.
    • Implémenter des tests d’égalité pour cette méthode, un test par cas de test identifié. Servez vous du gabarit comme point de départ.
    • Exécutez les tests.
    • Prenez une capture d’écran de l’onglet “Test Results” pour montrer que vos tests ont passé. L’enregistrez comme 4-unit-test.png dans le dossier “captures”.

  1. Depuis JEP 463 (pleinement intégré aux outils Java22+)

Accueil > Programmer avec Java > Structures de contrôle >

Écrire des conditions

Survol et attentes

Un programme peut prendre des décisions et, ainsi, devenir plus intelligent. Comme tout dans un ordinateur, ces décisions sont à base binaire, c’est-à-dire qu’elles sont basées sur des conditions qui sont soit vraies, soit fausses. Dans cette leçon, nous allons apprendre à écrire des conditions en Java. Nous allons aussi apprendre à utiliser des opérateurs logiques pour combiner des conditions.

Définitions

Les conditions sont utilisées pour sélectionner une branche d’instructions parmi un choix de branches. Elles sont aussi utilisées pour déterminer si une boucle doit se répéter ou se terminer. Il y a d’autres utilisations, comme décider si une méthode se termine ou non, mais toutes ces applications seront vues dans des leçons séparées.

Condition
une expression booléenne, soit une expression qui s’évalue à true ou false. On peut penser à des conditions comme une façon formelle de poser une question oui/non à un ordinateur.
boolean
type de valeur Java qui stocke la valeur true ou false. On peut utiliser ce type de valeur dans des variables qui indiquent l’état de différentes choses dans le programme.
Opérateur de comparaison
un opérateur qui compare deux valeurs et retourne un booléen. Les opérateurs de comparaison sont ==, !=, <, <=, >, >=.
Méthodes de comparaison
des méthodes qui comparent des objets et retournent un booléen. Par exemple, .equals pour les String et .hasNextInt pour un Scanner.
Opérateurs logiques
des opérateurs qui combinent des conditions. Les opérateurs logiques sont && (ET), || (OU) et ! (PAS).

Objectifs d’apprentissage

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

  • Décrire les trois types d’expressions booléennes de base : valeur booléenne, opération de comparaison et méthode de comparaison.
  • Reconnaître les différents opérateurs de comparaison : ==, !=, <, <=, > et >=.
  • Décrire les trois opérateurs logiques : &&, || et !.

Critères de succès

  • Dans mes programmes Java, je peux écrire des expressions booléennes simples qui utilisent des valeurs booléennes, des valeurs numériques et des Strings comme base de comparaison.

Variables booléennes (type boolean)

L’expression booléenne la plus simple est une valeur littérale booléenne : soit true (vrai), soit false (faux). On peut utiliser ces valeurs littérales directement ou assignées à des variables, par exemple, boolean repeat = true;.

Une variable booléenne est pratique pour représenter l’état de quelque chose, par exemples : repeat ci-dessus qui peut indiqur s’il faut répéter une action ou non ou boolean isOn = true; qui peut représenter si une fonctionnalité est activée ou non. Ce type d’usage des valeurs booléennes s’appelle un drapeau. On peut regarder son état dans des conditions ou modifier son état dans le code, par exemple isOn = false; pour éteindre l’ampoule.

Opérations de comparaison

Un autre type d’expression booléenne est une opération de comparaison. Une opération de comparaison compare deux valeurs et retourne une valeur booléenne. Par exemple, x == y est une expression de comparaison qui retourne true si x est égal à y et false sinon. Les opérateurs de comparaison sont :

  • == (égal),
  • != (différent/pas égal),
  • < (inférieur),
  • <= (inférieur ou égal),
  • > (supérieur)
  • et >= (supérieur ou égal).

Voici quelques exemples à tester dans une session jshell :

5 == 5; // notez : comparer l'égalité utilise deux "=", soit "=="
5 != 5;
5 < 5;
5 <= 5;
5 > 5;
5 >= 5;

int x = 4; // notez : assigner utilise un seul "="
int y = 5;
x == y;
x != y;
x < y;
x <= y;
x > y;
x >= y;
Quel sera le résultat de la comparaison suivante? 5 >= 6

false. L’opérateur de comparaison représente “plus grand ou égale à”

Considérant les valeurs de x et y ci-dessus cette comparaison donne quoi? (x + y) < (2 * x)

false. On évalue les parenthèses en premier, donnant 9 < 8 qui est faux.

Méthodes de comparaison

Il y a aussi des méthodes de comparaison, généralement utilisées pour comparer différents objets du même type. On doit utiliser une méthode de comparaison pour comparer des objets de type String. Par exemple, "abc".equals("abc") retourne true et "abc".equals("def") retourne false. Il y a quatre méthodes de comparaison pour les String :

  • equals (égal), par exemple "abc".equals("abc")
  • equalsIgnoreCase (égal, sans tenir compte de la casse), par exemple "abc".equalsIgnoreCase("ABC") retourne true
  • compareTo (inférieur, égal ou supérieur), qui retourne une valeur négative si la première valeur est inférieur, zéro si les deux valeurs sont égales et une valeur positive si la première valeur est supérieure. Par exemple "abc".compareTo("def") retourne une valeur négative (-3).
  • compareToIgnoreCase (inférieur, égal ou supérieur, sans tenir compte de la casse)

La raison pourquoi la comparaison directe d’objets comme des String ne fonctionne pas est expliquée dans le cours ICS4U. C’est en lien avec la représentation de ces valeurs en mémoire versus la façon dont les valeurs primitives (comme des int) sont représentées en mémoire.

Voici quelques exemples de comparaisons de String à tester dans une session jshell :

"abc".equals("abc");
"abc".equals("def");
"abc".equals("ABC");
"abc".equalsIgnoreCase("ABC");

"abc".compareTo("def"); // donne un chiffre
"abc".compareTo("def") == 0; // tester l'égalité (donne un booléen)
"abc".compareTo("def") < 0; // tester si "abc" est inférieur à "def"
"abc".compareTo("def") > 0; // tester si "abc" est supérieur à "def"

// La comparaison se fait à la base de la valeur Unicode des caractères
// et les majuscules sont AVANT les minuscules dans la table Unicode
"abc".compareTo("ABC"); // donne un chiffre positif
"abc".compareTo("ABC") > 0; // tester la supériorité (donne un booléen)
"abc".compareToIgnoreCase("ABC"); // donne un chiffre
"abc".compareToIgnoreCase("ABC") == 0; // tester l'égalité (donne un booléen)

Notez que pour utiliser les méthodes compareTo comme conditions, il faut utiliser le chiffre retourné par la méthode dans une expression de comparaison afin d’obtenir un résultat true ou false.

Quelle lettre est plus "grande" du point de vu d'un ordinateur, 'a' ou 'A'?

C’est 'a'! Sa valeur ASCII de 97 est 32 de plus que la valeur ASCII de 'A' (65). Et oui, les minuscules sont “plus grandes que” les majuscules dans le tableau ASCII.

Quel est le résultat de "oui"e.equals("Oui") ?

false. Les minuscules et les majuscules sont des caractères distinctes pour un ordinateur.

Quel est le résultat de "oui".equalsIgnoreCase("Oui") ?

true. La méthode spéciale .equalsIgnoreCase ignore les différences de case des lettres.

Opérateurs logiques - combiner des conditions

On peut combiner le résultat d’expressions booléennes avec les opérateurs logiques ET (&&) et OU (||). On peut aussi inverser le résultat d’une expression booléenne en le précédant avec l’opérateur PAS (!).

Comme avec d’autres opérateurs, il y a un ordre de priorité. L’opérateur PAS a la priorité la plus élevée, suivi de ET et, finalement, OU. On peut utiliser des parenthèses pour changer l’ordre d’évaluation.

Voici quelques exemples à tester dans une session jshell :

// exemples pour voir les opérateurs
true && true; // ET
true && false;
false && true;
false && false;
false || true; // OU
!true; // PAS
!false;
!true && true;
!(true && true); // effet des parenthèses

// exemples plus réalistes
int x = 5;
(x > 0) && (x < 10); // entre 0 et 10
(x < 0) || (x > 10); // en dehors de 0 à 10

// déterminer si je mange au restaurant utilisant deux variables booléennes
amHungry && restaurantOpen;

// l'équivalent de != pour la méthode de comparaison .equals()
!"abc".equals("def");
Quel est le résultat de l'opération !true ?

false. ! est l’opérateur “non” ou “inverse”, donnant toujours le contraire de la valeur qui le suit.

Quel est le résultat de l'opération "false || false || false || true" ?
tldr;
true. L’opérateur || signifie “ou”. Seulement une valeur dans la chaîne doit être vraie pour que tout soit vrai.
détails de l’évaluation
Parce que toutes les opérations sont les mêmes (||) l’évaluation progresse simplement de gauche à droite donnant : (false) || false || true ensuite (false) || true et finalement (true).
Quel est le résultat de l'opération "!false && false && true || false" sachant que l'ordre des opérations est : ! avant && avant || ?

false. L’opérateur && signifie “et” et donne vrai seulement si les deux valeurs sont vraies.

Voici l’ordre d’évaluation de l’expression :

  1. !false devient true
  2. Parce que && précède ||, on continue avec le premier && : (true) && false qui donne false
  3. Le deuxième && : (false) && true qui donne encore false
  4. Finalement le || (ou) : (false) || false donne aussi false.

Exercices

📚 Tester la compréhension

Répondre aux questions insérées dans les notes après les exemples.

🛠️ Pratique

A. Exemples à développer

Écrire des conditions appropriées pour donner true dans les situations suivantes. Utilisez des noms de variables appropriées si la valeur n’est pas fournie explicitement.

  1. Le choix de l’utilisateur est "a" (String).
  2. Le choix de l’utilisateur est 'a' (char). Indice 1 : on peut obtenir le premier caractère d’une réponse (p. ex.: pick) avec pick.charAt(0).Indice 2 : un char est un type primitif comme int.
  3. Il est tard mais je ne suis pas fatigué. Utilisez l’opérateur ! dans l’expression.
  4. Le nombre de points est plus grand que 21.
  5. Le nombre de vies est moins que 1.
  6. Je suis en vie et j’ai l’objet magique et j’ai battu le grand boss.
  7. Je veux écouter un texte de méditation et le son des vagues mais pas le son des oiseaux.

B. Problèmes de logique sur le site CodingBat (Enrichissement)

Démarrer sur ce site

Codinbat est un site qui offre des problèmes de logique à résoudre avec Java ou Python. C’est donc excellent pour pratiquer les conditions.

  1. Vous rendre sur le site codingbat.com/java
  2. En haut à droite, cliquer le lien pour créez un compte. Utiliser un courriel de votre choix et un mot de passe. Votre courriel est votre nom d’utilisateur et sert à la récupération du mot de passe. Aucune autre information personnelle n’est requise.
  3. Dans la section prefs sous “Teacher Share” ajoutez mon adresse courriel. Vous pouvez le trouver dans le Classroom ou dans votre Gmail de l’école dans la liste des contacts. Je pourrai alors voir vos progrès.
  4. Pour traduire la page, utilisez les outils de Chrome. MISE EN GARDE : à utiliser seulement pour comprendre la description du problème car la traduction affecte aussi le code de démarrage, brisant la syntaxe Java. Assurez-vous de changer la langue de traduction à “English” après avoir lu la description.

Astuces pour travailler dans CodingBat :

  • Votre solution pour chaque problème est une valeur de retour du type spécifié dans l’explication du problème et dans la signature de la méthode. Ça prend alors toujours une expression avec return dans votre code.
  • Les entrées de chaque problème sont les paramètres de la méthode et sont déjà déclarés pour vous. Vous devez utiliser ces paramètres dans vos conditions et autres opérations.
  • Vous testez votre solution en cliquant sur le bouton Go après avoir écrit votre code. Plusieurs cas de tests seront exécutés pour vérifier si votre solution est correcte et vous verrez exactement quels cas ont été réussis et lesquels ont échoués. Vous pouvez tester votre solution autant de fois que vous voulez.
  • Chaque problème dans cette séquence initiale de problèmes inclut une solution que vous pouvez consulter. Après les premiers problèmes, le bouton pour la solution est seulement accessible après avoir essayé le problème une fois.
  • Il y a des tutoriels accessible via des liens au bas de la page qui expliquent certaines des caractéristiques des données (String, tableaux, dictionnaires) et des méthodes communes pour les utiliser dans ces problèmes. Les tutoriels sur les String seront particulièrement utiles avec les problèmes de démarrage.
Exercices avec les conditions

C’est fort possible que vous ayez de la difficulté avec plusieurs de ces casse-têtes. C’est normal! Si vous avez de la difficulté, essayez de comprendre la solution fournie et de la réécrire vous-même. Le but est de vous familiariser avec les conditions et les opérateurs logiques afin que vous puissiez les utiliser dans des contextes de votre choix pas dans le contexte de problèmes de logique.

  1. Faire les problèmes suivants : les solutions à ces problèmes utilisent uniquement des comparaisons et des opérateurs de logique (&&, ||, !), en plus de quelques opérations mathématiques ou sur les String à l’occasion. Les problèmes à tenter sont dans la section Warmup-1 :
    • sleepIn
    • monkeyTrouble
    • makes10
    • nearHundred (utilise aussi la méthode Math.abs)
    • missingChar (utilise aussi la méthode substring)
    • backAround (utilise aussi les méthodes charAt et substring)
    • or35 (utilise l’opérateur modulo % pour trouver le reste de la division par 3 ou 5)
    • startHi (attention! la condition est assez complexe ici; utilise les méthodes length et substring)
    • icyHot (utilise aussi les méthodes Math.min et Math.max)
    • in1020
    • hasTeen (la condition est longue ici, avec plusieurs opérateurs logiques)
    • mixStart (utilise aussi les méthodes length, substring et equals)
    • lastDigit (utilise aussi l’opérateur modulo % pour trouver le reste de la division par 10)

Certaines solutions officielles à ces problèmes utilisent la sélection avec if et else pour résoudre les problèmes, mais si le problème est dans la liste ci-dessus, vous êtes capable de remplacer cet embranchement en utilisant les opérateurs logiques ET && et OU || pour produire une seule expression équivalente.

Accueil > Programmer avec Java > Structures de contrôle >

📚 Sélection

Survol et attentes

La sélection est un concept fondamental de la programmation. Elle permet au programme d’adapter son comportement selon l’état de différentes conditions. Bref, le programme devient plus intelligent. Dans cette leçon, nous allons voir comment utiliser la sélection en Java.

Définitions
Séléction
concept de programmation qui permet d’exécuter du code seulement si une condition est vraie. Cela crée plusieurs embranchements dans le code au lieu d’une seule séquence.
if et else
mots-clés utilisés pour la sélection en Java. Le bloc if déclare une condition et exécute son code si cette condition est vraie. Le bloc else suit un bloc if et exécute son code seulement si la condition du if était fausse.
Losange
forme utilisée pour représenter une condition dans un diagramme de flux. Son étiquette représente une question avec une réponse oui/non ou true / false, et il y a deux flèches qui quittent le bloc, une pour chaque réponse possible. Cela donne la représentation visuelle claire de l’embranchement.

Objectifs d’apprentissage

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

  • Reconnaître la syntaxe de la sélection en Java avec les blocs if, else if et else.
  • Lire et produire des diagrammes de flux incorporant des conditions et de l’embranchement.

Critères de succès

  • Je peux appliquer les conditions à différentes situations de sélection dans mes programmes Java.
  • Je peux créer une cascade de conditions mutuellement exclusives.
  • Je peux créer une suite de conditions indépendantes.

Sélection conditionnelle avec if et else

La sélection permet d’exécuter du code seulement si une condition est vraie. La condition est n’importe quelle expression booléenne, soit une expression qui s’évalue à true ou false :

if (condition) {
    // code à exécuter si la condition est vraie
}

if

On a aussi l’option d’exécuter du code si la condition est fausse en ajoutant un bloc else immédiatement après le bloc if :

if (condition) {
    // code à exécuter si la condition est vraie
} else {
    // code à exécuter si la condition est fausse
}

if_else

Plusieurs cas exclusifs avec else if

Si on a plusieurs cas exclusifs (des cas où seulement un cas peut être est vrai) à vérifier, on peut immédiatement déclarer un bloc if suivant le mot-clé else afin de poser la prochaine condition :

if (condition1) {
    // code à exécuter si la condition1 est vraie
} else if (condition2) {
    // code à exécuter si la condition2 est vraie
} else if (condition3) {
    // code à exécuter si la condition3 est vraie
} else {
    // code à exécuter si aucune des conditions précédentes n'est vraie
}

Chaque else représente ce qui se passe si le if précédant est faux. On peut enchaîner autant de blocs else if que nécessaire. Le bloc else final est optionnel, mais est souvent utile pour définir un cas par défaut, comme un message d’erreur.

cas_exclusifs

Cette façon d’enchaîner les conditions assure que seulement un bloc sera exécuté. Il faut donc faire attention de bien écrire les conditions afin de représenter tous les cas possibles et de ne pas en oublier. C’est un bon exemple de situation où faire des tests pour chaque cas, notamment les cas limites, est important.

Voici un exemple de code qui utilise plusieurs cas exclusifs dans le fichier LinkedConditions.java :

import java.util.*;

void main() {
    Scanner console = new Scanner(System.in);
    System.out.println("Quel est votre âge? ");
    int age = console.nextInt();

    if (age < 4) {
        System.out.println("Enfant préscolaire");
    } else if (age <= 11) {
        System.out.println("Enfant d'école élémentaire");
    } else if (age <= 14) {
        System.out.println("Adolescent d'école intermédiaire.");
    } else if (age <= 18) {
        System.out.println("Adolescent d'école secondaire.");
    } else {
        System.out.println("Adulte.");
    }
}

catégories d’âge

Quelle sera la sortie si l'utilisateur saisit la valeur 8?

À part l’invite de réponse, ce sera :

Enfant d'école élémentaire

La variable age prendra la valeur 8, donc la première condition (age < 4) sera fausse. On passe donc au else qui le suit qui déclare immédiatement une autre condition (age <= 11) qui est vraie. On entre alors dans ce bloc de code et affiche son message.

C'est quoi le diagramme de séquence d'exécution (suite des numéros de lignes exécutées) si l'utilisateur saisit la valeur 17?

On fait les 6 premières lignes en séquence pour ensuite arriver à ceci :

...
6
7
8
10
12
14
15
16
19
  • Notez surtout qu’on saute des lignes (les lignes 9, 11, et 13) : celles qui correspondent aux autres branches avec des conditions fausses
  • Notez aussi qu’on doit évaluer chacune de ces conditions (les lignes 8, 10 et 12) pour savoir sur quelle branche continuer (le côté false dans ce cas) …
  • Jusqu’à ce qu’on passe par le côté true d’une branche (la ligne 15). Son bloc se termine avec l’accolade fermante à la ligne 16 mais le reste de cette ligne et de la “cascade” est ignorée.
  • Le code se termine avec l’accolade fermante de main.

Plusieurs cas indépendants

Si on a plusieurs cas indépendants (des cas où plusieurs cas peuvent être vrais), on peut utiliser plusieurs blocs if :

if (condition1) {
    // code à exécuter si la condition1 est vraie
}
if (condition2) {
    // code à exécuter si la condition2 est vraie
}
if (condition3) {
    // code à exécuter si la condition3 est vraie
}

Chaque bloc if est indépendant des autres et un, plusieurs ou aucun bloc if peut être exécuté selon les conditions.

cas_indépendants

Voici un exemple de code qui utilise plusieurs cas indépendants dans le fichier IndependantConditions.java :

String input(String prompt) {
    System.out.print(prompt + " > ");
    return System.console().readLine(); 
                // plus simple mais moins puissante qu'un Scanner
}

void main() {
    String favColour = input("Couleur préférée");
    String favSeason = input("Saison préférée");
    int favNumber = Integer.parseInt(input("Nombre entier préférée")); 
        // on converti le String en int dans une 2ème opération

    if (favColour.equals("bleu")) {
        System.out.println("Le bleu est ma couleur préférée aussi!");
    }
    if (favSeason.equals("été")) {
        System.out.println("L'été est ma saison préférée aussi!");
    }
    if (favNumber == 7) {
        System.out.println("7 est mon nombre préféré aussi!");
    }
}

comparaison indépendantes

Si la condition est vraie à la ligne 13 (comparer les couleurs) est-ce qu'on continue avec les lignes de 16 à 20 ou est-ce qu'on les ignore?

On les fait quand même! Si le premier if est vrai ou faux on passe sur ces lignes tout de même parce qu’elles ne sont pas sur une branche else.

Exercices

📚 Tester la compréhension

Répondre aux questions insérées dans les notes après les exemples.

🛠️ Pratique

A. Exemples à développer

Structure du fichier et code de démarrage dans un nouveau fichier : Selection.java

import java.util.*;

final Scanner IN = new Scanner(System.in);

void calc() {
  // #1 : à développer
}

void dice() {
  // #2 : à développer
}

void main() {
  // appeler les autres méthodes pour les tester
}
  1. calc() : Demander deux valeurs de type double à l’utilisateur. Ensuite lui demander quelle opération qu’elle veut faire ("+", "-", "*", ou "/"). Utiliser la sélection (if, else if et else) pour afficher le résultat du calcul approprié.

    • Bonus : si l’utilisateur choisit "/" faire une sélection additionnelle (if) pour vérifier si le deuxième nombre est 0 et afficher un message d’erreur au lieu de faire le calcul.
  2. dice() : Utiliser l’opération (int)(Math.random() * 6 + 1) pour générer des valeurs aléatoires entre 1 et 6 (les valeurs d’une dé). Stocker trois de ces valeurs dans des variables de type int.

    • Utiliser la sélection pour vérifier si chaque valeur individeulle est un 6 (3 if). Pour chaque dé, afficher un message dans ce cas, mais ne faites rien dans le cas contraire.
    • Ensuite, utiliser une seule sélection pour voir si un nombre se répète (deux valeurs sont identiques) au moins une fois (1 if avec opérations logiques) et afficher un message approprié dans ce cas.
    • Finalement, vérifier si les trois valeurs sont identiques (1 if avec opérations logiques) et afficher un message approprié dans ce cas.

B. Problèmes de logique sur le site CodingBat (Enrichissement)

Comme avec la leçon précédente, c’est fort possible que vous ayez de la difficulté avec plusieurs de ces casse-têtes. Si un problème vous cause trop d’ennuis, vous pouvez passer à un autre. Il y en a beaucoup! Rappelez-vous que le but est de vous familiariser avec la sélection if-else afin que vous puissiez l’utiliser dans des contextes de votre choix pas dans le contexte de problèmes de logique.

  1. Tenter le reste des problèmes de Warmup-1. Ces autres problèmes utilisent des conditions et opérateurs logiques comme la liste précédente mais doivent aussi utiliser la sélection avec if et else. Les problèmes sont :
    • sumDouble
    • diff21
    • parrotTrouble
    • posNeg
    • notString
    • frontBack
    • front3
    • front22
    • loneTeen
    • delDel
    • startOz
    • intMax
    • close10
    • in3050
    • max1020
    • stringE
    • endUp

Le problème “everyNth” utilise aussi une boucle, ce qui sera vue dans la prochaine leçon.

Accueil > Programmer avec Java > Structures de contrôle >

📚 Boucles

Survol et attentes

Définitions

La répétition conditionnelle d’un bloc de code est un autre concept fondamental de la programmation. Elle permet aux programmes de devenir puissants en profitant de la vitesse d’exécution de l’ordinateur et prévient la répétition de code.

La boucle while est la plus polyvalente et peut servir à tous les contextes. La boucle for est plus spécialisée et est utilisée pour traiter une suite de nombres.

boucle
une structure de contrôle qui répète un bloc de code tant qu’une condition est vraie.

itération une seule exécution du bloc de code dans une boucle.

mécanisme de mise à jour
combinaison de variables et d’instructions qui ont l’objectif d’amener la condition de la boucle à false et prévenir les boucles infinies.
variable de contrôle
une variable qui est utilisée pour contrôler la condition de la boucle.
variable accumulateur
variable qu’on déclare avant une boucle qui collecte des valeurs pendant les itérations de la boucle, comme une somme ou une liste de noms.
drapeau booléen
une variable booléenne utilisé pour représenter l’état de quelque chose. Dans ce contexte, un drapeau booléen peut remplacer la condition de la boucle et sa valeur peut être modifiée à l’intérieur du bloc de code.
break
mot-clé Java qui force la sortie d’une boucle, peu importe la condition ou la position dans le bloc de code.
continue
mot-clé Java qui force la boucle à passer à l’itération suivante, ignorant toutes les instructions restantes dans le bloc de code.

Objectifs d’apprentissage

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

  • Décrire le rôle des trois éléments d’une boucle : condition, bloc de code, mise à jour.
  • Décrire trois façons de formuler la condition d’une boucle : avec une variable de contrôle, avec une variable booléenne, avec la constante true
  • Expliquer comment utiliser le mot-clé break pour sortir d’une boucle.

Critères de succès

  • Je peux écrire différentes structures de boucles selon le contexte du problème.
  • Je peux utiliser des variables booléennes pour éviter la répétition du code nécessaire pour valider une condition.

Boucle while

Voici la syntaxe de base pour une boucle while :

while (condition) {
    // bloc de code
    // mécanisme de mise à jour
}

boucle

C’est important de noter que l’exécution des instructions ne continue pas après le bloc de code mais revient à la condition. C’est ça qui forme la boucle. Si la condition est true, le bloc de code est répété. Si la condition est false, l’exécution continue après la boucle. Ici, le diagramme de flux donne une meilleure représentation de l’exécution réelle de étapes.

Exemple : Traiter une suite de nombres

Pour traiter une suite de nombres, on définit d’abord une variable de contrôle qui représente la valeur initiale de la suite. Ensuite, on définit la condition de la boucle en fonction de cette variable de contrôle. Finalement, on met à jour la variable de contrôle à l’intérieur du bloc de code.

Placez le code suivant dans un fichier qui s’appelle LoopExamples1.java et exécutez-le pour voir les résultats.

Par exemple, pour afficher les nombres de 1 à 10 :

Avec while

void whileLoop() {
    int i = 1;          // initialiser la variable de contrôle
    while (i <= 10) {   // condition utilisant la variable de contrôle
        System.out.println(i);
        i++; // mise à jour de la variable de contrôle (incrémenter)
    }
}

void main() {
    whileLoop();
}

boucle while 1

L'exécution du code précédent commence à quelle ligne?

La ligne 9, à la signature de main. La ligne 10 envoie ensuite l’exécution à la ligne 1 en appelant whileLoop().

Combien de fois est-ce que la ligne 2 sera-t-elle exécutée?

Une seule fois. Elle vient juste avant le début de la déclaration while.

Combien de fois est-ce que la ligne 4 sera-t-elle exécutée?

Dix fois : une pour chaque valeur de i qui donne un résultat true à i <= 10

Combien de fois est-ce que la ligne 3 sera-t-elle exécutée?

11 fois! Oui, on doit aussi visiter la condition pour la fois que la condition est fausse, quand i est égale à 11, brisant la boucle. La condition est donc évaluée une fois de plus que le nombre d’itération.

Quelle sera la sortie du code précédant?

La sortie est :

1
2
3
4
5
6
7
8
9
10

Avec for

La boucle for est spécialisée pour ce type de tâche. Elle inclut les trois éléments clés de la boucle directement dans sa déclaration. Sa syntaxe est la suivante :

for (initialisation; condition; mise à jour) {
    // bloc de code
}

Par exemple, pour afficher les nombres de 1 à 10 avec une boucle for :

void forLoop() {
    for (int i = 1; i <= 10; i++) { // les trois éléments séparés par des ;
        System.out.println(i);
    }
}

Si on appelle aussi cette méthode dans main, on aura exactement la même sortie qu’avec la version while. Ajoutez-le à votre fichier LoopExamples1.java pour le vérifier.

Défi

Pouvez vous trouver et afficher la somme de toutes les valeurs de 1 à 10 en modifiant une des boucles ci-dessus?

Indice : vous aurez besoin d’une variable accumulateur en plus de la variable de contrôle.

Exemple : Valider des données d’entrée

Un autre contexte pour une boucle est demander une réponse à l’utilisateur tant que la réponse n’est pas valide.

Une situation typique est lorsqu’on demande une confirmation oui/non à l’utilisateur. On peut utiliser une boucle while pour s’assurer que la réponse est valide.

Voici deux façons de le faire. Il en existe d’autres! Vous pouvez tester ces méthodes dans un nouveau fichier LoopExamples2.java.

Avec une variable de contrôle pour la condition de la boucle

import java.util.*;

final Scanner INPUT = new Scanner(System.in);

String again() {
    String answer = ""; // variable : contrôle ET accumulateur

    while (!(answer.equals("oui") || answer.equals("non"))) { 
        System.out.print("Voulez-vous continuer? (oui/non) > ");
        answer = INPUT.next().toLowerCase();
            // mise à jour avec une réponse en minuscules
    }
    return answer;
}

void main() {
    while (again().equals("oui")) {
        // code à répéter
    }
    System.out.println("Au revoir");
}

boucle while 2

Il y a deux conditions dans ce code.

Le plus complexe est dans again() : !(answer.equals("oui") || answer.equals("non")) :

  • Le ! au début indique qu’on veut inverser le résultat entre les parenthèses - c’est parce que ce qu’on décrit entre parenthèses correspond à ce que nous voulons, mais la boucle devrait se répéter dans le cas contraire
  • Le || est l’opérateur “ou” qui sera vrai si l’une ou l’autre des conditions est vraie.
  • On utilise .equals() pour comparer l’égalité parce qu’on compare deux String.

Plaçant tout ça ensemble avec le mot-clé while, on devrait lire :

si la réponse EST "oui" OU "non", NE PAS répéter le bloc de code

La deuxième condition est dans main : again().equals("oui") :

  • again retourne un String au programme qui est directement utilisé dans la comparaison

Voici un exemple d’interaction avec l’utilisateur pour ce programme :

Voulez-vous continuer? (oui/non) > peut-être
Voulez-vous continuer? (oui/non) > n
Voulez-vous continuer? (oui/non) > Non
Au revoir
Combien des fois est-que la condition de la boucle dans la méthode again() est-elle évaluée durant l'intéraction ci-dessus?

4 fois : une au début avec la valeur initiale de "" et une pour chaque réponse de l’utilisateur

Pourquoi est-ce que la réponse "Non" a-t-elle été acceptée?

INPUT.next() nous donne "Non" ce qui ne serait pas equals à "non". Mais ce n’est pas ce qui se rend à la condition : on passe la réponse brute à la méthode toLowerCase() qui convertit tout en minuscules. Ainsi, answer contient la valeur "non" ce qui permet à la condition d’être vraie.

Défi

La condition pour évaluer si la réponse était invalide (alors répéter la boucle) est la suivante :

!(answer.equals("oui") || answer.equals("non")) = si la réponse EST "oui" OU "non", NE PAS répéter le bloc de code

On peut écrire cette même condition d’une autre façon.

Quelle condition serait équivalente à "répéter la boucle si la réponse n'EST PAS "oui" ET la réponse n'EST PAS "non"?

!answer.equals("oui") && !answer.equals("non")

Exemple : Répéter un programme jusqu’à ce que l’utilisateur décide de le quitter

Un autre contexte commun est une boucle de programme intentionellement infinie. Pour ces cas, la déclaration de la boucle est while (true). C’est ce qui se passe avec les fenêtres de vos applications : l’affichage est rafraîchi en permanence jusqu’à ce que vous fermiez la fenêtre.

Mais comment quitter une boucle où la condition de boucle est une constante? Réponse : on utilise une instruction break à l’intérieur du bloc de code. L’instruction break est insérée dans une sélection qui évalue si la condition de sortie est atteinte.

while (true) { // passe tout droit
    // bloc de code
    if (condition) {
        break; // sortie de la boucle
    }
}

infinite_loop

Notez que dans le diagramme pour une boucle while (true) la condition de boucle se trouve à l’intérieur et parfois à la fin des instructions de la boucle. Le “while (true)” n’est pas représenté directement parce que ça n’affecte pas le flux des étapes.

Notez que le mot-clé while n’apparaît jamais dans ces diagrammes, ni les autres mot-clés (p. ex.: if, else, true, break). Il s’agit simplement de conditions et d’embranchements dans les diagrammes. Dans les divers cas d’itération, une des branches forme une boucle. Sinon on parle de sélection / d’embranchement tout court.

Retour sur la validation de données

On pourrait remplacer la version de main dans LoopExamples2.java avec la version suivante :

void main() {
    while (true) {
        // code de la tâche

        // code pour demander à l'utilisateur s'il veut continuer

        if (again().equals("non")) {
            break; // sortie de la boucle
        } else {
            // code pour réinitialiser les variables de la tâche
        }
    }
}

Pour d’autres tâches

Un autre exemple est pour faire la somme d’une série de nombres. On demande à l’utilisateur d’entrer un nombre à la fois. Si le nombre est -999, on quitte la boucle. Sinon, on ajoute le nombre à la somme.

Reprendre cet exemple dans un nouveau fichier LoopExamples3.java.

import java.util.*;

void main() {
    Scanner input = new Scanner(System.in);

    int sum = 0; // variable accumulateur

    while (true) {
        System.out.print(
            "Entrez un nombre entier (-999 pour quitter) : "
        );
        
        int num = input.nextInt(); // variable de contrôle
        if (num == -999) {
            break; // mot-clé pour quitter la boucle
        } else {
            sum += num;
        }
    }
    System.out.println("La somme est " + sum);
}

while avec break

Et voici deux exemples d’interaction avec l’utilisateur :

Entrez un nombre entier (-999 pour quitter) : 5
Entrez un nombre entier (-999 pour quitter) : 10
Entrez un nombre entier (-999 pour quitter) : 15
Entrez un nombre entier (-999 pour quitter) : -999
La somme est 30
Entrez un nombre entier (-999 pour quitter) : -3
Entrez un nombre entier (-999 pour quitter) : 0
Entrez un nombre entier (-999 pour quitter) : 3
Entrez un nombre entier (-999 pour quitter) : 6
Entrez un nombre entier (-999 pour quitter) : -999
La somme est 6

Exercices

Pratique

Travaillez dans le répertoire GitHub partagé par votre enseignant pour la pratique et les exercices.

Contexte

Voici un autre exemple qui nous donne la partie entière du logarithme d’une valeur x en base 2. On divise x jusqu’à ce que sa valeur soit inférieure à 1. Le nombre de divisions est le logarithme de x en base 2.

int x = 1024; // valeur à traiter; aussi la variable de contrôle
System.out.print("Le log2 de " + x + " est ");

int divCounter = 0;
while (x > 1) { // condition
    divCounter++;
    x = x/2; // mise à jour (divise par 2)
}
System.out.println(divCounter);

et sa sortie:

Le log2 de 1024 est 10

Problème à résoudre

Écrire une boucle while (ou for) qui utilise une mise à jour autre que l’incrémentation. Par exemple, on peut utiliser une décrémentation, une multiplication ou une division, ou n’importe quel calcul que vous souhaitez en autant que la variable de contrôle se rapproche de la condition false à chaque itération. L’exemple précédant pour le logarithme utilise une division.

Accueil > Programmer avec Java >

Gérer les exceptions avec try et catch

Survol et attentes

Définitions
Entrée invalide
une entrée qui ne correspond pas à une gamme de valeurs acceptables.
Exception
un événement imprévu qui cause le plantage d’un programme s’il n’est pas géré. Java inclut une classe Exception qui permet de gérer ces événements en fournissant des informations sur l’erreur qui s’est produite.
try et catch
blocs de code utilisés pour gérer les exceptions en Java. Le bloc try contient le code qui pourrait générer une exception, et le bloc catch contient le code qui gère l’exception si elle se produit.
throws
un mot-clé utilisé dans la signature d’une méthode pour indiquer qu’elle utilise du code qui peut générer une exception et ne le gère pas. La méthode passe ainsi la responsabilité de gérer l’exception à la méthode appelante.
Responsabilité unique
principe de conception de logiciel qui stipule qu’une classe ou une méthode devrait être responsable d’une seule tâche.

Objectifs d’apprentissage

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

  • Identifier les exceptions les plus communes en Java.
  • Savoir qu’elles entrées peuvent causer des exceptions et les distinguer de celles qui sont invalides pour d’autres raisons.
  • Comprendre la structure d’un bloc try et catch pour gérer les exceptions.

Critères de succès

  • Je peux gérer des cas qui causeraient autrement un plantage du programme en utilisant des blocs try et catch.

Responsabilité unique

En génie logicielle, il y a un principe qui s’appelle la responsabilité unique. Ce principe nous dit que chaque méthode doit être responsable d’une seule chose. C’est une des raisons pourquoi nous brisons nos algorithmes en méthodes plus petites : cela limite la responsabilité de main à l’organisation globale des opérations et permet à chaque méthode d’avoir un objectif clair.

Si on utilise du code qui peut générer une exception, on a deux choix :

  • ajouter une déclaration throws Exception à la signature de la méthode pour passer la gestion de l’exception à la méthode appelante, ou
  • utiliser un bloc try et catch pour gérer l’exception dans la méthode directement.

La deuxième option est préférable selon le principe de responsabilité unique, car la méthode gère les problèmes potentiels avec le code qu’il utilise pour accomplir sa tâche, et les autres méthodes n’ont pas besoin de s’en soucier : elles peuvent se concentrer sur leur propre tâche.

Bloc try et catch pour des entrées invalides

Le bloc try contient le code qui peut lancer une exception, et le bloc catch contient le code à faire si une exception est lancée. Voici la structure de base d’un bloc try et catch :

try {
    // code qui pourrait lancer une exception
} catch (Exception e) {
    // code à exécuter si une exception est lancée
}

Voici un exemple de code1 qui lance une exception si l’utilisateur entre un texte qui ne peut pas être converti en un nombre entier. Dans cet exemple, on ne laisse pas le programme planter, mais on demande à l’utilisateur de réessayer :

Fichier: ExceptionExample.java

import java.util.*;

Scanner input = new Scanner(System.in);

int getInt(String prompt) {
    // tentatives infinies jusqu'à ce que tout dans le try fonctionne
    while (true) { 
        try {
            System.out.print(prompt);
            int number = input.nextInt(); // peut planter
            return number; // quitte la méthode si tout va bien
        } catch (Exception e) { // si ça plante
            System.out.println("   Entrée invalide");
            input.next(); // vide l'entrée invalide du Scanner
        }
    }
}

void main() {
    int age = getInt("Entrez votre âge : "); // pas besoin de savoir que ça peut planter
    System.out.println("Vous avez soumis un âge de : " + age);
}

Notez que dans main on veut simplement obtenir un nombre entier et l’utiliser. Cela est possible parce que la méthode getInt gère les exceptions qui pourraient survenir lors de la conversion de l’entrée utilisateur en un nombre entier. Ainsi, chaque méthode s’occupe de sa propre responsabilité et n’a pas besoin de connaître les détails internes des autres méthodes. C’est un bon exemple de la responsabilité unique.

Voici une autre version de main 1 pour ce même problème, mais dans ce cas-ci, main gère une autre sorte d’entrée invalide : une entrée qui n’est pas un âge valide. Ce type d’erreur ne cause pas d’exception, mais pourrait causer des erreurs de logique dans le programme.

void main() {
    int age = getInt("Entrez votre âge : ");
    if (age < 0) { // gérer une valeur logiquement invalide
        System.out.println("Âge invalide");
    } else {
        System.out.println("Vous avez soumis un âge de : " + age);
    }
}

Bloc try et catch pour des ressources comme des fichiers

Nous étudions les fichiers en plus de détail dans la dernière section de cette unité

Si le programme génère une exception lors de l’utilisation d’une ressource comme un fichier, il est important de fermer la ressource avant de quitter le programme. Pour cela, on utilise une déclaration try avec une ressource. Dans cette version de la structure, la ressource est déclarée dans les parenthèses après le mot-clé try. La ressource est automatiquement fermée par Java à la fin du bloc try, même si une exception est lancée.

Voici la structure d’une déclaration try-catch quand le plantage est lié à la ressource utilisée :

try ( déclaration de la ressource qui peut planter ) {
    // code à exécuter avec la ressource si ça ne plante pas
} catch (Exception e) {
    // code à exécuter si ça plante
}

Voici un exemple où la ressource qui peut planter est un FileWriter pour écrire dans un fichier.

void writeMessageToFile(String message) {
    try (FileWriter output = new FileWriter("./data/output.txt")) {
        output.write(message);
    } catch (Exception e) {
        System.out.println("Chemin de fichier invalide");
    }
}

Voici un deuxième exemple où la ressource qui peut planter est l’objet File qu’on passe au Scanner pour extraire son contenu.

String getFileContents(Locale decimalFormat) {
    try (Scanner fileReader = new Scanner(new File("./data/input.txt"))) {
        fileReader.useLocale(decimalFormat);
        fileReader.useDelimiter("\\Z"); // next() s'arrête au caractère de fin de fichier
        return fileReader.next();
    } catch (Exception e) {
        System.out.println("Fichier introuvable");
        return null; // quittez la méthode avec une référence `null` pour le String
    }
}

Notez que dans ces deux exemples, on doit déclarer et initialiser la ressource plantable entre des parenthèses après le mot-clé try.

Exceptions spécifiques

Pour ce cours, c’est tout à fait normal d’utiliser Exception - la plus générale de toutes les exceptions - comme type d’exception dans le bloc catch. Mais en situation de production logicielle, c’est important de déclarer l’exception spécifique qui peut survenir dans le bloc catch.

Par exemple :

  • Dans le cas d’entrée invalide, le type d’exception qui est lancée est InputMismatchException (avec les méthodes de Scanner nextInt et nextDouble) ou NumberFormatException (si on utilise plutôt Integer.parseInt(input.next()) ou Double.parseDouble(input.next())). Ce sont des exceptions pour les erreurs de conversion de chaînes de caractères en nombres.
  • Dans le cas de la lecture ou l’écriture de fichiers, l’exception est IOException.

Utiliser une exception spécifique permet de gérer les exceptions de manière plus précise. En fait, cette structure nous permet de déclarer un bloc catch pour chaque type d’exception qui peut survenir. Le premier bloc catch qui correspond à l’exception lancée est celui qui est exécuté, comme une cascade if-else if, alors c’est important de les mettre dans l’ordre du plus spécifique (p. ex. IOException) au plus général (Exception).

Exercices

📚 Tester la compréhension

aucun quiz de vérification des concepts ici encore

🛠️ Pratique

Travaillez dans le répertoire GitHub partagé par votre enseignant pour la pratique et les exercices

  1. Créez une copie de ExceptionExample.java et le tester.
  2. Ajoutez une méthode double getDouble(String prompt) qui fonctionne de la même manière que getInt, mais pour les nombres à virgule flottante et ajouter une utilisation de cette méthode dans main.
  3. Remplacez System.out.println("Entrée invalide"); dans les blocs catch avec e.printStackTrace(); pour voir le message d’erreur complet sans toutefois faire planter le programme. Notez le type d’exception qui est lancée (celui qui s’affiche en premier dans le message d’erreur).

  1. Exemple applicant les classes implicites et le main d’instance de JEP 463 (pleinement intégré aux outils Java22+) ↩2

Accueil > Programmer avec Java > Structures de données >

📚 Tableau

Survol et attentes

Définitions
tableau
une structure de données qui stocke une collection d’éléments de même type. En anglais, ça s’appelle un array. En Java, les tableaux sont de taille fixe. Il sont déclarés avec des crochets droits [] après le type de données. P. ex. int[] tableau; pour un tableau d’entiers.
index
un nombre entier qui identifie la position d’un élément dans un tableau. Les index dans Java, comme dans la majorité des langages, commencent à 0. On accède à un élément spécifique d’un tableau avec tableau[index]. Si l’index fourni n’est pas valide (négatif ou >= length), Java génère une erreur ArrayIndexOutOfBoundsException.
length
un attribut d’un tableau qui indique le nombre d’éléments dans le tableau. Pour un tableau tableau, on peut obtenir la longueur avec tableau.length.
Arrays
une classe Java qui fournit des méthodes pour manipuler des tableaux, comme pour les afficher ou les trier sans avoir à développer ces algorithmes nous-mêmes.
traverser
parcourir tous les éléments d’un tableau. Il existe plusieurs façons de le faire en Java, notamment avec une boucle for, une boucle while ou une boucle for-each. C’est une technique de base pour un grand nombre d’algorithmes comme trouver des valeurs, compter des éléments, ou les trier.
variable accumulateur
une variable utilisée pour combiner les valeurs inidividuelles d’un tableau avec une opération quelconque. P. ex. une variable pour la somme de tous les éléments d’un tableau.
variable drapeau
une variable utilisée pour indiquer l’état de quelque chose. Si l’état est juste vrai/faux, on peut utiliser un boolean. Dans d’autre cas, l’état peut être une valeur numérique - comme la position dans un tableau - alors un int peut être plus approprié. Exemple : un drapeau booléen peut indiquer si un élément est trouvé dans un tableau ou non. Dans le même contexte un drapeu numérique peut indiquer l’index de l’élément trouvé ou -1 s’il n’est pas trouvé.

Objectifs d’apprentissage

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

  • Comprendre la structure d’un tableau, notamment l’utilisation des index
  • Obtenir ou modifier un élément spécifique d’un tableau
  • Traverser tous les éléments d’un tableau avec différents types de boucles

Critères de succès

  • Je peux organiser mes données dans des tableaux et les intégrer dans la logique de mes programmes Java.

Structure d’un tableau en mémoire

Crédit : Mustapha Ejjaaouani, 2024

Créer et utiliser des tableaux en Java

Crédit : Hervice Ngouffo, 2024

Traverser un tableau - trois façons différentes

Pour traverser un tableau, on utilise une boucle.

Voir les notes sur les boucles pour vous rafraîchir la mémoire.

Dans les exemples ci-dessous, on traverse une boucle simplement pour afficher chaque valeur du tableau.

Boucle while - lire ou modifier les éléments du tableau

La boucle while peut servir pour n’importe quelle boucle.

int[] tableau = {1, 2, 3, 4, 5};

int i = 0;
while (i < tableau.length) {
    System.out.println(tableau[i]);
    i++;
}

Boucle for - lire ou modifier les éléments du tableau

La boucle for est particulièrement bien adaptée pour les tableaux car les trois opérations sur l’index du tableau dans la version while sont regroupées dans la déclaration de la boucle for.

int[] tableau = {1, 2, 3, 4, 5};

for (int i = 0; i < tableau.length; i++) {
    System.out.println(tableau[i]);
}

Boucle for-each - lire seulement

La boucle for-each est une version spécialisée de la syntaxe for conçu spécifiquement pour traverser les collections, y compris les tableaux.

Dans cette boucle, on n’utilise pas les index du tableau - ils sont gérés automatiquement par Java. Plutôt, on utilise une variable temporaire pour chaque élément du tableau. Cette variable doit être du même type que les éléments du tableau.

int[] tableau = {1, 2, 3, 4, 5};

for (int element : tableau) {
    System.out.println(element);
}

La ligne for (int element : tableau) se lit comme “pour chaque élément de type int dans le tableau”.

Arrays

La classe Arrays de Java fournit des méthodes pratiques pour manipuler des tableaux.

toString()

Pour afficher les éléments d’un tableau sans écrire un algorithme spécifique, vous pouvez utiliser la commande suivante :

Array.toString(tableau);

Comparez la sortie de cette commande avec la commande System.out.println(tableau); pour voir la différence.

sort()

Pour trier un tableau en ordre croissant, vous pouvez utiliser la commande suivante :

Arrays.sort(tableau);

Cette méthode modifie directement les éléments de tableau ce qu’on appelle un tri en place. Aucun nouveau tableau est généré.

binarySearch()

Contexte : la recherche linéaire

Pour utiliser une recherche linéaire sur un tableau, on peut simplement le traverser élément-par-élément, comme ceci :

int recherche = 3;
boolean found = false;
for (int i = 0; i < tableau.length; i++) {
    if (tableau[i] == recherche) {
        found = true;
        break;
    }
}

S’il n’y a jamais d’égalité entre la valeur cherchée et un élément du tableau, found restera false. Sinon, on change found à true et on sort de la boucle.

La plupart des méthodes de recherche retourne un index valide si l’élément est trouvé, ou un index négatif si l’élément n’est pas trouvé. L’algorithme pour ça est identique à l’algorithme précédent, mais on remplace le drapeau booléen avec un drapeau numérique :

int recherche = 3;
int found = -1;
for (int i = 0; i < tableau.length; i++) {
    if (tableau[i] == recherche) {
        found = i;
        break;
    }
}

Recherche binaire

Par contre, si on a beaucoup d’éléments à chercher, une méthode plus efficace est la recherche binaire. Il y a une précondition, par contre : pour utiliser la recherche binaire, le tableau doit être trié.

Voici comment on peut utiliser la recherche binaire avec Arrays :

int recherche = 3;
Arrays.sort(tableau);
int index = Arrays.binarySearch(tableau, recherche);

Pour plus de détails sur les algorithmes de recherche et l’efficacité algorithmique, voir le cours ICS4U!

Exemple complet - faire la moyenne des éléments d’un tableau

Crédit : Hervice Ngouffo, 2024

Exercices

📚 Tester la compréhension

Quiz de vérification sur les tableaux et leurs index

🛠️ Pratique

Vous rendre sur le site codingbat.com et vous créer un compte avec votre courriel écolecatholique.ca. Ajouter mon courriel dans la section Teacher Share afin de me montrer vos progrès.

  1. Travaillez d’abord 3-4 exercices dans la section “Warmup-1” pour vous familiariser avec le système :
    • la formulation des problèmes
    • les entrées sous forme de paramètres
    • les sorties en tant que valeurs de retour
    • les tests automatisés
    • les exemples de solutions
  2. Faites 3-4 exercices dans la section “Array-1” pour vous familiariser avec la structure des tableaux (index et length).
  3. Faites 3-4 exerices dans la section “Array-2” pour vous familiariser à traverser des tableaux (incluant l’utilisation de continue et break).

La section “Array-3” est plus avancée (matrices / tableaux de tableaux) et peut être considérée comme un défi pour ce cours.

Accueil > Programmer avec Java > Structures de données >

📚 Strings

Survol et attentes

Définitions

L’analyse de texte est une compétence essentielle pour les programmeurs parce que la plupart des entrées arrivent initialement sous forme de texte : via la console, un fichier, ou un formulaire web. Les méthodes de la classe String de Java sont essentielles pour manipuler et analyser les chaînes de caractères.

On a déjà vue d’autres méthodes pour manipuler les String et pour les comparer. Vous pouvez les réviser ici (manipulations) et ici (comparaisons).

Objectifs d’apprentissage

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

  • Analyser le contenu des String avec des méthodes comme charAt, length, toCharArray, indexOf, lastIndexOf, substring, startsWith, endsWith et contains.
  • Nettoyer les entrées texte en utilisant des méthodes comme replace, strip et split.
  • Se familiariser avec les méthodes “parse” pour convertir des String en types primitifs, notamment Integer.parseInt et Double.parseDouble.

Critères de succès

  • Je peux utiliser une variété d’entrées texte et extraire leurs informations avec confiance dans mes propres programmes.

Méthodes qui utilisent un drapeau booléen (donne une réponse vrai/faux)

Ces méthodes sont utiles pour sonder le contenu d’un texte, soit comme réponse en soi ou comme condition pour une prochaine étape de traitement. Par exemple, on peut vérifier si un texte commence ou finit avec une certaine lettre (ou séquence de lettres) ou s’il contient un mot spécifique.

Voici quelques exemples :

String s1 = "Hello, World!";
boolean b1 = s1.startsWith("Hello"); // true
boolean b2 = s1.endsWith("World!"); // true
boolean b3 = s1.contains(" "); // true

Méthodes qui utilisent la nature tableau des String (se basent sur des index)

La valeur de chaque String est un tableaux de bytes représentant chacun des caractères Unicode dans le texte. On peut alors accéder à chaque caractère individuellement en utilisant un index et une méthode appropriée. On peut aussi sonder le texte et recevoir un index en retour au lieu d’une valeur booléenne.

charAt, length et toCharArray

La méthode charAt est utile pour accéder à un caractère spécifique dans une chaîne de caractères. On utilise un index pour spécifier la position du caractère, en commençant à 0 pour le premier caractère. Si l’index est plus grand que la longueur de la chaîne, une exception StringIndexOutOfBoundsException sera lancée. On peut obtenir la longueur de la chaîne avec la méthode length(). On peut aussi convertir une chaîne de caractères en un tableau de caractères avec la méthode toCharArray.

String s1 = "Hello, World!";
char c1 = s1.charAt(0); // 'H'
char c2 = s1.charAt(7); // 'W'
char c3 = s1.charAt(s1.length() - 1); // '!'
for (char c : s1.toCharArray()) {
    System.out.println((int)c); // affiche le code Unicode de chaque caractère
}

  • Notez que la valeur de retour de charAt est un char et non un String. Ça peut être utile pour les comparaisons (avec ==) ou pour obtenir la valeur Unicode, mais ça peut aussi causer des problèmes si on tente de le traiter comme un String.

indexOf et lastIndexOf

Ces méthodes sont utiles pour trouver la position d’un caractère ou d’une séquence de caractères dans une chaîne de caractères. La méthode indexOf retourne la position du premier caractère trouvé, tandis que lastIndexOf retourne la position du dernier caractère trouvé. Si le caractère ou la séquence n’est pas trouvé, -1 est retourné.

String s1 = "Hello, World!";
int i1 = s1.indexOf("o"); // 4
int i2 = s1.lastIndexOf("o"); // 8
int i3 = s1.indexOf("z"); // -1

La méthode indexOf peut prendre un deuxième argument pour spécifier la position de départ de la recherche. Ça peut être utile pour trouver des occurrences multiples d’un caractère ou d’une séquence.

String s1 = "Hello, World!";
int countOs = 0;
int i = 0;
while (i != -1 && i < s1.length()) {
    i = s1.indexOf("o", i); // commencer la recherche à partir de la position i
    if (i != -1) {
        countOs++;
        i++; // continuer la recherche après le dernier "o" trouvé
    }
}
System.out.println("On a trouvé " + countOs " 'o' dans le texte.");

substring

Cette méthode est utile pour extraire une partie d’une chaîne de caractères. On spécifie la position de départ et la position de fin (exclusif) pour extraire la sous-chaîne. Si on ne spécifie pas la position de fin, la sous-chaîne sera extraite jusqu’à la fin de la chaîne.

String s1 = "Hello, World!";
String s2 = s1.substring(7); // "World!"
String s3 = s1.substring(7, 9); // "Wo"

Méthodes pour nettoyer les String

replace

Cette méthode est utile pour remplacer un caractère ou une séquence de caractères par un autre. Par exemple, si on veut remplacer tous les espaces par des tirets dans une chaîne de caractères, on peut utiliser la méthode replace. Même chose pour remplacer les virgules avec des points.

Voici un exemple :

String s1 = "Dé connecté";
String s2 = s1.replace(" ", "-"); // "Dé-connecté"
String s3 = s1.replace(" ","").replace("Dé","Re"); // "Reconnecté"
  • Notez que le texte original n’est pas modifié. La méthode replace retourne une nouvelle chaîne de caractères avec les modifications.
  • Notez qu’on peut remplacer quelque chose avec le texte vide "" pour l’éliminer.
  • On peut aussi chaîner les appels de méthode pour effectuer plusieurs remplacements en une seule ligne. Ça marche parce que la méthode replace retourne une nouvelle chaîne de caractères qui devient le String utilisé pour la prochaine appel de replace.

strip

Cette méthode est pratique pour éliminer les espaces blancs inutiles au début et à la fin d’une chaîne de caractères. Elle est surtout importante avant de comparer des String, parce que les espaces feront partie de la comparaison s’ils sont encore présents.

Voici un exemple :

"  Hello, World!  ".equals("Hello, World!"); // false
"  Hello, World!  ".strip().equals("Hello, World!"); // true; strip enlève les espaces initiaux et finaux

Il existe aussi la méthode trim qui fait la même chose, mais elle ne reconnaît pas les espaces Unicode. La méthode strip est plus moderne et plus fiable.

split

Cette méthode est utile pour séparer une ligne de texte, obtenu notamment avec la méthode nextLine de Scanner, en plusieurs éléments. Par exemple, si on a une ligne de texte avec des mots séparés par des espaces, on peut les séparer en utilisant un espace " " comme délimiteur. Un format de fichier commun est le CSV (Comma Separated Values) où les valeurs sont séparées par des virgules ou des points-virgules. La méthode split est utile pour séparer ces valeurs en utilisant "," (ou ;) comme délimiteur.

Voici un exemple où on doit utiliser strip pour nettoyer les valeurs une fois qu’elles ont été séparées :

String s1 = "Hello, World!";
String[] words = s1.split(","); // ["Hello", " World!"]
for (int i = 0; i < words.length; i++) {
    words[i] = words[i].strip();
} // ["Hello", "World!"]

Alternative : Scanner

Une autre façon de séparer les valeurs d’une ligne de texte est d’utiliser un objet Scanner. Voici un exemple :

Scanner sc = new Scanner("Hello, World!");
sc.useDelimiter(",");
while (sc.hasNext()) {
    System.out.println(sc.next().strip());
}
sc.close();
  • On utilise la méthode useDelimiter pour spécifier le délimiteur entre les différents éléments du texte.
  • On utilise la méthode hasNext pour vérifier s’il reste des éléments à traiter dans la boucle.
  • On peut immédiatement appliquer strip sur chaque élément pour nettoyer les espaces blancs en le chaînant avec next.

Et voici un autre exemple où on connaît le type des données à chaque position sur la ligne :

// un Scanner pour la source de données
Scanner sc = new Scanner("Alice, 25, 3.14\nBob, 30, 2.71\n");
while (sc.hasNextLine()) { // boucle pour traiter multiples lignes dans la source
  String line = sc.nextLine();
  Scanner lsc = new Scanner(line); // un autre Scanner pour chaque ligne
  lsc.useDelimiter(",");
  String name = lsc.next().strip();
  int age = lsc.nextInt();
  double rating = lsc.nextDouble();
  lsc.close();
  System.out.printf("%s a %d ans et une cote de %.2f\n", name, age, rating);
}
sc.close();

La sortie de ce code sera :

Alice a 25 ans et une cote de 3.14
Bob a 30 ans et une cote de 2.71
  • On utilise un Scanner pour traiter chaque ligne de texte.
  • Rappel : le caractère \n est le caractère pour indiquer une nouvelle ligne.
  • Connaissant le format des données, on peut utiliser la méthode next, nextInt et nextDouble aux endroits appropriés.

Les méthodes “parse”

Ces méthodes sont utiles pour convertir des chaînes de caractères en types primitifs. Par exemple, si on veut convertir une chaîne de caractères en un entier, on peut utiliser la méthode Integer.parseInt.

Prenons le dernier exemple de l’alternative avec le Scanner. Si on utilise split sur chaque ligne au lieu d’un Scanner, on n’aura pas accès aux méthodes nextInt et nextDouble pour convertir les valeurs en entiers et en doubles. On doit alors utiliser les méthodes “parse” pour convertir les chaînes de caractères en types primitifs.

Voici ce même exemple réécrit avec split et les méthodes “parse” appropriées:

Scanner sc = new Scanner("Alice, 25, 3.14\nBob, 30, 2.71\n");
while (sc.hasNextLine()) { // boucle pour traiter multiples lignes dans la source
  String line = sc.nextLine();
  String[] values = line.split(",");
  String name = values[0].strip();
  int age = Integer.parseInt(values[1].strip());
  double rating = Double.parseDouble(values[2].strip());
  System.out.printf("%s a %d ans et une cote de %.2f\n", name, age, rating);
}
sc.close();
  • Notez qu’on réfère aux valeurs du tableau values avec leurs index
  • Notez aussi qu’il faut utiliser strip pour nettoyer les valeurs avant de les convertir en entiers ou en doubles, contrairement aux méthodes nextInt et nextDouble qui font ça automatiquement.

Exercices

🛠️ Pratique

Vous rendre sur le site codingbat.com pour les exercices suivants.

  1. Travaillez dans la section “String-1” pour vous familiariser avec les méthodes de la classe String.
  2. Travaillez dans la section “String-2” pour ajouter des boucles à vos analyses des entrées texte.

Accueil > Programmer avec Java >

🛠️ Lire et écrire des fichiers

Survol et attentes

Définitions

File : une classe Java qui permet de manipuler des fichiers et des répertoires. On importe cette classe avec import java.io.File;. File nous permet de créer, supprimer, renommer, etc. des fichiers et des répertoires. Au plus simple, on utilise un objet de type File comme source pour un Scanner.

  • Par exemple, Scanner fileReader = new Scanner(new File("nomDuFichier.txt")).

FileWriter : une classe Java qui permet d’écrire des données dans un fichier texte avec la méthode write(). On importe cette classe avec import java.io.*;. On a plusieurs options pour le créer un objet de ce type mais les deux les plus communs sont :

  • new FileWriter("nomDuFichier.txt") -> En créant ce FileWriter, le contenu existant du fichier sera remplacé par les appels de write.
  • new FileWriter("nomDuFichier.txt", true) -> En créant ce FileWriter et en assignant true au 2e paramètre, on ajoute du contenu à la fin du fichier avec les nouveaux appels de write.

objet anonyme : un objet qui n’a pas de nom, donc qui est utilisable seulement à l’endroit où il est déclaré. On peut créer un objet anonyme directement dans un appel de méthode.

  • Par exemple, new Scanner(new File("nomDuFichier.txt")) crée un objet Scanner qui lit le nouvel objet anonyme File.
  • Cette approche remplace la déclaration d’une variable pour l’objet, p. ex. File inputFile = new File("./data/input.txt"); suivie de Scanner fileReader = new Scanner(inputFile);.

chemin de fichier : séquence de dossiers à partir d’un point de référence, se terminant avec le nom complet du fichier, soit son nom et son extension.

  • Pour un projet Java, le point de référence est le dossier du projet, donc le chemin commence par ./.
  • Par exemple, ./data/input.txt pour un fichier texte input.txt qui se trouve dans un dossier data à la racine du projet.

références de dossier spéciales : On peut inclure certaines références spéciales au début d’un chemin comme :

  • .. pour le dossier parent,
  • . pour le dossier actuel,
  • ~ pour le dossier utilisateur et
  • / pour le dossier racine.

classe de données : une classe qui ne contient que des variables pour stocker des données. On l’utilise pour organiser des données lues d’un fichier texte, notamment en créent un tableau d’objets de cette classe.

Objectifs d’apprentissage

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

  • Reconnaître les ressemblances entre lire et écrire à la console et lire et écrire dans des fichiers texte.
  • Appliquer les techniques de Scanner pour lire des fichiers directement ou pour lire un String.
  • Utiliser les classes File et FileWriter pour manipuler des fichiers texte.

Critères de succès

  • Je peux intégrer la lecture de fichiers texte ou l’écriture de fichiers texte dans mes programmes Java.

Gérer les exceptions

Parce qu’on essaie d’accéder à une ressource - un fichier - qui n’existe peut-être pas (si on l’a mal nommé ou si le chemin est mal formé ou si son dossier n’a pas été crée en premier), on utilise généralement un bloc try-catch avec ressources pour l’action de lire ou écrire dans un fichier. Le format général est :

try (déclarer et initialiser la ressource) {
  // code pour lire ou écrire dans le fichier
  /* retourner une valeur utile et/ou assigner les valeurs lues 
  à des variables déclarées à l'extérieur du bloc try-catch */
} catch (Exception e) {
  // message d'erreur
  // retourner/assigner une valeur par défaut
}

Un avantage majeur du bloc try-catch est que la ressource est automatiquement fermée à la fin du bloc, même si une exception est lancée. Cela évite les fuites de ressources et les erreurs de programmation.

Un autre avantage est qu’en cas de plantage, on a une valeur connue qu’on peut utiliser dans une condition pour éviter que le reste du programme ne plante. Par exemple, si on sait que la valeur d’un String lue revient null en cas d’erreur, on peut tester si la valeur est null comme pré-condition pour continuer le programme.

Il y a deux exemples de code dans la leçon sur les exceptions qui représentent des bons points de départ pour lire et écrire des fichiers :

D’autres exemples d’algorithmes incluant des exceptions sont fournis plus bas.

Connaître la structure du fichier

Pour bien lire un fichier, il faut avoir une idée de sa structure ou, mieux, connaître exactement sa structure.

Cela vous permet de développer un plan pour lire le fichier, notamment :

  • Est-ce que la première ligne est spéciale, avec un chiffre indiquant le nombre de lignes de données, par exemple ? -> Cela vous permet de déclarer un tableau de taille appropriée pour les données et de fixer une limite ferme pour la boucle de lecture.
  • Est-ce que les données sur une ligne sont séparées par des virgules, des espaces, des tabulations, etc. ? -> Cela vous permet de configurer le Scanner correctement avec useDelimiter()
  • Quels types de données se trouvent à chaque position sur une ligne ? -> Cela vous permet de configurer la suite d’instructions next* pour lire les données
  • Est-ce que les nombres décimaux utilisent un point ou une virgule pour le séparateur décimal ? -> Cela vous permet de configurer le Scanner correctement avec useLocale()

Un objet sur mesure pour nos données

Connaissant la structure des données sur chaque ligne d’un fichier, vous pouvez vous casser la tête à savoir comment organiser toute l’information lue afin de pouvoir l’utiliser dans votre programme et pas seulement l’afficher à l’écran.

La solution est de créer une petite classe qui ne contient aucune méthode mais juste des variables pour stocker les données. On appelle cette classe une classe de données.1 2

Par exemple, plus loin on voit un exemple où on veut lire le mois (String), la température moyenne (double) et la précipitation (int) pour chaque mois de l’année. On pourrait créer une classe WeatherData avec les variables month, averageTemp et precipitation pour stocker ces données.

/** Définit notre propre structure de données */
class WeatherData {
  String month;
  double averageTemp;
  int precipitation;
}

void main() {
  // déclare un tableau qui contient des éléments de type WeatherData
  WeatherData[] monthlyWeather = new WeatherData[12]; 
  //... lire les données dans le tableau
}

Sans notre propre type de données on aurait dû utiliser des tableaux séparés de String, double et int pour stocker les données. Cela aurait été plus difficile à lire et à maintenir. Et dans Java, on ne peut retourner qu’une seule valeur d’une méthode, alors on n’aurait pas pu retourner les trois tableaux ensemble. On serait obligé de déclarer ces tableaux globalement (dans la classe).

Dans la boucle de lecture, on peut assigner les valeurs lues aux 3 variables d’instance de chacune des 12 objets de la classe WeatherData et ensuite retourner le tableau pour l’utiliser dans le reste du programme. Par exemple :

import java.io.*;
import java.util.*;

void main() {
  Scanner fileReader = new Scanner(new File("weather_data2022.txt"));
  WeatherData[] monthlyWeather = getWeatherData(fileReader);
  filereader.close();

  // ...code pour utiliser monthlyWeather
}

class WeatherData {
  String month;
  double averageTemp;
  int precipitation;
}

WeatherData[] getWeatherData(Scanner fileReader) {
  WeatherData[] weather = new WeatherData[12]; // créer un tableau de 12 objets
  for (int i = 0; i < 12; i++) {
    weather[i] = new WeatherData(); // initialiser chaque objet
    // initialiser ses variables
    weather[i].month = fileReader.next(); 
    weather[i].averageTemp = fileReader.nextDouble();
    weather[i].precipitation = fileReader.nextInt();
  }
  return weather; // retourner le tableau
}

Une classe comme WeatherData est le début de la programmation orientée-objet, le thème du cours ICS4U.

Lire un fichier texte avec un Scanner

Il y a plusieurs façons de lire un fichier texte avec un Scanner.

  • Pour un fichier simple, on peut créer un seul objet Scanner (pour le fichier) et utiliser une séquence de next* pour lire les données.
  • Pour un fichier qui contient des données structurées qui se répètent, soit un tableau de valeurs comme, p. ex., un fichier CSV, en plus du Scanner pour le fichier, on doit généralement créer une boucle pour lire les données ligne-par-ligne.
  • Pour un fichier de texte non structurée (comme une histoire), on peut extraire le contenu du fichier au complet dans un String et utiliser un Scanner pour lire ce String mot-par-mot ou phrase-par-phrase ou utiliser d’autres méthodes String pour l’enquêter.

Il y a plusieurs autres approches possibles, alors ces trois-ci ne sont que des points de départ. Dans tous les cas, l’algorithme ressemble généralement à ceci :

  1. Déclarer des variables pour stocker les données lues.
  2. Extraire les données dans un bloc try-catch avec ressources (la déclaration du Scanner pour le fichier); définir des valeurs par défaut en cas d’erreur.
  3. Si la valeur lue est la valeur en cas d’erreur (clause de garde) :
    • quitter le programme (ou éviter autrement l’utilisation du code qui dépend des valeurs lues).
  4. Sinon :
    • utiliser les données lues dans le reste du programme.
Lire un fichier simple

Prenons l’exemple d’un fichier qui conserve le meilleur pointage obtenu en jouant votre jeu. Le fichier highscore.txt pourrait ressembler à ceci :

David 99

On sait qu’il contient juste un nom (de type String) et un pointage (de type int). On peut lire ce fichier avec le code suivant :

void main() {
  String name; // variables déclarées à l'extérieur du bloc try-catch
  int score;

  try (Scanner fileReader = new Scanner(new File("./highscore.txt"))) {
    name = fileReader.next(); // valeurs en cas de succès
    score = fileReader.nextInt();
  } catch (Exception e) {
    System.out.println("Erreur de lecture du fichier.");
    name = null; // valeurs en cas d'erreur
    score = 0;
  }

  // précondition ("clause de garde")
  if (name == null) {
    System.out.println("Aucun pointage n'a été enregistré.");
    // possiblement quitter le programme avec return;
  }

  // ...code pour utiliser name et score
}
Lire un fichier structuré

Prenons l’exemple d’un fichier qui conserve les températures moyennes pour chaque mois de l’année, incluant les valeurs maximum et minimum, et la quantité de précipitation en mm. Le fichier weather.txt pourrait ressembler à ceci :

Ottawa
Mois;Tmoy(C);Tmin(C);Tmax(C);Précip(mm)
Janvier;-9,1;-18,2;11,4;114
Février;-11,3;-22,4;10,2;98
Mars;-8,2;-12,3;17,3;140
...
Décembre;-12,3;-16,4;12,3;87

On voit :

  • la première ligne donne juste le nom de la ville
  • la deuxième ligne donne les noms des colonnes et les unités de mesure
  • il y 12 lignes de données par la suite (1 pour chaque mois)
  • les valeurs sont séparées par des points-virgules
  • les nombres décimaux utilisent une virgule comme séparateur décimal

Si on s’intéresse juste aux valeurs des températures moyennes et de la précipation, on peut prévoir les étapes suivantes :

  • configurer le Scanner pour lire les données avec useDelimiter(";") et useLocale(Locale.CANADA_FRENCH)
  • des commandes spéciales pour lire la ville et ignorer la ligne avec les noms de colonne
  • une boucle qui fait 12 itérations pour lire les données mensuelles ligne-par-ligne : un next pour le mois, 1 nextDouble pour la température, 2 next qu’on ignore et un nextInt pour les précipitations

Une implémentation possible utilisant la classe WeatherData décrite plus haut est la suivante :

void main() {
  WeatherData[] weather; // variable déclarée à l'extérieur du bloc try-catch
  String city;

  try (Scanner fileReader = new Scanner(new File("./weather.txt"))) {
    fileReader.useDelimiter(";");
    fileReader.useLocale(Locale.CANADA_FRENCH);
    city = fileReader.nextLine(); // première ligne
    fileReader.nextLine(); // ignorer l'en-tête à la 2e ligne
    weather = new WeatherData[12]; // pour les 12 lignes de données
    for (int i = 0; i < 12; i++) {
      weather[i] = new WeatherData(); // initialiser chaque objet
      weather[i].month = fileReader.next(); // initialiser ses variables
      weather[i].averageTemp = fileReader.nextDouble();
      fileReader.next(); // ignorer la température minimale
      fileReader.next(); // ignorer la température maximale
      weather[i].precipitation = fileReader.nextInt();
    }
  } catch (Exception e) {
    System.out.println("Erreur de lecture du fichier.");
    weather = null; // valeurs en cas d'erreur
  }

  // précondition ("clause de garde")
  if (weather == null) {
    System.out.println("Aucune donnée météo n'a été enregistrée.");
    // possiblement quitter le programme avec return;
  }

  // ...code pour utiliser weather
}
Lire un fichier non structuré

Prenons l’exemple d’un fichier qui contient une histoire. Le fichier story.txt pourrait ressembler à ceci :

Il était une fois, dans une galaxie lointaine, très lointaine...

Ici le but est juste d’extraire le texte au complet afin de l’analyser par la suite. Une technique efficace est d’utiliser le caractère spéciale \\Z qui désigne la fin d’un fichier comme délimiteur du Scanner. Lire le ficher revient alors à un seul appel de next :

void main(){
  String story; // variable déclarée à l'extérieur du bloc try-catch

  try (Scanner fileReader = new Scanner(new File("./story.txt"))) {
    fileReader.useDelimiter("\\Z"); // configurer le Scanner
    story = fileReader.next(); // lire le fichier au complet
  } catch (Exception e) {
    System.out.println("Erreur de lecture du fichier.");
    story = null; // valeurs en cas d'erreur
  }

  // précondition ("clause de garde")
  if (story == null) {
    System.out.println("Aucune histoire n'a été enregistrée.");
    // possiblement quitter le programme avec return;
  }

  // ...code pour utiliser story, 
  // incluant potentiellement d'autres Scanners
}

🛠️ Pratique

Travaillez dans le répertoire GitHub partagé par votre enseignant pour la pratique et les exercices.

Lire un fichier simple

  1. Créez un fichier FileReading1.java et y ajouter sa méthode main.
  2. Créez un fichier texte moodJournal.txt qui contient la ligne suivante : 2024-04-27 heureux. Implémentez un algorithme dans main qui :
    1. Lit le contenu du fichier moodJournal.txt dans un bloc try-catch et affiche son contenu à la console.
    2. Lit le contenu du fichier moodJournal.txt dans un bloc try-catch et assigne les valeurs individuelles à des variables pour la date et l’humeur (deux String). Préparer un message à l’extérieur du bloc try-catch qui affiche la date et l’humeur.
    3. (DÉFI) Lit le contenu du fichier moodJournal.txt dans un bloc try-catch et assigne les valeurs individuelles à des variables pour l’année, le mois, le jour (3 int) et l’humeur (String).

      Trois pistes pour Scanner les parties de la date : (1) modifiez le délimiteur du Scanner (“-” et “ “), (2) utilisez d’abord next pour la date entière et déclare un Scanner sur ce texte avec un délimiteur de “-” pour saisir les int, (3) utiliser split("-") sur la date entière et Integer.parseInt sur chaque élément du tableau.

Lire un fichier structuré

  1. Créer un fichier FileReading2.java et y ajouter sa méthode main.

Écrire dans un fichier

  1. Créer un fichier FileWriting.java et y ajouter sa méthode main.

  1. Java offre aussi une structure de données appelée Record qui est plus spécifique qu’une classe mais exactement pour ce genre de situation, mais qui masque les données, ce qui est un concept plus avancé qu’on voit seulement en 12e année.

  2. Les exemples de code ci-dessous appliquent le JEP 463 (classe abstraite, méthode maind’instance), alors seulement la classe interne pour la structure de données est déclarée dans le code.

© 2022-2025 David Crowley