Groovy: Strings und Zeichen

Neben dem Rechnen mit Zahlen machen die String-Operationen normalerweise einen Großteil der alltäglichen Programmierung aus. Groovy bietet auch hier einige Erleichterungen und zusätzliche Möglichkeiten, die das Arbeiten mit Texten und Zeichen einfacher, übersichtlicher und sicherer als bei Java machen. Neben zusätzlichen Formen für String-Literale sind dies vor allem die String-Interpolation mit »GStrings« sowie die direkte Einbindung von regulären Ausdrücken in die Groovy-Sprache.

String-Literale

Bearbeiten

Ein normales String-Literal wird im Wesentlichen genauso gebildet wie in Java. Die einzige Abweichung besteht darin, dass als Begrenzungszeichen einfache Anführungszeichen dienen.

groovy> println '\'Alea iacta est!\' sagt der Lateiner.' 
'Alea iacta est!' sagt der Lateiner.

Eingebetteten Anführungszeichen muss ein Backslash vorangestellt werden, die bekannten Escape-Sequenzen für Sonderzeichen und Unicode-Zeichen stehen ebenfalls zur Verfügung. Sie können als Begrenzungszeichen auch doppelte Anführungszeichen verwenden, allerdings kennzeichnen Dollar-Zeichen dann Interpolationen (siehe weiter unten) und müssen ebenfalls mit einem Backslash versehen werden, wenn dies nicht erwünscht ist.

groovy> println "Dem \$-Zeichen muss ein \\-Zeichen vorangestellt werden."
Dem $-Zeichen muss ein \-Zeichen vorangestellt werden.

Schließlich kann auch noch der Slash (Schrägstrich) als Begrenzungszeichen dienen. In einem solchen String-Literal gilt der Backslash nicht als Escape-Zeichen, daher eignet sich diese Form insbesondere für reguläre Ausdrücke, aber auch beispielsweise für Windows-Pfadangaben:

groovy> p = java.util.regex.Pattern.compile(/\s\w+\s/)
groovy> println p
groovy> println (/\C:\Dokumente und Einstellungen\All Users\Dokumente/)
\s\w+\s
\C:\Dokumente und Einstellungen\All Users\Dokumente

Zwei Dinge sind bei in Schrägstrichen eingeschlossenen String-Literalen besonders zu beachten: Das Dollar-Zeichen dient auch hier, sofern es nicht am Ende des String steht, zur Kennzeichnung von String-Interpolationen, und die Sequenz \u leitet weiterhin eine Unicode-Sequenz ein. Beide können nicht ohne weiteres mit Backslash-Zeichen außer Kraft gesetzt werden; in der Praxis ist dies aber kaum von Bedeutung.

Die in Schrägstrichen eingeschlossenen String-Literale sind insbesonder bei regulären Ausdrücken von Vorteil, in denen häufig Backslash-Zeichen vorkommen und die in Java häufig nur mühsam zu formulieren und schwer zu lesen sind (siehe auch weiter unten).

Es gibt keine speziellen Literale für Einzelzeichen, sie können aber ohne Weiteres durch explizite oder implizite Typanpassung gebildet werden:

Character zeichenA = 'A'
Character zeichenB = "B"
def zeichenC = (char)'C'
def zeichenD = 'D' as Character
def zeichenE = 'E'.toCharacter()

String-Literale, die in einfache oder doppelte Anführungszeichen eingeschlossen sind, können auch über mehrere Zeilen reichen. Dazu müssen die Begrenzungszeichen nur verdreifacht werden. Ansonsten unterscheiden sie sich nicht von ihren einzeiligen Varianten.

a = '''Dies ist ein String,
der über mehrere
Zeilen reicht.'''
b = """Dieser Text
reicht ebenfalls
über mehrere Zeilen"""

GDK-Erweiterungen für String und StringBuffer

Bearbeiten

Die Groovy-Standardbibliothek enthält eine Fülle von Erweiterungen für die Klasse String. Beachten Sie, dass alle Operatoren und Methoden für Strings nie den String selbst modifizieren, sondern gegebenenfalls eine neue Instanz liefern. Methoden für StringBuffer, der im Gegensatz zu String veränderlich ist, können dagegen auch Veränderungen am aktuellen Objekt vornehmen. Die ab Java 5.0 eingeführte Klasse StringBuilder ist in Groovy 1.0 noch nicht berücksichtigt.

