Rheinwerk Computing < openbook > Rheinwerk Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.
 
Inhaltsverzeichnis
Vorwort
1 Neues in Java 8 und Java 7
2 Fortgeschrittene String-Verarbeitung
3 Threads und nebenläufige Programmierung
4 Datenstrukturen und Algorithmen
5 Raum und Zeit
6 Dateien, Verzeichnisse und Dateizugriffe
7 Datenströme
8 Die eXtensible Markup Language (XML)
9 Dateiformate
10 Grafische Oberflächen mit Swing
11 Grafikprogrammierung
12 JavaFX
13 Netzwerkprogrammierung
14 Verteilte Programmierung mit RMI
15 RESTful und SOAP-Web-Services
16 Technologien für die Infrastruktur
17 Typen, Reflection und Annotationen
18 Dynamische Übersetzung und Skriptsprachen
19 Logging und Monitoring
20 Sicherheitskonzepte
21 Datenbankmanagement mit JDBC
22 Java Native Interface (JNI)
23 Dienstprogramme für die Java-Umgebung
Stichwortverzeichnis

Jetzt Buch bestellen
Ihre Meinung?

Spacer
<< zurück
Java SE 8 Standard-Bibliothek von Christian Ullenboom
Das Handbuch für Java-Entwickler
Buch: Java SE 8 Standard-Bibliothek

Java SE 8 Standard-Bibliothek
Pfeil 2 Fortgeschrittene String-Verarbeitung
Pfeil 2.1 Erweitere Zeicheneigenschaften
Pfeil 2.1.1 isXXX(….)-Methoden
Pfeil 2.1.2 Unicode-Blöcke
Pfeil 2.1.3 Unicode-Skripte
Pfeil 2.2 Reguläre Ausdrücke
Pfeil 2.2.1 Pattern.matches(…) bzw. String#matches(…)
Pfeil 2.2.2 Die Klassen Pattern und Matcher
Pfeil 2.2.3 Finden und nicht matchen
Pfeil 2.2.4 Gruppen
Pfeil 2.2.5 Gierige und nicht gierige Operatoren *
Pfeil 2.2.6 Mit MatchResult alle Ergebnisse einsammeln *
Pfeil 2.2.7 Suchen und Ersetzen mit Mustern
Pfeil 2.2.8 Hangman Version 2
Pfeil 2.3 Zerlegen von Zeichenketten
Pfeil 2.3.1 Zerlegen von Zeichensequenzen über String oder Pattern
Pfeil 2.3.2 Mehr vom Scanner
Pfeil 2.3.3 Die Klasse StringTokenizer *
Pfeil 2.3.4 BreakIterator als Zeichen-, Wort-, Zeilen- und Satztrenner *
Pfeil 2.3.5 StreamTokenizer *
Pfeil 2.4 Zeichenkodierungen, XML/HTML-Entities, Base64 *
Pfeil 2.4.1 Unicode und 8-Bit-Abbildungen
Pfeil 2.4.2 Kodierungen über die Klasse String vornehmen
Pfeil 2.4.3 Das Paket java.nio.charset und der Typ Charset
Pfeil 2.4.4 Konvertieren mit OutputStreamWriter-/InputStreamReader-Klassen
Pfeil 2.4.5 XML/HTML-Entities ausmaskieren
Pfeil 2.4.6 Base64-Kodierung
Pfeil 2.5 Ausgaben formatieren
Pfeil 2.5.1 Die Formatter-Klasse *
Pfeil 2.5.2 Formatieren mit Masken *
Pfeil 2.5.3 Format-Klassen
Pfeil 2.5.4 Zahlen, Prozente und Währungen mit NumberFormat und DecimalFormat formatieren *
Pfeil 2.5.5 MessageFormat und Pluralbildung mit ChoiceFormat
Pfeil 2.6 Sprachabhängiges Vergleichen und Normalisierung *
Pfeil 2.6.1 Die Klasse Collator
Pfeil 2.6.2 Effiziente interne Speicherung für die Sortierung
Pfeil 2.6.3 Normalisierung
Pfeil 2.7 Phonetische Vergleiche *
Pfeil 2.8 Zum Weiterlesen
 
Zum Seitenanfang

2.2Reguläre Ausdrücke Zur vorigen ÜberschriftZur nächsten Überschrift

Ein regulärer Ausdruck (engl. regular expression) ist die Beschreibung eines Musters (engl. pattern). Reguläre Ausdrücke werden bei der Zeichenkettenverarbeitung beim Suchen und Ersetzen eingesetzt. Für folgende Szenarien bietet die Java-Bibliothek entsprechende Unterstützung an:

  • Frage nach einer kompletten Übereinstimmung: Passt eine Zeichenfolge komplett auf ein Muster? Wir nennen das match. Die Rückgabe einer solchen Anfrage ist einfach wahr oder falsch.

  • Finde Teil-Strings: Das Pattern beschreibt einen Teil-String, und gesucht sind alle Vorkommen dieses Musters in einem Such-String.

  • Ersetze Teilfolgen: Das Pattern beschreibt Wörter, die durch andere Wörter ersetzt werden.

  • Zerlegen einer Zeichenfolge: Das Muster steht für Trennzeichnen, sodass nach dem Zerlegen eine Sammlung von Zeichenfolgen entsteht.

