:: Enseignements :: ESIPE :: E4INFO :: 2025-2026 :: Java Avancé ::
![[LOGO]](http://igm.univ-mlv.fr/ens/resources/mlv.png) |
My little Ferry
|
Programmation orientée objet, interface, record, JSON, lambda, stream.
Le but de ce TP est d'implanter une application (enfin, quelques classes)
qui calculent le prix du transport (fare) que doivent payer
des particuliers ou des entreprises qui mettent des voitures (Car)
ou des camions (Truck) sur un Ferry.
Exercice 1 - Maven
Nous allons utiliser Maven avec la configuration, le
pom.xml, suivante
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>fr.uge.ferry</groupId>
<artifactId>ferry</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.20.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.13.4</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.0</version>
<configuration>
<release>23</release>
<compilerArgs>
<compilerArg>--enable-preview</compilerArg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.3</version>
<configuration>
<argLine>--enable-preview</argLine>
</configuration>
</plugin>
</plugins>
</build>
</project>
Créer un projet Maven (pas un projet Java) puis cocher
create simple project (important !)
au niveau du premier écran, puis passer à l'écran suivant en indiquant
Next.
Pour ce TP, le groupId est
fr.uge.ferry , l'artefactId est
ferry et
la version est
0.0.1-SNAPSHOT. Pour finir, cliquer sur
Finish.
Exercice 2 - Let's hope that my little ferry fare well
Le but de notre application est de traiter un texte au format JSON
décrivant une liste de voitures et de camions pour calculer et indiquer
le prix du transport aux propriétaires de voiture et aux entreprises qui possèdent des camions.
Une voiture est définie par trois propriétés,
ownerName qui indique le propriétaire,
passengers qui indique le nombre de passagers,
et children qui indique le nombre d'enfants parmi les passagers.
Un camion est défini par deux propriétés,
companyName qui indique le nom de l'entreprise et
weight qui indique le poids du camion en "kg" (c'est un nombre entier).
Notre texte JSON est un tableau d'objets qui sont soit des voitures,
soit des camions.
On peut noter que le format JSON n'est pas typé, il n'y a pas d'information
qui dit que le premier objet est une voiture et le second un camion.
En tant qu'humain, on le voit aux noms des champs des objets.
Et dans le code, il faudra expliquer que si notre objet a un champ "ownerName"
c'est une voiture, sinon c'est un camion.
[
{
"ownerName": "John",
"passengers": 2,
"children": 1
},
{
"companyName": "World Inc",
"weight": 1000
}
]
Le prix du transport est regroupé par nom de propriétaire pour les voitures
et par nom d'entreprise pour les camions. Le prix pour une voiture
est 100 fois le nombre de passagers. Le prix pour un camion est 2 fois son poids.
Par exemple, pour le texte JSON ci-dessus, calculer le prix (
fare) revient
à renvoyer le dictionnaire suivant
{
"John": 200,
"World Inc": 2000
}
qui vent dire que John doit payer 200 et que l'entreprise World Inc doit payer 2000.
Pour décoder le texte au format JSON, nous allons utiliser la librairie externe
jackson
qui est capable de convertir respectivement une description d'objet ou de tableau JSON
en record ou en
java.util.List.
Pour décoder un texte JSON avec
jackson, on utilise un
ObjectReader
que l'on va créer ainsi
ObjectReader reader = new ObjectMapper()
.reader();
Puis, pour décoder un texte (une
String dans notre cas), on va créer un
JsonParser
avec le code
JsonParser parser = reader.createParser(jsonText);
Ensuite, si on demande à voir le texte comme une liste de tortues,
on utilise la méthode
readValueAs comme ceci :
List<Turtle> turtles = parser.readValueAs(new TypeReference<List<Turtle>>() {});
Note : nous verrons la syntaxe
new Type () {} un peu plus tard en cours.
-
Dans un premier temps, dans le package fr.uge.ferry,
créer une voiture Car avec les bonnes propriétés.
Vérifier que les tests unitaires marqués "Q1" passent.
Note : les tests des questions suivantes utilisent
sûrement des classes ou méthodes qui, à ce stade, n'ont pas encore été écrites,
donc le code risque de ne pas compiler.
Commentez les autres tests, et vous les dé-commenterez
au fur et à mesure.
-
On souhaite maintenant écrire une classe FerryParser ayant une
méthode parse qui prend en paramètre un texte au format JSON
et renvoie une liste de voitures.
L'objet ObjectReader est un peu gros (comprendre lent à initialiser,
car il a plein de champs), il est non modifiable,
on va le stocker dans une constante pour éviter de le recalculer à chaque fois.
Notre classe FerryParser ne devrait pas avoir d'état.
Comment faire pour empêcher que l'on puisse faire un new FerryParser() ?
Comme on écrit du code propre, on va aussi empêcher l'héritage.
Écrire la classe FerryParser ainsi que la méthode parse(jsonText).
Vérifier que les tests "Q2" passent (et les tests "Q1" doivent continuer de passer).
Note : comme vous êtes moins bête qu'un LLM, vous avez vu que JsonParser
implante l'interface AutoCloseable et donc vous avez écrit votre code
en conséquence.
-
On souhaite créer la classe FerryFare qui permet, étant donné une liste de voitures,
de calculer les prix que doivent payer les propriétaires.
La méthode computeFare(cars) prend en paramètre une liste
de voitures et renvoie un dictionnaire non modifiable qui indique pour chaque propriétaire
le prix à payer sachant qu'il est possible que plusieurs voitures
aient le même propriétaire.
Comment s'appelle l'interface des dictionnaires en Java ?
Quelle implantation de dictionnaire allons-nous utiliser ?
Il y a plusieurs façons d'écrire computeFare(cars). On va en écrire
plusieurs, ainsi, on pourra comparer.
- Écrire une version utilisant getOrDefault et put.
- Écrire une version utilisant merge.
- Écrire une version utilisant un Collectors.groupingBy.
Quelle est, selon vous, la meilleure façon d'écrire ?
(Laisser en commentaire les autres façons.)
Pour chaque version, vérifier que les tests marqués "Q3" passent.
Note: pour renvoyer une Map non modifiable, on utilise Collections.unmodifiableMap()
plutôt que Map.copyOf() car ici, nous créons le dictionnaire nous même,
ce n'est pas un dictionnaire qui vient de l'extérieur, donc la copie n'est pas nécessaire.
-
On souhaite maintenant ajouter le support des camions (Truck).
Pour l'instant, on ne va pas s'occuper de la partie JSON, mais uniquement
de la partie de calcul des prix.
Comment doit-on changer la méthode computeFare pour
pouvoir prendre en paramètre une liste contenant des voitures et
des camions ?
Note : en fait, on veut aussi pouvoir prendre en paramètre des
List<Car> ou des List<Truck>, donc des listes de sous-types.
On va le revoir mais "sous-type de" s'écrit en Java, "? extends"
(et oui, c'est un mot-clé avec un espace au milieu, soupir).
Pour le code, vous avez le droit de faire des fonctions (méthodes privées)
intermédiaires !
Vérifier que les tests marqués "Q4" passent.
-
On souhaite maintenant pouvoir décoder des listes de voitures et de camions au format JSON.
Pour cela, il faut modifier la configuration de l'ObjectReader
de jackson pour lui dire comment distinguer un camion d'une voiture.
On va définir la classe CarOrTruckDeserializer.java.
Cette classe regarde si l'objet JSON courant (vue comme un arbre de nœuds) possède un champ "ownerName"
si c'est le cas, l'arbre est désérialisé comme une voiture sinon comme un camion.
Et on va changer la configuration de jackson comme cela
ObjectReader reader = new ObjectMapper()
.registerModule(new SimpleModule().addDeserializer(Vehicle.class, new CarOrTruckDeserializer()))
.reader();
Grosso modo, ça dit que si on cherche à déserializer un Vehicle,
on va utiliser le CarOrTruckDeserializer
Note : on peut aussi spécifier ce genre de chose avec des annotations,
c'est moins flexible et nous vous laissons découvrir cela en projet.
Modifier la classe FerryParser en conséquence.
Vérifier que les tests marqués "Q5" passent.
-
On veut changer FerryFare.computeFare pour que le dictionnaire renvoyé
conserve l'ordre d'insertion. Autrement dit, si on a deux voitures, c1 et c2,
dans cet ordre dans la liste prise en paramètre, alors si elles n'ont pas le même propriétaire,
leurs propriétaires doivent être dans le même ordre dans le dictionnaire renvoyé.
Modifier FerryFare.computeFare en conséquence.
Vérifier que les tests marqués "Q6" passent.
-
On souhaite ajouter une nouvelle méthode computeFareWithFleetDiscount(list, fleetSize)
à la classe FerryFare qui calcule les prix avec une remise de 10%
(on prend le prix et on le multiplie par 90 / 100) si jamais
le propriétaire des voitures (respectivement l'entreprise des camions)
a moins de fleetSize voitures (resp. camions) pour un même ferry.
Implanter la méthode computeFareWithFleetDiscount.
Vérifier que les tests marqués "Q7" passent.
-
Enfin, on se rend compte qu'économiquement, on devrait faire le contraire,
c'est-à-dire faire des remises quand on a plus de fleetSize voitures/camions,
ou alors que l'on pourrait faire des remises en fonction du nombre d'enfants.
Donc au lieu de spécifier "en dur" comment on fait la remise,
on veut pouvoir passer en paramètre de computeFareWithFleetDiscount
une lambda qui indique comment faire la remise (en fonction du tarif d'origine et de la liste des véhicules).
Voici un exemple qui fait une réduction de 15% si le prix est supérieur à 1000
et s'il y a plus de deux véhicules dans la flotte
// if fare > 1000 and fleet size >= 2, apply 15% discount
var fare = FerryFare.computeFareWithFleetDiscount(list,
(fareAmount, fleet) ->
fareAmount > 1000 && fleet.size() >= 2 ? fareAmount * 85 / 100 : fareAmount);
Écrire le code de cette version de la méthode computeFareWithFleetDiscount
pour que l'exemple ci-dessus fonctionne.
Vérifier que les tests marqués "Q7" passent.
Note : il y a plusieurs façons d'implanter computeFareWithFleetDiscount avec
un seul Stream, si vous ne voyez pas, vous pouvez séparer le calcul en deux parties.
-
Enfin, si vous ne l'avez pas déjà fait, factoriser le code pour que l'une des méthodes
computeFareWithFleetDiscount appelle l'autre.
Bien sûr, comme il s'agit d'un refactoring technique, les tests doivent continuer à passer.
© Université de Marne-la-Vallée