2. Mise en œuvre des expressions régulières

2.1. Fonctionnement d'une regexp

Techniquement une expression régulière est une chaîne de caractères, écrite dans une syntaxe particulière, propre à la bibliothèque d'expressions régulières que l'on utilise. Dans son fonctionnement le plus simple, le moteur d'expression régulière compare cette expression régulière à une chaîne de caractères (un texte) quelconque. Il retourne un booléen qui est vrai si le texte correspond à l'expression régulière proposée, et faux si ce n'est pas le cas. Dans un fonctionnement plus évolué, le moteur permet de faire des manipulations plus complexes, telles que des recherches multiples, des substitutions de chaînes, etc...

2.2. Fonctionnement de l'API en Java

En Java, les choses fonctionnent à l'aide de deux classes. La première encapsule une expression régulière donnée : Pattern. Cette classe a pour objet de compiler l'expression régulière fournie. La seconde classe, Matcher, permet de comparer une expression régulière à un texte, et de faire différentes opérations dessus.

2.3. Un premier exemple

La façon la plus simple d'utiliser une expression régulière est d'utiliser la méthode statique matches de la classe Pattern.

Exemple 40. Utilisation de la méthode statique Pattern.matches

String texte =  "Quand le ciel bas et lourd" ;  // texte à tester    
 boolean b = Pattern.matches("a.*", texte) ;

Le booléen b sera vrai si texte contient une chaîne de caractères commençant par la lettre a. Dans ce moteur d'expression régulière, .* correspond à n'importe quel nombre de caractères quelconques. Une façon plus complexe, mais équivalente, de l'écrire est de passer par un objet Matcher.

Exemple 41. Comparaison par utilisation d'un Matcher

String texte =  "Quand le ciel bas et lourd" ;  // texte à tester    
Pattern p = Pattern.compile("a.*") ;      
Matcher m = p.matcher(texte) ;    
 boolean b = m.matches() ;

La première méthode est bien adaptée aux cas les plus simples. La seconde permet de réutiliser l'objet matcher, et de faire des opérations plus complexes qu'une simple recherche.

2.4. Classe Pattern

La première méthode de cette classe permet de créer un Pattern, il s'agit de la méthode statique compile, qui prend une expression régulière en paramètre, et un flag optionnel. Nous verrons les flags utilisables dans la suite. Ensuite, on trouve trois méthodes utilitaires : flags(), pattern() et toString(), qui retournent respectivement les éventuels flags déclarés sur ce pattern, et le pattern proprement dit. La méthode toString() est surchargée dans la classe Pattern, elle retourne simplement l'expression régulière de ce pattern. La méthode statique matcher(String) permet de comparer les textes passés en paramètre avec cette expression régulière, sans avoir à construire d'objet Matcher. Enfin les deux méthodes split(String) et split(String, int) permettent de découper un texte en fonction de l'expression régulière passée en paramètre. Elles retournent toutes les deux un tableau de String, limité au nombre de cases éventuellement passé en paramètres. Voyons son fonctionnement sur un exemple.

Exemple 42. Découpage par la méthode matcher.split()

Pattern p = Pattern.compile("\n") ;  
String texte =   
     "Pen Duick\n" +  
     "Belle Poule\n" +  
     "Perle\n" +  
     "Pourquoi pas" ;  
              
