Quantificatori Regex in Java
Come funzionano i quantificatori regex Java (*, +, ?, {n,m}) nelle modalità greedy, reluctant e possessive.
Un quantificatore specifica quante volte l'elemento che lo precede può ripetersi. * significa "zero o più volte", + significa "una o più volte", ? significa "zero o una volta", e {n,m} indica un intervallo preciso. Questi concetti sono comuni a tutti i dialetti regex. Ciò che crea confusione in Java è che ogni quantificatore esiste in tre modalità — greedy, reluctant e possessive — ed è la modalità, non il conteggio, a determinare come il motore regex di java.util.regex scorre l'input.
Questa pagina tratta i quattro quantificatori di base, le tre modalità di corrispondenza, il motivo per cui i pattern greedy eccedono nella corrispondenza e come i quantificatori possessive proteggono dal backtracking catastrofico. Si presuppone che tu sappia già come compilare un Pattern ed eseguire un Matcher; in caso contrario, inizia con Pattern e Matcher. Per i metacaratteri da ripetere, consulta le classi di caratteri, e per catturare il testo ripetuto, consulta i gruppi.
I quattro quantificatori di base
Applica un quantificatore a un singolo carattere, a una classe di caratteri o a un gruppo, e controlla la ripetizione dell'elemento immediatamente alla sua sinistra:
| Quantificatore | Ripete l'elemento precedente | Esempio | Corrisponde a |
|---|---|---|---|
* | zero o più volte | ab*c | ac, abc, abbbc |
+ | una o più volte | ab+c | abc, abbc (non ac) |
? | zero o una volta | colou?r | color, colour |
{n} | esattamente n volte | \d{4} | un anno a 4 cifre |
{n,} | n o più volte | \d{2,} | due o più cifre |
{n,m} | tra n e m volte | \d{3,5} | da 3 a 5 cifre |
Pattern.matches("ab*c", "abbbc"); // true — three b's
Pattern.matches("ab+c", "ac"); // false — '+' needs at least one b
Pattern.matches("colou?r", "color");// true — the 'u' is optional
Pattern.matches("\\d{3,5}", "1234");// true — four digits is within 3..5Il greedy è il comportamento predefinito
Un quantificatore senza suffisso è greedy: consuma quanta più input possibile, poi retrocede — restituendo un carattere alla volta — finché il resto del pattern riesce a corrispondere. Ecco perché un ingenuo <.+> su HTML assorbe molto più di un singolo tag:
String html = "<b>one</b>";
Matcher m = Pattern.compile("<.+>").matcher(html);
m.find();
m.group(); // "<b>one</b>" — '.+' ate everything, then backed up to the last '>'Il motore ha prima acquisito l'intera stringa, non ha trovato il > finale, e ha retrocesso finché un > non si è allineato — fermandosi sull'ultimo.
Reluctant: aggiungi ? per prendere il meno possibile
Aggiungendo un ? a qualsiasi quantificatore (*?, +?, ??, {n,m}?), esso diventa reluctant (detto anche lazy): corrisponde al minor numero di ripetizioni prima di espandersi solo se necessario. Questo è ciò che di solito si vuole quando si analizzano token delimitati:
String html = "<b>one</b>";
Matcher m = Pattern.compile("<.+?>").matcher(html);
m.find();
m.group(); // "<b>" — stopped at the first '>'Stesso pattern, un carattere in più, comportamento opposto: il greedy <.+> restituisce l'intera stringa, mentre il reluctant <.+?> restituisce solo il primo tag.
Possessive: aggiungi + e non cedere mai
Aggiungendo un + (*+, ++, ?+, {n,m}+), il quantificatore diventa possessive: acquisisce il più possibile come un greedy, ma si rifiuta di retrocedere. Se il resto del pattern fallisce, l'intera corrispondenza fallisce — non c'è modo di tornare indietro per recuperarla.
// Possessive '.++' eats the final '>' too and won't return it, so no '>' is left
Pattern.compile("<.++>").matcher("<b>one</b>").find(); // falsePerché rinunciare a tale flessibilità? Velocità e sicurezza. Poiché un quantificatore possessive non riconsidere mai, non può cadere nel backtracking catastrofico — l'esplosione esponenziale che blocca un thread su pattern come (a+)+b alimentato da una lunga sequenza di a senza b. Il possessive a++b risponde "nessuna corrispondenza" quasi istantaneamente.
| Modalità | Sintassi | Strategia | Retrocede? |
|---|---|---|---|
| Greedy | X* | prende il massimo, poi cede se necessario | sì |
| Reluctant | X*? | prende il minimo, poi aggiunge se necessario | sì |
| Possessive | X*+ | prende il massimo e lo mantiene | no |
Un esempio pratico: tutte e tre le modalità a confronto
Questo programma esegue lo stesso input attraverso quantificatori greedy, reluctant e possessive, poi esercita l'intervallo {n,m} e un quantificatore di gruppo. Tutto qui è puro java.util.regex del JDK.
Cosa ricavare dall'esecuzione:
- Il greedy
<.+>ha stampato<b>one</b><i>two</i>— l'intera stringa. Ha consumato tutto, poi ha retrocesso fino all'ultimo>, motivo per cui i pattern greedy eccedono nella corrispondenza tra delimitatori. - Il reluctant
<.+?>ha stampato<b>dallo stesso identico input. Il singolo?ha invertito la strategia da "massimo" a "minimo", fermandosi al primo>— la soluzione per analizzare un tag alla volta. - Il possessive
<.++>ha stampatomatches=false. Ha inghiottito il>finale e si è rifiutato di restituirlo, così il>finale nel pattern non aveva nulla da corrispondere e l'intero tentativo è fallito — il prezzo del non retrocedere mai. \d{3,5}ha rifiutato12(no match, cifre insufficienti), ha accettato123e12345integralmente, e su1234567ha corrisposto solo a12345(len 5) — il limite superiore5lo ha bloccato anche se erano disponibili altre cifre.- Il pattern di gruppo
(\w+\s*){2,3}ha corrisposto aalpha beta gamma— tre parole, il massimo — dimostrando che il quantificatore si applicava all'intero gruppo tra parentesi, ea++bha restituitofalsequasi istantaneamente su una lunga sequenza diasenzab, mostrando come i quantificatori possessive evitino il backtracking catastrofico.
Quale modalità dovrei usare?
- Scegli il reluctant (
*?,+?) quando fai la corrispondenza di contenuti tra delimitatori — virgolette, tag, parentesi, blocchi delimitati. Si ferma al primo delimitatore di chiusura invece dell'ultimo, il che è quasi sempre quello che si intende. - Mantieni il greedy (predefinito) quando vuoi genuinamente la corrispondenza più lunga possibile, o quando esiste una sola corrispondenza possibile e il
?/+extra sarebbe solo rumore. - Usa il possessive (
*+,++) come strumento di prestazione e sicurezza su input non controllato. Poiché non retrocede mai, non può innescare il backtracking catastrofico, ma farà anche fallire corrispondenze che un quantificatore greedy avrebbe recuperato — quindi applicalo solo dove sai che il backtracking è inutile.
Un comune rimedio nel mondo reale: un'espressione regolare che funziona su input piccoli ma si blocca su input grandi è di solito un quantificatore greedy all'interno di un gruppo, come (\w+)+. Rendere il quantificatore interno possessive ((\w++)+ o ristrutturare il pattern) elimina l'esplosione esponenziale.