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

In questa terza parte della mia serie di articoli «Le novità di Matcher in Java 9» descriverò i due nuovi metodi replaceFirst e replaceAll introdotti nella classe java.util.regex.Matcher in Java 9.

Premessa

Nella seconda parte ho descritto diversi metodi di Matcher, alcuni esistenti fin da Java 1.4, altri disponibili da Java 9. A fronte di tutta la discussione fatta in quella parte, dovrebbero essere quindi abbastanza evidenti le seguenti questioni:

  1. I “vecchi” metodi replaceFirst(String) e replaceAll(String) sono molto limitati in quanto usano solo una semplice stringa di replacement. Con questi metodi si possono fare delle sostituzioni un po’ particolari (ma comunque basilari) solamente sfruttando i “gruppi” in modo appropriato.

  2. I metodi appendReplacement e appendTail (sia quelli “vecchi” con StringBuffer che quelli “nuovi” con StringBuilder) permettono di fare sostituzioni “complesse” ma richiedono anche più codice: bisogna creare esplicitamente un buffer, fare un ciclo while (in cui si testa il find()), eseguire appendReplacement all’interno del ciclo ed eseguire appendTail dopo la fine del ciclo.

Si può fare di meglio? A partire da Java 9, sì, è possibile fare molto di meglio sfruttando i nuovi replaceFirst e replaceAll. Questi metodi possono di fatto rendere, in un certo senso, “obsoleti” quelli elencati prima in molti casi di sostituzioni “complesse” (come l’esempio di conversione da km a miglia che ho proposto nella seconda parte).

I nuovi metodi replaceFirst e replaceAll

In Java 9 sono stati aggiunti nella classe Matcher due metodi molto interessanti:

  • public String replaceFirst(Function<MatchResult,String> replacer)
  • public String replaceAll(Function<MatchResult,String> replacer)

La differenza tra replaceFirst e replaceAll l’ho già spiegata nella precedente parte ma giusto come “rinfresco”: replaceFirst considera solo la prima occorrenza mentre replaceAll considera tutte le occorrenze.

Perché questi metodi sono così “interessanti”? Per un motivo molto semplice: hanno come parametro un Function<MatchResult,String>. Function è una delle functional interface presenti nel nuovo package java.util.function aggiunto in Java 8. In modo particolare, Function serve a rappresentare e descrivere una “funzione” che in questo caso specifico ha come parametro un MatchResult e come risultato un String.

MatchResult è una interfaccia che esiste da Java 5 e rappresenta in maniera astratta il risultato di un match della espressione regolare all’interno di un testo. Tramite i pochi metodi (solo 7) di MatchResult è possibile ottenere varie informazioni sulla occorrenza trovata, tra cui naturalmente l’intero testo della occorrenza e anche la parte di testo per ciascun “gruppo” specifico che il pattern della espressione regolare può aver definito.

I nuovi metodi replaceFirst e replaceAll quindi lavorano nel seguente modo: per ogni occorrenza della espressione regolare che viene trovata nel testo, viene invocata quella funzione passando come argomento un oggetto che implementa MatchResult. La funzione può estrarre dal MatchResult le informazioni che servono sulla occorrenza, può fare tutta la logica che vuole, anche molto complessa se necessario, e infine deve restituire una stringa che sarà usata come sostituzione della occorrenza trovata.

I due nuovi metodi permettono in pratica di sfruttare una tecnica che si chiama behavior parameterization. Si tratta di un pattern abbastanza generale nella programmazione che consiste nel poter passare un “comportamento” come parametro di un metodo (o “funzione” o API, detto in generale). Il metodo ha quindi la possibilità di eseguire quel “comportamento” all’interno del suo algoritmo al fine di realizzare la logica che deve fare.

Questo approccio, tecnicamente, si poteva già fare anche prima di Java 8 ma in maniera più lunga e scomoda, perché richiedeva di definire una classe apposita (normale o ad esempio “anonima”) tipicamente per implementare una interfaccia specifica. A partire da Java 8 il behavior parameterization è molto più semplice e praticabile, grazie alle lambda expression, ai method reference e al concetto delle functional interface.

Esempio completo

Per l’esempio completo si può riutilizzare la stessa frase e la stessa espressione regolare che ho già utilizzato nell’esempio presentato nella seconda parte. Anche l’obiettivo resta quindi lo stesso. In questo modo sarà molto facile fare un confronto tra il codice precedente e quello che propongo di seguito.

