Mise en place
ReCHor – étape 1
1. Introduction
Le but de cette première étape est d'écrire des classes et interfaces représentant quelques concepts importants du projet — les arrêts de transport public, les voyages (relations), etc. — ainsi que le code permettant de les représenter sous forme textuelle.
Comme toutes les descriptions d'étapes, celle-ci commence par une introduction aux concepts nécessaires à sa réalisation (§2), suivie d'une présentation de leur mise en œuvre en Java (§3).
Si vous travaillez en groupe, vous êtes toutefois priés de lire, avant de continuer, les guides Travailler en groupe et Synchroniser son travail, qui vous aideront à bien vous organiser.
2. Concepts
2.1. Arrêt
Un arrêt (stop en anglais) est un endroit où un véhicule de transport public peut s'arrêter afin de laisser monter et/ou descendre des passagers. Dans ce projet, nous en distinguerons deux types :
- les gares (stations), qui correspondent soit à ce que l'on entend effectivement par « gare » dans le langage courant, comme la gare de Lausanne, soit à des arrêts assez petits pour que la distinction entre les différentes voies/quais ne soit pas jugée nécessaire,
- les voies ou quais (platforms), qui correspondent aux différentes voies ou quais d'une gare ferroviaire ou routière.
Les arrêts possèdent un nom (p.ex. Lausanne) et une position géographique, donnée par un couple longitude/latitude (p.ex. 6.62909°, 46.51679°). Les voies ou quais possèdent de plus un nom de voie (ou de quai). En Suisse, ce nom est généralement — mais pas toujours — un nombre pour les voies ferroviaires, éventuellement accompagné d'une désignation de secteur (1, 3BC, etc.) et une lettre pour les quais de gare routière (A, B, etc.).
Il n'est pas forcément évident de décider ce qui constitue un arrêt, et des choix qui peuvent sembler surprenants ont parfois été faits en Suisse.
Par exemple, dans les données d'horaires officielles que nous utiliserons, l'arrêt du métro m1 à l'EPFL est représenté par un unique arrêt — une gare —, malgré le fait qu'il y ait deux voies.
À l'inverse, de nombreux arrêts existent pour la gare de Lausanne. Non seulement l'arrêt nommé Lausanne, qui correspond à la gare elle-même, ainsi que des arrêts correspondants aux différentes voies ferroviaires (1, 3 à 8 et 70), mais aussi l'arrêt de bus nommé Lausanne, gare, qui n'est représenté que par un seul arrêt alors que les différents bus s'arrêtent à plusieurs endroits assez distants les uns des autres.
2.2. Ligne
Un véhicule de transport public se déplace toujours en suivant une ligne (route), p. ex. la ligne de métro m1, ou alors la ligne IR 15 des CFF qui relie Genève-Aéroport à Lucerne en desservant une dizaine d'arrêts intermédiaires entre les deux.
Le sens dans lequel un véhicule se déplace sur une ligne est généralement indiqué par sa destination finale — Genève-Aéroport ou Lucerne dans le cas de la ligne IR 15.
2.3. Voyage
Nous nommerons voyage (journey) un déplacement entre deux arrêts de transports publics. Un voyage est ce que nous avons appelé une relation dans l'introduction au projet, terme que nous n'utiliserons plus par la suite.
Un voyage est constitué d'une ou plusieurs étapes (legs), chacune d'entre elles partant d'un arrêt de départ à un instant donné et arrivant à un arrêt d'arrivée à un instant ultérieur. Il existe deux types différents d'étapes :
- celles effectuées à pied, soit entre deux arrêts voisins ou alors au sein d'un même arrêt lors d'un changement,
- celles effectuées en transport public — train, bus, métro, etc.
Les étapes effectuées en transport public peuvent éventuellement comporter des arrêts intermédiaires entre l'arrêt de départ de l'étape et celui d'arrivée. De plus, elles sont toujours effectuées à bord d'un unique véhicule qui suit une ligne donnée, dans une direction donnée.
Un voyage est valide si et seulement si ses étapes satisfont les conditions suivantes :
- l'instant de départ d'une étape ne précède pas l'instant d'arrivée de l'étape précédente, s'il y en a une,
- l'arrêt de départ d'une étape est identique à l'arrêt d'arrivée de l'étape précédente, s'il y en a une,
- les étapes à pied alternent avec les étapes en transport public.
Un exemple de voyage valide pourrait être celui présenté dans l'introduction au projet qui va de l'arrêt Ecublens VD, EPFL (départ à 16h13 le 18 février 2025) à l'arrêt Gruyères (arrivée à 17h57). Il est constitué de neuf étapes, les cinq premières étant :
- de Ecublens VD, EPFL (dép. à 16h13) à Renens VD, gare (arr. à 16h19), avec le métro de la ligne m1 en direction de Renens VD, gare,
- de Renens VD, gare (dép. à 16h19) à Renens VD, voie 4 (arr. à 16h22), à pied (trajet entre deux arrêts voisins),
- de Renens VD, voie 4 (dép. à 16h26) à Lausanne, voie 5 (arr. à 16h33) avec le train de la ligne R 4 en direction de Bex,
- de Lausanne, voie 5 (dép. à 16h33) à Lausanne, voie 1 (arr. à 16h38), à pied (changement au sein de la même gare),
- de Lausanne, voie 1 (dép. à 16h40) à Romont FR, voie 2 (arr. à 17h13) avec le train de la ligne IR 15 en direction de Luzern (Lucerne).
Il va sans dire que les voyages jouent un rôle central dans ce projet, dont le but principal est de calculer l'ensemble des voyages permettant de relier de manière optimale deux arrêts, à une date et une heure donnée.
2.4. Représentation textuelle
Plusieurs des concepts décrits ci-dessus, par exemple les étapes d'un voyage, doivent pouvoir être représentés sous forme de texte afin d'être affichés à l'écran ou alors inclus dans un événement de calendrier. Les sections qui suivent décrivent la manière dont nous représenterons textuellement ces concepts — et d'autres — dans ce projet.
2.4.1. Durée
Une durée — p. ex. d'un voyage ou d'une de ses étapes — est représentée textuellement sous la forme d'un nombre d'heures et de minutes, ou de minutes seulement pour les durées inférieures à une heure. Les secondes ne sont jamais incluses, car toutes les durées que nous manipulons ne sont précises qu'à la minute près.
Ainsi, une durée de douze minutes est représentée textuellement par la chaîne :
12 min
tandis qu'une durée d'une heure et trois minutes l'est par la chaîne :
1 h 3 min
Notez la présence d'espaces entre les nombres et les unités.
Toutes les durées, y compris celles plus longues qu'un jour — qui devraient être rares voire inexistantes dans ce projet — sont représentées de cette manière. Ainsi, une durée de deux jours exactement est représentée textuellement par la chaîne :
48 h 0 min
2.4.2. Heure
Une heure — p. ex. de départ ou d'arrivée — est représentée textuellement en séparant par la lettre h
le nombre d'heures pleines, sur un ou deux chiffres (de 0 à 23), du nombre de minutes dans l'heure, sur deux chiffres (de 00 à 59).
Ainsi, midi et demi est représenté textuellement par la chaîne :
12h30
tandis que 5 minutes après minuit l'est par la chaîne :
0h05
Notez l'absence d'espaces dans cette représentation, la lettre h
jouant ici plutôt un rôle de séparateur que d'unité.
2.4.3. Voie ou quai
Comme nous l'avons vu, la convention veut qu'en Suisse les voies de chemin de fer soient identifiées par des nombres, tandis que les quais des gares routières le soient par des lettres.
Dès lors, une voie/quai dont le nom commence par un chiffre est représentée textuellement par son nom précédé de « voie », par exemple :
voie 1AB
tandis qu'une voie/quai dont le nom ne commence pas par un chiffre est représentée textuellement par son nom précédé de « quai », par exemple :
quai A
2.4.4. Étape à pied
Une étape à pied est représentée par une description de son type (changement ou trajet entre deux arrêts voisins) et sa durée, entre parenthèses.
Ainsi, un changement d'une durée de 5 minutes est représenté par la chaîne :
changement (5 min)
tandis qu'un trajet entre deux arrêts voisins, d'une durée de 3 minutes, l'est par la chaîne :
trajet à pied (3 min)
Bien entendu, la durée entre parenthèses est représentée de la manière décrite à la §2.4.1 plus haut.
2.4.5. Étape en transport public
Une étape en transport public a une représentation textuelle relativement complexe, due au grand nombre d'attributs qu'elle possède. Dans l'ordre, les attributs qui font partie de sa représentation textuelle sont :
- l'heure de départ,
- le nom de la gare de départ,
- le nom de la voie/quai de départ (s'il existe),
- le nom de la gare d'arrivée,
- l'heure d'arrivée,
- le nom de la voie/quai d'arrivée (s'il existe).
Les noms des voies ou quais sont placés entre parenthèses, de même que l'heure d'arrivée précédée de « arr. ». De plus, une flèche (→) sépare les informations concernant le départ de celles concernant l'arrivée.
Par exemple, la troisième étape du voyage donné en exemple à la §2.3 est représentée par la chaîne suivante :
16h26 Renens VD (voie 4) → Lausanne (arr. 16h33 voie 5)
Lorsqu'un des arrêts est une gare et pas une voie (ou un quai), le nom de la voie (ou du quai) n'apparaît bien entendu pas, et si, en conséquence, la première paire de parenthèses devait être vide, elle n'apparaît simplement pas. Ainsi, la première étape du voyage d'exemple est représentée par la chaîne suivante :
16h13 Ecublens VD, EPFL → Renens VD, gare (arr. 16h19)
Il faut noter que cette représentation textuelle des étapes en transport public n'est pas celle qui est utilisée dans l'interface graphique. Dans cette interface, une étape en transport public est représentée de manière partiellement graphique.
2.4.6. Ligne et sens de parcours
Toute étape en transport public se fait à bord d'un véhicule qui parcourt une ligne dans un sens donné. Cette information est représentée textuellement par le nom de la ligne et le nom de la destination finale précédée de « Direction ».
Par exemple, le train parcourant la ligne IR 15 de Genève-Aéroport à Luzern est représenté textuellement par la chaîne :
IR 15 Direction Luzern
3. Mise en œuvre Java
Les concepts importants pour cette étape ayant été introduits, il est temps de décrire leur mise en œuvre en Java. En plus des classes spécifiques à cette étape, une classe « utilitaire » est à réaliser : Preconditions
, qui offre une méthode de validation d'argument.
Toutes les classes et interfaces de ce projet appartiendront au paquetage ch.epfl.rechor
— qui sera désigné par le terme paquetage principal dans les descriptions d'étapes — ou à l'un de ses sous-paquetages.
Attention : jusqu'à l'étape 6 du projet (incluse), vous devez suivre à la lettre les instructions qui vous sont données dans l'énoncé, et vous n'avez pas le droit d'apporter la moindre modification à l'interface publique des classes, interfaces et types énumérés décrits.
En d'autres termes, vous ne pouvez pas ajouter des classes, interfaces ou types énumérés publics à votre projet, ni ajouter des attributs ou méthodes publics aux classes, interfaces et types énumérés décrits dans l'énoncé. Vous pouvez par contre définir des méthodes et attributs privés si cela vous semble judicieux.
Cette restriction sera levée dès l'étape 7.
3.1. Installation de Java et d'IntelliJ
Avant de commencer à programmer, il faut vous assurer que la version 21 de Java est bien installée sur votre ordinateur, car c'est celle qui sera utilisée pour ce projet.
Si vous ne l'avez pas encore installée, rendez-vous sur le site Web du projet Adoptium, téléchargez le programme d'installation correspondant à votre système d'exploitation (macOS, Linux ou Windows), et exécutez-le.
Cela fait, si vous n'avez pas encore installé IntelliJ, téléchargez la version Community Edition — et pas la version Ultimate qui apparaît au sommet de la page — depuis le site de JetBrains et installez-la.
3.2. Importation du squelette
Une fois Java et IntelliJ installés, vous pouvez télécharger le squelette de projet que nous mettons à votre disposition. Il s'agit d'une archive Zip dont vous devrez tout d'abord extraire le contenu à un emplacement de votre choix sur votre ordinateur. (Notez que certains navigateurs comme Safari extraient automatiquement le contenu de telles archives.)
Une fois le contenu de l'archive extrait, vous constaterez qu'il se trouve en totalité dans un dossier nommé ReCHor
. Lancez IntelliJ, choisissez Open, sélectionnez ce dossier, et finalement cliquez Ok.
Le dossier ReCHor
contient les sous-dossiers suivants :
resources
, destiné à contenir les « ressources » utiles au projet, en l'occurrence des icônes représentant les différents moyens de transport,src
, destiné à contenir le code source de votre projet, ainsi que divers fichiers que nous mettrons à votre disposition, et qui contient pour l'instant :SignatureChecks_1.java
, un fichier de vérification de signatures pour l'étape 1, qui ne devrait plus contenir d'erreur lorsque vous aurez terminé la rédaction de cette étape,Submit.java
, un programme qui vous permettra de rendre votre projet à la fin de chaque semaine,
test
, destiné à contenir le code des tests unitaires de votre projet que nous vous fournirons ou que vous écrirez vous-même, et qui contient pour l'instant les tests de l'étape 1 — fournis exceptionnellement pour faciliter votre démarrage.
Le dossier resources
doit être marqué comme « dossier de ressources » pour qu'il soit correctement géré par IntelliJ. Pour cela, faites un clic droit sur lui puis sélectionnez Mark Directory As puis Resources Root. Vérifiez ensuite que le panneau Project d'IntelliJ ressemble à l'image ci-dessous.

