:: Enseignements :: Master :: M1 :: 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 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.

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

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

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

  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édiaires !
    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 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.

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

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

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

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