Ein Pattern-Matcher ist die »Maschine«, die reguläre Ausdrücke verarbeitet. Zugriff auf diese Mustermaschine bietet die Klasse Matcher. Dazu kommt die Klasse Pattern, die die regulären Ausdrücke in einem vorcompilierten Format repräsentiert. Beide Klassen befinden sich im Paket java.util.regex. Um die Sache etwas zu vereinfachen, gibt es bei String zwei kleine Hilfsmethoden, die im Hintergrund auf die Klassen verweisen, um eine einfachere API anbieten zu können; diese nennen sich auch Fassaden-Methoden.

 
Zum Seitenanfang

2.2.1Pattern.matches(…) bzw. String#matches(…) Zur vorigen ÜberschriftZur nächsten Überschrift

Die statische Methode java.util.regex.Pattern.matches(…) und die Objektmethode matches(…) der Klasse String testen, ob ein regulärer Ausdruck eine Zeichenfolge komplett beschreibt.

Wir wollen testen, ob eine Zeichenfolge in einfache Hochkommata eingeschlossen ist:

Ausdruck

Ergebnis

Pattern.matches( "'.*'", "'Hallo Welt'" )

true

"'Hallo Welt'".matches( "'.*'" )

true

Pattern.matches( "'.*'", "''" )

true

Pattern.matches( "'.*'", "Hallo Welt" )

false

Pattern.matches( "'.*'", "'Hallo Welt" )

false

Tabelle 2.1Einfache reguläre Ausdrücke und ihr Ergebnis

Der Punkt im regulären Ausdruck steht für ein beliebiges Zeichen, und der folgende Stern ist ein Quantifizierer, der wahllos viele beliebige Zeichen erlaubt.

Regeln für reguläre Ausdrücke

Für reguläre Ausdrücke existiert eine ganze Menge an Regeln. Während die meisten Zeichen aus dem Alphabet erlaubt sind, besitzen Zeichen wie der Punkt, die Klammer, ein Sternchen und einige weitere Zeichen Sonderfunktionen. So maskiert auch ein vorangestelltes »\« das folgende Sonderzeichen aus, was bei besonderen Zeichen wie ».« oder »\« wichtig ist. Zunächst gilt es, die Anzahl an Wiederholungen zu bestimmen. Dazu dient ein Quantifizierer (auch Wiederholungsfaktor genannt). Drei wichtige gibt es. Für eine Zeichenkette X gilt:

Quantifizierer

Anzahl an Wiederholungen

X?

X kommt einmal oder keinmal vor.

X*

X kommt keinmal oder beliebig oft vor.

X+

X kommt einmal oder beliebig oft vor.

Tabelle 2.2Quantifizierer im Umgang mit einer Zeichenkette X

Eine Sonderform ist X(?!Y) – das drückt aus, dass der reguläre Ausdruck Y dem regulären Ausdruck Xnicht folgen darf (die API-Dokumentation spricht von »zero-width negative lookahead«).

Ausdruck

Ergebnis

Pattern.matches( "0", "0" )

true

Pattern.matches( "0", "1" )

false

Pattern.matches( "0", "00" )

false

Pattern.matches( "0*", "0000" )

true

Pattern.matches( "0*", "01" )

false

Pattern.matches( "0\\*", "01" )

false

Pattern.matches( "0\\*", "0*" )

true

Tabelle 2.3Beispiele für reguläre Ausdrücke mit Wiederholungen

Da in regulären Ausdrücken oftmals ein Bereich von Zeichen, etwa alle Buchstaben, abgedeckt werden muss, gibt es die Möglichkeit, Zeichenklassen zu definieren.

Zeichenklasse

Enthält

[aeiuo]

Zeichen a, e, i, o oder u

[^aeiuo]

nicht die Zeichen a, e, i, o, u

[0-9a-fA-F]

Zeichen 0, 1, 2, …, 9 oder Groß-/Kleinbuchstaben a, b, c, d, e, f

Tabelle 2.4Definition von Zeichenklassen

Das »^« definiert negative Zeichenklassen, also Zeichen, die nicht vorkommen dürfen. Mit dem »-« lässt sich ein Bereich von Zeichen angeben.

Listing 2.1RegExDemo.java, main(), Ausschnitt

System.out.println( Pattern.matches( "[01]*", "0" ) ); // true
System.out.println( Pattern.matches( "[01]*", "01001" ) ); // true
System.out.println( Pattern.matches( "[0123456789]*", "112" ) ); // true

Daneben gibt es vordefinierte Zeichenklassen, die in erster Linie Schreibarbeit ersparen. Die wichtigsten sind:

Zeichenklasse

Enthält

.

jedes Zeichen

\d

Ziffer: [0-9]

\D

keine Ziffer: [^0-9] bzw. [^\d]

\s

Weißraum: [ \t\n\x0B\f\r]

\S

keinen Weißraum: [^\s]

\w

Wortzeichen: [a-zA-Z_0-9]

\W

keine Wortzeichen: [^\w]

\p{Blank}

Leerzeichen oder Tab: [ \t]

\p{Lower}, \p{Upper}

einen Klein-/Großbuchstaben: [a-z] bzw. [A-Z]

\p{Alpha}

einen Buchstaben: [\p{Lower}\p{Upper}]

\p{Alnum}