3.3. Classe Preconditions
Fréquemment, les méthodes d'un programme exigent que leurs arguments satisfassent certaines conditions. Par exemple, une méthode déterminant la valeur maximale d'un tableau d'entiers exige que ce tableau contienne au moins un élément.
De telles conditions sont souvent appelées préconditions (preconditions) car elles doivent être satisfaites avant l'appel d'une méthode : c'est à l'appelant de s'assurer qu'il n'appelle la méthode qu'avec des arguments valides.
En Java, la convention veut que chaque méthode vérifie, autant que possible, ses préconditions et lève une exception — souvent IllegalArgumentException
— si l'une d'entre elles n'est pas satisfaite. Par exemple, une méthode max
calculant la valeur maximale d'un tableau d'entiers, et exigeant logiquement que celui-ci contienne au moins un élément, pourrait commencer ainsi :
int max(int[] array) { if (! (array.length > 0)) throw new IllegalArgumentException(); // … reste du code }
(Notez au passage que la méthode max
ne déclare pas qu'elle lève potentiellement IllegalArgumentException
au moyen d'une clause throws
, car cette exception est de type unchecked.)
La première classe à réaliser dans le cadre de ce projet a pour but de faciliter l'écriture de telles préconditions. En l'utilisant, la méthode ci-dessus pourrait être simplifiée ainsi :
int max(int[] array) { Preconditions.checkArgument(array.length > 0); // … reste du code }
Cette classe est nommée Preconditions
et appartient au paquetage principal. Elle est publique et finale et n'offre rien d'autre que la méthode checkArgument
décrite plus bas. Elle a toutefois la particularité d'avoir un constructeur par défaut privé :
public final class Preconditions { private Preconditions() {} // … méthode(s) }
Le but de ce constructeur privé est de rendre impossible la création d'instances de la classe, puisque cela n'a clairement aucun sens — elle ne sert que de conteneur à une méthode statique. Dans la suite du projet, nous définirons plusieurs autres classes du même type, que nous appellerons dès maintenant classes non instanciables.
La méthode publique (et statique) offerte par la classe Preconditions
est :
void checkArgument(boolean shouldBeTrue)
- qui lève l'exception
IllegalArgumentException
si son argument est faux, et ne fait rien sinon.
3.4. Type énuméré Vehicle
Le type énuméré Vehicle
du sous-paquetage journey
— donc du paquetage ch.epfl.rechor.journey
— représente les différents types de véhicules de transport public en Suisse. Il comporte les sept valeurs suivantes, qui sont données ici dans l'ordre dans lequel elles doivent apparaître dans le type énuméré :
TRAM
- qui représente un tram,
METRO
- qui représente un métro,
TRAIN
- qui représente un train,
BUS
- qui représente un bus ou un car,
FERRY
- qui représente un bac ou un autre type de bateau,
AERIAL_LIFT
- qui représente une télécabine ou un autre type de transport aérien à câble,
FUNICULAR
- qui représente un funiculaire.
En plus de ces valeurs, le type énuméré Vehicle
offre l'attribut public, statique et final suivant :
List<Vehicle> ALL
- une liste immuable contenant la totalité des valeurs du type énuméré, dans leur ordre de définition (voir les conseils de programmation ci-dessous).
3.4.1. Conseils de programmation
La définition de l'attribut ALL
peut vous poser quelques problèmes, car le concept de liste, représenté en Java par l'interface List
, n'a pas encore été vu au cours. Toutefois, vous avez vu au premier semestre le concept de tableau dynamique (ArrayList
), qui est une sorte de liste. Pour l'instant, vous pouvez donc admettre que le type List
est synonyme du type ArrayList
, et que les méthodes offertes par une valeur de type ArrayList
le sont aussi par une valeur de type List
.
Sachant cela, il n'est pas très difficile de définir l'attribut ALL
en s'aidant des deux méthodes statiques suivantes :
- la méthode
values
définie pour tous les types énumérés et retournant un tableau contenant les éléments du type énuméré dans leur ordre de définition, - la méthode
of
de l'interfaceList
, qui retourne une liste immuable ayant les mêmes éléments que le tableau qu'on lui passe en argument.
L'extrait de programme ci-dessous, dont vous pouvez vous inspirer, montre comment la méthode of
de List
peut être utilisée pour obtenir une liste immuable (seasons
) des noms de saisons stockés dans un tableau (seasonsArray
) :
String[] seasonsArray = new String[] { "printemps", "été", "automne", "hiver" }; List<String> seasons = List.of(seasonsArray);
3.5. Enregistrements Java
Avant de décrire Stop
, la prochaine classe à mettre en œuvre pour cette étape, il convient de décrire le concept de classe enregistrement (record class), ou simplement enregistrement (record), introduit dans la version 17 de Java.
Un enregistrement est un type particulier de classe qui peut se définir au moyen d'une syntaxe plus concise qu'une classe normale. Dans cette syntaxe, le mot-clef class
est remplacé par record
et les attributs de la classe sont donnés entre parenthèses, après son nom.
Par exemple, un enregistrement nommé Complex
représentant un nombre complexe dont les attributs sont sa partie réelle re
et sa partie imaginaire im
— tous deux de type double
— peut se définir ainsi :
public record Complex(double re, double im) { }
Cette définition est équivalente à celle d'une classe finale (!) dotée de :
- deux attributs privés et finaux nommés
re
etim
de typedouble
, - un constructeur prenant en argument la valeur de ces attributs et les initialisant,
- des méthodes d'accès (getters) publics nommés
re()
etim()
pour ces attributs, - une méthode
equals
retournant vrai si et seulement si l'objet qu'on lui passe est aussi une instance deComplex
et que ses attributs sont égaux à ceux dethis
, - une méthode
hashCode
compatible avec la méthodeequals
— le but de cette méthode et la signification de sa compatibilité avecequals
seront examinés ultérieurement dans le cours, - une méthode
toString
retournant une chaîne composée du nom de la classe et du nom et de la valeur des attributs de l'instance, p. ex.Complex[re=1.0, im=2.0]
.
En d'autres termes, la définition plus haut, qui tient sur une ligne, est équivalente à la définition suivante, qui n'utilise que des concepts que vous connaissez déjà :
public final class Complex { private final double re; private final double im; public Complex(double re, double im) { this.re = re; this.im = im; } public double re() { return re; } public double im() { return im; } @Override public boolean equals(Object that) { // … vrai ssi that : // 1. est aussi une instance de Complex, et // 2. ses attributs re et im sont identiques. } @Override public int hashCode() { // … code omis car peu important } @Override public String toString() { return "Complex[re=" + re + ", im=" + im + "]"; } }
Comme cet exemple l'illustre, les enregistrements permettent d'éviter d'écrire beaucoup de code répétitif, ce que les anglophones appellent du boilerplate code. Il faut toutefois bien comprendre qu'en dehors d'une syntaxe très concise, les enregistrements n'apportent — pour l'instant en tout cas — rien de nouveau à Java, dans le sens où il est toujours possible de récrire un enregistrement en une classe Java équivalente, comme ci-dessus. En cela, les enregistrements sont similaires aux types énumérés.
Il est bien entendu possible de définir des méthodes dans un enregistrement, qui viennent s'ajouter à celles définies automatiquement. Par exemple, pour doter l'enregistrement Complex
d'une méthode modulus
retournant son module, il suffit de l'ajouter entre les accolades, ainsi :
public record Complex(double re, double im) { public double modulus() { return Math.hypot(re, im); } }
(La méthode Math.hypot(x,y)
retourne \(\sqrt{x^2 + y^2}\)).
Finalement, il est aussi possible de définir ce que l'on nomme un constructeur compact (compact constructor), qui augmente le constructeur que Java ajoute par défaut aux enregistrements. Un constructeur compact doit son nom au fait qu'il semble ne prendre aucun argument, et n'initialise pas explicitement les attributs. En réalité, il prend des arguments qui sont les mêmes que ceux de l'enregistrement (re
et im
dans notre exemple), et Java lui ajoute automatiquement des affectations de ces arguments aux attributs correspondants.
Par exemple, on pourrait vouloir ajouter un constructeur compact à la classe Complex
pour lever une exception si l'un des arguments passés au constructeur était une valeur NaN
(not a number, une valeur invalide). On pourrait le faire ainsi :
public record Complex(double re, double im) { public Complex { // constructeur compact if (Double.isNaN(re) || Double.isNaN(im)) throw new IllegalArgumentException(); } // … méthode modulus }
Ce constructeur compact serait automatiquement traduit en :
public final class Complex { // … attributs re et im public Complex(double re, double im) { if (Double.isNaN(re) || Double.isNaN(im)) throw new IllegalArgumentException(); this.re = re; // ajouté automatiquement this.im = im; // ajouté automatiquement } // … méthodes modulus, re, im, hashCode, etc. }
Les enregistrements ne seront pas décrits en détail dans le cours, mais seront introduits au moyen d'exemples similaires à ceux ci-dessus dans la suite du projet. Les personnes intéressées par les détails de leur fonctionnement pourront se rapporter à la §8.10 (Record Classes) de la spécification du langage.
3.6. Enregistrement Stop
L'enregistrement Stop
du sous-paquetage journey
, public, représente un
arrêt de transport public. Cet enregistrement est utilisé à la fois pour les gares et pour les voies ou quais. Il possède les attributs suivants :
String name
- le nom de l'arrêt, ou le nom de la gare à laquelle appartient l'arrêt s'il représente une voie ou un quai,
String platformName
- le nom de la voie ou du quai de l'arrêt, ou
null
si l'arrêt correspond à une gare, double longitude
- la longitude de la position de l'arrêt, en degrés,
double latitude
- la latitude de la position de l'arrêt, en degrés.
Par exemple, l'arrêt correspondant à la gare de Lausanne pourrait être construit ainsi :
Stop l = new Stop("Lausanne", null, 6.62909, 46.51679);
tandis que celui correspondant à la voie 70 de cette même gare pourrait l'être ainsi :
Stop l70 = new Stop("Lausanne", "70", 6.62909, 46.51679);
Stop
possède un constructeur compact chargé de valider les arguments et qui :
- lève une
NullPointerException
siname
estnull
, ou - lève une
IllegalArgumentException
silongitude
n'est pas compris entre ±180° (inclus) ou silatitude
n'est pas compris entre ±90° (inclus).
Stop
ne possède aucune autre méthode que celles ajoutées automatiquement aux enregistrements par Java.
3.6.1. Conseils de programmation
Dans le constructeur compact, utilisez la méthode requireNonNull
de Objects
pour lever une NullPointerException
lorsque name
est null
.
Pour valider la longitude et la latitude, utilisez bien entendu la méthode checkArgument
de Preconditions
.
3.7. Enregistrement Journey
L'enregistrement Journey
, du sous-paquetage journey
, public et immuable, représente un voyage. Il possède un unique attribut :
List<Leg> legs
- les étapes du voyage.
Le type Leg
est celui d'une interface imbriquée à l'intérieur de l'enregistrement Journey
et décrite à la §3.8. Comme expliqué dans cette section et celles qui suivent, trois enregistrements (IntermediateStop
, Transport
et Foot
) sont imbriqués dans Leg
. Cela signifie que Journey
a la structure suivante :
public record Journey(/* … attributs */) { public interface Leg { public record IntermediateStop(/* … attributs */) { /* … constructeur et/ou méthodes */ } /* … autres enregistrements imbriqués, méthodes */ } /* … constructeur et/ou méthodes */ }
En raison de cette imbrication, la notation pointée doit être utilisée depuis l'extérieur pour accéder à l'interface Leg
et aux enregistrements IntermediateStop
, Transport
et Foot
. Par exemple, pour créer une instance de l'enregistrement IntermediateStop
, on écrira :
new Journey.Leg.IntermediateStop(/* … arguments */);
Journey
possède un constructeur compact qui valide la liste des étapes reçue en vérifiant que :
- elle n'est pas vide,
- les étapes à pied alternent avec celles en transport,
- pour toutes les étapes sauf la première, l'instant de départ ne précède pas celui d'arrivée de la précédente,
- pour toutes les étapes sauf la première, l'arrêt de départ est identique à l'arrêt d'arrivée de la précédente.
Si l'une de ces conditions n'est pas satisfaite, le constructeur compact lève une IllegalArgumentException
. Sinon, dans le but de garantir l'immuabilité de la classe, il copie la liste des étapes en utilisant la technique décrite dans les conseils de programmation plus bas.
Journey
offre les méthodes publiques suivantes :
Stop depStop()
- qui retourne l'arrêt de départ du voyage, c.-à-d. celui de sa première étape,
Stop arrStop()
- qui retourne l'arrêt d'arrivée du voyage, c.-à-d. celui de sa dernière étape,
LocalDateTime depTime()
- qui retourne la date/heure de début du voyage, c.-à-d. celle de sa première étape,
LocalDateTime arrTime()
- qui retourne la date/heure de fin du voyage, c.-à-d. celle de sa dernière étape,
Duration duration()
- qui retourne la durée totale du voyage, c.-à-d. celle séparant la date/heure de fin de celle de début.
La classe LocalDateTime
, qui représente un couple date/heure sans indication de fuseau horaire, fait partie de la bibliothèque Java. Cette classe jouera un rôle très important dans ce projet, et il est donc conseillé de prendre un moment pour lire sa documentation.
3.7.1. Conseils de programmation
- Copie des étapes à la construction
Pour que la classe
Journey
soit immuable, son constructeur compact doit copier la liste des étapes reçue en argument (legs
) dans une liste immuable.Cette copie garantit d'une part que, même si le contenu de la liste passée au constructeur change après la création d'une instance, ces changements n'affecteront pas l'instance ; et d'autre part que la liste stockée dans l'enregistrement ne peut pas être modifiée.
La copie peut se faire au moyen de la méthode
copyOf
deList
, dont le résultat doit être stocké à nouveau dans l'argumentlegs
afin d'y remplacer la liste initiale, dont le contenu est susceptible de changer :legs = List.copyOf(legs);
- Accès à la première et dernière étape
3.8. Interface Journey.Leg
L'interface Leg
, publique et imbriquée dans l'enregistrement Journey
, représente une étape d'un voyage. Cette interface est implémentée par deux enregistrements, Transport
(décrit à la §3.10) et Foot
(décrit à la §3.11), qui correspondent aux deux types d'étapes — en transport et à pied.
L'interface Leg
offre les méthodes suivantes, toutes abstraites sauf la dernière :
Stop depStop()
- qui retourne l'arrêt de départ de l'étape,
LocalDateTime depTime()
- qui retourne la date/heure de départ de l'étape,
Stop arrStop()
- qui retourne l'arrêt d'arrivée de l'étape,
LocalDateTime arrTime()
- qui retourne la date/heure d'arrivée de l'étape,
List<IntermediateStop> intermediateStops()
- qui retourne la liste des arrêts intermédiaires de l'étape, le type
IntermediateStop
étant décrit à la §3.9, default Duration duration()
- qui retourne la durée de l'étape.
La classe Duration
, qui représente une durée, est une classe de la bibliothèque Java qui provient du même paquetage que LocalDateTime
(java.time
). La lecture de sa documentation est aussi fortement recommandée.
3.8.1. Conseils de programmation
La durée d'une étape est bien entendu celle qui sépare sa date/heure d'arrivée de sa date/heure de départ. La méthode statique between
de Duration
est utile pour la déterminer.
3.9. Enregistrement Journey.Leg.IntermediateStop
L'enregistrement IntermediateStop
, public et imbriqué dans l'interface Leg
, représente un arrêt intermédiaire d'une étape. Il s'agit d'un arrêt auquel le moyen de transport utilisé s'arrête effectivement, mais qui se trouve entre l'arrêt de départ et l'arrêt d'arrivée de l'étape. Cet enregistrement possède les attributs suivants :
Stop stop
- l'arrêt intermédiaire en question,
LocalDateTime arrTime
- la date/heure d'arrivée à l'arrêt,
LocalDateTime depTime
- la date/heure de départ de l'arrêt.
IntermediateStop
possède un constructeur compact qui valide les arguments en vérifiant que :
stop
n'est pasnull
(avecrequireNonNull
),- la date/heure de départ n'est pas antérieure à celle d'arrivée (attention, les deux peuvent être égales !).
Notez que, comme il s'agit d'un arrêt intermédiaire, le véhicule y arrive avant d'en repartir, raison pour laquelle la date/heure d'arrivée doit précéder (ou être identique à) celle de départ.
Comme d'habitude, une NullPointerException
est levée si la première vérification échoue, et une IllegalArgumentException
si la seconde échoue.
3.9.1. Conseils de programmation
La méthode isBefore
de LocalDateTime
est utile pour valider les dates/heures de départ et d'arrivée.
3.10. Enregistrement Journey.Leg.Transport
L'enregistrement Transport
, public et imbriqué dans l'interface Leg
, représente une étape effectuée en transport public. Il implémente donc Leg
et possède les attributs suivants :
Stop depStop
- l'arrêt de départ de l'étape,
LocalDateTime depTime
- la date/heure de départ de l'étape,
Stop arrStop
- l'arrêt d'arrivée de l'étape,
LocalDateTime arrTime
- la date/heure d'arrivée de l'étape,
List<IntermediateStop> intermediateStops
- les éventuels arrêts intermédiaires de l'étape,
Vehicle vehicle
- le type de véhicule utilisé pour cette étape,
String route
- le nom de la ligne sur laquelle circule le véhicule utilisé pour cette étape,
String destination
- le nom de la destination finale du véhicule utilisé pour cette étape.
Le constructeur compact de Transport
valide les arguments en vérifiant que :
- aucun d'entre eux n'est
null
, - la date/heure d'arrivée n'est pas antérieure à celle de départ.
De plus, il copie la liste des arrêts intermédiaires afin de garantir l'immuabilité de la classe.
Transport
ne possède aucune autre méthode que celles ajoutées automatiquement aux enregistrements par Java. Notez que ces méthodes ajoutées par Java sont celles qui implémentent les méthodes abstraites de l'interface Leg
. Par exemple, la méthode d'accès pour l'attribut depStop
, ajoutée automatiquement, implémente la méthode depStop()
de Leg
.
3.10.1. Conseils de programmation
Notez bien que des appels à requireNonNull
ne sont nécessaires dans le constructeur que pour les arguments qui ne sont pas manipulés d'une manière ou d'une autre avant d'être stockés dans l'enregistrement. Par exemple, il n'est pas nécessaire d'utiliser requireNonNull
pour intermediateStops
puisque cet argument est passé à copyOf
, qui lèvera elle-même une exception si cet argument est null
.
De manière générale, requireNonNull
n'est vraiment utile que dans les constructeurs, et uniquement lorsqu'on désire s'assurer qu'un argument qu'on se contente de stocker dans un attribut de la classe n'est pas null
.
3.11. Enregistrement Journey.Leg.Foot
L'enregistrement Foot
, public et imbriqué dans l'interface Leg
, représente une étape effectuée à pied. Il implémente donc Leg
et possède les attributs suivants :
Stop depStop
- l'arrêt de départ de l'étape,
LocalDateTime depTime
- la date/heure de départ de l'étape,
Stop arrStop
- l'arrêt d'arrivée de l'étape,
LocalDateTime arrTime
- la date/heure d'arrivée de l'étape.
Le constructeur compact de Foot
fait les mêmes vérifications que celui de Transport
. En plus de ce constructeur compact, Foot
possède deux méthodes publiques :
List<IntermediateStop> intermediateStops()
- qui implémente la méthode abstraite correspondante de l'interface
Leg
et retourne une liste vide, car une étape à pied ne comporte jamais d'arrêts intermédiaires, boolean isTransfer()
- qui retourne vrai ssi l'étape est un changement au sein de la même gare, c.-à-d. si le nom (!) de l'arrêt de départ est identique à celui de l'arrêt d'arrivée.
Soyez attentifs au fait qu'une étape à pied entre deux arrêts différents peut être considérée comme un changement si ces arrêts ont le même nom. Par exemple, une étape entre la voie 70 et la voie 1 de la gare de Lausanne est un changement, même si les arrêts sont différents. Seul le fait que le nom de (la gare de) ces deux arrêts soit identique compte.
3.12. Interfaces scellées en Java
En Java, une interface peut normalement être implémentée par n'importe quelle classe. Par exemple, l'interface Leg
déclarée ci-dessus pourrait très bien être implémentée par d'autres classes que celles déclarées à l'intérieur de Leg
(Transport
et Foot
).
Dans ce cas particulier, cela n'est toutefois pas souhaitable, car nous savons que les seuls types d'étapes qui existent dans le projet sont ces deux-là. Il serait donc bien de pouvoir communiquer cela à Java, afin d'interdire la définition d'autres classes implémentant l'interface Leg
.
Cela peut se faire en scellant (seal) l'interface Leg
, simplement en ajoutant le mot-clef sealed
à l'interface, ainsi :
public sealed interface Leg { … }
Lorsqu'une interface est ainsi scellée, les seules classes qui ont le droit de l'implémenter sont celles se trouvant dans la même « unité de compilation », c.-à-d. le même fichier Java.
3.13. Classe FormatterFr
La classe FormatterFr
du paquetage principal, publique, finale et non instanciable, contient des méthodes statiques permettant d'obtenir la représentation textuelle de différents types de données, au format décrit à la §2.4. Il s'agit de :
String formatDuration(Duration duration)
- qui retourne la représentation textuelle, décrite à la §2.4.1, de la durée donnée,
String formatTime(LocalDateTime dateTime)
- qui retourne la représentation textuelle, décrite à la §2.4.2, de l'heure de la date/heure donnée,
String formatPlatformName(Stop stop)
- qui retourne la représentation textuelle, décrite à la §2.4.3, de la voie ou quai de l'arrêt donné — ou une chaîne vide si l'arrêt en question est une gare, ou si son nom de voie/quai est vide,
String formatLeg(Journey.Leg.Foot footLeg)
- qui retourne la représentation textuelle, décrite à la §2.4.4, de l'étape à pied donnée,
String formatLeg(Journey.Leg.Transport leg)
- qui retourne la représentation textuelle, décrite à la §2.4.5, de l'étape en transport donnée,
String formatRouteDestination(Journey.Leg.Transport transportLeg)
- qui retourne la représentation textuelle, décrite à la §2.4.6, de la ligne et du sens de parcours du véhicule emprunté pour effectuer l'étape donnée.
3.13.1. Conseils de programmation
- Méthode
formatTime
Pour formater une date/heure dans
formatTime
, il faut utiliser une instance deDateTimeFormatter
configurée correctement. La manière la plus simple et la plus propre d'en obtenir une consiste à utiliser le bâtisseur prévu pour cela, à savoir la classeDateTimeFormatterBuilder
. Par exemple, pour formater une date selon la convention utilisée en suisse (jour/mois/année), on pourrait créer un formateur ainsi :DateTimeFormatter fmt = new DateTimeFormatterBuilder() .appendValue(ChronoField.DAY_OF_MONTH) .appendLiteral('/') .appendValue(ChronoField.MONTH_OF_YEAR) .appendLiteral('/') .appendValue(ChronoField.YEAR) .toFormatter();
qu'on pourrait ensuite utiliser ainsi :
fmt.format(LocalDate.of(2025, Month.FEBRUARY, 18));
pour obtenir la chaîne
18/2/2025
. - Méthode
formatLeg
La manière la plus simple de construire la représentation textuelle d'une étape effectuée en transport consiste à le faire petit à petit, en y ajoutant progressivement les parties requises — nom de l'arrêt de départ, voie ou quai de ce même arrêt, etc. La classe
StringBuilder
de la bibliothèque Java est idéale pour construire de manière progressive une chaîne de caractères, et est plus efficace que l'opérateur de concaténation (+
).
3.14. Tests
Pour vous aider à démarrer ce projet, des tests unitaires JUnit vous sont exceptionnellement fournis pour cette étape, et se trouvent dans le dossier test
du squelette. Une fois que vous aurez terminé la rédaction des classes de cette étape, vous pourrez les exécuter en :
- ajoutant JUnit à votre projet, comme expliqué dans notre guide à ce sujet,
- effectuant un clic droit sur le dossier
test
du projet et sélectionnant Run 'All Tests'.
3.15. Documentation
Une fois les tests exécutés avec succès, il vous reste à documenter la totalité des entités publiques (classes, attributs et méthodes) définies dans cette étape, au moyen de commentaires Javadoc, comme décrit dans le guide consacré à ce sujet. Vous pouvez écrire ces commentaires en français ou en anglais, en fonction de votre préférence, mais vous ne devez utiliser qu'une seule langue pour tout le projet.
4. Résumé
Pour cette étape, vous devez :
- installer la version 21 (et ni une plus récente, ni une plus ancienne !) de Java sur votre ordinateur, ainsi que la dernière version d'IntelliJ IDEA Community Edition (attention : n'installez pas la version Ultimate) ,
- écrire les classes et interfaces
Preconditions
,Vehicle
,Stop
,Journey
,Journey.Leg
,Journey.Leg.IntermediateStop
,Journey.Leg.Transport
,Journey.Leg.Foot
etFormatterFr
selon les indications ci-dessus, - vérifier que les tests que nous vous fournissons s'exécutent sans erreur, et dans le cas contraire, corriger votre code,
- documenter la totalité des entités publiques que vous avez définies,
- (optionnel mais fortement recommandé) rendre votre code au plus tard le 21 février 2025 à 18h00, en exécutant le programme
Submit
fourni dans le squelette, après avoir modifié les définitions des variablesTOKEN_1
etTOKEN_2
pour qu'elles contiennent les jetons individuels des deux membres du groupe, disponibles sur la page privée de chacun d'eux. (Les personnes travaillant seules doivent mettre leur jeton individuel dans les deux variables.)
Ce premier rendu n'est pas noté, mais celui de la prochaine étape le sera. Dès lors, il est vivement conseillé de faire un rendu de test cette semaine afin de se familiariser avec la procédure à suivre.