:: Enseignements :: ESIPE :: E4INFO :: 2025-2026 :: Java Avancé ::
![[LOGO]](http://igm.univ-mlv.fr/ens/resources/mlv.png) |
Sed, the stream editor
|
Programmation Orientée Donnée (DOP), Programmation Orientée Objet (OOP) vs Lambda et entrées/sorties.
Le but de ce TP est d'implanter une petite partie des commandes d'un outil comme sed.
Pour les machines des salles de TP, si
java --version n'affiche pas la version 25,
vous pouvez utiliser les versions que Rémi a installées sur son compte enseignant.
-
Java 25 est là /home/ens/edu-forax/java/jdk-25
Pour exécuter la commande java : /home/ens/edu-forax/java/jdk-25/bin/java
-
Pour démarrer Eclipse: /home/ens/edu-forax/java/eclipse-light/eclipse
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.sed</groupId>
<artifactId>sed</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<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>25</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.3</version>
</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.sed , l'artefactId est
sed et
la version est
0.0.1-SNAPSHOT. Pour finir, cliquer sur
Finish.
Exercice 2 - Astra inclinant, sed non obligant
Le but de cet exercice est de créer un petit éditeur comme
sed.
Pour ceux qui ne connaîtraient pas
sed, c'est un utilitaire en ligne de commande qui prend en entrée
un fichier et génère en sortie un nouveau fichier en effectuant des transformations ligne à ligne.
L'utilitaire
sed traite le fichier ligne à ligne, il ne stocke pas tout le fichier en
mémoire. Ce n'était pas une solution viable à la création de sed en 1974, et ce n'est toujours pas
une solution viable maintenant que l'on peut avoir des fichiers de plusieurs centaines de giga-octets.
On parle de traitement en flux, en stream en anglais, d'où le nom de Stream EDitor, sed.
L'application prend en premier paramètre une chaîne de caractères qui contient les différentes
commandes à exécuter. Les commandes peuvent être composées en concaténant les lettres.
Par exemple "rl" remplace les tabs par des espaces (r) puis met la chaîne de caractères en minuscules (l).
Voilà le
main de l'application que nous voulons implanter.
static void main(String[] args) {
if (args.length!= 3) {
System.err.println(
"""
Usage: <commands> <input.txt> <output.txt>
commands:
l lower case
r replace each tab by a space
*9 replace one star by 9 stars
*4l replace one star by 4 stars and to lower case
""");
System.exit(1);
return;
}
var transformer = parseTransformer(args[0]);
var inputPath = Path.of(args[1]);
var outputPath = Path.of(args[2]);
try {
rewrite(inputPath, outputPath, transformer);
} catch (IOException e) {
System.err.println("error " + e.getMessage());
System.exit(1);
}
}
La méthode
parseTransformer créer un objet
Transformer
(pas ce genre de
Transformer là...)
qui sert à représenter la transformation décrite par la commande, qui est elle-même spécifiée sous forme de chaîne de caractères.
Par exemple, on pourra créer un
LowerCaseTransformer pour la commande "l".
La méthode
rewrite parcourt chaque ligne du fichier
inputPath,
transforme celle-ci en utilisant le
transformer et écrit la ligne résultante
dans le fichier
outputPath.
-
Dans un premier temps, on va se limiter à des chaînes de caractères contenant
une seule commande ; on verra comment généraliser plus tard.
On a besoin d'écrire deux méthodes :
-
La méthode parseTransformer(command) prend en paramètre une commande
(* suivie d'un chiffre, l ou r) et créée un objet Transformer
correspondant, qui permettra d'appliquer sur une ligne de fichier la transformation spécifiée par la commande
.
-
La méthode rewrite(bufferedReader, writer, transformer) prend chaque ligne
du reader (en fait, on utilise un BufferedReader plutôt qu'un
Reader, car BufferedReader a une méthode
BufferedReader.readLine()
bien pratique) et écrit sa version transformée dans le writer.
On s'occupera plus tard d'écrire la méthode rewrite qui prend des Path en paramètre.
Note : attention, readLine() supprime le caractère '\n' de fin de ligne,
donc quand on écrit dans le Writer, il faut l'ajouter !
Écrire une classe StreamEditor.
Juste par ce que c'est pratique pour le TP, nous déclarerons tous le code (les méthodes, mais aussi les classes)
dans la classe StreamEditor (oui, en Java, on peut mettre des classes dans les classes,
on verra les détails dans un prochain cours).
Écrire la méthode publique parseTransformer(command).
Puis écrire la méthode publique rewrite(bufferedReader, writer, transformer).
Ici, vous devez utiliser le pattern matching pour associer à un Transformer la transformation à effectuer.
Vérifier que les tests marqués "Q1" passent.
Note : il existe deux méthodes replace() sur la classe String,
String.replace(char, char) et
String.replace(String, String).
-
On veut que notre programme fonctionne de la même façon, quelle que soit la machine sur laquelle il tourne,
hors, il y a de grandes chances que votre code mette en minuscules
en fonction de la Locale courante.
Lisez la doc de
String.toLowerCase()
et corrigez votre code.
Vérifier que les tests marqués "Q2" passent.
-
En fait, on veut écrire 3 versions du même code (pour comparer), vous venez d'écrire la version
utilisant le pattern matching, on va maintenant écrire les versions utilisant le polymorphisme
puis dans la question suivante, une version utilisant les lambdas.
Rappeler ce qu'est le polymorphisme.
Comment le mettre en place ?
Commenter le code précédent, et écrire une nouvelle version
utilisant le polymorphisme.
Vérifier que les tests marqués "Q3" passent.
-
On souhaite maintenant écrire une version avec des lambdas.
En effet, une lambda est une instance qui implante une interface,
donc il est possible d'implanter les Transformer avec
des lambdas.
Vérifier que les tests marqués "Q4" passent.
-
Selon vous, dans quel cas doit-on utiliser chaque technique ?
-
On souhaite maintenant implanter le support de plusieurs commandes
(par exemple "2*l2*"), il faut donc falloir modifier le fonctionnement de la méthode parseTransformer.
Si on réfléchit un petit peu, il faut
- découper la chaîne de caractères en commandes, sachant que les commandes font 1 ou 2 lettres
- pour chaque commande, créer le Transformer correspondant, donc on obtient
une nouvelle liste de commandes
- transformer une liste de commandes en une seule commande, pour éviter
de changer la méthode rewrite
Nous allons implanter ces étapes dans les questions suivantes.
Premièrement, pour découper une chaîne de caractères en commandes, on va transformer
la chaîne de caractères contenant les commandes en itérateur de caractères, ainsi,
il sera facile de lire un ou plusieurs caractères.
Implanter la méthode characterIterator(commands) qui prend une chaîne de
caractères en paramètre et renvoie un itérateur des caractères.
Vérifier que les tests marqués "Q6" passent.
Note : si vous ne voyez pas comment faire, vous pouvez dans un premier temps créer
une liste puis demander son itérateur.
Note 2 : quand vous serez un peu plus grands, on verra comment écrire les itérateurs
directement.
-
Maintenant, on va écrire la méthode parseOneTransformer(iterator)
qui utilise l'itérateur pris en paramètre pour renvoyer le Transformer
correspondant au(x) un ou deux prochain(s) caractère(s) de
l'itérateur.
L'idée est que l'on pourra appeler cette méthode en boucle tant qu'il restera
des caractères accessibles par l'itérateur pour obtenir tous les
Transformers.
Écrire la méthode parseOneTransformer(iterator).
Vérifier que les tests marqués "Q7" passent.
-
Écrire la méthode parseAllTransformers(iterator) qui renvoie
la liste de Transformer dans l'ordre de l'itérateur.
Vérifier que les tests marqués "Q8" passent.
-
Enfin, écrire la méthode createOneTransformer(transformers)
qui prend en paramètre une liste de Transformer et renvoie le Transformer
qui applique les différents Transformer dans l'ordre.
Une fois la méthode createOneTransformer(transformers) écrite,
changer le code de parseTransformer(commands) pour gérer plusieurs commandes.
Vérifier que les tests marqués "Q9" passent.
-
Enfin, pour que le main fonctionne, on souhaite écrire la méthode
rewrite(input, output, transformer), qui prend deux Path,
le premier est le fichier contenant les lignes à réécrire, le second
est le fichier où l'on écrit le résultat.
Écrire la méthode rewrite(input, output, transformer).
Vérifier que les tests marqués "Q10" passent.
Note : quand on ouvre un fichier, il ne faut pas oublier de le fermer, proprement
(sinon, le fichier peut être vide, car l'OS, pour optimiser, ne l'écrira que lorsqu'il sera fermé !).
-
Pour les plus balèzes, il existe une façon fonctionnelle d'écrire
parseAllTransformers et createOneTransformer en un seul
Stream pour éviter d'utiliser une liste intermédiaire.
L'idée est de créer un Stream à partir d'un itérateur (il y a plusieurs façons de faire)
et d'utiliser reduce (le bon) pour fusionner les Transformer 2 à 2.
Réécrire parseTransformer de façon fonctionnelle,
en utilisant juste les méthodes characterIterator
et parseOneTransformer.
Les testent doivent continuer à passer.
Note : c'est moins de lignes à écrire, mais malheureusement,
c'est aussi moins efficace... mais c'est sympa comme exercice.
Note 2 : pour ceux qui ont du mal avec le reduce sur les Stream,
c'est la même idée que le fold en Haskell.
© Université de Marne-la-Vallée