ein alphanumerisches Zeichen: [\p{Alpha}\p{Digit}]

\p{Punct}

ein Punktzeichen: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~

\p{Graph}

ein sichtbares Zeichen: [\p{Alnum}\p{Punct}]

\p{Print}

ein druckbares Zeichen: [\p{Graph}]

Tabelle 2.5Vordefinierte Zeichenklassen

Weitere vordefinierte Zeichenklassen listet die API-Dokumentation http://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html auf.

[»]Hinweis

Eine ganze Reihe von Zeichen hat Sonderfunktionen und muss mit einem Backslash ausmaskiert werden. Dazu zählen unter anderem der Punkt, runde und geschweifte Klammern. Nach einem Punkt zu suchen geht daher nicht mit ".", sondern mit "\\.".

Bei den Wortzeichen handelt es sich standardmäßig um die ASCII-Zeichen und nicht um deutsche Zeichen mit unseren Umlauten oder allgemeine Unicode-Zeichen. Eine umfassende Übersicht liefert die API-Dokumentation der Klasse java.util.regex.Pattern.

Listing 2.2RegExDemo.java, main(), Ausschnitt

System.out.println( Pattern.matches( "\\d*", "112" ) ); // true
System.out.println( Pattern.matches( "\\d*", "112a" ) ); // false
System.out.println( Pattern.matches( "\\d*.", "112a" ) ); // true
System.out.println( Pattern.matches( ".\\d*.", "x112a" ) ); // true

[+]Tipp

Die Methode contains(…) der String-Klasse testet nur Teilzeichenfolgen, aber diese Zeichenfolge ist kein regulärer Ausdruck (sonst würde so etwas wie contains(".") auch eine völlig andere Bedeutung haben). Wer ein s.contains("pattern") sucht, kann es als s.matches(".*pattern.*") umschreiben.

 
Zum Seitenanfang

2.2.2Die Klassen Pattern und Matcher Zur vorigen ÜberschriftZur nächsten Überschrift

Der Aufruf der Objektmethode matches(String) auf einem String-Objekt bzw. das statische Pattern.matches(String, CharSequence) ist nur eine Abkürzung für die Übersetzung eines Patterns und Anwendung von matches() auf einem Matcher-Objekt.

String#matches(…)

Pattern.matches(…)

public boolean
matches(String regex) {
return Pattern.matches(regex, this);
}
public static boolean
matches(String regex, CharSequence input) {
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(input);
return m.matches();
}

Tabelle 2.6Implementierungen der beiden matches()-Methoden

Während also die Objektmethode matches(String) von String zu Pattern.matches(String, CharSequence) delegiert, steht hinter der statischen Fassadenmethode in Pattern die wirkliche Nutzung der beiden zentralen Klassen Pattern für das Muster und Matcher für die Mustermaschine. Für unser erstes Beispiel Pattern.matches("'.*'", "'Hallo Welt'") hätten wir also äquivalent schreiben können:

Pattern p = Pattern.compile( "'.*'" );
Matcher m = p.matcher( "'Hallo Welt'" );
boolean b = m.matches();

[»]Hinweis

Bei mehrmaliger Anwendung des gleichen Patterns sollte es compiliert gecacht werden, denn das immer wieder nötige Übersetzen über die Objektmethode String#matches(String) bzw. die Klassenmethode Pattern.matches(String, CharSequence) kostet Speicher und Laufzeit.

