:: Enseignements :: ESIPE :: E4INFO :: 2025-2026 :: Java Avancé ::
[LOGO]

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.

Les tests JUnit 5 de cet exercice sont FerryTest.java.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

  9. 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.