:: 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 calcul 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 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 prendre un texte au format JSON
qui décrit une liste de voiture et de camion et d'indiquer
qu'elle est 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 poid 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 est pas typé, il n'y a pas d'information
qui dit que le premier objet et une voiture et le second un camion,
en tant qu'humain, on le voit aux noms des champs des objets,
dans le code, il faudra expliquer que si notre objet à le 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 de l'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 poid.
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 librarie externe
jackson
qui est capable de convertir une description d'un objet ou d'un tableau JSON
en respectivement un record ou une
java.util.List.
Pour décoder un texte JSON avec jackson, on utilise un
ObjectReader
que l'on va créer comme cela
ObjectReader reader = new ObjectMapper()
.reader();
Puis pour décoder un texte, un String dans notre cas, on va créer un
JsonParser
avec le texte
JsonParser parser = reader.createParser(jsonText);
puis si on demande de voir le texte comme une liste de tortue,
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: le mieux est de commenter les autres tests, et de les dé-commenter
au fur et à mesure, en effet, les tests des questions suivantes utilisent
surement des classes qui à ce stade n'ont pas encore été écrites,
donc le code risque de ne pas compiler.
-
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 voiture.
Comme l'objet ObjectReader est un peut gros (comprendre lent à initialiser,
car il a plein de champs), que l'on ne le modifie pas après création,
et thread-safe (vous verrez en concurrence ce que cela veut dire),
on va donc le stocker comme une constante.
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 que les tests "Q1" continuent 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 de calculer les prix
pour une liste de voitures que doivent payer les propriétaires.
La méthode computeFare(cars) prend en paramètre une liste
de voitures et renvoie un dictionnaire 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 comme cela, on pourra comparer.
- Ecrire une version utilisant getOrDefault et put
- Ecrire une version utilisant merge
- Ecrire 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.
-
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édiaire !
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 reconnaitre 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 l'on va changer la configuration de jackson comme cela
ObjectReader reader = new ObjectMapper()
.registerModule(new SimpleModule().addDeserializer(Vehicle.class, new CarOrTruckDeserializer()))
.reader();
qui dit grosso modo, 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ée
conserve l'ordre d'insertion. C'est-à-dire, si on a deux voitures, c1 et c2
dans cet ordre dans la liste prise en paramètre, alors si elles n'ont le même propriétaire,
leur propriétaire doit ê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)
à moins (ou égal) 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,
faire des remises quand on a plus de fleetSize voitures/camions,
ou alors que l'on pourrait faire des remises en fonction des enfants.
Donc au lieu de spécifier en dure comment on fait la remise,
on veut pouvoir passer en paramètre de computeFareWithFleetDiscount
une lambda qui indique comment faire la remise.
Voici un exemple qui fait une réduction de 15% si le prix est supérieur à 1000
et il y a plus de deux vehicles 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 appele l'autre.
Bien sûr, comme il s'agit d'un refactoring technique, les tests doivent continuer à passer.
© Université de Marne-la-Vallée