Operatoren und Methoden zur Textmanipulation

Bearbeiten

Während Java nur einen Operator für Strings kennt, nämlich das +-Zeichen zum Verketten, gibt es in Groovy einige weitere Operatoren, die durch korrespondierende vordefinierte Methoden implementiert sind. Folgende Tabelle zeigt die verfügbaren Operatoren mit Ausnahme der Index-Operatoren, auf die wir weiter unten zu sprechen kommen.

Operator Operanden-Typ Funktionalität
+ Object Verkettet den links stehenden String mit dem in einen String umgewandelten rechts stehenden Objekt.
Beispiel: 'ABC'+123 ergibt 'ABC123'.
<< Object Verkettet den links stehenden String mit dem in einen String umgewandelten rechts stehenden Objekt; das Ergebnis ist ein StringBuffer, mit dem auf gleiche Weise weitere Verkettungen vorgenommen werden können.
Beispiel: 'ABC'<<123 ergibt einen StringBuffer mit dem Inhalt »ABC123«.
- Object Entfernt das erste Vorkommen des in einen String umgewandelten rechts stehenden Objekts aus dem links stehenden String.
Beispiel: 'ABCDABCD'‒'BC' ergibt 'ADABCD'.
* Number Vervielfacht den links stehenden String um die rechts stehende Zahl.
Beispiel: 'AB' * 3 ergibt 'ABABAB'.
++ Erhöht das letzte Zeichen im String um 1.
Beispiel: 'AB'++ oder ++'AB' ergibt 'AC'.
-- Vermindert das letzte Zeichen im String um 1.
Beispiel: 'AB'-- oder --'AB' ergibt 'AA'.

Daneben gibt es noch eine Reihe zusätzlicher Methoden, die das Manipulieren von Zeichenketten etwas vereinfachen.

Die Methoden padLeft(), padRight() und center() verlängern den aktuellen String auf die angegebene Länge, wobei die Füllzeichen im ersten Fall links, im zweiten Fall rechts und im dritten Fall beidseitig hinzugefügt werden. Wenn der String bereits so lang wie gewünscht oder länger ist, wird er unverändert zurückgegeben.

groovy> '|' + 'ABC'.padLeft(5) + '|'
===> | ABC|
groovy> '|' + 'ABC'.padRight(7,'-.') + '|'
===> |ABC-.-.|
groovy> '|' + 'ABC'.center(7) + '|'
===> | ABC |

Die Methode reverse() kehrt die Reihenfolge der Zeichen im aktuellen String um.

groovy> 'Nur du Gudrun'.reverse()
===> nurduG ud ruN

Die Methode replaceAll() durchsucht den aktuellen String anhand eines regulären Ausdrucks und ruft für jedes Vorkommen eine Closure auf. Die Closure erhält als Aufrufargumente die Werte der gefundenen Match-Gruppen übergeben. Das folgende Beispiel setzt vor alle Großbuchstaben einen Unterstrich und ersetzt alle Kleinbuchstaben durch Großbuchstaben.

groovy> 'dateOfMonth'.replaceAll(/([A-Z])?([a-z]*)/) {
  a0,a1,a2 -> (a1==null ? '' : '_'+a1)+a2.toUpperCase() }
===> DATE_OF_MONTH

String-Inhalt prüfen

Bearbeiten

Die Methode count() prüft, wie oft ein Teilstring in dem aktuellen String enthalten ist. Überlappungen werden dabei mitgezählt.

groovy> 'ABCABC'.count 'BC' 
===> 2
groovy> 'AAAA'.count 'AA' 
===> 3

Die Methode eachMatch() nimmt einen String als regulären Ausdruck an und ruft für jede Übereinstimmung im aktuellen String eine Closure auf. Die Closure erhält als Argument ein String-Array mit allen Match-Gruppen. Das folgende Beispiel sucht im aktuellen String alle Zeichenfolgen, die aus einem Groß- und einem oder mehreren Kleinbuchstaben bestehen. Das ausgegebene Array besteht jeweils aus dem gesamten Match und den beiden darin enthaltenen Match-Gruppen.

