Traductions pour la plateforme Java

Enrichir for each

Article d'origine: 

Adopter une approche de type "Moindre surprise" de l'itération

Résumé: 

Dans ce volet de Groovy par la pratique, Scott Davis donne une liste étourdissante de manières d'itérer dans des ... listes. Et des tableaux. Et des fichiers. Et des URLs. Et ainsi de suite. Ce qui est le plus impressionnant est que Groovy fournit une mécanisme consistant pour parcourir toutes ces collections, et d'autres encore.

L'itération est une des bases de la programmation. Vous êtes constamment confronté à des choses - un objet List, File, un ResulSet JDBC - qu'il faut parcourir élément par élément. Le langage Java vous donne presque toujours les possibilités de parcours dont vous avez besoin, mais il est frustrant de ne pas avoir de moyen standardisé de le faire. L'approche Groovy de l'itération est l'un des aspects éminemment pratiques par lesquels la programmation Groovy se distingue de la programmation Java. A travers une série d'exemples (tous disponibles en téléchargement), vous apprendrez dans cet article de quelle manière la versatilité de la méthode Groovy each() vous permet de laisser les singularités des itérations Java derrière vous.

Stratégies d'itération Java

Supposez que vous avez une java.util.List de langages de programmation. Le Listing 1 montre la manière programmatique de dire "Je connais chacun de ceux-là" en Java:

Listing 1: Itération dans une liste en Java

import java.util.*;

public class ListTest{
    public static void main(String[] args){
        List<String> list = new ArrayList<String>();
	list.add("Java");
	list.add("Groovy");
	list.add("JavaScript");

	for(Iterator<String> i = list.iterator(); i.hasNext();){
	    String language = i.next();
	    System.out.println("I know " + language);
        }
    }
}

Grâce à l'interface java.lang.Iterable qui est partagée par la plupart des classes de collection, vous pouvez itérer sur java.util.Set ou java.util.Queue de la même manière.

A présent supposez que les langages sont stockés dans une java.util.Map. Essayer d'obtenir un itérateur sur une Map échoue à la compilation - Les Maps n'implémentent pas l'interface Iterable. Heureusement, vous pouvez appeler map.KeySet pour récupérer un Set, ce qui vous permet de continuer. Cette différence mineure vous a ralentit un peu, sans être insurmontable. Il fallait simplement vous rappeler que les classes List, Set et Queue implémentent Iterable, mais pas la classe Map, même si elles appartiennent toutes au même package .

Maintenant supposez que les langages sont stockés dans un tableau de String. Un tableau est une structure de données, pas une classe. Vous ne pouvez pas appeler .iterator() sur une tableau de String, et vous êtes obligé d'utiliser une stratégie d'itération légèrement différente. A nouveau, vous êtes gêné mais il existe des solutions, comme vous pouvez le voir dans le Listing 2:

Listing 2. Itération dans un tableau Java

public class ArrayTest{
    public static void main(String[] args){
        String[] list = {"Java", "Groovy", "JavaScript"};
    
        for(int i = 0; i < list.length; i++) {
            String language = list[i];
            System.out.println("I know " + language);            
        }
    }
}

Mais un instant - que devient la syntaxe for-each introduite par Java 5 (voir les Ressources) ? Cela fonctionne pour toute classe implémentant Iterable, ainsi que pour les tableaux, comme le montre le Listing 3:

Listing 3. La boucle for-each du langage Java

import java.util.*;

public class MixedTest{
    public static void main(String[] args){
        List<String> list = new ArrayList<String>();
        list.add("Java");
        list.add("Groovy");
        list.add("JavaScript");
    
        for(String language: list){
             System.out.println("I know " + language);      
        }

        String[] list2 = {"Java", "Groovy", "JavaScript"};
        for(String language: list2){
            System.out.println("I know " + language);      
        }
    }
}

