Java – Le novità di Matcher in Java 9: Parte 4

In questa quarta (e ultima) parte della mia serie di articoli «Le novità di Matcher in Java 9» descriverò il nuovo metodo results introdotto nella classe java.util.regex.Matcher in Java 9.

Nella seconda e terza parte di questa serie, i vari metodi di Matcher che sono stati descritti (sia quelli “vecchi” da Java 1.4 che quelli “nuovi” da Java 9) hanno come obiettivo primario la ricerca e sostituzione delle occorrenze trovate con un’altra stringa, che può essere “fissa” oppure determinata con della logica o restituita da una apposita “funzione” rappresentata dalla functional interface Function<MatchResult,String>.

Il nuovo metodo results innanzitutto ha la seguente forma:

  • public Stream<MatchResult> results()

Anche questo metodo è molto “interessante” ma il suo obiettivo è radicalmente differente. Questo metodo infatti non serve per fare sostituzioni nel testo. Il suo scopo è un altro: dare la possibilità di operare sui risultati di match della espressione regolare in una maniera più “funzionale” sfruttando la nuova Stream API introdotta in Java 8.

Nella Stream API uno stream rappresenta un “flusso” di dati su cui si possono compiere svariate operazioni in sequenza tra cui, per citarne alcune, operazioni di “filtro”, “mappatura” (nel senso di trasformazione da x a y), “riduzione”, “raggruppamento” e altro.

Nel caso del metodo results, esso fornisce un Stream<MatchResult> ovvero un flusso di oggetti MatchResult. Questa è la stessa interfaccia di cui ho già parlato nella terza parte. MatchResult rappresenta il risultato di un singolo match della espressione regolare e tramite i suoi pochi metodi è possibile ottenere varie informazioni sulla occorrenza trovata.

Il nuovo metodo results quindi serve in generale per estrarre tutte le occorrenze trovate e operare su di esse costruendo una pipeline (“tubatura”, se vogliamo tradurlo in italiano) di operazioni con lo scopo ad esempio di filtrarle, trasformarle, raggrupparle o convogliarle in una apposita collezione.

Esempio completo

L’esempio completo dell’uso di results è abbastanza semplice. Dato il testo della seguente frase:

Oggi è davvero una bellissima giornata per una sessione di programmazione con il linguaggio Java per utilizzare le espressioni regolari.

Si vuole ottenere un set (nel senso di java.util.Set) che contiene tutte le occorrenze “distinte” composte da un carattere “doppio”, cioè che si ripete uguale due volte consecutivamente. Inoltre si vuole che il set sia ordinato, quindi l’ideale sarebbe creare un java.util.TreeSet che è una implementazione “sorted” di Set. Con la frase di esempio si vuole quindi ottenere un set che contiene tutte le lettere “doppie” (che in questo caso sono le consonanti) presenti nel testo: "gg", "ll", "mm", "ss", "vv", "zz".

Prima di vedere il codice, è bene valutare l’espressione regolare da utilizzare e quali operazioni applicare con la Stream API.

Per la espressione regolare purtroppo non si può utilizzare “banalmente” il pattern .. perché rappresenta certamente due caratteri qualunque ma non necessariamente uguali. In casi come questi bisogna usare due altri costrutti delle espressioni regolari: i capturing group (i “gruppi”) e i back-reference.

Il back-reference è un costrutto un po’ particolare e “avanzato” nelle espressioni regolari e si specifica nel pattern usando la forma \n dove n è un numero 1, 2, 3... che fa riferimento ad un capturing group. Quando il motore delle espressioni regolari incontra un back-reference, si aspetta di catturare esattamente la stessa sequenza di caratteri che è stata catturata dal gruppo referenziato.

Il pattern finale da usare nell’esempio sarà quindi (.)\1 . Per chiarire meglio il senso del back-reference, si può mostrare cosa avviene quando il motore delle espressioni regolari sta analizzando la parola "davvero".

da         ➜ nessun match
 av        ➜ nessun match
  vv       ➜ match! corrisponde al pattern (.)\1
   ve
    er     ➜ nessun match
     ro    ➜ nessun match

Nella parte "vv" si ha un match perché il gruppo (.) ha catturato il primo "v" e il back-reference \1 trova il secondo carattere "v" che è lo stesso carattere catturato dal gruppo referenziato, quindi c’è corrispondenza. Da notare che siccome la seconda "v" è già stata catturata, il motore delle espressioni regolari non considera la sequenza "ve" e passa quindi subito a valutare "er".

Riguardo la Stream API, la logica da applicare sarà quindi la seguente:

  1. Il metodo results fornisce un Stream<MatchResult> ma a noi serve ottenere il testo di ciascuna occorrenza. Quindi si può applicare l’operazione di map di Stream per ottenere un Stream<String> utilizzando una funzione di mapping che estrae il testo dell’occorrenza con il metodo group() di MatchResult. Si può fare con una lambda expression mr -> mr.group() o in alternativa con un method reference MatchResult::group.

  2. Avendo un Stream<String> possiamo convogliare tutte le occorrenze in un set utilizzando l’operazione di collect di Stream. Esiste già un Collector che si chiama Collectors.toSet() ma non fornisce un set “ordinato”. In questo caso è necessario usare il Collectors.toCollection() specificando come supplier il costruttore di TreeSet.

Il codice dell’esempio è il seguente:

import static java.util.stream.Collectors.toCollection;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ProvaMatcherResultsJava9 {
    public static void main(String[] args) {
        String testo = "Oggi è davvero una bellissima giornata per una "
                     + "sessione di programmazione con il linguaggio "
                     + "Java per utilizzare le espressioni regolari.";

        Pattern pattern = Pattern.compile("(.)\\1");
        Matcher matcher = pattern.matcher(testo);

        Set<String> doppie = matcher.results()
                .map(mr -> mr.group())
                .collect(toCollection(TreeSet::new));

        System.out.println("doppie = " + String.join(", ", doppie));
    }
}

Eseguendo il programma, l’output è questo:

doppie = gg, ll, mm, ss, vv, zz

Come si può vedere, è stato possibile estrarre tutte le lettere “doppie” e inserirle in un TreeSet senza fare “cicli”, senza usare variabili temporanee e con appena 3 righe di codice (che si possono anche, volendo, compattare in una sola). 😉

Da notare che la variabile doppie, per semplicità, l’ho dichiarata di tipo Set<String> ma se fosse necessario si può mettere più specificatamente TreeSet<String> poiché questo è il tipo esatto fornito dal metodo collect dello Stream<String> (dedotto dal compilatore per inferenza in base al supplier fornito al collector toCollection).