groovy> ''''Alles leere Worte!'.eachMatch (/([A-Z])([a-z]+)/) { println it }
{"Alles", "A", "lles"}
{"Worte", "W", "orte"}

String und StringBuffer als Liste von Zeichen

Bearbeiten

Eine weitere Gruppe von vordefinierten Methoden ermöglicht es, einen String oder StringBuffer wie eine Liste oder ein Array aus einzelnen Zeichen zu behandeln. Über den Index-Operator können Sie lesend auf einzelne Zeichen oder mehrere Zeichen zuzugreifen.

groovy> text = 'YOGAVORGIRLS'
groovy> println text [0] // Zugriff über Index
groovy> println text [-1] // Negativer Index
groovy> println text [0..3] // Index-Bereich
groovy> println text [2,6,1,5,4,0,7,9,3,8,10,11] // Index-Liste
Y
S
YOGA
AGOY
GROOVYGRAILS

Das obige Beispiel funktioniert in gleicher Weise mit Objekten vom Typ String, StringBuffer, Character-Array und Character-Liste, allerdings sind die Ergebnistypen bei Listenobjekten und Arrays für Index-Bereiche und Index-Listen wiederum Listen und keine Strings. Sie können dies ausprobieren, indem Sie die erste Zeile im obigen Beispiel durch folgende Varianten ersetzen:

groovy> text = new StringBuffer('YOGAVORGIRLS')
groovy> text = ['Y','O','G','A','V','O','R','G','I','R','L','S']
groovy> text = ['Y','O','G','A','V','O','R','G','I','R','L','S']
    as Character[]

Außerdem sind die oben aufgeführten vordefinierten Methoden contains(), count(), reverse() sowie der <<-Operator in gleicher Weise für Strings wie für Listen definiert, sodass auch in dieser Hinsicht eine ähnliche Behandlung möglich ist.

groovy> text = 'NERHEGEB'
groovy> println text.reverse()

Interpolieren mit GStrings

Bearbeiten

Wenn Sie ein String-Literal mit doppelten Anführungszeichen oder Schrägstrichen einkleiden, haben Sie die Möglichkeit, einzelne Werte oder ganze Groovy-Ausdrücke einzufügen. Eine solche, als String-Interpolation bezeichnete Operation wird immer durch ein Dollar-Zeichen ($) eingeleitet. Ein einzelner Wert kann aus einem oder mehreren, mit Punkten verbundenen Namen bestehen.

groovy> zeit = new Date()
groovy> println "Aktuelle Zeit: $zeit"
groovy> println "Aktuelles Datum: $zeit.date.$zeit.month.$zeit.year"
Aktuelle Zeit: Sat Jan 06 22:52:56 CET 2007
Aktuelles Datum: 6.0.107

Komplexere Ausdrücke und solche, auf die unmittelbar Buchstaben oder Ziffern folgen, müssen zusätzlich durch geschweifte Klammern ({}) eingeschlossen werden. Dabei kann es sich durchaus um ein größeres Programmstück handeln; bei mehrzeiligen Strings (die bei Groovy mit drei Anführungszeichen abgegrenzt sein müssen), können sie auch über mehrere Zeilen gehen.

groovy> zeit = new Date()
groovy> println "Aktuelle Zeit: ${zeit.toString()}"
groovy> println "Aktuelles Datum: ${zeit.date}.${1+zeit.month}.${1900+zeit.year}"
Aktuelle Zeit: Sat Jan 06 22:55:27 CET 2007
Aktuelles Datum: 6.1.2007

Wenn sie in einem solchen String ein Dollarzeichen ausgeben möchten, müssen Sie es mit einem vorgestellten Backslash versehen. Das gilt allerdengs nicht für Strings, die in Schrägstriche eingeschlossen sind. In Strings mit einfachen Anführungszeichen sind Interpolationen nicht möglich.

groovy> betrag = 500
groovy> println "Zahlen Sie \$ $betrag"
groovy> println (/Zahlen Sie $ $betrag/)
groovy> println 'Zahlen Sie \$ $betrag'
Zahlen Sie $ 500
Zahlen Sie $ 500
Zahlen Sie $ $betrag

Die Möglichkeit, Text und Programmstücke beliebig zu mischen, erweist sich an vielen Stellen als außerordentlich nützlich. Allerdings gibt es auch hier einige Besonderheiten zu beachten ‒ und es zeigen sich einige zusätzliche Möglichkeiten, die man dem auf den ersten Blick sehr simpel aussehenden Konstrukt zunächst nicht ansieht.

Die Klasse GString

Bearbeiten

Sobald der Groovy-Compiler einen String mit interpoliertem Ausdruck vorfindet, erzeugt er nicht eine String-Instanz, sondern ein Objekt einer von groovy.lang.GString abgeleiteten Klasse; im Grunde haben wir es hier also nicht mit einem Literal sondern mit einer Art Konstruktor zu tun.

Die interpolierten Ausdrücke werden zwar zur Zeit der Erzeugung des Objekts ausgewertet, aber nicht mit den umgebenden konstanten Teilen des String verknüpft. Vielmehr werden die Ergebnisse der ausgewerteten Ausdrücke und die konstanten Stringteile innerhalb des GString-Objekts einzeln aufbewahrt und können auch einzeln mit Hilfe der Listen-Properties strings und values ausgelesen werden.

groovy> x = 123; y = -200
groovy> gstring = "Die Summe von $x und $y ist ${x+y}"
groovy> println gstring
groovy> println "Strings: " + gstring.strings
groovy> println "Werte: " + gstring.values
Die Summe von 123 und -200 ist -77
Strings: {"Die Summe von ", " und ", " ist ", ""}
Werte: {123, -200, -77}

Wie Sie sehen, enthält strings die vier Teilstrings, von denen die Ausdrücke umgeben sind, wobei der letzte leer ist, und values die drei ausgewerteten Integer-Ausdrücke.

Diese Möglichkeit, die Bestandteile eines GString nachträglich auszuwerten, bietet einige interessante Möglichkeiten. Beispielsweise ermöglicht die Groovy-Standardbibliothek mit Hilfe von GStrings äußerst komfortable SQL-Zugriffe auf Datenbanken (siehe Datenbanken).

Beachten Sie aber, dass GStrings wegen dieser eingebetteten Werte anders als normale Java-Strings nicht unveränderbar sind. Das wird sofort klar, wenn man bedenkt, dass die Ergebnisse der eingefügten Ausdrücke unter Umständen noch nachträglich modifiziert werden können.

groovy> wort = new StringBuffer('SINN')
groovy> satz = "Das Wort lautet: $wort"
groovy> println satz
groovy> wort.insert(0,'UN')
groovy> println satz
Das Wort lautet: SINN
Das Wort lautet: UNSINN

Hier ist die Variable wort ein StringBuffer, also ein veränderbares Objekt. Indem wir es, nachdem es in den GString satz eingebunden worden ist, noch einmal verändern, beeinflussen wir auch den Inhalt des GString. In vielen Fällen sind GStrings Objekte mit kurzer Lebenserwartung. Wenn Sie jedoch durch String-Interpolation einen String erzeugen wollen, dem noch ein längeres Leben bevorsteht und der nicht veränderlich sein soll, sollten Sie ihn sicherheitshalber gleich mit toString() oder durch Typanpassung in einen ganz normalen Java-String umwandeln. Auch wenn Sie das Ergebnis als Schlüssel in einer Map benutzen möchten, ist die zu empfehlen. Eine nachträgliche Veränderung der Zutaten kann dem String dann nichts mehr anhaben.

groovy> wort = new StringBuffer('SINN')
groovy> satz = "Das Wort lautet: $wort" as String // satz ist String-Objekt
groovy> println satz
groovy> wort.insert(0,'UN')
groovy> println satz
Das Wort lautet: SINN
Das Wort lautet: SINN

Die Umwandlung in ein String-Objekt kann auch aus Effizienzgründen vorteilhaft sein. Zwar können fast alle String-Methoden und -Operationen in gleicher Weise auch auf GString-Instanzen angewendet werden, da der GString alle Methodenaufrufe, die nicht von ihm selbst ausgeführt werden können, an einen ad hoc mit toString() gebildeten String weiterleitet. Sie können sich leicht vorstellen, dass dieses Neuerzeugen von Strings nicht besonders effizient ist, wenn es häufiger vorkommt.

Reguläre Ausdrücke

Bearbeiten

Groovy bietet wie viele Skriptsprachen eine direkte Unterstützung für die Arbeit mit regulären Ausdrücken durch spezielle Operatoren. Auch die weiter oben schon eingeführte Möglichkeit, String-Literale zu bilden, in denen der Backslash nicht als Escape-Zeichen ausgewertet wird, macht den Umgang mit regulären Ausdrücken wesentlich angenehmer.

Der unäre Operator ~ wandelt ‒ über die für den String-Typ vordefinierte Methode negate() ‒ einen String in ein Pattern-Objekt um.

Jede der folgenden drei Anweisungen erzeugt das gleiche Pattern, das sich zum Prüfen des Formats einer Telefonnummer mit optionaler Vorwahl und optionaler Durchwahl eignet.

groovy> p = java.util.regex.Pattern.compile('(\\(0\\d+\\))?(\\d+\\s)*\\d+(-\\d+)?')
groovy> println p
groovy> p = ~'(\\(0\\d+\\))?(\\d+\\s)*\\d+(-\\d+)?' 
groovy> println p
groovy> p = ~/(\(0\d+\))?(\d+\s)*\d+(-\d+)?/
groovy> println p
(\(0\d+\))?(\d+\s)*\d+(-\d+)?
(\(0\d+\))?(\d+\s)*\d+(-\d+)?
(\(0\d+\))?(\d+\s)*\d+(-\d+)?

Während dieser String-Operator eher praktisch ist, wenn Sie mit Java-Klassen arbeiten, deren Methoden ein Pattern-Objekt erwarten, arbeiten Sie Groovy-intern besser mit zwei speziellen Operatoren, die Ihnen das Arbeiten mit regulären Ausdrücken noch weiter erleichtern.

Der Match-Operator ==~ führt einen Mustervergleich zwischen zwei Strings aus, wobei der zweite String einen regulären Ausdruck enthält.

groovy> println '(0123)45 67-' ==~ /(\(0\d+\))?(\d+\s)*\d+(-\d+)?/
groovy> println '(0123)45 67-1' ==~ /(\(0\d+\))?(\d+\s)*\d+(-\d+)?/
false
true

Dies kann man mit der normalen matches()-Methode der String-Klasse fast genau so bequem haben. Ein wirklicher Fortschritt gegenüber Java ergibt sich aus den Möglichkeiten, die Groovy für das Arbeiten mit Match-Ergebnissen bietet.

Der spezielle Find-Operator =~ liefert ein java.util.regex.Matcher-Objekt, mit dem einzelne mit einen regulären Ausdruck übereinstimmende Teilstrings lokalisiert werden können. Für die weitere Verarbeitung stehen verschiedene vordefinierter Methoden und Operatoren zur Verfügung. Im folgenden Beispiel suchen wir nach Teilstrings, die aus einem oder mehreren Buchstaben und optional nachfolgenden Ziffern bestehen.

groovy> matcher = 'A23 b35 ZZ X14 33' =~ /([a-zA-Z]+)+(\d*)/

Nun können wir beispielsweise die verschiedenen Fundstellen mit einer for-Schleife durchlaufen:

groovy> for (match in matcher) { print "[$match] " }
[A23] [b35] [ZZ] [X14]

Auf die einzelnen Fundstellen kann man auch wie auf die Elemente eines Arrays zugreifen, das Ergebnis ist hier aber jeweils ein Array mit den verschiedenen Match-Gruppen; hier sind dies der gesamte gefundene Teilstring und jeweils die darin enthaltenen Buchstaben und ggf. die Ziffern.

groovy> println matcher[1]
groovy> println matcher[2]
["b35", "b", "35"]
["ZZ", "ZZ", ""]

Schließlich können wir die Suchergebnisse auch mit den verschiedenen Closure-Methoden durchlaufen. Beispielsweise erhält eine Closure mit each() die einzelnen Match-Gruppen direkt als Argumente übergeben.

groovy> matcher.each { x0, x1, x2 -> println "Buchstaben: $x1 / Ziffern: $x2" }
Buchstaben: A / Ziffern: 23
Buchstaben: b / Ziffern: 35
Buchstaben: ZZ / Ziffern: 
Buchstaben: X / Ziffern: 14