Giusto per ribadire il contesto dell’esempio, data la seguente frase (fonte Wikipedia sulla città di Torino):

Torino dista 57 km da Asti, 79 km da Vercelli, 84 km da Biella, 93 km da Alessandria, 96 km da Novara, 98 km da Cuneo, 155 km da Verbania.

Si vuole catturare tutte le parti del testo che hanno la forma "nnn km" e sostituirle con l’equivalente in miglia terrestri (es. sostituire "57 km" con "35,4 miglia").

Come si vedrà a breve, però, non ci sarà più bisogno di fare un “ciclo”, né di creare un “buffer” e neanche di usare la coppia di metodi appendReplacement e appendTail. Dato che nel testo ci sono più occorrenze da sostituire, sarà sufficiente sfruttare il nuovo metodo replaceAll, mettendo in pratica quella functional interface tramite una apposita lambda expression.

Il codice dell’esempio è il seguente:

import java.util.regex.Pattern;

public class ProvaMatcherReplaceAllJava9 {
    public static void main(String[] args) {
        String testo = "Torino dista 57 km da Asti, 79 km da Vercelli, "
                     + "84 km da Biella, 93 km da Alessandria, 96 km da Novara, "
                     + "98 km da Cuneo, 155 km da Verbania.";

        Pattern pattern = Pattern.compile("(\\d{1,3}) km");
        Matcher matcher = pattern.matcher(testo);

        String testo2 = matcher.replaceAll(mr -> formattaMigliaDaKm(mr.group(1)));

        System.out.println(testo);
        System.out.println();
        System.out.println(testo2);
    }

    private static String formattaMigliaDaKm(String kmStr) {
        int km = Integer.parseInt(kmStr);
        double miglia = km / 1.609344;
        return String.format("%.1f miglia", miglia);
    }
}

Eseguendo il programma, l’output è esattamente lo stesso già mostrato in precedenza:

Torino dista 57 km da Asti, 79 km da Vercelli, 84 km da Biella, 93 km da Alessandria, 96 km da Novara, 98 km da Cuneo, 155 km da Verbania.

Torino dista 35,4 miglia da Asti, 49,1 miglia da Vercelli, 52,2 miglia da Biella, 57,8 miglia da Alessandria, 59,7 miglia da Novara, 60,9 miglia da Cuneo, 96,3 miglia da Verbania.

Riguardo l’espressione regolare valgono le stesse considerazioni che ho già fatto in precedenza. La cosa invece importante da notare è che ora la sostituzione si può fare con una sola riga di codice!

String testo2 = matcher.replaceAll(mr -> formattaMigliaDaKm(mr.group(1)));

In questa riga la parte più rilevante è sicuramente la lambda expression che ho evidenziato. La interfaccia Function<T,R> parametrizzata come Function<MatchResult,String> ha sostanzialmente un metodo astratto con questa forma:

String apply(MatchResult)

La lambda expression utilizzata nell’esempio corrisponde perfettamente al metodo String apply(MatchResult) della functional interface Function<MatchResult,String> perché:

  1. Il parametro mr della lambda viene dedotto di tipo MatchResult tramite “inferenza” dei tipi. Il compilatore deduce questo basandosi sul contesto in cui la lambda viene usata, in questo caso passata come argomento ad un metodo che riceve un Function<MatchResult,String>. E con questa parametrizzazione di Function, il metodo apply ha come parametro proprio un MatchResult, quindi questo è il tipo del parametro mr.

  2. Il metodo privato formattaMigliaDaKm ha come tipo di ritorno String e questo corrisponde al tipo di ritorno del metodo apply di Function<MatchResult,String> che è appunto String.

Conclusioni

Con questo articolo e con l’esempio proposto dovrebbe risultare ben chiara l’utilità dei nuovi metodi replaceFirst e replaceAll. La possibilità di poter passare un “comportamento” a questi metodi è un aspetto molto interessante che permette non solo di scrivere meno codice ma anche di rendere l’applicazione più flessibile.

Immaginiamo ad esempio di avere anche un ulteriore metodo formattaPiediDaKm (l’unità di misura “piede internazionale”: 1 piede = 0,3048 metri). Si potrebbero tenere due variabili del tipo:

Function<MatchResult,String> kmInMigliaFunc = mr -> formattaMigliaDaKm(mr.group(1));
Function<MatchResult,String> kmInPiediFunc = mr -> formattaPiediDaKm(mr.group(1));

Poi in base ad una qualche scelta fatta con della “logica” o dell’input utente, si può passare una o l’altra variabile al replaceAll.