Ainsi désormais il est possible d'itérer sur les tableaux et les collections (à l'exception des Maps) de la même façon. Mais qu'en serait-il si les langages étaient stockés dans un java.io.File ? Ou dans un ResultSet JDBC ? Un document XML ? Un java.util.StringTokenizer ? Dans chaque cas, vous devez utiliser une stratégie d'itération un peu différente. Cette approche n'était pas intentionnelle - les différentes API ont été développées à des époques différentes par différents développeurs - mais le fait est qu'il vous faut connaitre une demi-douzaine de stratégies d'itération Java et, plus important, les cas spécifiques qui mènent à leur utilisation.

Eric S. Raymond à écrit sur le sujet de "La Règle de Moindre Surprise" dans son livre "L'Art de la programmation Unix" (voir les Ressources, le livre est disponible en français). Il dit: "Pour concevoir des interfaces utilisables, il est préférable quand c'est possible de ne pas concevoir un modèle entièrement nouveau d'interface. La nouveauté est un obstacle à l'appropriation; cela met une charge d'apprentissage sur les épaules de l'utilisateur, aussi minimisez-la.". La position de Groovy vis-à-vis de l'itération suit le conseil de Raymond. Avec Groovy, qu'il s'agisse de parcourir presque n'importe quelle structure, tout ce dont vous avez besoin de vous rappeler est each().

Itération de List en Groovy

Pour commencer, je vais transformer l'exemple à base de List du Listing 3 en Groovy. Au lieu de manipuler la List dans une boucle for (ce qui, finalement, n"a pas l'air très orienté objet, n'est-ce-pas ?), on appelle simplement la méthode each() de la liste et on passe le résultat à une fermeture.

Créez un fichier nommé listTest.groovy et ajoutez-y le code du Listing 4:

Listing 4. Itération sur une liste en Groovy

def list = ["Java", "Groovy", "JavaScript"]
list.each{language->
  println language
}

La première ligne du Listing 4 est la syntaxe Groovy raccourcie pour construire une java.util.ArrayList. Vous pouvez ajouter println list.class au script pour le vérifier. Ensuite, il suffit d'appeler each() sur la liste et d'afficher la variable language dans le corps de la fermeture. On a donné un nom à la variable language au début de la fermeture avec l'instruction language->. Si vous ne fournissez pas de nom de variable, Groovy l'appelle par défaut it. Entrez groovy listTest en ligne de commande pour exécuter listTest.groovy.

Le Listing 5 donne deux versions à chaque fois plus courtes du code du Listing 4:

Listing 5. Itération utilisant la variable it de Groovy

// plus court, en utilisant la variable it
def list = ["Java", "Groovy", "JavaScript"]
list.each{ println it }

// Encore plus court, en utilisant une liste anonyme
["Java", "Groovy", "JavaScript"].each{ println it }

Groovy permet d'utiliser la méthode each() sur les tableaux et les Lists indifféremment. Pour changer ArrayList en tableau de String, ajoutez as String[] à la fin de la ligne, comme le montre le Listing 6:

Listing 6. Itération sur un tableau en Groovy

def list = ["Java", "Groovy", "JavaScript"] as String[]
list.each{println it}

Le fait que la méthode each() soit présente partout en Groovy, associé à la syntaxe raccourcie pour les getters (getClass() et class donnent le même résultat), permet d'écrire du code tout à la fois concis et expressif. Par exemple, disons que vous voulez afficher toutes les méthodes publiques d'une classe donnée en utilisant la réflexion. Le Listing 7 donne un exemple:

Listing 7. Réflexion avec Groovy

def s = "Hello World"
println s
println s.class
s.class.methods.each{println it}

//sortie:
$ groovy reflectionTest.groovy 
Hello World
class java.lang.String
public int java.lang.String.hashCode()
public volatile int java.lang.String.compareTo(java.lang.Object)
public int java.lang.String.compareTo(java.lang.String)
public boolean java.lang.String.equals(java.lang.Object)

La dernière ligne du script appelle la méthode getClass(). Une instance de java.lang.Class possède une méthode getMethods qui retourne un tableau. En chainant tout cela et en appelant each() sur le tableau de Methods retourné, vous faites pas mal de choses en une seule ligne de code.