String [] bateaux = p.split(texte) ;  
 for (int i =  0 ; i < tableau.length ; i++) {  
    System.out.println("[" + i +  "] = " + tableau[i]) ;

Ici, nous définissons un pattern qui correspond au caractère de passage à la ligne. Le découpage de notre texte consiste donc à repérer tous les passages à la ligne, et à couper la chaîne de caractères à cet endroit. Le résultat de l'exécution de ce code est le suivant.
 [0] = Pen Duick
 [1] = Belle Poule
 [2] = Perle
 [3] = Pourquoi pas
Passer un paramètre entier à la méthode split() signifie que l'on veut arrêter le découpage lorsque l'on a le nombre de morceaux donnés. Passer 3 à cette méthode donnera donc le résultat suivant.
 [0] = Pen Duick
 [1] = Belle Poule
 [2] = Perle\nPourquoi pas
On a bien trois éléments dans le tableau de résultat. Enfin la méthode statique quote() permet de retourner la chaîne de caractères passée en paramètres en tant que chaîne à rechercher.

2.5. Classe Matcher

Dès que l'on veut faire des opérations sophistiquées sur un texte à l'aide d'une expression régulière, on utilise la classe Matcher. Les méthodes de la classe Pattern ne permettent que d'utiliser les opérations basiques. Les trois opérations de recherche supportés par la classe Matcher sont les suivantes :
  • Méthode matches() : cette méthode permet de tester si le texte correspond à l'intégralité de l'expression régulière. Par exemple, ce texte commence-t-il par la lettre a ?
  • Méthode lookingAt() : cherche si le texte commence par le pattern fourni. La différence entre matches() et lookingAt() est un peu subtile, nous allons voir un exemple.
  • Méthode find() : examine le texte, et recherche les occurrences du pattern dedans. Des appels successifs à cette méthode permettent de balayer l'ensemble du texte recherché. Lorsque cette méthode a trouvé une occurence du pattern, alors il est possible d'analyser le texte en utilisant les méthodes start(), end() et group().
Examinons sur quelques exemples les différences entre matches() et lookingAt().

Tableau 2. Différences entre matches() et lookingAt()

Pattern Texte matches() lookingAt()
P.* Pen Duick true true
P.* Belle Poule false false
P Pen Duick false true
P Belle Poule false false

Le pattern P.* correspond à une chaîne de caractères de longueur quelconque, commençant par un P. Il ne correspond pas au texte Belle Poule , en revanche il correspond à Pen Duick , que ce soit au sens de matches() et lookingAt(). Le pattern P correspond à la chaîne de caractères "P", ne comportant que ce seul caractère. La méthode matches() compare les chaînes de caractères dans leur intégralité. Elle retourne donc false pour les textes Pen Duick et Belle Poule . En revanche, la méthode lookingAt() cherche si le texte commence par le pattern considéré. Pour P, la réponse est true pour Pen Duick , false pour Belle Poule .

2.6. Utilisation des méthode find() et group()

La méthode group() permet de retourner le dernier élément textuel trouvé, en général par un find(). Prenons un premier exemple très simple, qui va nous permettre d'introduire la notion de pattern greedy et reluctant . Analysons le texte suivant : "(1 + 2)*4 / (2 + 2)", dans lequel on souhaite repérer les groupes de parenthèses : "(1 + 2)" et "(2 + 2)". Analysons ce texte à l'aide du pattern "\\(.*\\)". Ce pattern impose que la région trouvée commence par une parenthèse ouvrante ( \\(), suivi de n'importe quelle suite de caractères, éventuellement vide ( .*), fermée par une parenthèse fermante ( \\)). Les doubles backslashes sont ici présents car les parenthèses ont une signification particulière lorsque l'on écrit des expressions régulières. Elles doivent donc échapper à cette signification. Le code d'analyse est le suivant.

Exemple 43. Exemple d'utilisation de find() et group()

Pattern p = Pattern.compile("\\(.*\\)") ;  
  
String s =  "(1 + 2)*4 / (2 + 2)" ;  
Matcher m = p.matcher(s) ;  
  
 while (m.find()) {
    System.out.println("groupe = " + m.group()) ;
}

Le résultat de cette analyse n'est probablement pas celui que nous attendions.
 > groupe = (1 + 2)*4 / (2 + 2)
Que s'est-il passé ? En fait, si l'on examine de près l'expression retournée, elle commence bien par une parenthèse ouvrante, et se termine bien par une parenthèse fermante. Damned ! Ce n'est pas le résultat que nous avions prévu ! Heureusement, comme les choses en ce bas monde ne sont pas toujours si mal faites, ce cas a en fait été prévu. En réalité, les expressions régulières fonctionnent par défaut de façon greedy . Cela signifie que lorsqu'un pattern est cherché dans un texte, c'est toujours la portion la plus large possible de ce texte qui est sélectionnée. Dans notre exemple, lorsque le moteur de recherche rencontre la première parenthèse ouvrante, il marque le début du groupe, et va marquer la fin du groupe sur la dernière parenthèse fermante qu'il trouve, sans prendre en compte l'autre parenthèse ouvrante sur laquelle il est passé. Le mode qui nous intéresse pour répondre à notre problème n'est pas le mode greedy , mais le mode reluctant , qui prend la première parenthèse fermante, plutôt que la dernière. Il est activé en ajoutant un ? à notre pattern, juste après le *. Notre pattern devient alors le suivant.
Pattern p = Pattern.compile("\\(.*?\\)") ;  // notons le ? supplémentaire
Le résultat devient alors :
 > groupe = (1 + 2)
 > groupe = (2 + 2)
On voit bien là le côté merveilleux des expressions régulières : rechercher dans le fouillis d'une syntaxe complètement hermétique, le caractère qui fait que tout le système s'effondre...

2.7. Méthodes de remplacement

Il existe différentes façons de remplacer un élément textuel par un autre, plus ou moins complexe. La façon la plus simple consiste à utiliser la méthode replaceAll() de la classe Matcher, comme dans l'exemple suivant.

Exemple 44. Utilisation de matcher.replaceAll()

String texte =  "un - deux - trois - quatre" ;  
  
Pattern p = Pattern.compile("-") ;  
Matcher m = p.matcher(texte) ;  
  
String texteRemplace = m.replaceAll(";") ; 

La chaîne de caractères finale contient bien :
 > un ; deux ; trois ; quatre
Il existe également une méthode replaceFirst() qui ne remplace que la première occurrence du morceau de texte recherché. Cette méthode ne peut pas convenir si l'on souhaite remplacer chaque occurrence du morceau de texte, par un autre morceau, variable, éventuellement dépendant du morceau remplacé. Dans ce cas, il faut utiliser la méthode appendReplacement(), suivant la méthode de programmation suivante.

Exemple 45. Utilisation de matcher.appendReplacement()

String texte =  "un - deux - trois - quatre FIN" ;  
  
Pattern p = Pattern.compile("-") ;  
Matcher m = p.matcher(texte) ;  
  
StringBuffer sb =  new StringBuffer() ;  
 int i =  1 ;  
 while (m.find()) {  
    m.appendReplacement(sb,  "[" + i++ +  "]") ;  
}  
m.appendTail(sb) ;

Le contenu du StringBuffer à l'issue de l'exécution de ce code est :
 > un [1] deux [2] trois [3] quatre FIN
L'appel final à la méthode appendTail() est indispensable : c'est lui qui recopie la fin du texte dans le StringBuffer (ici "FIN").

2.8. Sélection de régions

Il est possible de n'opérer que sur une partie du texte à analyser. On dispose pour cela de la méthode region(int, int), qui fixe les limites de cette région. Les limites basses et hautes de cette région peuvent être retrouvées grâce aux deux méthodes regionStart() et regionEnd(). Elles retournent toutes deux un int. Par défaut regionStart est inclus dans la région, et regionEnd en est exclu.
Java API avancées
Retour au blog Java le soir
Cours & Tutoriaux
Table des matières
API Collection
1. Introduction
2. Interface Collection
2.1. Notion de Collection
2.2. Détail des méthodes disponibles
2.3. Interface Iterator
2.4. Implémentation, exemples d'utilisation
3. Interface List
3.1. Notion de List
3.2. Détail des méthodes disponibles
3.3. Interface ListIterator
3.4. Implémentations, exemples d'utilisation
4. Interface Set
4.1. Notion de Set
4.2. Implémentations HashSet et LinkedHashSet
4.3. Exemples d'utilisation
5. Interface SortedSet
5.1. Notion de SortedSet
5.2. Détails des méthodes disponibles
5.3. Exemples d'utilisation
6. Interface NavigableSet
6.1. Notion de NavigableSet
6.2. Détails des méthodes disponibles
6.3. Exemple d'utilisation
7. Interfaces Queue et Deque
7.1. Notion de file d'attente
7.2. Détail des méthodes disponibles
7.3. Utilisation des interfaces Queue et Deque
8. Tables de hachage
8.1. Notion de table de hachage
8.2. Interface Map
8.3. Interface Map.Entry
8.4. Interface SortedMap
8.5. Interface NavigableMap
8.6. Implémentations
8.7. Exemples d'utilisation
9. Classes utilitaires Collections et Arrays
9.1. Introduction
9.2. Classe Arrays
9.3. Classe Collections
Génériques
1. Introduction
2. Un premier exemple
2.1. Une première classe générique
2.2. Une première méthode générique
3. Contraindre un type générique
3.1. Problème posé
3.2. Contraindre un type générique
4. Implémentation des génériques
4.1. Type erasure
4.2. Types génériques et casts
4.3. Type générique et exception
4.4. Construction d'une instance générique
4.5. Génériques et membres statiques
4.6. Collisions de méthodes génériques
4.7. Implémentation de plusieurs types identiques
5. Type <?>
5.1. Introduction
5.2. Type ? extension d'un type
5.3. Type ? super-type d'un type
Expressions régulières
1. Introduction
2. Mise en œuvre des expressions régulières
2.1. Fonctionnement d'une regexp
2.2. Fonctionnement de l'API en Java
2.3. Un premier exemple
2.4. Classe Pattern
2.5. Classe Matcher
2.6. Utilisation des méthode find() et group()
2.7. Méthodes de remplacement
2.8. Sélection de régions
3. Syntaxe des expressions régulières
3.1. Notion de classe
3.2. Étude d'un cas réel
3.3. Recherche d'un mot précis
3.4. Recherche de deux mots précis
3.5. Recherche d'un mot commençant par une lettre donnée
3.6. Cas de mots comportant des caractères accentués
3.7. Recherche sur les lignes
Introspection
1. Introduction
2. La classe Class
2.1. Utilisation de Class
2.2. Méthodes disponibles
2.3. Remarque sur la propriété Accessible
2.4. Type d'une classe
2.5. Création d'une instance à partir d'un objet Class
2.6. Cas des énumérations
3. Les classes Method et Constructor
3.1. Utilisation de Method
3.2. Utilisation de Constructor
3.3. Méthodes disponibles
3.4. Invocation d'une méthode par introspection
4. La classe Field
4.1. Utilisation de Field
4.2. Méthodes disponibles
4.3. Accès à un champ par introspection
5. La classe Modifier
Programmation concurrente
1. Introduction
2. Lançons nos premiers threads
2.1. Introduction
2.2. Un premier thread, extension de Thread
2.3. Un deuxième thread, implémentation de Runnable
2.4. Remarque sur la méthode Thread.sleep(long)
2.5. Arrêter un thread
3. Concurrence d'accès
3.1. Notion d'état
3.2. Exemple de concurrence d'accès sur un état
3.3. Analyse de la concurrence d'accès
3.4. Solution au problème
3.5. Champs volatile
4. Synchronisation
4.1. Définition d'un bloc synchronisé
4.2. Fonctionnement d'un bloc synchronisé
4.3. Notion de deadlock
4.4. Bonnes pratiques pour la synchronisation de threads
5. Opérations atomiques
5.1. Atomicité d'une opération
5.2. Solutions disponibles
5.3. Variables atomiques
6. Collections synchronisées et concurrentes
6.1. Introduction
6.2. Position du problème
6.3. Solutions proposées
7. Files d'attente
7.1. Introduction, pattern producteur / consommateur
7.2. Interface BlockingQueue<E>
7.3. Implémentations de BlockingQueue
7.4. Exemple de producteur / consommateur
7.5. Arrêter un producteur / consommateur : pilule empoisonnée
8. Classes utilitaires de l'API Concurrent
8.1. Introduction
8.2. Énumération TimeUnit
8.3. Interface Callable<V>
8.4. Interfaces Future<V> et RunnableFuture<V>
8.5. Interface ScheduledFuture<V> et RunnableScheduledFuture<V>
9. Pattern executor
9.1. Notion de réserve de threads
9.2. Interface Executor
9.3. Interface ExecutorService
9.4. Interface ScheduledExecutorService
9.5. Classe Executors
9.6. Pattern de lancement de tâches
10. Classes de contrôle d'accès
10.1. Introduction
10.2. Interfaces Lock et ReadWriteLock
10.3. Notion de verrou réentrant
10.4. Classe RentrantLock
10.5. Classe ReadWriteRentrantLock
11. Sémaphores, barrières et latches
11.1. Introduction
11.2. Notion de sémaphore, classe Semaphore
11.3. Notion de latch, classe CountDownLatch
11.4. Notion de barrière, classe CyclicBarrier