Java Pattern- und Matcher-Klassen
Muster kompilieren und Eingaben in Java mit den Klassen Pattern und Matcher abgleichen.
Reguläre Ausdrücke in Java sind im Paket java.util.regex untergebracht, und nahezu der gesamte Inhalt dieses Pakets besteht aus zwei Klassen: Pattern und Matcher. Ein Pattern ist ein kompilierter regulärer Ausdruck — die Regel. Ein Matcher ist die Engine, die diese Regel auf eine Eingabe anwendet und meldet, was sie gefunden hat. Die Trennung der beiden ermöglicht es, einen Ausdruck einmal zu kompilieren und für Tausende von Eingaben wiederzuverwenden — das ist der Unterschied zwischen schnellem und langsamem Regex-Code.
Dieses Kapitel behandelt das Kompilieren eines Pattern, die Steuerung eines Matcher, den wichtigen Unterschied zwischen find() und matches(), das Erfassen von Gruppen und benannten Gruppen sowie Flags. Wer noch keine Erfahrung mit der Syntax hat, sollte zuerst die Kapitel zur Regex-Einführung und Regex-Syntax lesen.
Pattern: einmal kompilieren, immer wiederverwenden
Pattern.compile(String regex) parst die Regex-Syntax und gibt ein unveränderliches, thread-sicheres Pattern zurück. Die Kompilierung ist der aufwändige Schritt — führe sie daher einmal durch, typischerweise in einem static final-Feld, und teile das Ergebnis. Die Hilfsmethoden von String (matches, replaceAll, split) kompilieren das Muster bei jedem Aufruf neu, was für einen einzelnen Aufruf in Ordnung ist, in einer Schleife jedoch verschwenderisch ist.
// Good: compile once, reuse
private static final Pattern EMAIL =
Pattern.compile("[\\w.+-]+@[\\w.-]+\\.[a-z]{2,}");
// Wasteful in a loop: String.matches recompiles every iteration
for (String s : lines) {
if (s.matches("[\\w.+-]+@[\\w.-]+\\.[a-z]{2,}")) { /* ... */ }
}Beachte die doppelten Backslashes: \\w im Java-Quellcode ist das Regex-Token \w, da der Backslash zunächst Javas eigene String-Escape-Verarbeitung überstehen muss.
Matcher: die zustandsbehaftete Engine
Ein Pattern enthält weder Eingabe noch Position — es ist lediglich die Regel. Der Aufruf von pattern.matcher(input) erzeugt einen Matcher, der an diese Eingabe gebunden ist. Der Matcher trägt den gesamten veränderlichen Zustand: die aktuelle Suchposition, die Grenzen des letzten Treffers und die erfassten Gruppen. Da er zustandsbehaftet ist, ist ein Matcher nicht thread-sicher; jeder Thread sollte seinen eigenen besitzen.
| Methode | Funktion |
|---|---|
matches() | Prüft, ob die gesamte Eingabe mit dem Muster übereinstimmt |
lookingAt() | Prüft, ob die Eingabe am Anfang übereinstimmt (muss nicht bis zum Ende reichen) |
find() | Findet den nächsten Treffer irgendwo in der Eingabe; gibt true zurück und rückt vor |
group() / group(n) | Gibt den gesamten Treffer oder die Erfassungsgruppe n zurück |
start() / end() | Index des ersten Zeichens des Treffers und eines nach dem letzten |
replaceAll(repl) | Ersetzt jeden Treffer, mit $1, $2 Rückverweisen auf Gruppen |
reset() | Setzt den Matcher auf Position null zurück (optional mit neuer Eingabe) |
find() versus matches(): die häufigste Verwechslung
matches() ist an den gesamten String gebunden — es gibt nur dann true zurück, wenn das Muster die gesamte Eingabe verbraucht. find() ist ein Scanner: Es sucht das Muster irgendwo und kann wiederholt aufgerufen werden, um jedes Vorkommen zu durchlaufen.
Pattern p = Pattern.compile("\\d+");
System.out.println(p.matcher("abc123").matches()); // false — whole string isn't digits
System.out.println(p.matcher("abc123").find()); // true — found "123" inside
Matcher m = p.matcher("a1 b22 c333");
while (m.find()) {
System.out.println(m.group() + " @ " + m.start()); // 1@1, 22@4, 333@8
}Ein häufiger Fehler ist der Aufruf von group(), bevor ein erfolgreiches matches()/find() stattgefunden hat — das wirft eine IllegalStateException, da noch kein Treffer vorhanden ist, der ausgelesen werden könnte.
Erfassungsgruppen, benannte Gruppen und Ersetzungen
Klammern in einem regulären Ausdruck erzeugen Erfassungsgruppen, die von links nach rechts ab 1 nummeriert werden (Gruppe 0 ist der gesamte Treffer). Java unterstützt auch benannte Gruppen mit (?<name>...), die über group("name") ausgelesen werden — weitaus lesbarer als das Zählen von Klammern. In Ersetzungsstrings fügen $1 und ${name} den von einer Gruppe erfassten Inhalt ein. Das Kapitel über Regex-Gruppen geht tiefer auf nicht-erfassende Gruppen und Rückverweise ein.
Pattern date = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})");
Matcher m = date.matcher("2024-11-30");
if (m.matches()) {
System.out.println(m.group("year")); // 2024
System.out.println(m.group(3)); // 30 (third numbered group)
}
// Reformat using back-references
System.out.println("2024-11-30".replaceFirst(
"(\\d{4})-(\\d{2})-(\\d{2})", "$3/$2/$1")); // 30/11/2024Flags: Groß-/Kleinschreibung ignorieren, mehrzeilig und mehr
Flags werden als zweites Argument an Pattern.compile übergeben und mit | kombiniert. Die am häufigsten verwendeten sind CASE_INSENSITIVE, MULTILINE (damit ^/$ an Zeilenumbrüchen übereinstimmen) und DOTALL (damit . auch Zeilenumbrüche erfasst). Dieselben Flags können inline mit (?i), (?m), (?s) gesetzt werden. Das Kapitel über Regex-Flags enthält die vollständige Liste und deren Kompromisse.
Pattern p = Pattern.compile("error", Pattern.CASE_INSENSITIVE);
System.out.println(p.matcher("FATAL ERROR").find()); // true
// Equivalent inline form:
Pattern.compile("(?i)error");Ein praktisches Beispiel: Parsen einer Log-Zeile mit einem kompilierten Muster
Dieses Programm kompiliert ein Datumsmuster einmal und führt einen Matcher durch seine Schritte — es durchsucht alle Daten in einem String mit find(), vergleicht find() mit matches(), formatiert über Gruppen-Rückverweise um, teilt bei Leerzeichen auf und liest Werte aus benannten Gruppen aus.
Was man dem Ablauf entnehmen kann:
find()ist ein Scanner mit Gedächtnis. Diewhile (m.find())-Schleife fand beide Daten — an Index6und Index23— weil jeder Aufruf dort fortsetzt, wo der vorherige Treffer endete. So zählt man alle Vorkommen auf, und deshalb kam die Gesamtzahl auf2.matches()ist alles oder nichts.matches whole log?gabfalseaus, weil die Daten in anderen Text eingebettet sind, währendmatches one date?trueausgab, weil"2024-01-15"die gesamte Eingabe ist. Verwendefind()zum Suchen undmatches()zum Validieren.- Nummerierte Gruppen sind während eines Treffers adressierbar. Innerhalb der Schleife gab
m.group(1)nur das vierstellige Jahr (2024) zurück, währendm.group()das gesamte Datum zurückgab — Gruppe0ist der Treffer, Gruppen1..nsind die geklammerten Erfassungen von links nach rechts. - Rückverweise ordnen Text um.
replaceAll("$3/$2/$1")wandelte jedesYYYY-MM-DDinDD/MM/YYYYum und erzeugtestart 15/01/2024 build 30/11/2024 done— die Engine ersetzte jede erfasste Gruppe in die Ersetzungsvorlage. - Benannte Gruppen lesen sich wie Felder. Das Aufteilen von
"a b c"bei\s+fasste die Leerzeichen-Folgen zu3Teilen zusammen, und das benannte Muster ermöglichte es,nm.group("user")undnm.group("host")aliceundw3docs.comnach Namen statt nach fragilen Positionsnummern auszulesen.