Mais l'omniprésente méthode each() n'est pas limitée aux Lists et aux tableaux, comme l'est la structure for-each de Java. En Java, l'histoire s'arrête là. En Groovy, on commence juste à s'échauffer.

Itération sur une Map

Comme nous l'avons vu précédemment, en Java on ne peut pas itérer directement sur une Map. En Groovy, ce n'est pas un problème, comme le Listing 8 le montre:

Listing 8. Itération sur une Map en Groovy

def map = ["Java":"server", "Groovy":"server", "JavaScript":"web"]
map.each{ println it }

Pour accéder au contenu des paires nom/valeur, vous pouvez soit utiliser les méthodes implicites getKey() et getValue(), soit nommer explicitement les variables au début de la fermeture, comme le montre le Listing 9:

Listing 9. Récupérer clé et valeur d'une Map

def map = ["Java":"server", "Groovy":"server", "JavaScript":"web"]
map.each{ 
  println it.key
  println it.value 
}

map.each{k,v->
  println k
  println v
}

Comme vous pouvez le voir, itérer sur une Map est aussi naturel qu'itérer sur toute autre collection.

Avant que nous ne continuions avec l'exemple d'itération suivant, il est préférable que vous connaissiez une autre facilité syntaxique concernant les Maps en Groovy. Au lieu d'appeler map.get("Java") comme vous le feriez en Java, vous pouvez le raccourcir en map.Java, comme le montre le Listing 10:

Listing 10. Récupérer les valeurs d'une Map

def map = ["Java":"server", "Groovy":"server", "JavaScript":"web"]

//Résultats identiques
println map.get("Java")
println map.Java

Bien que ce raccourci syntaxique pour les Maps soit indéniablement sympathique, il est aussi la cause d'erreurs courantes lors de l'utilisation de la réflexion sur les Maps. Un appel à list.class retourne un java.util.ArrayList, tandis que l'appel map.class retourne null. Ceci vient du fait que le raccourci qui permet de récupérer un élément de la Map surcharge l'appel du get classique. Aucun élément de la Map n'a pour clé "class", donc l'appel retourne null, comme dans l'exemple du Listing 11:

Listing 11. Les Maps en Groovy et null

def list = ["Java", "Groovy", "JavaScript"]
println list.class
// java.util.ArrayList

def map = ["Java":"server", "Groovy":"server", "JavaScript":"web"]
println map.class
// null

map.class = "I am a map element"
println map.class
// I am a map element

println map.getClass()
// class java.util.LinkedHashMap

C'est l'un des rares cas où Groovy viole la Règle de Moindre Surprise, mais du fait que récupérer les éléments d'une Map est bien plus courant qu'utiliser la réflexion, c'est une exception avec laquelle on peut composer.

Itération dans une String

Maintenant que vous commencez à vous habituer à la méthode each(), nous allons voir qu'elle se retrouve dans toutes sortes d'endroits intéressants. Supposez que vous souhaitiez itérer dans une String, lettre par lettre. La méthode each() est là pour vous aider, comme le montre le Listing 12:

Listing 12: Itération dans une String

def name = "Jane Smith"
name.each{letter->
  println letter
}

Cela ouvre toutes sortes de possibilités, par exemple remplacer les espaces avec des soulignés, comme dans le Listing 13:

Listing 13. Remplacer les espaces par des soulignés

def name = "Jane Smith"
println "replace spaces"
name.each{
  if(it == " "){
    print "_"
  }else{
    print it
  }
}

// sortie
Jane_Smith

Bien sûr, pour ce qui est de remplacer une seule lettre, Groovy offre une méthode replace plus concise. Vous pourriez résumer tout le code du Listing 13 en une seule ligne: "Jane Smith".replace(" ", "_"). Mais pour des manipulations plus compliquées, la méthode each() est parfaite.

Itération dans un Range (ndT: intervalle)

Groovy offre un type natif Range qui ne demande qu'à être itéré. Tout ce qui est séparé par deux points (comme 1..10) est un Range. Le Listing 14 donne un exemple:

Listing 14. Itération dans un Range

def range = 5..10
range.each{
  println it
}

//sortie:
5
6
7
8
9
10

Les Ranges ne sont pas limités aux Integers. Voyez le code du Listing 15, qui itère sur un Range de Dates:

Listing 15. Itération sur des Date

def today = new Date()
def nextWeek = today + 7
(today..nextWeek).each{
  println it
}

//sortie:
Thu Mar 12 04:49:35 MDT 2009
Fri Mar 13 04:49:35 MDT 2009
Sat Mar 14 04:49:35 MDT 2009
Sun Mar 15 04:49:35 MDT 2009
Mon Mar 16 04:49:35 MDT 2009
Tue Mar 17 04:49:35 MDT 2009
Wed Mar 18 04:49:35 MDT 2009
Thu Mar 19 04:49:35 MDT 2009

Comme vous le constatez, la méthode each() apparait exactement là où vous l'attendriez. Le langage Java n'a pas de type natif Range, mais il existe un concept similaire sous la forme d'enums. Et, sans surprise, each() vous y attend aussi.

Iteration dans des enums

Une enum Java est un ensemble arbitraire de valeurs stockées dans un ordre spécifique. Le Listing 16 montre comme la méthode each() s'associe naturellement aux enums, comme elle le fait avec l'opérateur Range:

Listing 16. Itération sur une enum

enum DAY{
  MONDAY, TUESDAY, WEDNESDAY, THURSDAY,
    FRIDAY, SATURDAY, SUNDAY
}

DAY.each{
  println it
}

(DAY.MONDAY..DAY.FRIDAY).each{
  println it
}

Il existe des cas particuliers en Groovy où le nom de la méthode each() n'est pas assez expressif. Dans les exemples suivants, vous verrez la méthode each() décorée d'une façon appropriée à son contexte d'utilisation. La méthode Groovy eachRow() en est un exemple parfait.

Itération SQL

Quand on a affaire à des tables de bases de données relationnelles, il est courant de dire: "J'ai un traitement spécifique à effectuer sur chacune des lignes de la table.". Mettez celà en contraste avec les exemples précédents, où vous êtiez plus susceptible de dire, "J'ai un traitement spécifique à effectuer sur chacun des langages de la liste.". Dans cet esprit, un objet groovy.sql.Sql offre une méthode eachRow(), comme le montre le Listing 17:

Listing 17. Itération dans un ResultSet

import groovy.sql.*

def sql = Sql.newInstance(
   "jdbc:derby://localhost:1527/MyDbTest;create=true",
   "username",
   "password",
   "org.apache.derby.jdbc.ClientDriver")

println("grab a specific field")
sql.eachRow("select name from languages"){ row ->
    println row.name
}

println("grab all fields")
sql.eachRow("select * from languages"){ row ->
    println("Name: ${row.name}")
    println("Version: ${row.version}")
    println("URL: ${row.url}\n")
}

La première ligne du script instancie un nouvel objet Sql en passant une chaine de connexion JDBC, un nom d'utilisateur, un mot de passe, et un driver JDBC. Ensuite, vous pouvez appeler la méthode eachRow(), en lui passant en argument l'instruction SQL select. Dans la fermeture, vous pouvez référencer les noms de colonnes (name, version, url) comme s'il existait réellement des methodes getName(), getVersion() et getUrl().

C'est considérablement plus propre que l'équivalent Java où vous devez jongler avec les DriverManagers, les Connections, les Statements, et les ResultSets JDBC, pour ensuite nettoyer tout cà dans un océan de blocs try/catch/finally imbriqués.

Dans le cas de l'objet Sql, vous pourriez objecter que les noms each() ou eachRow() sont tout aussi valables. Mais dans les exemples suivants, je pense que vous serez d'accord pour dire que le nom each() est loin d'être suffisamment expressif.

Itération sur un fichier