final class java.util.regex.Pattern
implements Serializable
  • static Pattern compile(String regex)
    Übersetzt den regulären Ausdruck in ein Pattern-Objekt.

  • static Pattern compile(String regex, int flags)
    Übersetzt den regulären Ausdruck in ein Pattern-Objekt mit Flags. Als Flags sind CASE_INSENSITIVE, MULTILINE, DOTALL, UNICODE_CASE, CANON_EQ und UNICODE_CHARACTER_CLASS erlaubt.

  • int flags()
    Liefert die Flags, auf die geprüft wird.

  • Matcher matcher(CharSequence input)
    Liefert ein Matcher-Objekt, das prüft.

  • static boolean matches(String regex, CharSequence input)
    Liefert true, wenn der reguläre Ausdruck regex auf die Eingabe passt.

  • static Stringquote(String s)
    Maskiert die Metazeichen/Escape-Sequenzen aus. So liefert Pattern.quote("*.[\\d") den String \Q*.[\d\E.

  • String pattern()
    Liefert den regulären Ausdruck, den das Pattern-Objekt repräsentiert.

Zwei weitere split(…)-Methoden finden sich bei String und Pattern; Abschnitt 2.3.1, »Zerlegen von Zeichensequenzen über String oder Pattern«, kommt darauf zurück.

final class java.util.regex.Matcher
implements MatchResult
  • boolean matches()
    Testet den gesamten bei Pattern übergebenen String auf Übereinstimmung.

  • Matcher reset()
    Setzt die Zustände vom Matcher zurück.

  • Matcher reset(CharSequence input)
    Setzt die Zustände zurück und weist eine neue Zeichenfolge zu. Normalerweise bekommt der Matcher den String von Pattern übergeben, doch mit dieser Methode lässt sich eine neue zu testende Zeichenfolge zuweisen.

  • boolean lookingAt()
    Schaut, ob der Anfang der Eingabe mit dem regulären Ausdruck übereinstimmt.

final class java.lang.String
implements Serializable, Comparable<String>, CharSequence
  • boolean matches(String regex)
    Äquivalent zu Pattern.matches(regex, this /*String*/).

Pattern-Flags *

Die Flags sind in speziellen Situationen ganz hilfreich, etwa wenn die Groß-/Kleinschreibung keine Rolle spielt oder sich die Suche über eine Zeile erstrecken soll. Doch Java zwingt uns nicht, die Pattern-Klasse zu nutzen, um die Flags einsetzen zu können, sondern erlaubt es, mit einer speziellen Schreibweise die Flags auch im regulären Ausdruck selbst anzugeben, was die Nutzung bei String#matches(String) ermöglicht.

Flag in der Pattern-Klasse

Eingebetteter Flag-Ausdruck

Pattern.CASE_INSENSITIVE

(?i)

Pattern.COMMENTS

(?x)

Pattern.MULTILINE

(?m)

Pattern.DOTALL

(?s)

Pattern.UNICODE_CASE

(?u)

Pattern.UNICODE_CHARACTER_CLASS

(?U)

Pattern.UNIX_LINES

(?d)

Tabelle 2.7Pattern-Flags

In einem regulären Ausdruck sind die Varianten rechts sehr praktisch, da sie an unterschiedlichen Positionen ein- und ausgeschaltet werden können. Ein nach dem Fragezeichen platziertes Minus stellt die Option wieder ab, etwa "(?i)jetzt insensitive(?-i) wieder sensitive". Mehrere Flag-Ausdrücke lassen sich auch zusammensetzen, etwa zu "(?ims)".

In der Praxis häufiger im Einsatz sind Pattern.DOTALL/(?s), Pattern.CASE_INSENSITIVE/(?i) und Pattern.MULTILINE/(?m). Es folgen Beispiele, wobei wir MULTILINE bei den Wortgrenzen vorstellen.

Standardmäßig matcht der ».« kein Zeilenendezeichen, sodass ein regulärer Ausdruck einen Zeilenumbruch nicht erkennt. Das lässt sich mit dem Pattern.DOTALL-Flag bzw. (?s) ändern.

[zB]Beispiel

Die Auswirkung vom DOTALL bzw.(?s):

System.out.println( "wau wau miau".matches( "wau.+wau.*" ) ); // true
System.out.println( "wau\nwau miau".matches( "wau.+wau.*" ) ); // false
System.out.println( "wau wau miau".matches( "(?s)wau.+wau.*" ) ); // true
System.out.println( "wau\nwau miau".matches( "(?s)wau.+wau.*" ) ); // true

Quantifizierer und Wiederholungen *

Neben den Quantifizierern ? (einmal oder keinmal), * (keinmal oder beliebig oft) und + (einmal oder beliebig oft) gibt es drei weitere Quantifizierer, die es erlauben, die Anzahl eines Vorkommens genauer zu beschreiben:

  • X{n}. X muss genau n-mal vorkommen.

  • X{n,}. X kommt mindestens n-mal vor.

  • X{n,m}. X kommt mindestens n-, aber maximal m-mal vor.

[zB]Beispiel

Eine E-Mail-Adresse endet mit einem Domain-Namen, der zwei oder drei Zeichen lang ist:

Static Pattern p = Pattern.compile( "[\\w|-]+@\\w[\\w|-]*\\.[a-z]{2,3}" );

Ränder und Grenzen testen *

Die bisherigen Ausdrücke waren nicht ortsgebunden, sondern haben geprüft, ob es irgendwo im String eine Übereinstimmung gibt. Dateiendungen zum Beispiel sind aber – wie der Name schon sagt – am Ende zu prüfen, genauso wie ein URL-Protokoll wie »http://« am Anfang stehen muss. Um diese Anforderungen mit berücksichtigen zu können, können bestimmte Positionen mit in einem regulären Ausdruck gefordert werden. Die Pattern-API erlaubt folgende Matcher:

Matcher

Bedeutung

^

Beginn einer Zeile

$

Ende einer Zeile

\b

Wortgrenze

\B

keine Wortgrenze

\A

Beginn der Eingabe

\Z

Ende der Eingabe ohne Zeilenabschlusszeichen wie \n oder \r

\z

Ende der Eingabe mit allen Zeilenabschlusszeichen

\G

Ende des vorherigen Matches, sehr speziell für iterative Suchvorgänge

Tabelle 2.8Erlaubte Matcher

Wichtig ist zu verstehen, dass diese Matcher keine »Breite« haben, also nicht wirklich ein Zeichen oder eine Zeichenfolge matchen, sondern lediglich die Position beschreiben.

Die Matcher ^ und $ lösen gut das Problem mit den Dateiendungen und HTTP-Protokollen und leisten gute Dienste bei bestimmten Löschanweisungen.

[zB]Beispiel

Die String-Methode trim() schneidet den Weißraum vorne und hinten ab. Mit replaceAll(…) und den Matchern für den Beginn und das Ende einer Zeile ist schnell ein Ausdruck gefunden, der nur den Weißraum vorne oder nur hinten entfernt:

String s = " \tWo ist die Programmiersprache des Lächelns?\t\t ";
String ltrim = s.replaceAll( "^\\s+", "" );
String rtrim = s.replaceAll( "\\s+$", "" );
System.out.printf( "'%s'%n", ltrim ); // 'Wo ist die Programmiersprache des
// Lächelns? '
System.out.printf( "'%s'%n", rtrim ); // ' Wo ist die Programmiersprache des
// Lächelns?'

Der Matcher \b ist nützlich, wenn es darum geht, ein Wort umrandet von Weißraum in einer Teilzeichenkette zu finden. In der Zeichenkette »Spaß in China innerhalb der Grenzen« wird die Suche nach »in« drei Fundstellen ergeben, aber \bin\b nur eine und \bin\B auch eine, und zwar »innerhalb«. Es matcht demnach ein \b genau die Stelle, bei der ein \w auf ein \W folgt (bzw. andersherum).

Multiline-Modus *

Normalerweise sind ^ und $nicht zeilenorientiert, das heißt, es ist ihnen egal, ob im String Zeilenumbruchzeichen wie \n oder \r vorkommen oder nicht. Mitunter soll der Test aber lokal auf einer Zeile stattfinden – hierzu muss der Multiline-Modus aktiviert werden.

[zB]Beispiel

Teste, ob eine E-Mail die Zeile »Hi,« enthält:

System.out.println( "Hi,".matches( ".*^Hi,$.*" ) );
System.out.println( "Fwd:\nHi,mir geht's gut!".matches( ".*^Hi,$.*" ) );
System.out.println( "Fwd:\nHi,\nmir geht's gut!".matches( "(?sm).*^Hi,$.*" ) );

Der Test auf ».*^Hi,$.*« gibt im ersten Fall true zurück, da der String wirklich matcht und wir auch überhaupt keinen Zeilentrenner haben, der uns Probleme bereiten könnte. Die zweite Zeile aber liefert false, da sie global mit »Fwd« und nicht mit »Hi« beginnt und mit »!« endet statt mit einem Komma. Führen wir den Test mit der Option (?sm) zeilenweise durch und überspringen wir die Zeilentrenner, dann ist das Ergebnis true, denn die zweite Zeile in

Fwd:
Hi,
mir geht's gut!

passt genau auf unseren regulären Ausdruck.

Der Multiline-Modus erklärt auch den Grund, warum es gleich mehrere Grenz-Matcher gibt. Die Matches \A und \Z bzw. \z sind im Prinzip wie ^ und $, unterscheiden sich aber dann, wenn der Multiline-Modus aktiviert ist. Dann arbeiten (wie im Beispiel) ^ und $ zeilenorientiert, \A und \Z bzw. \z aber nie – die letzten drei Matcher kennen Zeilentrenner überhaupt nicht. Damit ist "Fwd:\nHi,\nalles OK!".matches("(?sm).*\\AHi,\\Z.*") auch trotz (?sm) ganz einfach false.

Es bleiben \z und \Z. Sie unterscheiden, ob bei Zeilen, die abschließende Zeilentrenner wie \n oder \r besitzen, diese Zeilentrenner mit zum Match gehören oder nicht. Das \z ist wie $ ein Matcher auf das absolute Ende inklusive aller Zeilentrenner. Das große \Z ignoriert am Ende stehende Zeilentrenner, sodass sozusagen der Match schon vorher zu Ende ist.

[zB]Beispiel

Das Trennzeichen beim split(…) soll einmal \z und einmal \Z sein:

String[] tokens1 = "Lena singt\r\n".split( "\\z" );
String[] tolens2 = "Lena singt\r\n".split( "\\Z" );
System.out.printf( "%d %s%n", tokens1.length, Arrays.toString( tokens1 ) );
System.out.printf( "%d %s%n", tolens2.length, Arrays.toString( tolens2 ) );

Bei \z gehören alle Zeilentrenner zum String, und daher ist die Ausgabe:

1 [Lena singt

]

Die zweite Ausgabe ist:

2 [Lena singt,
]

Und die abschließenden Zeilentrenner sind ein zweites Token.

 
Zum Seitenanfang

2.2.3Finden und nicht matchen Zur vorigen ÜberschriftZur nächsten Überschrift

Bisher haben wir mit regulären Ausdrücken lediglich festgestellt, ob eine Zeichenfolge vollständig auf ein Muster passt. Doch genau genommen hat Matcher drei Aufgaben:

  • Abgleich: Das Pattern muss auf die gesamte Eingabe passen. Das übernimmt matches().

  • Anschauen: Die Eingabe muss mit dem Muster beginnen. Das prüft lookingAt().

  • Finden: Das Muster muss irgendwie in der Eingabe vorkommen.

Die Matcher-Klasse kann also feststellen, ob sich eine durch ein Muster beschriebene Teilfolge im String befindet. Dazu dient die Methode find(…). Sie hat zwei Aufgaben: Zunächst sucht sie nach einer Fundstelle und gibt bei Erfolg true zurück. Das Nächste ist, dass jedes Matcher-Objekt einen Zustand mit Fundstellen besitzt, den find() aktualisiert. Einem Matcher-Objekt entlockt die Methode group(…) den erkannten Substring, und start(…)/end(…) liefert die Positionen. Wiederholte Aufrufe von find(…) setzen die Positionen weiter:

Listing 2.3RegExAllNumbers.java, main()

String s = "'Demnach, welcher verheiratet, der tut wohl; welcher aber " +
"nicht verheiratet, der tut besser.' 1. Korinther 7, 38";
Matcher matcher = Pattern.compile( "\\d+" ).matcher( s );
while ( matcher.find() )
System.out.printf( "%s an Position [%d,%d]%n",
matcher.group(),
matcher.start(), matcher.end() );

Die Ausgabe des Zahlenfinders ist:

1 an Position [94,95]
7 an Position [107,108]
38 an Position [110,112]

Die gefundene Gruppe und die Start-/Endposition des letztes Ergebnisses liefern group(…), start(…) und end(…). Die Methoden gibt es in drei Versionen. Für die komplette Übereinstimmung sind es group(), start(), end(), und matcher.group() entspricht dem Teil-String von matcher.start(g) bis matcher.end(g). Gruppen können mit einem Index bzw. mit einem Namen versehen werden, auf die dann group(int), group(String) zugreifen, und die Position liefern start(int)/start(String) und end(int)/end(String).

[zB]Beispiel

Da es in der String-Klasse zwar ein contains(CharSequence), aber kein containsIgnoreCase(CharSequence) gibt, lässt sich für diesen Zweck entweder ein Ausdruck wie s1.toLowerCase().contains(s2.toLowerCase()) formen oder ein Pattern-Flag verwenden:

String s1 = "Prince Michael I, Paris, Prince Michael II (Blanket)";
String s2 = "PARIS";
boolean in = Pattern.compile( Pattern.quote( s2 ),
Pattern.CASE_INSENSITIVE ).matcher( s1 ).find();
System.out.println( in ); // true
 
Zum Seitenanfang

2.2.4Gruppen Zur vorigen ÜberschriftZur nächsten Überschrift

In regulären Ausdrücken lassen sich Teilausdrücke in runde Klammern setzen, und diese bilden dann Gruppen; im Englischen heißen sie capturing groups. Gruppen haben zwei Vorteile:

  • An die Gruppe gesetzte Operationen wirken für die Gruppe. Beispiel: Der Ausdruck (\d\d)+ steht für eine gerade Anzahl von Ziffern.

  • Eine Gruppe fängt einen Teil-String, und auf den lässt sich gezielt zurückgreifen.

[zB]Beispiel

Ein String enthält zwei Zahlen, die mit einem Doppelpunkt getrennt sind. Wir interessieren uns für die erste und zweite Zahl:

Matcher matcher = Pattern.compile( "(\\d+):(\\d+)").matcher( "1:123" );
if ( matcher.find() )
System.out.println( matcher.group(1) + "," + matcher.group(2) ); // 1,123

Der Trick ist also, bei group(int) einen Index anzugeben; der beginnt bei 1.

[zB]Beispiel

Ein String aus einer Datei enthält einen Block in geschweiften Klammern. Wir interessieren uns für den Block:

String input = "line1\n{line2\nline3}\nline4";
Matcher matcher = Pattern.compile( "\\{(.*)\\}",
Pattern.MULTILINE | Pattern.DOTALL ).matcher( input );
if ( matcher.find() )
System.out.println( matcher.group( 1 ) ); // line1, dann Umbruch, line2

Bei der Angabe ohne Index, also group(), ist die Rückgabe alles, was find() gefunden hat. Die Fundstellen sind natürlich Teil-Strings der gesamten Gruppe.

 
Zum Seitenanfang

2.2.5Gierige und nicht gierige Operatoren * Zur vorigen ÜberschriftZur nächsten Überschrift

Die drei Operatoren ?, * und + haben die Eigenschaft, die längste mögliche Zeichenfolge abzudecken – das nennt sich gierig (engl. greedy). Deutlich wird diese Eigenschaft bei dem Versuch, in einem HTML-String alle fett gesetzten Teile zu finden. Gesucht ist also ein Ausdruck, der im String

String string = "Echt <b>fett</b>. <b>Cool</b>!";

die Teilfolgen <b>fett</b> und <b>Cool</b> erkennt. Der erste Versuch für ein Programm könnte so aussehen:

Pattern pattern = Pattern.compile( "<b>.*</b>" );
Matcher matcher = pattern.matcher( string );
while ( matcher.find() )
System.out.println( matcher.group() );

Nun ist die Ausgabe aber <b>fett</b>. <b>Cool</b>! Das verwundert nicht, denn mit dem Wissen, dass * gierig ist, passt <b>.*</b> auf die Zeichenkette vom ersten <b> bis zum letzten </b>.

Die Lösung ist der Einsatz eines nicht gierigen Operators (auch genügsam, zurückhaltend, non-greedy oder reluctant genannt). In diesem Fall wird hinter den Qualifizierer einfach ein Fragezeichen gestellt:

Gieriger Operator

Nicht gieriger Operator

X?

X??

X*

X*?

X+

X+?

X{n}

X{n}?

X{n,}

X{n,}?

X{n,m}

X{n,m}?

Tabelle 2.9Gierige und nicht gierige Operatoren

Mit einem solchen nicht gierigen Operator lösen wir einfach das Fettproblem:

Listing 2.4RegExFindBold.java, main()

Pattern pattern = Pattern.compile( "<b>.*?</b>" );
Matcher matcher = pattern.matcher( "Echt <b>fett</b>. <b>Cool</b>!" );
while ( matcher.find() )
System.out.println( matcher.group() );

Wie gewünscht ist die Ausgabe:

<b>fett</b>
<b>Cool</b>
 
Zum Seitenanfang

2.2.6Mit MatchResult alle Ergebnisse einsammeln * Zur vorigen ÜberschriftZur nächsten Überschrift

Die Schnittstelle java.util.regex.MatchResult deklariert Operationen, die Zugriff auf das Ergebnis (String, Startposition, Endposition, Anzahl der Gruppen) eines Matches ermöglichen. Ein Matcher-Objekt wird dafür mit toMatchResult() nach dem MatchResult-Objekt gefragt.

Ein einfaches Beispiel verdeutlicht die Arbeitsweise: Die eigene statische Utility-Methode findMatches() soll für ein Muster und eine Zeichenkette alle Ergebnisse zurückliefern:

Listing 2.5MatchResultDemo.java, Teil 1

static Iterable<MatchResult> findMatches( String pattern, CharSequence s ) {
Collection<MatchResult> results = new ArrayList<>();

for ( Matcher m = Pattern.compile(pattern).matcher(s); m.find(); )
results.add( m.toMatchResult() );

return results;
}
Die Matcher-Klasse implementiert die MatcherResult-Schnittstelle.

Abbildung 2.1Die Matcher-Klasse implementiert die MatcherResult-Schnittstelle.

Die Methode liefert ein einfaches Iterable zurück, was in unserem Beispiel ausreicht, um die Methode auf der rechten Seite des Doppelpunktes vom erweiterten for nutzen zu können. Vor dem Schleifendurchlauf übersetzt compile(…) den Muster-String in ein Pattern-Objekt, und matcher(…) gibt Zugang zum konkreten Mustererkenner, also Matcher-Objekt. Die Bedingung der Schleife ist so, dass pro Durchlauf ein Muster erkannt wird. Im Rumpf der Schleife sammelt die Ergebnisliste die MatchResult-Objekte, die die Funddaten repräsentieren. Nach Ablauf der Schleife liefert die Methode die gesammelten Objekte zurück. Das Programm greift mit Iterable, Collection und ArrayList auf Typen aus den Datenstrukturen zurück.

Ein paar Programmzeilen zeigen schnell die Möglichkeiten. Ein einfaches Muster soll für ISBN-10-Nummern stehen – ohne Leerzeichen oder Bindestriche:

Listing 2.6MatchResultDemo.java, Teil 2

String pattern = "\\d{9,10}[\\d|x|X]";
String s = "Insel: 3898425266, Reguläre Ausdrücke: 3897213494";

for ( MatchResult r : findMatches( pattern, s ) )
System.out.println( r.group() + " von " + r.start() + " bis " + r.end() );

Das Ergebnis auf der Konsole ist:

3898425266 von 7 bis 17
3897213494 von 39 bis 49

Die Informationen in einem MatchResult entsprechen also einem Zustand eines Matchers während des Parsens, genauer gesagt nach dem Erkennen einer Zeichenfolge. Daher implementiert auch die Klasse Matcher die Schnittstelle MatchResult.

 
Zum Seitenanfang

2.2.7Suchen und Ersetzen mit Mustern Zur vorigen ÜberschriftZur nächsten Überschrift

Von der Pattern/Matcher-Klasse haben wir bisher zwei Eigenschaften kennengelernt: zum einen, wie sie prüft, ob eine komplette Zeichenkette auf ein Muster passt, und zum anderen die Suchmöglichkeit, dass find(…) uns sagt, an welchen Stellen ein Muster in einer Zeichenkette vorkommt. Für den zweiten Fall gibt es noch eine Erweiterung, dass nämlich die Pattern-Klasse die Fundstellen nicht nur ermittelt, sondern sie auch durch etwas anderes ersetzen kann.

[zB]Beispiel

In einem String sollen alle Nicht-JVM-Sprachen ausgepiept werden:

String text = "Ich mag Java, Groovy und auch ObjectiveC und PHP.";
Matcher matcher = Pattern.compile("ObjectiveC|PHP" ).matcher( text );
StringBuffer sb = new StringBuffer();
while ( matcher.find() )
matcher.appendReplacement( sb, "[PIEP]" );
matcher.appendTail( sb );
System.out.println( sb ); // Ich mag Java, Groovy und auch [PIEP] und [PIEP].

Um mit dem Mechanismus »Suchen und Ersetzen« zu arbeiten, wird zunächst ein StringBuffer aufgebaut, denn in dem echten String kann Pattern die Fundstellen nicht ersetzen. (Leider ist ein StringBuffer nötig, die API akzeptiert keinen StringBuilder.) Erkennt der Matcher ein Muster, ersetzt appendReplacement(…) es durch eine Alternative, die in den StringBuffer kommt. So wächst der StringBuffer von Schritt zu Schritt. Nach der letzten Fundstelle setzt appendTail(…) das noch verbleibende Teilstück an den StringBuffer.

Toll an appendReplacement(…) ist, dass die Ersetzung nicht einfach nur ein einfacher String ist, sondern dass er mit $ Zugriff auf die Suchgruppe hat. Damit lassen sich sehr elegante Lösungen bauen. Nehmen wir an, wir müssen in einer Zeichenkette alle URLs in HTML-Hyperlinks konvertieren. Dann rahmen wir einfach jede Fundstelle in die nötigen HTML-Tags ein. In Quellcode sieht das so aus:

Listing 2.7RegExSearchAndReplace.java, main()

String text = "Hi, schau mal bei http://stackoverflow.com/ " +
"oder http://www.tutego.de/ vorbei.";
String regex = "http://[a-zA-Z0-9\\-\\.]+\\.[a-zA-Z]{2,3}(\\S*)?";
Matcher matcher = Pattern.compile( regex ).matcher( text );
StringBuffer sb = new StringBuffer( text.length() );

while ( matcher.find() )
matcher.appendReplacement( sb, "<a href=\"$0\">$0</a>" );

matcher.appendTail( sb );

System.out.println( sb );

Der StringBuffer enthält dann zum Schluss "Hi, schau mal bei <a href="http://stackoverflow.com/">http://stackoverflow.com/</a> oder <a href="http://www.tutego.de/">http://www.tutego.de/</a> vorbei.". (Der gewählte reguläre Ausdruck für URLs ist kurz, aber nicht vollständig. Für das Beispiel spielt das aber keine Rolle.)

[»]Hinweis

Der Ersetzungsausdruck "<a href=\"$0\">$0</a>" enthält mit $ Steuerzeichen für den Matcher. Wenn die Ersetzung aber überhaupt nicht mit $n auf das gefundene Wort zurückgreift, sollten die beiden Sonderzeichen \ und $ ausmaskiert werden. Auf diese Weise werden merkwürdige Fehler vermieden, wenn doch in der Ersetzung ein Dollar-Zeichen oder ein Backslash vorkommt. Das Ausmaskieren übernimmt die Methode quoteReplacement(…), sodass sich zum Beispiel Folgendes ergibt:

matcher.appendReplacement( sb, Matcher.quoteReplacement( replacement ) );
 
Zum Seitenanfang

2.2.8Hangman Version 2 Zur vorigen ÜberschriftZur nächsten Überschrift

Im ersten Band der Insel wurde ein Hangman-Spiel vorgestellt, das Ersetzungen ohne reguläre Ausdrücke vornimmt. Mit regulären Ausdrücken lässt sich eine ganz spezielle Aufgabe unseres Hangman-Spiels noch verbessern. Wir hatten die Aufgabe, dass ungeratene Zeichen durch einen Unterstrich ersetzt werden. Diese Ersetzung kann sehr gut replaceAll(…) übernehmen:

Listing 2.8Hangman2.java

import java.util.*;

public class Hangman2 {
@SuppressWarnings( "resource" )
public static void main( String[] args ) {
String[] hangmanWords = { "samoa", "tonga", "fiji", "vanuatu" };
String hangmanWord = hangmanWords[ new Random().nextInt( hangmanWords.length ) ];

String usedChars = "";
String guessedWord = hangmanWord.replaceAll( ".", "_" );

for ( int guesses = 0; ; guesses++ ) {
if ( guesses == 10 ) {
System.out.printf( "Nach 10 Versuchen ist jetzt Schluss. Sorry! Apropos,
das Wort war '%s'.", hangmanWord );
break;
}

System.out.printf( "Runde %d. Bisher geraten: %s. Was wählst du für
ein Zeichen?%n", guesses, guessedWord );
char guessedChar = new java.util.Scanner( System.in ).next().charAt( 0 );

if ( usedChars.indexOf( guessedChar ) >= 0 )
System.out.printf( "%c hast du schon mal getippt!%n", guessedChar );
else { // Zeichen wurde noch nicht benutzt
usedChars += guessedChar;
if ( hangmanWord.indexOf( guessedChar ) >= 0 ) {
guessedWord = hangmanWord.replaceAll( "[^"+usedChars+"]", "_" );
if ( guessedWord.contains( "_" ) )
System.out.printf( "Gut geraten, '%s' gibt es im Wort. Aber es fehlt
noch was!%n", guessedChar );
else {
System.out.printf( "Gratulation, du hast das Wort '%s' erraten!",
hangmanWord );
break;
}
}
else { // hangmanWord.indexOf( c ) == -1
System.out.printf( "Pech gehabt, %c kommt im Wort nicht vor!%n",
guessedChar );
}
}
}
}
}

 


Ihre Meinung

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.

<< zurück
 Zum Rheinwerk-Shop
Zum Rheinwerk-Shop: Java SE 8 Standard-Bibliothek Java SE 8 Standard-Bibliothek
Jetzt Buch bestellen

 Buchempfehlungen
Zum Rheinwerk-Shop: Java ist auch eine Insel
Java ist auch eine Insel


Zum Rheinwerk-Shop: Professionell entwickeln mit Java EE 8
Professionell entwickeln mit Java EE 8


Zum Rheinwerk-Shop: Besser coden
Besser coden


Zum Rheinwerk-Shop: Entwurfsmuster
Entwurfsmuster


Zum Rheinwerk-Shop: IT-Projektmanagement
IT-Projektmanagement


 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und der Schweiz
InfoInfo

 
 


Copyright © Rheinwerk Verlag GmbH 2018
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das Openbook denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt.
Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

 
Nutzungsbestimmungen | Datenschutz | Impressum

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de

Cookie-Einstellungen ändern