Je rêve rarement d'itérer sur un java.io.File ligne par ligne en pur Java. Quand on en a fini avec tous les BufferedReaders et FileReaders imbriqués, sans compter la gestion des exceptions en fin de processus, on a eu le temps d'oublier ce qu'on voulait faire au départ.

Le Listing 18 montre le traitement complet en Java:

Listing 18. Itération dans un fichier en Java

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

public class WalkFile {
   public static void main(String[] args) {
      BufferedReader br = null;
      try {
         br = new BufferedReader(new FileReader("languages.txt"));
         String line = null;
         while((line = br.readLine()) != null) {
            System.out.println("I know " + line);
         }
      }
      catch(FileNotFoundException e) {
         e.printStackTrace();
      }
      catch(IOException e) {
         e.printStackTrace();
      }
      finally {
         if(br != null) {
            try {
               br.close();
            }
            catch(IOException e) {
               e.printStackTrace();
            }
         }
      }
   }
}

Le Listing 19 montre le traitement équivalent en Groovy:

Listing 19. Itération dans un fichier en Groovy

def f = new File("languages.txt")
f.eachLine{language->
  println "I know ${language}"
}

C'est là où la concision de Groovy est vraiment remarquable. J'espère que vous voyez maintenant pourquoi j'appelle Groovy "un DSL pour les programmeurs Java".

Gardez présent à l'esprit que nous manipulons la même classe java.io.File en Groovy et en Java. Si le fichier n'existe pas, le code Groovy lancera la même FileNotFoundException que le code Java. La différence est qu'en Groovy il n'y a pas d'exceptions vérifiées. C'est ma prérogative - et non une contrainte du langage - d'envelopper la structure eachLine() dans un bloc try/catch/finally. Pour un simple script en ligne de commande, je donne la priorité à la brièveté du code comme dans le Listing 19. Si la même itération doit s'exécuter au sein d'un serveur d'application, je ne serais pas aussi cavalier en ne traitant pas les exceptions. J'envelopperais dans ce cas le bloc eachLine() dans le même bloc try/catch que la version Java.

La classe File offre plusieurs variantes de la méthode each(). L'une d'elles est splitEachLine(String separateur, Closure fermeture). Ceci signifie que vous pouvez non seulement itérer dans le fichier ligne par ligne, mais aussi découper les lignes en même temps. Le Listing 20 montre un exemple:

Listing 20. Découper les lignes d'un fichier

// languages.txt
// Notez l'espace entre le langage et la version
Java 1.5
Groovy 1.6
JavaScript 1.x 

// splitTest.groovy
def f = new File("languages.txt")
f.splitEachLine(" "){words->
  words.each{ println it }
}

// sortie
Java
1.5
Groovy
1.6
JavaScript
1.x

Si vous traitez des fichiers binaires, Groovy possède aussi une méthode eachByte().

Bien entendu, un File du langage Java n'est pas toujours un fichier - parfois c'est un répertoire. Groovy offre plusieurs variantes de each() pour manipuler également les sous-répertoires.

Itération dans un répertoire

Il est commode d'utiliser Groovy comme un langage de script shell (ou script de traitement par lots), grâce à la facilité avec laquelle on peut interagir avec le système de fichiers. Pour avoir la liste des contenus du répertoire courant, voyez le Listing 21:

Listing 21. Itération dans un répertoire

def dir = new File(".")
dir.eachFile{file->
  println file
}

La méthode eachFile() retourne à la fois les fichiers et les sous-répertoires. En utilisant les méthodes isFile() et isDirectory() de Java, on peut commencer à faire des choses plus sophistiquées. Le Listing 22 donne un exemple:

Listing 22. Distinguer les fichiers des répertoires

def dir = new File(".")
dir.eachFile{file->
  if(file.isFile()){
    println "FILE: ${file}"    
  }else if(file.isDirectory()){
    println "DIR:  ${file}"
  }else{
    println "Uh, I'm not sure what it is..."
  }
}

Du fait que les méthodes Java retournent de simples booléens, vous pouvez raccourcir ce code avec un opérateur ternaire Java. Le Listing 23 donne un exemple:

Listing 23. Opérateur ternaire

def dir = new File(".")
dir.eachFile{file->
  println file.isDirectory() ? "DIR:  ${file}" : "FILE: ${file}"
}

Si on ne veut que les répertoires, on peut utiliser eachDir() au lieu de eachFile(). Il existe aussi des méthodes eachDirMatch() et eachDirRecurse().

Comme vous le voyez, la seule méthode each() manquerait d'expressivité pour File. La sémantique de la méthode each() typique est conservée sur les Files, mais les noms de méthodes sont plus parlants pour fournir des informations supplémentaires sur les fonctionnalités étendues.

Itération dans une URL

Une fois que vous avez compris comment itérer dans un File, vous pouvez utiliser les mêmes principles pour itérer dans la réponse d'une requête HTTP. Groovy fournit une méthode eachLine() pratique (et familière) sur java.net.URL.

Par exemple, le Listing 24 parcourt le code HTML de la page d'accueil de ibm.com, ligne par ligne:

Listing 24. Itération dans une URL

def url = new URL("http://www.ibm.com")
url.eachLine{line->
  println line
}

Itération XML

Vous avez vu comment utiliser la méthode eachLine() sur les fichiers et les URLs. XML présente un problème un peu différent: vous êtes probablement moins intéressé par un parcours ligne à ligne du document que par un parcours élément par élément.

Par exemple, disons que la liste des langages est stockée dans un fichier nommé languages.xml, présenté dans le Listing 25:

Listing 25. Le fichier languages.xml

<langs>
  <language>Java</language>
  <language>Groovy</language>
  <language>JavaScript</language>
</langs>

Groovy fournit une méthode each() que vous pouvez utiliser, mais au prix d'une petite entorse. Si vous analysez le XML au moyen d'une classe native Groovy nommée XMLSlurper, vous pouvez parcourir les éléments avec each(). Regardez le Listing 26, par exemple:

Listing 26. Itération XML

def langs = new XmlSlurper().parse("languages.xml")
langs.language.each{
  println it
}

//sortie
Java
Groovy
JavaScript

L'instruction langs.language.each ramène tous les éléments nommés <language> sous l'élément <langs>. S'il s'y trouvait aussi des éléments <format> et <server>, ils n'apparaitraient pas dans le résultat de la méthode each().

Et si cela n'est pas assez impressionnant, maintenant imaginez que ce XML soit accessible par le biais d'un service Web de type Rest et non pas par un fichier du système. Remplacez ce chemin vers le fichier par une URL, et ne changez rien d'autre au code, comme le montre le Listing 27:

Listing 27. Itération sur du XML provenant d'un appel à un service Web

def langs = new XmlSlurper().parse("http://somewhere.com/languages")
langs.language.each{
  println it
}

C'est une utilisation assez concise et élégante de la méthode each(), n'est ce pas ?

Conclusion

Le plus beau de tout cet exercice fondé sur each(), c'est qu'il nous permet de beaucoup avancer dans notre connaissance de Groovy avec peu d'efforts. Il n'y a plus beaucoup de nouveautés sur le sujet des itérations Groovy une fois que vous connaissez la méthode each(). Et comme Raymond le dirait, c'est justement ce qui est intéressant. A partir du moment où vous savez itérer dans une List, vous savez en même temps itérer dans un tableau, une Map, une String, un Range, une enum, un ResultSet SQL, un File, un répertoire, une URL, et même sur les éléments d'un document XML.

Les derniers exemples de cet article ont effleuré l'analyse d'un XML avec XmlSlurper. La prochaine fois, je reviendrai sur ce sujet pour vous montrer comme il est facile d'analyser du XML avec Groovy. Vous verrez à la fois XmlParser et XmlSlurper à l'action, et vous vous ferez une meilleure idée de la raison pour laquelle Groovy possède deux classes similaires et pourtant distinctes ayant pour objectif principal d'analyser du  XML. En attendant, j'espère que vous trouverez beaucoup d'utilisations pratiques de Groovy.