Galileo Computing <openbook>
Galileo Computing - Programming the Net
Galileo Computing - Programming the Net


Java ist auch eine Insel von Christian Ullenboom
Programmieren für die Java 2-Plattform in der Version 1.4
Buch: Java ist auch eine Insel - Zum Katalog
gp Kapitel 4 Der Umgang mit Zeichenketten
  gp 4.1 Strings und deren Anwendung
    gp 4.1.1 String-Objekte für konstante Zeichenketten
    gp 4.1.2 String-Objekte verraten viel
    gp 4.1.3 Gut, dass wir verglichen haben
    gp 4.1.4 Stringteile extrahieren
    gp 4.1.5 Veränderte Strings liefern
    gp 4.1.6 Typen in Zeichenketten konvertieren
  gp 4.2 Veränderbare Zeichenketten mit der Klasse StringBuffer
    gp 4.2.1 Anlegen von StringBuffer-Objekten
    gp 4.2.2 Die Länge eines StringBuffer-Objekts lesen und setzen
    gp 4.2.3 Daten anhängen
    gp 4.2.4 Zeichen(folgen) setzen, erfragen, löschen
  gp 4.3 Vergleiche von Zeichenketten als String und StringBuffer
    gp 4.3.1 Sollte es ein equals() und hash() bei StringBuffer geben?
  gp 4.4 Ein paar kleine Helfer
    gp 4.4.1 Strings einer gegebenen Länge erzeugen und rechtsbündig ausgeben
    gp 4.4.2 Teile im String ersetzen
  gp 4.5 Zeichenkodierungen umwandeln
  gp 4.6 Sprachabhängiges Vergleichen mit der Collator–Klasse
    gp 4.6.1 Effiziente interne Speicherung für Sortierung
  gp 4.7 Die Klasse StringTokenizer
  gp 4.8 StreamTokenizer
  gp 4.9 Formatieren mit Format-Objekten
    gp 4.9.1 Ausgaben formatieren
    gp 4.9.2 Dezimalzahlformatierung
  gp 4.10 Reguläre Ausdrücke
    gp 4.10.1 Splitten von Zeichenketten
    gp 4.10.2 split() in String
    gp 4.10.3 Das Paket gnu.regexp
  gp 4.11 Überprüfung der E-Mail-Adressen und Kreditkarteninformationen
    gp 4.11.1 Gültige E-Mail-Adressen
    gp 4.11.2 Kreditkartennummern testen

Kapitel 4 Der Umgang mit Zeichenketten

Ohne Unterschied macht Gleichheit keinen Spaß.
– Dieter Hildebrandt


Galileo Computing

4.1 Strings und deren Anwendung  downtop

Ein String ist eine Sammlung von Zeichen, die im Speicher geordnet abgelegt werden. Die Zeichen sind einem Zeichensatz entnommen, der in Java dem Unicode-Standard entspricht. In Java ist eine Symbiose zwischen String als Objekt und String als eingebauten Datentyp vorgenommen worden. Die Sprache ermöglicht zwar die direkte Konstruktion von String-Objekten aus String-Literalen (Zeichenketten in doppelten Anführungszeichen) und Konkatenation (Aneinanderreihung von Strings) von mehreren Strings, aber alle weiteren Operationen sind als Methoden in den Klassen String und StringBuffer realisiert. Die Klasse String repräsentiert Zeichenketten, die sich nicht ändern, zum Beispiel in einem println()-Aufruf. In Strings diesen Typs können wir suchen und vergleichen, aber es können keine Zeichen im String verändert werden. Es gibt einige Methoden, die scheinbar Veränderungen an Strings vornehmen, aber sie erzeugen in Wahrheit neue String-Objekte, die die veränderten Zeichenreihen repräsentieren. So entsteht beim Aneinanderhängen zweier String-Objekte als Ergebnis ein drittes String-Objekt für die zusammengefügte Zeichenreihe. Die Klasse StringBuffer repräsentiert im Gegensatz dazu dynamische, beliebig änderbare Zeichenreihen.

Die Klassen String und StringBuffer abstrahieren die Funktionsweise und die Speicherung von Zeichenketten. Sie entsprechen der idealen Umsetzung von objektorientierter Programmierung. Die Daten werden gekapselt (die tatsächliche Zeichenkette ist in der Klasse als privates Feld gegen Zugriffe von außen gesichert) und selbst die Länge ist ein Attribut der Klasse.


Galileo Computing

4.1.1 String-Objekte für konstante Zeichenketten  downtop

Damit wir Zeichenketten nutzen können, muss ein Objekt der Klasse String oder StringBuffer erzeugt sein. Nutzen wir Literale, so müssen wir String-Objekte nicht von Hand mit new erzeugen, denn für jedes Zeichenketten-Literal im Programm wird (bei Bedarf) automatisch ein entsprechendes String-Objekt erzeugt. Dies passiert für jede konstante Zeichenkette höchstens einmal, egal wie oft sie im Programmverlauf benutzt wird. (Denken wir hier wieder an das Beispiel println()). Neben den String-Literalen, die uns Strings erzeugen, können wir auch direkt einen Konstruktor der Klasse Stringvon denen es neun gibt – aufrufen. Als String-Literal wird eine konstante Zeichenkette betrachtet. So ergeben beide folgenden Zeilen die Referenz auf ein String-Objekt:

String str = new String("Aha, ein String");
String str = "Aha, ein String ";

Die erste Lösung ist jedoch unnötig, da ein zusätzliches String-Objekt erzeugt wird.


gp  String()
Erzeugt ein neues Objekt ohne Zeichen (den leeren String »«).
gp  String( String string )
Erzeugt ein neues Objekt mit einer Kopie von string. Dieser wird selten benötigt, da String-Objekte unveränderbar (immutable) sind.
gp  String( char[] )
Erzeugt ein neues Objekt und konvertiert die im char-Feld vorhandenen Zeichen in das String-Objekt.
gp  String( char [], int offset, int length )
Erzeugt wie String(char[]) einen String aus einem Ausschnitt eines Zeichen-Felds. Der verwendete Ausschnitt beginnt bei dem Index offset und umfasst length Zeichen.
gp  String( byte[] )
Erzeugt ein neues Objekt aus dem Byte-Feld. Das Byte-Array enthält keine Unicode-Zeichen, sondern eine Folge von Bytes, die nach der Standardkodierung der jeweiligen Plattform in Zeichen umgewandelt werden.
gp  String( byte[], int offset, int length )
Erzeugt wie String(byte[]) einen String aus einem Ausschnitt eines Byte-Felds.
gp  String( byte[], String ) throws UnsupportedEncodingException
Erzeugt einen neuen String von einem Byte-Array mit Hilfe einer speziellen Zeichen-Kodierung, die die Umwandlung von Bytes in Unicode-Zeichen festlegt.
gp  String( byte[], int offset, int length, String )
throws UnsupportedEncodingException
Erzeugt einen neuen String mit einem Teil des Byte-Arrays mit Hilfe einer speziellen Zeichen-Kodierung.
gp  String( StringBuffer )
Erzeugt aus einem veränderlichen StringBuffer-Objekt ein unveränderliches String- Objekt, das dieselbe Zeichenreihe repräsentiert.

Durch

String str = "";

oder

String str = new String();

werden String-Objekte erzeugt, die keine Zeichen enthalten. Diesen String nennen wir dann leeren String, Leer-String oder Null-String. Der letzte Begriff ist leider etwas unglücklich und führt oft zur Verwechselung mit Folgendem:

String s = null;
System.out.println( s );     // null

Hier bekommen wir keinen leeren String und bei der Benutzung im Methodenaufruf, etwa s.length() eine NullPointerException.


Galileo Computing

4.1.2 String-Objekte verraten viel  downtop

String-Objekte verwalten intern die Zeichenreihe, die sie repräsentieren und bieten eine Vielzahl von Methoden, um die Eigenschaften des Objekts preiszugeben. Eine Methode haben wir schon benutzt: length(). Für String-Objekte ist diese so implementiert, dass die Anzahl der Zeichen im String (die Länge des Strings) zurückgegeben wird.

Beispiel "Hallo".length() hat fünf Zeichen. Die Leerzeichen und Sonderzeichen werden natürlich mitgezählt.


Galileo Computing

4.1.3 Gut, dass wir verglichen haben  downtop

Um Strings zu vergleichen existieren eine Menge von Möglichkeiten und Optionen. Oft wollen wir einen konstanten String mit einer Benutzereingabe vergleichen. Hier gibt es die aus der Klasse Object geerbte, aber in der Klasse String überschriebene Methode equals(). Die Methode gibt true zurück, falls die Strings Zeichen für Zeichen übereinstimmen. Groß- und Kleinschreibung werden dabei unterschieden. Mit equalsIgnoreCase() werden zwei Zeichenketten verglichen, ohne dass auf die Groß/Kleinschreibung geachtet wird.

Beispiel equals() liefert für result1 den Wert false und equalsIgnoreCase() für result2 den Wert true.
String str = "REISEPASS";
boolean result1 = str.equals( "Reisepass" 
);
boolean result2 = str.equalsIgnoreCase( 
"ReISePaSs" );

Sortierung mit der größer/kleiner-Relation

Wie equals() und equalsIgnoreCase() vergleichen auch die Methoden compareTo(String) und compareToIgnoreCase(String) zwei Strings. Bei equals() muss wegen der überschriebenen equals()-Methode aus Object der Parametertyp auch Object sein. Natürlich stellt equals()Gleichheit aber nur dann fest, wenn der Parameter auch vom Typ String ist. (Bei beliebigen Objekten wird nicht die Methode toString() aufgerufen.) Selbst Vergleiche mit einem inhaltsgleichen StringBuffer-Objekt ergeben immer false. Der Rückgabewert von compareTo() ist auch kein boolean, sondern ein int. Das Ergebnis signalisiert, ob der Parameterstring kleiner oder größer als das aufrufende String-Objekt ist, bzw. mit diesem übereinstimmt. Das ist zum Beispiel in einer Sortierfunktion wichtig. Der Sortieralgorithmus muss beim Vergleich zweier Strings wissen, wie sie einzusortieren sind.

Beispiel Sei s der String »Peter«, dann gilt:
s.compareTo( "Maria" 
)  > 0 //"Peter" ist lexikographisch größer als "Maria"
s.compareTo( "Peter" ) == 0
s.compareTo( 
"Zoe" )    < 0

Der von compareTo() gemachte Vergleich basiert nur auf der internen numerischen Codierung der Unicode-Zeichen. Die Vergleichfunktion berücksichtigt nicht die landestypischen Besonderheiten, etwa die übliche Behandlung der deutschen Umlaute. Dafür müssten wir Collator-Klassen nutzen, die später vorgestellt werden. Obwohl auch compareTo(Object) angeboten wird, beachtet sie keine anderen Typen außer String, wie die Implementierung zeigt:

public int compareTo( Object o ){
  return compareTo( (String)o );
}

compareToIgnoreCase() ist vergleichbar mit equalsIgnoreCase(), bei der die Groß/Kleinschreibung keine Rolle spielt. Bei Sun wird dies intern mit einem Comparator implementiert, der zwei beliebige Objekte – für Zeichenketten natürlich vom Typ String – in eine Reihenfolge bringt. Die virtuelle Maschine Kaffe geht mit ihrer Bibliotheks-Implementierung an das Problem pragmatischer heran:

public int compareToIgnoreCase( String that ) {
  return toUpperCase().toLowerCase().compareTo(
    that.toUpperCase().toLowerCase() );
}

Endet der String mit ... Beginnt er mit ...

Interessiert uns, ob der String mit einer bestimmten Zeichenfolge beginnt (wir wollen dies Präfix nennen), so rufen wir die startsWith()-Methode auf. "http://trullala. tralla".startsWith("http") ergibt true. Eine ähnliche Funktion gibt es für Suffixe: endsWith(). Sie überprüft, ob ein String mit einer Zeichenfolge am Ende übereinstimmt.

Beispiel endsWith() für Dateinamen.

Die Methode ist praktisch für Dateinamenendungen:

String filename = "Echolallie.gif";
boolean issesGif = filename.endsWith( ".gif" 
);

Um die erste Position eines Zeichens im String zu finden, verwenden wir die indexOf() Methode. Als Parameter lässt sich unter anderem ein Zeichen oder ein String vorgeben, der gesucht wird.

Beispiel Ein Zeichen mit indexOf() suchen.
String str = "Dieter Doof";
int index = str.indexOf( 'D' );

Im Beispiel ist index gleich 0, da an der Position 0 das erste Mal ein »D« vorkommt. Die Zeichen in einem String werden, wie Array-Elemente ab 0 durchnumeriert. Falls das gesuchte Zeichen in dem String nicht vorkommt, gibt die Methode indexOf() als Ergebnis –1 zurück.

Beispiel Um das nächste »D« zu finden, können wir zwei weitere Versionen von indexOf() verwenden.
index = str.indexOf( 
'D', index+1 );

Mit dem Ausdruck index+1 als Argument der Methode wird in unserem Beispiel ab der Stelle 1 weitergesucht. Das Resultat der Methode ist dann 7. Ist der Index kleiner 0, so wird dies ignoriert und automatisch auf 0 gesetzt.

Beispiel Beschreibt das Zeichen c ein Escape-Zeichen wie einen Tabulator oder ein Return, dann soll die Bearbeitung weitergeführt werden.
if ( "\b\t\n\f\r\"\\".indexOf(c) 
>= 0 )
{
  ...
}

Genauso wie am Anfang gesucht werden kann, ist es möglich, auch am Ende zu beginnen.

Beispiel Dazu dient die Methode lastIndexOf()
String str = "That is a string";
int index = str.lastIndexOf( 'i' );

Hier ist index gleich 13. Genauso wie bei indexOf() existiert eine überladene Version, die rückwärts ab einer bestimmten Stelle nach dem nächsten Vorkommen von »i« sucht. Wir schreiben:

index = str.lastIndexOf( 
'i', index-1 );

Nun ist der Index 5.

Hinweis Die Parameter der char-orientierten Methoden indexOf() und lastIndexOf() sind alle vom Typ int und nicht, wie erwartet, vom Typ char und int. Das zu suchende Zeichen wird als erstes int-Argument übergeben. Die Umwandlung des char in ein int nimmt der Java-Compiler automatisch vor, so dass dies nicht weiter auffällt. Bedauerlicherweise kann es dadurch aber zu Verwechselungen bei der Reihenfolge der Parameter kommen: Bei s.indexOf(start, c) wird der erste Parameter start als Zeichen interpretiert und das gewünschte Zeichen c als Startposition der Suche.

Es gibt noch eine weitere Version von indexOf() und lastIndexOf(), die nach einem Teilstring (engl. Substring) suchen. Die Versionen erlauben ebenfalls einen zweiten Parameter, der den Startindex bestimmt.

Beispiel indexOf() mit der Suche nach einem Teilstring:
String str = "In Deutschland gibt es immer noch 
ein Ruhrgebiet, "+
  "obwohl es diese Krankheit schon lange nicht mehr geben soll.";

String s = "es";
int index = str.indexOf( s, str.indexOf(s)+1 
);

Die nächste Suchposition wird ausgehend von der alten Finderposition errechnet. Das Ergebnis ist 57, da dort zum zweiten Mal das Wort »es« auftaucht.

Stringteile vergleichen

Eine Erweiterung der ganz-oder-gar-nicht Vergleichsfunktionen bietet die Methode regionMatches(), mit der Teile einer Zeichenkette mit Teilen einer anderen verglichen werden können. Ist der erste Parameter von regionMatches() ein Wahrheitswert mit der Belegung true, dann spielt Groß/Kleinschreibung keine Rolle . Der Rückgabewert ist wie bei equalsXXX() ein boolean.

Beispiel Der Aufruf von regionMatches() ergibt true.
String s = "Deutsche Kinder sind zu dick";
s.regionMatches( 9, "Bewegungsarmut bei 
Kindern", 19, 6 );

Die Methode beginnt den Vergleich am neunten Zeichen, also bei »K« im String s und dem neunzehnten Buchstaben in dem Vergleichsstring, ebenfalls ein »K«. Dabei beginnt die Zählung der Zeichen wieder bei 0. Ab diesen beiden Positionen werden 6 Zeichen verglichen. Im Beispiel ergibt der Vergleich von »Kinder« und »Kinder« dann true.

Beispiel Sollte der Vergleich unabhängig von der Groß/Kleinschreibweise stattfinden, ist das erste Argument der überladenen Funktion true.
s.regionMatches( true, 
9, "Bewegungsarmut bei Kindern", 19, 6 );

Die Sun-Implementierung realisiert die beiden überladenen Varianten unabhängig voneinander. regionMatches(a,b,c,d) wird also nicht auf regionMatches(false,a,b,c,d) zurückgeführt. Die Betrachtung unabhängig von kleinen und großen Buchstaben ist somit schneller. Kaffe macht es sich da einfacher und leitet den Aufruf weiter.

Beispiel regionMatches() ist so universell, dass sich auch startsWith()und endsWith() damit implementieren lassen.
public boolean startsWith( String prefix ) {
  return regionMatches( false, 0, prefix,
                        0, prefix.length() );
}

public boolean startsWith( String prefix, int toffset ) {
  return regionMatches( false, toffset, prefix, 0,
                        prefix.length() );
}
public boolean endsWith( String suffix ) {
   return regionMatches( false, count-suffix.count,
                        suffix, 0, suffix.count);
}


Galileo Computing

4.1.4 Stringteile extrahieren  downtop

Die vielleicht wichtigste Funktion der Klasse String ist charAt(int index). Diese Methode liefert das entsprechende Zeichen an einer Stelle, die »Index« genannt wird. Dies bietet eine Möglichkeit, die Zeichen eines Strings (zusammen mit der Methode length()) durchzulaufen. Ist der Index kleiner Null oder größer bzw. gleich der Anzahl der Zeichen im String, so löst die Methode eine StringIndexOutOfBoundsException(Fehlerstelle) aus.

Beispiel Liefere das erste und letzte Zeichen im String s:
String s = "Ich bin nicht dick! Ich habe nur weiche 
Formen.";
char first = s.charAt( 0 );
char last = s.charAt( s.length()-1 );

Wir müssen bedenken, dass die Zählung wieder bei 0 beginnt. Daher müssen wir von der Länge des Strings eine Stelle abziehen. Da der Vergleich auf den korrekten Bereich bei jedem Zugriff auf charAt() stattfindet, ist zu überlegen, ob der String bei mehrmaligem Zugriff nicht statt dessen einmalig in ein eigenes Zeichen-Array kopiert werden sollte.

Zeichenfolgen als Array aus dem String extrahieren

Eine Erweiterung von charAt() ist getChars(), die Zeichen aus einem angegeben Bereich in ein übergebenes Feld kopiert:

String s = "Body-Mass-Index  = " +
  "Körpergewicht (kg) / Körpergröße (m) / Körpergröße 
(m)";
char chars[] = new char[13];
s.getChars( 18, 18+13, chars, 0 );

s.getChars() kopiert ab Position 18 aus dem String s 13 Zeichen in die Elemente des Arrays chars. Das erste Zeichen aus dem Ausschnitt steht dann in chars[0].getChars() muss natürlich wieder testen, ob die gegeben Parameter im grünen Bereich liegen. Das heißt, ob der Startwert nicht < 0 ist und ob der Endwert nicht über die Größe des Strings hinausgeht. Passt das nicht, löst die Methode eine StringIndexOutOfBoundsException aus. Liegt zudem der Startwert hinter dem Endwert, gibt es ebenfalls eine StringIndexOutOfBoundsException, die anzeigt, wie groß die Differenz der Positionen ist. Am Besten ist es, die Endposition aus der Startposition zu berechnen, wie im Beispiel. Passen die Werte, kopiert die Implementierung der Methode getChars() mittels System.arraycopy() die Zeichen aus dem internen Array des String-Objekts in das von uns angegebene Ziel.

Möchten wir den kompletten Inhalt eines Strings als ein Array von Zeichen, so können wir die Methode toCharArray() verwenden. Für häufigen Zugriff auf einen String bewirkt dies eine Geschwindigkeitssteigerung. toCharArray() arbeitet intern auch mit getChars(). Als Ziel-Array wird ein neues Array Objekt angelegt, welches wir dann zurückbekommen.

Beispiel Die untersten 4 Bits von i in eine hexadezimale Ziffer umwandeln:
char c = "0123456789ABCDEF".toCharArray()[i%16];

Für diesen speziellen Fall wäre charAt() zwar schneller gewesen, jedoch demonstriert das Beispiel, dass wir per [] auch direkt auf die Array-Elemente eines Methodenergebnisses zugreifen können. Das ist völlig korrekt, denn toCharArray() liefert ein Array als Ergebnis.

Teile eines Strings als String

Wollen wir bei den Teilstrings keine Zeichenfelder bekommen, sondern bei dem Typ String bleiben, so greifen wir zur Methode substring(), die in zwei Varianten existiert. Sie liefern beide ein neues String-Objekt zurück, dass einem Teil des Originals entspricht.

Beispiel substring(int), die das Ende (oder das Endstück) eines Strings ab einer bestimmten Position als neue Zeichenkette liefert.
String s1 = "Die erste McDonalds Filiale öffnete 
1971 in München";
String s2 = s1.substring( 44 );

Der String s2 ist dann »München«. Der Index von substring() gibt die Startposition an, ab dem Zeichen in die neue Teilzeichenkette kopiert werden. substring()liefert den Teil von diesem Zeichen bis zum Ende des ursprünglichen Strings.

Wollten wir die Teilzeichenkette genauer spezifizieren, so nutzen wir die zweite Variante von substring(). Ihre Parameter geben den Anfang und das Ende des gewünschten Ausschnitts an:

String s1 = "fettleibig : adipös";
String s2 = s1.substring(4, 8);       // 
leib

Wie man sieht, bezeichnet die Endposition das erste Zeichen des ursprünglichen Strings, das nicht mehr zur Teilzeichenkette dazugehören soll. Bei genauerer Betrachtung ist substring(int) nichts anderes als eine Spezialisierung von substring(int, int), denn die erste Variante mit dem Startindex lässt sich auch schreiben als:

s.substring( beginIndex, s.length() );

Selbstverständlich kommen nun diverse Indexüberprüfungen hinzu, die wir von match Region() kennen. Eine StringIndexOutOfBoundsException meldet fehlerhafte Positionsangaben. Stimmen diese, konstruiert ein spezieller String-Konstruktor ein neues String-Objekt als Auszug des Originals.


Galileo Computing

4.1.5 Veränderte Strings liefern  downtop

Obwohl String-Objekte selbst unveränderlich sind, bietet die Klasse String Methoden an, die aus einer Zeichenkette Teile herausnehmen oder Teile hinzufügen. Diese Änderungen werden natürlich nicht am String-Objekt vorgenommen, sondern die Methode liefert eine Referenz auf ein neues String-Objekt mit verändertem Inhalt zurück.

Zeichen ersetzen

Die replace()-Methode ist ein Beispiel für diese Vorgehensweise.

Beispiel Ändere den in einer Zeichenkette vorkommenden Buchstaben »o« in »u«:
String s1 = "Honolulu";
String s2 = s1.replace( 'o', 'u' );

Der String s1 wird selbst nicht verändert, nur ein neues String-Objekt mit dem Inhalt »Honolulu« wird erzeugt. Die replace()-Methode betrachtet dafür intern eine Kopie des Zeichenfelds und geht dann Schritt für Schritt das Feld ab und führt die Ersetzungen aus. Die replace()-Methode ersetzt dabei alle Zeichen. Eine Variante, die nur das erste Zeichen ersetzt, müssen wir uns selber schreiben.

Anhängen an Strings

Eine weitere Methode erlaubt das Anhängen von Teilen an einen String. Wir haben dies schon öfters mit dem Plus-Operator realisiert. Die Methode von String dazu heißt concat(String). Wir werden später sehen, dass die StringBuffer-Klasse dies noch weiter treibt und eine Methode append() mit der gleichen Funktionalität anbietet, die Methode aber für unterschiedliche Typen überladen ist. Das steckt auch hinter dem Plus-Operator. Der Compiler wandelt dies automatisch in eine Kette von append()-Aufrufen um.

Beispiel Hänge hinter eine Zeichenkette das aktuelle Tages-Datum:
String s1 = "Das aktuelle Datum ist: ";
String s2 = new Date().toString();
String s3 = s1.concat( s2 );

Die concat()-Methode arbeitet relativ zügig und effizienter als der Plus-Operator, der einen temporären Stringpuffer anlegt. Doch mit dem Plus-Operator ist es hübscher anzusehen. (Aber wie das so ist: Sieht nett aus, aber ...)

Beispiel Ähnlich zum oberen Beispiel können wir schreiben:
String s3 = "Das aktuelle Datum ist: " + new Date().toString();

Es geht sogar noch kürzer, denn der Plus-Operator ruft automatisch toString() bei Objekten auf:

String s3 = "Das aktuelle Datum ist: " + new Date();

concat() legt ein internes Feld an, kopiert die beiden Zeichenreihen per getChars() hinein und liefert mit einem String-Konstruktor die resultierende Zeichenkette.

Groß/Kleinschreibung

Die Klasse Character definiert einige statische Methoden, um einzelne Zeichen in Groß/Kleinbuchstaben umzuwandeln. Die Schleife, die das für jedes Zeichen macht, können wir uns sparen, denn dazu gibt es die Methoden toUpperCase() und toLowerCase() in der Klasse String. Interessant ist an beiden Methoden, dass sie einige sprachabhängige Feinheiten beachten. So zum Beispiel, dass es im Deutschen kein großes »ß« gibt; denn »ß« wird zu »SS«. Gammelige Textverarbeitungen bekommen das manchmal nicht auf die Reihe und im Inhaltsverzeichnis steht dann so etwas wie »SPAß IN DER NAßZELLE«. Aber bei möglichen Missverständnissen müsste ß auch zu SZ werden, vergleiche SPASS IN MASZEN mit SPASS IN MASSEN (ähnliches Beispiel steht im Duden). Diese Umwandlung ist aber nur von klein nach groß von Bedeutung. Für beide Konvertierungsrichtungen gibt es jedoch im Türkischen Spezialfälle, bei denen die Zuordnung zwischen Groß- und Kleinbuchstaben von der Festlegung in anderen Sprachen abweicht.

Beispiel Konvertierung von groß in klein und umgekehrt:
String s1 = "Spaß in der Naßzelle.";
String s2 = s1.toLowerCase().toUpperCase();
System.out.println(s2.length()-s1.length());  // Ergebnis: 2

Das Beispiel dient zugleich als Warnung, dass sich im Fall von »ß« die Länge der Zeichenkette vergrößert. Das kann zu Problemen führen, wenn vorher Speicherplatz bereitgestellt wurde. Dann könnte die neue Zeichenkette nicht mehr in den Speicherbereich passen. Arbeiten wir nur mit String-Objekten, haben wir dieses Problem glücklicherweise nicht. Aber berechnen wir etwa für einen Texteditor die Darstellungsbreite einer Zeichenkette in Pixeln auf diese Weise, dann sind Fehler vorprogrammiert.

Um länderspezifische Besonderheiten zu berücksichtigen, lassen sich die toXXXCase()-Methoden zusätzlich mit einem Locale-Objekt füttern. Wir gehen in einem eigenen Kapitel auf Sprachumgebungen und die Klasse Locale ein. Die parameterlosen Methoden wählen die Sprachumgebung gemäß den Länder-Einstellungen des Betriebssystems:

public String toLowerCase() {
  return toLowerCase(Locale.getDefault());
}

Ähnliches steht bei toUpperCase().

Leerzeichen entfernen

In einer Benutzereingabe oder Konfigurationsdatei stehen nicht selten vor oder hinter dem wichtigen Teil eines Texts Leerzeichen. Vor der Bearbeitung sollten sie entfernt werden. Die Stringklasse bietet dazu trim() an. Diese Methode entfernt Leer- und ähnliche Füllzeichen am Anfang und Ende eines Strings. Andere Trendy-Sprachen wie Visual Basic bieten dazu noch trim()-Funktionen an, die nur die Leerzeichen vorher oder nachher verwerfen. Die Javabibliothek bietet das leider nicht.

Beispiel Leerzeichen zur Konvertierung einer Zahl abschneiden:
String s = " 1234    ".trim();
int i = Integer.parseInt( s );

Die Konvertierungsfunktion selbst schneidet keine Leerzeichen ab und würde einen Parserfehler melden. Die Helden der Javabibliothek haben allerdings bei Float.parseFloat() und Double.parseDouble() anders gedacht. Hier wird die Zeichenkette vorher schlankgetrimmt. parseInt() unterstützt verschiedene Zahlensysteme, nicht jedoch Gleitkommazahlen. Auch die Verwandtschaft zwischen den valueOf()- und den parseXXX()-Methoden ist in der Klasse Integer gerade andersherum beschrieben als bei Double und Float.


Galileo Computing

4.1.6 Typen in Zeichenketten konvertieren  downtop

Bevor ein Datentyp auf dem Bildschirm ausgegeben werden kann, zum Drucker geschickt oder in einer ASCII-Datei gespeichert wird, muss er in einen String konvertiert werden. Wenn wir etwa die Zahl 7 ohne Umwandlung ausgeben würden, hätten wir keine 7 auf dem Bildschirm, sondern einen Pieps.

Die String-Repräsentation eines primitiven Werts oder eines Objekts kennt die überladene Methode valueOf(). Sie konvertiert einen Datentyp in einen String. Alle valueOf()-Methoden sind statisch.

Beispiel Konvertierungen einiger Datentypen in Strings:
String s1 = String.valueOf( 
10 );
String s2 = String.valueOf( Math.PI );
String s3 = String.valueOf( new Date() 
);
String s4 = String.valueOf( 1 < 2 );

Die eigentliche Umwandlung erledigen in den meisten Fällen jedoch keine Methoden der Klasse String. Vielmehr leitet eine valueOf()-Methode den Aufruf meist an die zuständige Klasse weiter. Schauen wir uns einige Implementierungen an:

public static String valueOf( boolean b ) {
  return b ? "true" : "false";
}

Die Methode gibt einfach das passende Literal zurück, unabhängig von der Landessprache. Hier ist die Funktionalität direkt ausprogrammiert. Die Klasse Boolean, die Wrapperklasse für den primitiven Datentyp boolean, ist nicht im Spiel, obwohl es ordentlicher wäre. Genauso bei einem Zeichenfeld:

public static String valueOf( char data[] ) {
  return new String(data);
}

Wir können den Konstruktor nutzen, der aus dem Zeichenfeld ein String-Objekt konstruiert.

valueOf() wälzt die Arbeit bei allen anderen primitiven Datentypen ab. Hier kommen die Wrapperklassen ins Spiel:

public static String valueOf( int i ) {
  return Integer.toString(i, 10);
}

Das ist wichtig einzusehen, denn es ist nicht Aufgabe der Klasse String sich darum zu kümmern, wie eine Zahl in eine Zeichenfolge umgewandet wird.

Es bleibt abschließend die Frage nach valueOf(Object). Hier kann weder eine Wrapperklasse helfen noch String selbst etwas machen. Hier ist jedes Objekt gefragt:

public static String valueOf( Object obj ) {
  return (obj == null) ? "null" : obj.toString();
}

Und ein Objekt kann helfen, da jedes eine toString()-Methode besitzt. Dynamisch gebunden (dazu später mehr) landet der Aufruf in der jeweiligen Klasse des Objekts, sofern diese die Methode toString() überschreibt.


Galileo Computing

4.2 Veränderbare Zeichenketten mit der Klasse StringBuffer  downtop

Zeichenketten, die in der virtuellen Maschine in String-Objekten gespeichert sind, haben die Eigenschaft, dass ihr Inhalt nicht mehr verändert werden kann. Anders verhalten sich die Exemplare der Klasse StringBuffer, an denen sich Veränderungen vornehmen lassen. Die Veränderungen betreffen anschließend das StringBuffer-Objekt selbst und es wird kein neu erzeugtes Objekt als Ergebnis geliefert, wie zum Beispiel beim Plus-Operator und der concat()-Methode bei herkömmlichen String-Objekten. Vordergründig ist die Implementierung von String-Objekten und StringBuffer-Objekten jedoch dieselbe. In beiden Fällen nutzen die Klassen ein internes Zeichenfeld. Bei String-Objekten kann jedoch subString() ohne Kopie der Teilzeichenkette auskommen und das interne Feld lässt sich mehrfach verwenden. StringBuffer muss das eigene Array immer wieder kopieren.


Galileo Computing

4.2.1 Anlegen von StringBuffer-Objekten  downtop

Mit drei Konstruktoren lassen sich StringBuffer-Objekte generieren.


gp  StringBuffer()
Legt ein StringBuffer-Objekt an, das die leere Zeichenreihe enthält und Platz für (zunächst) bis zu 16 Zeichen bietet. Bei Bedarf wird automatisch Platz für weitere Zeichen bereitgestellt.
gp  StringBuffer( int length )
Wie oben, jedoch reicht die anfängliche Kapazität des StringBuffer-Objekts für die angegebene Anzahl Zeichen.
gp  StringBuffer( String str )
Ein StringBuffer, welches eine Kopie der Zeichen aus str enthält. Zusätzlich wird bereits Platz für 16 weitere Zeichen eingeplant.

Da nur String-Objekte von der Sprache bevorzugt werden, bleibt uns nur der explizite Aufruf eines Konstruktors, um StringBuffer-Exemplare anzulegen. Alle String-Literale in Anführungszeichen werden zu Exemplaren der Klasse String.

Hinweis Weder in der Klasse String noch in StringBuffer existiert ein Konstuktor, der explizit ein char als Parameter besitzt, um aus dem angegebenen Zeichen eine Zeichenkette aufzubauen. Dennoch beschwert sich der Compiler bei new StringBuffer(char) nicht, denn der Compiler passt das Zeichen zu einem int an und es wird der StringBuffer-Konstruktor mit der Längenangabe als Parameter aufgerufen. Dieser besorgt dann Speicherplatz für die angegebene Anzahl Elemente, etwa bei einem »*« Platz für 42 Zeichen, da 42 der ASCII-Code des Zeichens * ist. Zu allem Überfluss enthält das resultierende Objekt statt des Sternchens nur die leere Zeichenreihe. Korrekt ist daher nur Folgendes für ein Zeichen c:
StringBuffer s = new StringBuffer( "" + c );

Eine andere Variante ist:

StringBuffer s = new StringBuffer().append(c);


Galileo Computing

4.2.2 Die Länge eines StringBuffer-Objekts lesen und setzen  downtop

Wie bei einem String lässt sich die Länge, die Anzahl der enthaltenen Zeichen mit der Methode length() erfragen. StringBuffer haben jedoch auch eine interne Puffergröße, die sich mit capacity() erfragen lässt und im Konstruktor wie beschrieben festgelegt wird. In diesem Puffer, der genauer gesagt ein Array vom Typ char ist, werden die Operationen wie Ausschneiden oder Anhängen von Zeichen vorgenommen. Während length() die Anzahl der Zeichen angibt, ist capacity() immer größer oder gleich length() und sagt etwas darüber aus, wie viele Zeichen der Puffer noch aufnehmen kann, ohne dass intern ein neues, größeres Feld benötigt würde:

StringBuffer sb = new StringBuffer( "Blub" );
int length   = sb.length();        //4
int capacity = sb.capacity();      // 20

So ergibt sb.length() 4, aber sb.capacity() ergibt 4+16=20.

Ändern der Länge

Soll der StringBuffer mehr Daten aufnehmen, so ändert setLength() die Länge auf eine angegebene Anzahl von Zeichen. Der Parameter ist die neue Länge. Ist sie kleiner als length(), so wird der Rest der Zeichenkette einfach abgeschnitten. Die Größe des internen Puffers ändert sich dadurch nicht. Ist setLength() größer, so vergrößert sich der Puffer, und die Methode füllt die übrigen Zeichen mit Nullzeichen ’\0’ auf. Die Methode ensureCapacity() fordert, dass der interne Puffer für eine bestimmte Anzahl von Zeichen ausreicht. Wenn nötig, legt sie ein neues, vergrößertes char-Array an, verändert aber nicht die Zeichenfolge, die durch das StringBuffer-Objekt repräsentiert wird.


Galileo Computing

4.2.3 Daten anhängen  downtop

Die häufigste Anwendung von StringBuffer-Objekten ist das Zusammenfügen von Texten aus Daten unterschiedlichen Typs. Dazu definiert StringBuffer eine Reihe von append()-Methoden, die mit unterschiedlichen Datentypen überladen sind.

Jede append()-Methode verändert den StringBuffer und liefert als Rückgabewert noch eine Referenz darauf zurück. Das hat den großen Vorteil, dass sich Aufrufe der append()-Methoden einfach hinter einander setzen (kaskadieren) lassen:

StringBuffer sb = new StringBuffer( "Zippel" );
sb.append( "di ").append(" zoppel").append("!");

Die Methoden append() hängt immer ans Ende an und vergrößert den internen Platz falls es nötig ist. Ein neues StringBuffer-Objekt wird nicht erzeugt.


Galileo Computing

4.2.4 Zeichen(folgen) setzen, erfragen, löschen  downtop

Die bekannten Anfrage-Methoden aus String finden wir auch beim StringBuffer wieder. So verhalten sich charAt() und getChars()bei Exemplaren beider Klassen identisch. Neu ist setCharAt(), da in einem StringBuffer-Zeichen verändert werden können.

Beispiel Ändere das erste Zeichen im Puffer in einen Großbuchstaben:
StringBuffer sb = new StringBuffer( "peter und Paul" 
);
char c = Character.toUpperCase( sb.getChar(0) );
sb.setCharAt( 0, c );

Der erste Parameter ist die Position des zu setzenden Zeichens.


Auch substring(int start) und substring(int start, int end) sind aus der Klasse String bekannt.

Eine Folge von Zeichen lässt sich durch delete(int start, int end) löschen. deleteCharAt (int index) löscht nur ein Zeichen. In beiden Fällen wird ein inkorrekter Index durch eine StringIndexOutOfBoundsException bestraft. Die Methode replace(int start, int end, String str) löscht zuerst die Zeichen zwischen start und end und fügt anschließend den neuen String str ab start ein. Dabei sind die Endpositionen, wie immer, exklusiv, das heißt, sie geben das erste Zeichen hinter dem zu verändernden Ausschnitt an. Die Methode insert(int offset, Typ) fügt die Zeichenketten-Repräsentation eines Werts vom Typ Typ an die Stelle offset ein. Sie ähnelt der überladenen append()-Methode. Für char-Arrays existiert insert() in einer abgewandelten Art: insert(int index, char str[], int offset, int len). Es wird nicht das komplette Array in den StringBuffer übernommen, sondern nur ein Ausschnitt:

StringBuffer s = new StringBuffer( "String Buffer" 
);
int val = 15;
s.insert( 6, val );

Eine weitere Methode reverse() dreht die Zeichenfolge um.

Beispiel Teste, ob der String s ein Palindrom ist.

Palindrome lesen sich von vorne genauso wie von hinten. Etwa OTTO.

boolean isPalindrom =
  new StringBuffer(s).reverse().toString().equals(s);


Galileo Computing

4.3 Vergleiche von Zeichenketten als String und StringBuffer  downtop

Ein Blick in die API-Dokumentation der Klasse String zeigt eine equals()-Methode, mit der Zeichenketten vom Typ String verglichen werden können. Leider vergleicht die Methode nur String/String-Paare, aber keine String/StringBuffer-Paare. Das bedeutet, einen String mit einem StringBuffer zu vergleichen führt zwar bei equals() zu keinem Compiler-Fehler (da equals(Object) alles entgegennimmt), aber auch zu nichts sinnvollem, da der der Vergleich immer false ist. Denn die Implementierung testet, ob das an equals() übergebene Parameterobjekt instanceof String ist. Uns bleibt daher nichts anderes übrig, als bei einem StringBuffer den Vergleich auf String-Ebene durchzuführen. In der Version 1.4 der Java Standardbibliothek hat sich das jedoch mit der String-Funktion contentEquals(StringBuffer) geändert. Die Methode liefert true, wenn der StringBuffer und der betrachtende String den gleichen Zeicheninhalt haben. Die interne Länge spielt keine Rolle. Ist sb=null, wird eine NullPointerException ausgelöst.

Beispiel Vergleiche einen String mit einem StringBuffer:
String s = "Ulli";
StringBuffer sb = new StringBuffer( "Ulli" );
boolean b1 = s.equals(sb);                  // false
boolean b1 = s.equals(sb.toString());      // true

Wollen wir zwei StringBuffer-Objekte miteinander vergleichen, werden wir noch mehr enttäuscht. Die Klasse StringBuffer definiert überhaupt keine eigene equals()-Methode. Es gibt zwar eine, doch die wird von der Klasse Object geerbt und das heißt, nur Objektreferenzen werden verglichen. Wenn also zwei verschiedene StringBuffer-Objekte mit gleichem Inhalt mit equals() verglichen werden, kommt trotzdem immer false heraus.

Beispiel Um den inhaltlichen Vergleich von zwei StringBuffer-Objekten zu realisieren, müssen wie diese erst mit toString() in Strings umwandeln.
StringBuffer sb1 = new StringBuffer( "Saftpresse" 
);
StringBuffer sb2 = new StringBuffer( "Saftpresse" );
boolean b1 = sb1.equals( sb2 );                     // false
boolean b2 = sb1.toString().equals(sb2.toString()); // true


Galileo Computing

4.3.1 Sollte es ein equals() und hash() bei StringBuffer geben?  downtop

Die Betrachtung von gerade zeigt, dass eine Methode equals(), welche den Inhalt von StringBuffer-Objekten vergleicht, nicht schlecht wäre. Dennoch besteht das Problem, wann StringBuffer-Objekte als gleich angesehen werden sollen. Das ist interessant, denn StringBuffer-Objekte sind nicht nur durch ihren Inhalt bestimmt, sondern auch durch die Größe ihres internen Puffers, ihre Kapazität. Sollte equals() den Rückgabewert true haben, wenn die Inhalte gleich sind oder nur wenn Inhalt und Puffergröße gleich sind? Da jeder Entwickler andere Ansichten über die Gleichheit besitzt, bleibt es bei dem standardmäßigen Test auf identische Objektreferenzen.

hashCode()

Eine hashCode()-Methode liefert für alle inhaltsgleichen Objekte denselben, im Idealfall eineindeutigen Zahlenwert, der hilft, diese Objekte von davon unterschiedlichen Objekten zu unterscheiden. Dieser Zahlenwert wird bei Zeichenketten etwa wie folgt gebildet:

hash = 0;
for ( int i = 0; i < s.length(); i++ )
  hash = 31*hash + s.charAt(i);

Die Klasse String besitzt eine hashCode()-Methode, wie oben gezeigt. StringBuffer erbt die Implementierung aus der Klasse Object unverändert. Die Klasse selbst bietet keine Implementierung an, und zwar bei der hashCode()-Methode aus dem gleichen Grund wie bei equals().


Galileo Computing

4.4 Ein paar kleine Helfer  downtop


Galileo Computing

4.4.1 Strings einer gegebenen Länge erzeugen und rechtsbündig ausgeben  downtop

An einigen Stellen in den Bibliotheken gibt es noch Nachholbedarf. Zum Beispiel an einer Funktion, die eine Zeichenkette einer vorgegebenen Länge aus einem einzelnen Zeichnen erzeugt. Selbst in einfachsten Basic-Dialekten gibt es solche Funktionen. In Java müssen wir diese jedoch selbst entwickeln.

Zuerst ist zu fragen, ob die Zeichenkette als String oder als StringBuffer bereitgestellt werden soll? In der Regel wird dies ein String sein. Auch ohne Bibliotheksfunktionen lässt sich mit dem Plus-Operator eine Zeichenkette in einer Schleife zusammensetzen. Das ist sicherlich die erste Idee:

String s = "";

 

for ( int i=0; i<len; i++ )
  s += c;

Hier ist len die Länge des Ergebnis-Strings und c das Zeichen.

In einer kritischen Geschwindigkeitsbetrachtung fällt das dauernde Erzeugen von temporären StringBuffer-Objekten auf. Die Lösung ist langsam. Anders könnten wir dies lösen, in dem wir ein char-Feld der passenden Größe erzeugen, dies mit den Zeichen füllen und anschließend einmalig in einen String konvertieren. Anstelle des Zeichenfeldes könnten wir auch gleich einen StringBuffer wählen.

Falls wir immer ein fixes Zeichen verwenden und die Stringlänge in einem festen Bereich bleibt, so ist eine andere Möglichkeit noch viel eleganter (aber nicht unbedingt schneller). Sie arbeitet mit der substring()-Methode. Wir schneiden aus einem großen String mit festen Zeichen einfach einen String mit der benötigen Länge heraus. Damit lässt sich auch flott eine Zeile formulieren, die einen Text mit so vielen Leerzeichen füllt, dass dieser rechtsbündig ist:

text = "                  ".substring( text.length() 
);

Die Anzahl der Zeichen muss natürlich mit der Zeichenkettenlänge harmonisieren.


Galileo Computing

4.4.2 Teile im String ersetzen  downtop

Die Klassenbibliothek stellt dafür keine Funktionen bereit. Daher müssen wir uns diese Methode selber implementieren. Nachfolgendes Programm ersetzt in einem String s alle Vorkommen des Strings search durch replace.

Listing 4.1   SubstituteDemo.java
class SubstituteDemo
{
  public static String
  substr( String s, String search, String replace )
  {
     StringBuffer s2 = new StringBuffer ();
     int i = 0, j = 0;
     int len = search.length();

     while ( j > -1 )
     {
         j = s.indexOf( search, i );

         if ( j > -1 )
         {
           s2.append( s.substring(i,j) );
           s2.append( replace );
           i = j + len;
         }
     }
     s2.append( s.substring(i, s.length()) );

     return s2.toString();
  }

  public static void main( String args[] )
  {
   System.out.println(
   substr( "Die Deutschen gucken im Schnitt täglich 201"
             + " Minuten in die Röhre",
   "i",
   "ia" ) );
    // Quelle: GfK
  }
}

Die Klasse StringBuffer stellt die Ersetzungsoperation replace(int start, int end, String s) bereit. Über diese und indexOf() ließe sich wiederum eine ähnliche Methode substr()für StringBuffer-Objekte konstruieren.


Galileo Computing

4.5 Zeichenkodierungen umwandeln  downtop

Zeichen sind in Java immer in Unicode kodiert und ein String ist eine Folge von Zeichen. Wollen wir diese Zeichenkette etwa in eine Datei schreiben, so kann es bei Zeichen, die nicht im ASCII-Code enthalten sind, zu Problemen kommen. Die String-Klasse bietet daher die Methode getBytes(String encoding) an, die den String in eine spezielle Kodierung umwandeln kann. Eine Übersicht der verfügbaren Kodierungen (engl. encodings) ist unter http://java.sun.com/products/jdk/1.2/docs/guide/internat/encoding.doc.html zu finden. Die Kodierung könnte etwa »Cp850« heißen, die den alten IBM-Zeichensatz bezeichnet. Die Windows-NT Konsole nutzt zum Beispiel dieses Format. Für den alten EBCDIC-Zeichensatz ist die Codepage »Cp037«. Die Kodierung übernehmen unterschiedliche Klassen, die auch etwa vom Dienstprogramm native2ascii benutzt werden. Die Klasse OutputStreamWriter erzeugt einen neuen Datenstrom mit einer neuen Kodierung.

Beispiel Abschließend soll ein Beispiel mit einem OutputStreamWriter-Objekt gezeigt werden, die die Codepage 850 verwendet, damit auch unter der DOS-Konsole die Umlaute korrekt erscheinen. Listing 4.2  
GetBytesConverter.java
import java.io.*;

public class GetBytesConverter
{
  public static void main( String args[] )
  {
    try
    {
      System.out.println( "Ich kann Ä Ü Ö und ß" 
);

      PrintWriter out = new PrintWriter(
        new OutputStreamWriter( System.out, "Cp850") );

      out.println("Ich kann Ä Ü Ö und ß");
      out.flush();
    }
    catch ( UnsupportedEncodingException e ) { System.err.println(e); 
}
  }

}



Galileo Computing

4.6 Sprachabhängiges Vergleichen mit der Collator–Klasse  downtop

Mit der Collator-Klasse ist es möglich, Zeichenketten nach jeweils landesüblichen Kriterien zu vergleichen. So werden die Sprachbesonderheiten jedes Landes beachtet.

Beispiel Für die deutsche Sprache gilt, dass »ä« zwischen »a« und »b« äquivalent zu ’ae’ einsortiert wird und nicht so, wie es der ASCII-Zeichensatz das Zeichen einordnet, hinter dem »z«. Ähnliches gilt für das »ß«. Auch das Spanische hat seine Besonderheiten im Alphabet. Hier gilt das »ch« und das »ll« als einzelner Buchstabe.

Ein Collator-Objekt wird vor seiner Benutzung mit getInstance() erzeugt. Dieser Funktion kann auch ein Argument übergeben werden, mit dem der jeweils gewünschte Ländercode ausgewählt werden kann: getInstance(Locale.GERMAN) ermöglicht richtiges Vergleichen für deutsche Zeichenketten. Die Länderbezeichnungen sind Konstanten aus der Locale-Klasse.

abstract class java.text.Collator
implements Comparator, Cloneable

gp  static Collator getInstance()
Liefert einen Collator für die aktuelle Landessprache.
gp  static Collator getInstance( Locale desiredLocale )
Liefert einen Collator für die gewünschte Sprache.
gp  abstract int compare( String source, String target )
Vergleicht die beiden Zeichenketten auf ihre Ordnung. Der Rückgabewert ist entweder <0, 0 oder >0.
gp  int compare( Object o1, Object o2 )
Vergleicht die beiden Argumente auf ihre Ordnung. Ruft compare((String)o1, (String)o2) auf.
Listing 4.3   CollatorDemo.java
import java.util.*;
import java.text.*;

class CollatorDemo
{
  public static void comp( Collator col, String a, String b )
  {
    if ( col.compare( a, b ) < 0 )
      System.out.println( a+" < "+b );

    if ( col.compare( a, b ) == 0 )
      System.out.println( a+" = "+b );

    if ( col.compare( a, b ) > 0 )
      System.out.println( a+" > "+b );
  }

  public static void main( String args[] )
  {
    Collator col = Collator.getInstance( Locale.GERMAN );

    System.out.println( "Strength = PRIMARY" );
    col.setStrength( Collator.PRIMARY );
    comp( col, "abc", "ABC" );
    comp( col, "Quäken", "Quaken" );
    comp( col, "boß", "boss" );
    comp( col, "boß", "boxen" );

    System.out.println( "\nStrength =  SECONDARY" );
    col.setStrength( Collator.SECONDARY );
    comp( col, "abc", "ABC" );
    comp( col, "Quäken", "Quaken" );
    comp( col, "boß", "boss" );
    comp( col, "boß", "boxen" );

    System.out.println( "\nStrength =  TERTIARY" );
    col.setStrength( Collator.TERTIARY );
    comp( col, "abc", "ABC" );
    comp( col, "Quäken", "Quaken" );
    comp( col, "boß", "boss" );
    comp( col, "boß", "boxen" );
  }
}

Die Ausgabe ist Folgende.

Strength = PRIMARY
abc = ABC
Quäken = Quaken
boß = boss
boß < boxen

Strength =  SECONDARY
abc = ABC
Quäken > Quaken
boß = boss
boß < boxen

Strength =  TERTIARY
abc < ABC
Quäken > Quaken
boß > boss
boß < boxen

Die Collator-Klasse besitzt viele sinnvolle Methoden, die über die Vergleichs-Funktionalität der String- und StringBuffer-Klasse hinausgehen. So ist es über der Funktion setStrength() möglich, die Toleranz bei Vergleichen einzustellen. Beispielsweise erkennt der tolerante Vergleich »abc« und »ABC« als gleich.


Galileo Computing

4.6.1 Effiziente interne Speicherung für Sortierung  downtop

Obwohl sich mit der Collator-Klasse sprachspezifische Vergleiche korrekt umsetzen lassen, ist die Geschwindigkeit gegenüber einem normalen String-Vergleich schlechter. Daher bietet die Collator-Klasse die Objektmethode getCollationKey() an, die ein CollationKey-Objekt liefert, das schnellere Vergleiche zulässt.

Collator col = Collator.getInstance( Locale.GERMAN 
);
CollationKey key1 = col.getCollationKey( "ätzend" );
CollationKey key2 = col.getCollationKey( "Bremsspur" );

Durch CollationKeys lässt sich die Performance bei Vergleichen zusätzlich verbessern, da der landesspezifische String in einen dazu passenden, normalen Javastring umgewandelt wird, der dann schneller gemäß der internen Unicode Zeichenkodierung verglichen werden kann. Dies bietet sich zum Beispiel beim Sortieren einer Tabelle an, wo mehrere Vergleiche mit dem gleichen String durchgeführt werden müssen. Der Vergleich wird mit ompareTo(CollationKey) durchgeführt.

Beispiel Der Vergleich von key1 und key2 lässt sich durch folgende Zeile ausdrücken.
int comp = key2.compareTo( 
key1 );

Das Ergebnis ist wie bei der compare()-Methode bei Collator-Objekten entweder <0, 0 oder >0.


class java.text.CollationKey
implements Comparable

gp  int compareTo( CollationKey target )
Vergleicht zwei CollationKey Objekte miteinander.
gp  int compareTo( Object o )
Vergleicht den aktuellen CollationKey mit dem angegeben Objekt.
Ruft compareTo ((CollationKey)o) auf.
gp  boolean equals( Object target )
Testet die beiden CollationKey Objekte auf Gleichheit.
gp  String getSourceString()
Liefert den String zum CollationKey.
gp  int hashCode()
Berechnet den Hashcode für den CollationKey.
gp  byte[] toByteArray()
Konvertiert den CollationKey in eine Folge von Bytes.
abstract class java.text.Collator
implements Comparator, Cloneable

gp  abstract CollationKey getCollationKey( String source )
Liefert einen CollationKey für den konkreten String.

Galileo Computing

4.7 Die Klasse StringTokenizer  downtop

Die Klasse StringTokenizer hilft uns, eine Zeichenkette in Token zu zerlegen. Ein Token ist ein Teil eines Strings, welches durch bestimmte Trennzeichen (engl. Delimiter) von anderen Token getrennt wird. Nehmen wir als Beispiel den Satz »Moderne Musik ist Instrumentenspielen nach Noten« (Peter Sellers). Wählen wir Leerzeichen als Trennzeichen, lauten die einzelnen Token »Moderne«, »Musik«, usw. Der String-Tokenizer ist nicht an bestimmte Trenner gebunden, sie können vielmehr völlig frei gewählt werden. Nur in der Voreinstellung sind Tabulator, Leerzeichen und Zeilentrenner die Delimiter.

Hinweis In der Implementierung von StringTokenizer können nur einzelne Zeichen als Trenner verwendet werden. Es sind keine Zeichenfolgen denkbar wie »:=«.

Abbildung

Beispiel Um einen String mit Hilfe eines StringTokenizer-Objekts zu zerlegen, wird dem Konstruktor der Klasse der zu unterteilende Text als Parameter übergeben:
StringTokenizer tokenizer = new StringTokenizer(
  "Schweigen kann die grausamste Lüge sein." );

Sollen andere Zeichen als die voreingestellen Trenner den Satz zerlegen, kann dem Konstruktor als zweiter String eine Liste von Trennern übergeben werden. Jedes Zeichen, das in diesem String vorkommt, fungiert als einzelnes Trennzeichen:

StringTokenizer st = new StringTokenizer(
                       "Blue=0000ff\nGreen:00ff00\n", "=:\n" );

Um den Text abzulaufen, gibt es die Methoden nextToken() und hasMoreTokens(). Die Methode nextToken() liefert das nächste Token im String. Ist kein Token mehr vorhanden, wird eine NoSuchElementException ausgelöst. Damit wir frei von diesen Überraschungen sind, können wir mit der Methode hasMoreTokens() nachfragen, ob noch weitere Token anliegen. Das folgende Stück Programmtext zeigt die leichte Benutzung der Klasse:

String satz = "Faulheit ist der Hang zur Ruhe ohne 
"
              + "vorhergehende Arbeit";

StringTokenizer tokenizer = new StringTokenizer( satz );

while ( tokenizer.hasMoreTokens() )
     System.out.println( tokenizer.nextToken() );

Neben den beiden Konstruktoren existiert noch ein dritter, der auch die Trennzeichen als eigenständige Bestandteile bei nextToken() übermittelt.

Abbildung

class java.util.StringTokenizer
implements Enumeration

gp  StringTokenizer( String str, String delim, boolean returnTokens )
Ein StringTokenizer für str, wobei jedes Zeichen in delim als Trennzeichen gilt. Ist returnTokens gleich true, so werden die Trennzeichen beim Aufzählen mit zurückgegeben.
gp  StringTokenizer( String str,String delim )
Ein StringTokenizer für str, wobei alle Zeichen in delim als Trennzeichen gelten. Entspricht dem Aufruf von this(str, delim, false);
gp  StringTokenizer( String str )
Ein StringTokenizer für str. Entspricht dem Aufruf von
this(str, " \t\n\r\f", false); Die Trennzeichen sind Leerzeichen, Tabulator, Zeilenende und Seitenvorschub.
gp  boolean hasMoreTokens()
Testet, ob weitere Token verfügbar sind.
gp  String nextToken()
Liefert das nächste Token vom StringTokenizer.
gp  String nextToken( String delim )
Die Delimiterzeichen werden erst neu gesetzt und anschließend wird das nächste Token geholt.
gp  boolean hasMoreElements()
Ist gleich dem Aufruf von hasMoreTokens(). Existiert nur, damit das Objekt als Enumeration benutzt werden kann.
gp  Object nextElement()
Ist gleich dem Aufruf von nextToken(). Existiert nur, damit das Objekt als Enumeration benutzt werden kann. Der weniger spezifischer Ergebnistyp Object macht eine Typumwandlung erforderlich.
gp  int countTokens()
Zählt die Anzahl der noch möglichen nextToken()-Methodenaufrufe durch. Die aktuelle Position wird dadurch nicht berührt.

nextToken() und nextElement() können eine NoSuchElementException auslösen.


Galileo Computing

4.8 StreamTokenizer  downtop

Die Klasse StreamTokenizer aus dem io-Paket arbeitet noch spezialisierter als die StringTokenizer Klasse aus dem util-Paket. Im Gegensatz zum Tokenizer arbeitet die Klasse aber nicht auf Strings, sondern auf einem Datenstrom, genauer gesagt, einem Reader. Die Klasse InputStream sollte nicht mehr verwendet werden, da diese nur auf Bytes und nicht auf Unicode-Zeichen arbeitet. Doch beachtet ein StreamTokenizer sowieso keine Unicode-Eingabe, sondern nur Zeichen aus dem Bereich von \u0000 bis \u00FF.

Während des Parsens werden bestimmte Merkmale aus dem Text erkannt, so unter anderem Bezeichner (etwa Schlüsselworte), Zahlen, Strings in Anführungszeichen und verschiedene Kommentar-Arten (C-Stil oder C++-Stil). Verschiedene Java Tools von Sun verwenden intern einen StreamTokenizer, um ihre Eingabedateien zu verarbeiten, etwa das policy-Tool für die Rechteverwaltung. Der Erkennungsvorgang wird an Hand einer Syntaxtabelle überprüft. Diese Tabelle enthält zum Beispiel die Zeichen, die ein Schlüsselwort identifizieren oder die Zeichen, die Trennzeichen sind. Jedes gelesene Zeichen wird dann keinem, einem oder mehreren Attributen zugeordnet. Diese Attribute fallen in die Kategorie Trennzeichen, alphanumerische Zeichen, Zahlen, Hochkomma bzw. Anführungszeichen oder Kommentarzeichen.

Zur Benutzung der Klasse wird zunächst ein StreamTokenizer-Objekt erzeugt und dann die Syntaxtabellen initialisiert. Ob Kommentarzeilen überlesen werden sollen, wird durch

st.slashSlashComments( 
true );       // Kommentar
st.slashStarComments( true );        /* 
Kommentar */

gesteuert. Die erste Methode überliest im Eingabestrom alle Zeichen bis zum Return. Die zweite Methode überliest nur alles bis zum Stern/Slash. Geschachtelte Kommentare sind hier nicht möglich.

Beim Lesen des Dateistroms mit nextToken() kann über bestimmte Flags erfragt werden, ob im Stream ein Wort bzw. Bezeichner (TT_WORD), eine Zahl (TT_NUMBER), das Ende der Datei (TT_EOF) oder das Ende der Zeile (TT_EOL) vorliegt. Wichtig ist, eolIsSignificant(true) zu setzen, da andernfalls der StreamTokenizer nie ein TT_EOL findet. Wurde ein Wort erkannt, dann werden alle Zeichen in Kleinbuchstaben konvertiert. Dies lässt sich über die Methode lowerCaseMode(boolean) einstellen. Nach der Initialisierung eines StreamTokenizer-Objekts wird normalerweise solange nextToken() aufgerufen, bis die Eingabe keine neuen Zeichen mehr hergibt, also ein TT_EOF-Token erkannt wurde.

Beispiel Die folgende Klasse liest die Eingabe aus einer Datei und gibt die erkannten Textteile aus.

Listing 4.4   StreamTokenizerDemo.java
import java.io.*;

class StreamTokenizerDemo
{
  public static void main( String args[] ) throws IOException
  {
    String fn = "StreamTokenizerDemo.java";

    StreamTokenizer st = new StreamTokenizer(
                           new FileReader(fn) );

//     st.slashSlashComments( true );
    st.slashStarComments( true );
    st.ordinaryChar( '/' );
    st.parseNumbers();
    st.eolIsSignificant( true );

    int tval;

    while ( ( tval=st.nextToken() ) != st.TT_EOF )
    {
      if ( tval == st.TT_NUMBER )
        System.out.println( "Nummer: " + st.nval );

      else if ( tval == st.TT_WORD )
        System.out.println( "Wort: " + st.sval );

      else if ( tval == st.TT_EOL )
        System.out.println( "Ende der Zeile" );

      else
        System.out.println( "Zeichen: " + (char) st.ttype );
    }
  }
}

Die Ausgabe des Programms beginnt wie folgt:

Zeichen: /
Zeichen: /
Wort: Version
Nummer: 1.1
Ende der Zeile
Ende der Zeile
Wort: import
Wort: java.io.
Zeichen: *
Zeichen: ;
Ende der Zeile
Ende der Zeile
Wort: class
Wort: StreamTokenizerDemo
Ende der Zeile
Zeichen: {
Ende der Zeile
Wort: public

gp  StreamTokenizer( Reader r )
Erzeugt einen Tokenizer, der den Datenstrom zerlegt. Der Konstruktor, der das ganze auch mit einem InputStream macht, ist veraltet.
gp  void resetSyntax()
Reinitialisiert die Syntaxtabelle des Tokenizers, sodass kein Zeichen eine Sonderbehandlung genießt. Mit ordinaryChar() lässt sich das Verhalten eines Zeichen bestimmen.
gp  void wordChars( int low, int hi )
Zeichen im Bereich von low <= c <= high werden als Bestandteile von Wörtern erkannt, dementsprechend zusammengefasst und als Word-Token übergeben.
gp  void whitespaceChars( int low, int hi )
Zeichen im Bereich von low <= c <= high werden als Trennzeichen erkannt
gp  void ordinaryChars( int low, int hi )
Zeichen im Bereich von low <= c <= high genießen keine Sonderbehandlung und werden als normale Zeichen einzeln behandelt.
gp  void ordinaryChar( int ch )
Das Zeichen besitzt keine zusätzliche Funktion, ist zum Beispiel kein Kommentarzeichen, Trennsymbol oder Nummernzeichen. Spezialform für ordinaryChars(ch, ch).
gp  void parseNumbers()
Zahlen (Zahl-Literale) sollen vom Tokenizer erkannt werden. In der Syntaxtabelle gelten die zwölf Zeichen 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ., – als numerisch. Liegt eine Ganz- oder Fließkommazahl vor, so wird der Zahlenwert in nval abgelegt und das Token ergibt im Attribut ttype den Wert TT_NUMBER.
gp  void commentChar( int ch )
Gibt das Zeichen an, welches einen einzeiligen Kommentar einleitet. Alle nachfolgenden Zeichen werden dann bis zum Zeilenende ignoriert. So ließen sich beispielsweise FORTRAN-Kommentare nach commentChar( 'C' ) überlesen.
gp  void slashStarComments( boolean flag )
Der Tokenizer soll Kommentare im C-Stil (/* Müll */) erkennen oder nicht
gp  void slashSlashComments( boolean flag )
Der Tokenizer soll Kommentare im C++-Stil (// Zeile ) erkennen oder nicht
gp  void lowerCaseMode( boolean fl )
Liegt in ttype ein Token vom Typ TT_WORD vor, so wird dies automatisch in Kleinschreibweise konvertiert, falls fl gleich true ist.
gp  int nextToken() throws IOException
Liefert das nächste Token im Strom. Der Typ des Tokens wird im Attribut ttype hinterlegt. Zusätzliche Informationen befinden sich im Attribut nval (Nummer) oder sval (Zeichenkette). In der Regel wird solange geparst, bis das Token TT_EOF zurückgegeben wird.
gp  void pushBack()
Legt das aktuelle Token in den Eingabestrom zurück. Ein Aufruf von nextToken() liefert erneut den aktuellen Wert im Attribut ttype und ändert nval oder sval nicht.
gp  int lineno()
Liefert die aktuelle Zeilennummer in der Eingabedatei.

Konsoleneingaben über StreamTokenizer einlesen

Da der StreamTokenizer mit nextToken() Zeichenketten liefert, können wir ihn mit dem Standardeingabestrom des Betriebssystems System.in initialisieren und anschließend von der Konsole in dem Attribut in.sval eingegebene Strings auslesen. So lassen sich Eingaben einfach verarbeiten.

StreamTokenizer in = new StreamTokenizer( 
System.in );
System.out.print( "Wie heißt du? ");
in.nextToken();
System.out.println( "Hallo " + in.sval );

Erweitern und Schwächen

Obwohl die nextToken()-Funktion eine ganze Menge an Konvertierungen durchführt, erkennt sie keine in der Exponentialdarstellung geschriebenen Zahlen. Bei einer Gleitkommazahl wie -31.415E-1 versagt die Konvertierung und liefert nur -31.415 als Token vom Typ TT_NUMBER. Da StreamTokenizer nicht final ist, kann jeder die Klasse so erweitern, dass sie zum Beispiel TT_FLOAT_NUMBER bei einer Gleitkommazahl liefert.


Galileo Computing

4.9 Formatieren mit Format-Objekten  downtop

Zahlen, Datumsangaben und Text können auf verschiedenste Art und Weise formatiert werden. Unter Java wird dieses Formatierungsverhalten in einer abstrakten Klasse Format fixiert. Sie definiert die Funktionalität, um zum einen (landes-)sprachabhängige Ausgaben zu erzeugen, und zum anderen eingegebene Zeichenketten entsprechend zu zerlegen. Dazu stellt die Klasse Format die Methoden format() und parseObject() bereit. Jede Zeichenkette, die vom Format-Objekt erzeugt wurde, ist auch mit dem Parser wieder einlesbar.

Im JDK erweitern drei Klassen Format: DateFormat, MessageFormat und NumberFormat. Sie übernehmen die Ein-/Ausgabe für Datumsangaben, allgemeine Programmmeldungen und für Zahlen. Das Beispiel in der Tabelle zeigt Anwendungen der Klassen.

Tabelle 4.1   Beispiele für konkrete Format-Klassen
Ergebnis Formatiert mit
23 August 1998 DateFormat
1 plus 1 ist 2 MessageFormat
$12,349.00 NumberFormat

Jede dieser Klassen implementiert auf jeden Fall die Methoden zur Ausgabe format() und zur Erkennung von parseObject().

abstract class java.text.Format
implements Serializable, Cloneable

gp  String format( Object obj )
Formatiert das Objekt obj und gibt eine Zeichenkette zurück.
gp  abstract StringBuffer format( Object obj, StringBuffer toAppendTo, FieldPosition pos )
Formatiert ein Objekt und hängt den Text an den angegebenen StringBuffer an. Kann die Zeichenkette nicht mit format() nach den Regeln des Format-Objekts ausgegeben werden, löst die Methode eine IllegalArgumentException aus. Ist die Formatierungsanweisung falsch, so gibt format() das Unicode-Zeichen \uFFFD zurück.
gp  Object parseObject( String source )
Parsed den Text von Anfang an.
gp  abstract Object parseObject( String source, ParsePosition pos )
Der Text wird ab der Stelle pos umgewandelt. Konnte während des Parsens mit parse Object(String) die Zeichenkette nicht zurückübersetzt werden, so folgt eine Parse Exception.parseObject(String, ParsePosition) verändert das ParsePosition-Objekt nicht und gibt die Null-Referenz zurück.
gp  Object clone()
Gibt eine Kopie zurück.

Die Mehrzahl der Format-Unterklassen implementieren auch die folgenden Methoden:

gp  getInstance
Für ein Formatierungs-Objekt mit den Formatierungsregeln für das voreingestellte Land.
gp  getInstance( locale )
Für ein Formatierungs-Objekt mit den Formatierungsregeln für das angegebene Land.

Einige der Unterklassen implementieren ein noch spezielleres Verhalten. So zum Beispiel die Klasse NumberFormat, die mit getPercentInstance() und getCurrencyInstance() zwei Methoden zum angepassten Formatieren dieser speziellen Arten von Zahlen bietet.

Die Unterklassen von Format erlauben es dem Benutzer auch, weitere Objekte zu erzeugen, die an die speziellen Sprachbesonderheiten zusätzlicher Länder angepasst sind. Unterklassen, die dem Benutzer diese Möglichkeiten erlauben, sollten die Abfrage der unterstützten Länder mit

public static Locale[] getAvailableLocales()

ebenso implementieren.


Galileo Computing

4.9.1 Ausgaben formatieren  downtop

MessageFormat ist eine konkrete Unterklasse der abstrakten Klasse Format. Sie dient dazu, Nachrichten sprachunabhängig zu erzeugen. Das heißt, die tatsächliche sprachabhängige Ausgabe wird soweit wie möglich nach hinten geschoben und erst dann erzeugt, wenn die Nachricht dem Benutzer angezeigt werden soll. Durch MessageFormat werden nur Formatierungsanweisungen gegeben, und die wirklichen Informationen (also die Objekte als Informationsträger) werden zur Laufzeit eingesetzt. Dabei enthalten die Formatierungsanweisungen Platzhalter für diese Objekte (ähnlich wie printf() in C(++)). In der Regel werden Daten (die Argumente) erst zur Laufzeit ermittelt, wie etwa die Zeilennummer einer Fehlerstelle in einer Eingabedatei.

Beispiel Eine Anwendung des Formatierers

Der format()-Befehl formatiert die Argumente, die in einem Objekt-Feld abgelegt sind, mit dem Aussehen, wie es im Konstruktor des MessageFormat-Objekts angegeben wurde.

Object testArgs[] = { new Long(31415), "SchnelleLotte" 
};

MessageFormat form = new MessageFormat(
    "Anzahl Dateien auf der Festplatte \"{1}\": {0}.");
System.out.println(form.format(testArgs));

Die Ausgabe mit unterschiedlichen testArgs ist:

Anzahl Dateien auf der Festplatte "SchnelleLotte": 
0.
Anzahl Dateien auf der Festplatte "SchnelleLotte": 1.
Anzahl Dateien auf der Festplatte "SchnelleLotte": 31,415.

Die Argumente aus dem Array werden über die Platzhalter wie {0} in die Nachricht eingefügt. Die Nummern entsprechen der Reihenfolge der Argumente in Array. Einige Einträge im Array können ungenutzt bleiben. Fehlt allerdings das einem Platzhalter entsprechende Element im Feld, so wird eine ParseException geworfen.

class java.text.MessageFormat
extends Format

gp  MessageFormat( String pattern )
Erzeugt ein MessageFormat-Objekt mit dem angegeben Pattern.

Gebenüber anderen Format-Klassen zeigt MessageFormat eine Besonderheit, denn MessageFormat-Objekte werden über ihren Konstruktor erzeugt und nicht über getInstance(). Der Grund ist, dass üblicherweise die Erzeugungs-Methoden – damit sind die getInstance() Varianten gemeint – eine komplexe Initialisierung durchlaufen, die die landesspezifischen Einstellungen festlegen. MessageFormat ist aber nicht an eine bestimmte Sprache gebunden, benötigt folglich auch keine Initialisierung.

Bildungsgesetz für Message-Formate

Der Mustertext für eine Nachricht kann für die einzelnen Platzhalter eine bestimmte Art der Formatierung vorschreiben. Ein Bildungsgesetz in Form einer Grammatik schreibt die Bauweise vor:

messageFormatPattern :=
  string ( "{" messageFormatElement "}" string )*

messageFormatElement := argument { "," elementFormat }

elementFormat := "time" { "," datetimeStyle }
  | "date" { "," datetimeStyle }
  | "number" { "," numberStyle }
  | "choice" { "," choiceStyle }

datetimeStyle := "short"
  | "medium"
  | "long"
  | "full"
  | dateFormatPattern

numberStyle := "currency"
  | "percent"
  | "integer"
  | numberFormatPattern

choiceStyle := choiceFormatPattern
Tabelle 4.2   Bedeutung der Metazeichen
* Die Angabe kann beliebig wiederholt werden.
{} Optionales. Kann vorkommen, muss aber nicht.
() Bildung von Gruppen, um größere Wiederholungen zu beschreiben.
| Definition von Alternativen. Entweder das eine oder das andere.

Hinweis Bei den geschweiften Klammern besteht Verwechselungsgefahr als Message-Platzhalter oder Metazeichen der Grammatik.

Beispiel Die Klasse MessageFormat in der Praxis: Listing 4.5   MessageFormatDemo.java
import java.text.*;
import java.util.*;

public class MessageFormatDemo
{
  public static void main( String args[] )
  {
    Object[] arguments = {
      new Date(System.currentTimeMillis()),
        "die Antwort auf alle Fragen",
        new Integer(42)
    };

    String result = MessageFormat.format(
      "Am {0,date} um {0,time} ist {1} wie immer {2,number,integer}.",
      arguments );

    System.out.println( result );
  }
}

Dies erzeugt die Ausgabe

Am 21.09.2001 um 23:08:08 ist die Antwort auf alle 
Fragen wie immer 42.


Galileo Computing

4.9.2 Dezimalzahlformatierung  downtop

Die Klasse DecimalFormat dient zur formatierten Ausgabe von Zahlen. Dem Konstruktor wird ein Formatierungsstring übergeben, sozusagen eine Vorlage wie die Zahlen zu formatieren sind. Die Formatierung einer Zahl durch DecimalFormat erfolgt mit Rücksicht auf die aktuell eingestellte Sprache.

Listing 4.6   DezimalFormatTest.java
import java.text.*;

public class DezimalFormatTest
{
  public static void main( String args[] )
  {
    double d = 12345.67890;

    DecimalFormat df = new DecimalFormat( 
"###,##0.00" );
    System.out.println( df.format(d) );
  }
}

Der Formatierungsstring kann eine Menge von Formatierungsanweisungen vertragen.

Tabelle 4.3   Formatierungsanweisungen für DecimalFormat
Symbol Bedeutung
0 Repräsentiert eine Ziffer
# Eine Ziffer. Ist an dieser Stelle keine angegeben, bleibt die Stelle leer
. Trennt Vor- und Nachkommastellen
, Gruppiert die Ziffern (Eine Gruppe ist so groß wie der Abstand von ’,’ zu ’.’)
; Trennzeichen für mehrere Formate
- Das Standard-Zeichen für das negativ Präfix
% Die Zahl wird mit 100 multipliziert und als Prozentwert ausgewiesen
%% Genau wie % nur mit Promille
- Nationales Währungssymbol (DM für Deutschland)
-- Internationales Währungssymbol (DEM für Deutschland)
X Alle anderen Zeichen können ganz normal benutzt werden
Ausmarkieren von speziellen Symbolen im Präfix oder Suffix benutzt

Beispiel Auswirkungen der Formatanweisungen auf die Zahlen 12345.6789, 400.56, 1234567.789, 5.815 und -42.17.

Tabelle 4.4   Beispiel für verschiedene Formatanweisungen
Format Zahl Ergebnis
####,###.##- 12345.6789 12.345,68DM
####,###.##- 400.56 400,56DM
####,###.##- 1234567.789 1.234.567,79DM
####,###.##- 5.815 5,82DM
####,###.##- -42.17 -42,17DM
####,##.#######-- 12345.6789 1.23.45,6789DEM
####,##.#######-- 400.56 4.00,56DEM
####,##.#######-- 1234567.789 1.23.45.67,789DEM
####,##.#######-- 5.815 5,815DEM
####,##.#######-- -42.17 -42,17DEM
###,00.00- 12345.6789 1.23.45,68DM
###,00.00- 400.56 04.00,56DM
###,00.00- 1234567.789 1.23.45.67,79DM
###,00.00- 5.815 00.05,82DM
###,00.00- -42.17 -00.42,17DM


Galileo Computing

4.10 Reguläre Ausdrücke  downtop

Mit dem Paket java.util.regex lässt sich mit Hilfe der Klassen Matcher und Pattern eine Zeichenkette gegen ein Muster prüfen. Mit dieser leistungsfähigen Implementierung lässt sich leicht die Funktionalität von Dienstprogrammen wie awk, sed, emacs, perl oder grep nachbilden.

Im Prinzip besteht das Erkennen immer aus dem Aufbau eines Pattern-Objekts mit dem regulären Ausdruck und der Prüfung:

Pattern p = Pattern.compile( "a*b" );
Matcher m = p.matcher( "aaaaab" );
boolean b = m.matches();
Die einzelnen Zeilen lassen sich auch zusammenfassen:
boolean b = Pattern.matches( "a*b", "aaaaab" );

Für reguläre Ausdrücke existieren eine ganze Anzahl von Regeln. Eine Übersicht findet sich in der API-Hilfe in das Klasse java.util.regex.Pattern. Widmen wir uns zunächst dem Funktionenangebot:

class java.util.regex.Pattern
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 und CANON_EQ erlaubt.

int flags()

Liefert die Flags, nach denen 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.

String pattern()

Liefert den regulären Ausdruck, den das Pattern repräsentiert.

String[] split(CharSequence input 
)

Spaltet die Eingabesequenz auf.

String[] split(CharSequence input, 
int limit )

Spaltet die Eingabesequenz auf, liefert jedoch maximal begrenzt viele Teilzeichenfolgen.

Einige Methoden sind lediglich Abkürzungen. So macht der statische Aufruf Pattern. matches() nichts anderes, als ein Pattern zu compileren:

Pattern.compile(regex).matcher(input).matches()

Galileo Computing

4.10.1 Splitten von Zeichenketten  downtop

Mit der Funktion split() aus Pattern oder String kann ein Trennfolge definiert werden, die eine Zeichenkette in Teilzeichenketten zerlegt, ähnlich wie es der StringTokenizer macht. Der StringTokenizer ist jedoch beschränkt auf einzelne Zeichen als Trennsymbole, während die Methode split() einen regulären Ausdruck zur Beschreibung der Trennsymbole verwendet:

class java.util.regex.Pattern

String[] split( CharSequence input 
)

Zerlegt die Zeichenfolge input in Teilzeichenketten, wie es das aktuelle Pattern-Objekt befiehlt.

String[] split( CharSequence input, 
int limit )

Wie split(CharSequence), nur durch limit begrenzt viele Teilzeichenketten.

Beispiel Datumsformate sollen in der Reihenfolge Tag, Monat, Jahr eingegeben werden. Die Trennzeichen sollen jedoch entweder Punkt, Minus oder Slash sein. Damit lässt sich ein Pattern aufbauen, das folgendes Format hat:
Pattern p = Pattern.compile( "[/.-]" );

In diesem Fall funktioniert dies auch noch mit new StringTokenizer(...,"/.-"), doch längere Trennsymbole wie := oder :: würden mit StringTokenizer nicht mehr funktionieren.

An die split()-Funktion übergeben wir als Parameter die Zeichenkette, die zerlegt werden soll. Werfen wir ein Blick auf ein Beispiel, welches die Felder in der Methode date() ausgibt:

Listing 4.7   SplitDemo.java
import java.util.*;
import java.util.regex.*;

public class SplitDemo
{
  public static void date( String fields[] )
  {
    for ( int i=0; i<fields.length; i++ )
      System.out.print( fields[i] + " " );

    System.out.println();
  }

  public static void main( String args[] )
  {
    Pattern p = Pattern.compile( "[/.-]" );

    date( p.split( "12-3-1973" ) );
    date( p.split( "12.3.1973" ) );
    date( p.split( "12/3/1973" ) );
  }
}

Galileo Computing

4.10.2 split() in String  downtop

In der String-Klasse gibt es auch die Funktion split(). Sie ist eine Objektmethode, die die aktuelle Zeichenkette, die das String-Objekt repräsentiert, zerlegt. Diese Methode benötigt jetzt umgekehrt den regulären Ausdruck als Parameter. Die Implementierung delegiert jedoch die eigentliche Arbeit an das Pattern-Objekt:

public String[] split(String regex, int limit)
{
  return Pattern.compile(regex).split(this, limit);
}

public String[] split(String regex)
{
  return split(regex, 0);
}
class java.lang.String implements CharSequence, Serializable

gp  String[] split( String regex )
Zerlegt die aktuelle Zeichenkette mit dem regulären Ausdruck.
gp  String[] split( String regex, int limit )
Zerlegt die aktuelle Zeichenkette mit dem regulären Ausdruck, liefert jedoch maximal begrenzt viele Teilzeichenfolgen.

Galileo Computing

4.10.3 Das Paket gnu.regexp  downtop

Das gnu.regexp-Paket ist eine Java Implementierung der klassischen Bibliothek für reguläre Ausdrücke auf der Basis endlicher Automaten. Das Paket in der Version 1.1.3 ist vom Juni 2001 und lässt sich unter ftp://ftp.cacas.org/pub/java/gnu.regexp-1.1.3.tar.gz laden. Das Paket enthält das Java-Archiv und den kompletten Quellcode, da es unter der GNU Library General Public License steht. Eine ausführliche Beschreibung der Ausdrücke findet sich in der Hilfe »Syntax and Usage Notes«, die dem gnu.regexp-Paket beiliegt.


Galileo Computing

4.11 Überprüfung der E-Mail-Adressen und Kreditkarteninformationen  downtop


Galileo Computing

4.11.1 Gültige E-Mail-Adressen  downtop

Benutzer von Webseiten werden an vielen Stellen aufgefordert, ihre E-Mail-Adresse für Nachrichtenverteiler und Informationen einzutragen. Hier kommt es häufiger vor, dass Kunden nicht ihre richtige Adresse eintragen, sondern irgendwelchen Unsinn. Schlauerweise sollte man schon im Vorfeld grobe Fehler erkennen und abblocken. Bei konventionellen HTML-Seiten bieten sich hierfür JavaScript-Überprüfungen an. Wir wollen aber eine pure Java-Lösung vorstellen, die speziell für Servlets interessant ist.

Schauen wir uns die Elemente einer korrekten E-Mail-Adresse an: Spende@KuchenFuer Ulli.org. Wir haben einen Namen, der mindestens ein Buchstabe lang ist. Das heißt, das @-Zeichen darf nicht am Anfang stehen. Genauso gut muss eine Endung existieren. Daher darf der hinterste Punkt nicht an letzter Stelle auftauchen. Dann müssen wir noch testen, ob überhaupt eine Zeichenkette eingetragen ist, und ob mindestens ein Punkt hinter dem @-Zeichen steht. Folgendes Programm testet eine Eingabe entsprechend:

Listing 4.8   EmailChecker.java
class EMailChecker
{
  static boolean isValidEMail( String s )
  {
    s = s.trim();

     int at, dot, len=s.length();

    // s nicht angegeben (oder nur Whitespaces), oder kein @ bzw .

    if ( (len == 0) ||
         ((at=s.indexOf('@')) == -1) || ((dot=s.lastIndexOf('.')) == 
-1) )
      return false;

    // keine EMailadresse vor @ Zeichen oder . vor &

     if ( (at == 0) || (dot < at) )
      return false;

    // Mindestens zwei Zeichen für die Endung

    return dot+2 < len;
  }

  public static void main( String args[] )
  {
    System.out.println( isValidEMail("Ulli@Java-Tutor.com") );

    System.out.println( isValidEMail("   ") );
    System.out.println( isValidEMail("doofnase") );
    System.out.println( isValidEMail("@Java-Tutor.com") );
    System.out.println( isValidEMail("Ulli@Java-Tutor") );
    System.out.println( isValidEMail("Ulli@Java-Tutor.") );
  }
}

Eine intelligente Erweiterung wäre, die Endung mit allen möglichen Länderendungen (de, nl, tv, ...) bzw. Domain-Kennungen (com, net, org, ...) zu validieren. Zusätzlich könnten wir versuchen, eine Verbindung zu dem angegebenen Server aufzubauen, um die Adresse zu testen. Wir könnten auch auf Namensdienste zurückgreifen und versuchen, herauszufinden, ob der Nutzer eingetragen ist. Denkbar ist auch eine Überprüfung in Newsgruppen, oder ob der Benutzer sich in auffälligen Foren bewegt usw. Der Fantasie sind keine Grenzen gesetzt.


Galileo Computing

4.11.2 Kreditkartennummern testen  toptop

E-Commerce-Lösungen sind im Internet mittlerweile häufig anzutreffen. Lassen sich für kleine Beträge Sonderlösungen finden, werden für größere Beträge immer noch Kreditkarten verwendet. Grund genug für uns Java-Programmierer, die Nummern der Karten zu testen, um herauszufinden, ob uns nicht ein Anwender täuschen wollte.

Die Nummer einer Kreditkarte setzt sich nicht willkürlich zusammen. Die Nummern von Karten eines bestimmten Herstellers bestehen aus einer festen Anzahl von meistens vierzehn bis sechzehn Ziffern. Als Kennung für einen Hersteller (Visa , MasterCard, American Express) ist jeder Nummer eine zusätzliche Kennung von einer bis vier Ziffern vorangestellt. Die Ziffern der Kartennummer werden durch mathematische Verfahren überprüft. Wir wollen eines dieser Verfahren auch kennen lernen; den so genannten Luhn-Algorithmus. Dieser Algorithmus testet die Korrektheit des Aufbaus einer Nummernfolge. Die letzte Ziffer ist oft eine berechnete Checksummen-Ziffer.

Die folgende Tabelle gibt eine Übersicht über einige Kartenhersteller. Sie führt die Kennung, die Länge der Kartennummer und ein gültiges Beispiel auf:

Tabelle 4.5   Kreditkartenhersteller und ihre Kennnummern
Hersteller Anfang Gesamtlänge Beispiel
Visa 4 13, 16 4111 1111 1111 1111
Master 51,52,53,54,55 16 5500 0000 0000 0004
Diner’s Club 30,36,38 14 3000 0000 0000 04
American Express 34, 37 15 3400 0000 0000 009
Discover 6011 16 6011 0000 0000 0004
en Route 2014, 2149 15 2014 0000 0000 009
JCB 3088,3096,3112,3158,3337,3528 16 3088 0000 0000 0009

Neben den Herstellern sind auch folgende Nummern von den ausgebenden Banken im Umlauf: Manufacturers Hanover Trust (1033), Citibank (1035), Chemical Bank (1263), Chase Manhattan (1665), Bank of America (4024), Citicorp (4128), New Era Bank (4209), HHBC (4302), Imperial Savings (4310), MBNA (4313), California Federal (4317), Wells Fargo (5282), Citibank (5424), Wells Fargo (5410), Bank of New York (5432), MBNA (6017). Carte Blanche und Diner’s Club sind gleich. 10 

Die Überprüfung mit dem Luhn-Algorithmus

Der Luhn-Algorithmus (auch modulus 10 oder mod 10-Algorithmus genannt) basiert auf dem ANSI-Vorschlag X4.13. Er wurde Ende 1960 von einer Gruppe Mathematiker ausgearbeitet und veröffentlicht. Danach nutzten Kreditkartenhersteller dieses Verfahren zur Prüfung der Kreditkartennummern. Auch die Versichertennummer in Kanada, die Canadian Social Insurance Number (CSIN), wird über das Luhn-Verfahren geprüft.

Der Algorithmus testet, ob die letzte Ziffer der Kreditkartennummer korrekt zu den angegebenen Zahlen passt. Die Testziffer wird von allen Ziffern, außer der letzten Ziffer, berechnet und anschließend mit der angegeben Testziffer verglichen. Stimmt sie überein, ist die Karte seitens der Prüfnummer in Ordnung. Wir wollen das Verfahren hier nicht näher vertiefen, sondern einfach den Algorithmus angeben:

Listing 4.9   LuhnTest.java
class LuhnTest
{
  static boolean luhnTest( String s )
  {
    int len = s.length();

    int digits[] = new int[len];

    for ( int i = 0; i < len; i++ )
    {
      try {
        digits[i] = Integer.parseInt( s.substring(i,i+1) );
      }
      catch ( NumberFormatException e ) {
        System.err.println( e );
        return false;
      }
    }

    int sum=0;

    while ( len > 0 )
    {
      sum += digits[len-1];
      len--;

      if ( len > 0 )
      {
        int digit = 2*digits[len-1];
        sum += ( digit>9) ? digit-9 : digit;

        len--;
      }
    }

    return ( sum%10 == 0 );
  }


  static boolean isVisa( String s )
  {
    if ( ( (s.length() == 16) || (s.length() == 13) ) &&
          (s.charAt(0) == '4') )
      return luhnTest( s );

    return false;
  }

  public static void main( String args[] )
  {
    System.out.println( luhnTest( "4111111111111111" ) );
    System.out.println( luhnTest( "5500000000000004" ) );
    System.out.println( luhnTest( "340000000000009" ) );
    System.out.println( luhnTest( "30000000000004" ) );
    System.out.println( luhnTest( "6011000000000004" ) );
    System.out.println( luhnTest( "201400000000009" ) );
    System.out.println( luhnTest( "3088000000000009" 
) );
    System.out.println( luhnTest( "9238475098787444" ) );

    System.out.println( isVisa( "4111111111111111" ) );
    System.out.println( isVisa( "5500000000000004" ) );


    // Böse: Visa-Nummer generieren

    char c[] = "4123456789123456".toCharArray();

    while ( !isVisa(new String(c)) )
      c[(int)(Math.random()*c.length-1)+1] = (char)('0'+Math.random()*9.9);

    System.out.println( c );
  }
}

Im Quelltext ist eine zusätzliche Methode eingebaut, die testet, ob die Karte von Visa ist. Dazu müssen wir nur testen, ob die erste Ziffer eine 4 ist und ob die gesamte Zahl nach dem Luhn-Verfahren gültig ist. Andere Tests sind genauso einfach durchzuführen. Eine Erweiterung wäre, die Methode fehlertoleranter zu gestalten, indem Trennzeichen herausgefiltert werden. Dies und die Implementierung der übrigen Tests überlasse ich als Übung den Lesern.

Beispiel Mit diesen Methoden ist es natürlich leicht möglich, Nummern zu erzeugen. Betrachten wir folgendes:
char c[] = "4123456789123456".toCharArray();
while ( !isVisa(new String(c)) )
  c[(int)(Math.random()*c.length-1)+1] =
                            (char)('0'+Math.random()*9.9);

System.out.println( c );

Wir beginnen mit einer vorgegebenen, unsinnigen Karten-Nummer, deren erste Stelle »4« ist, wie für eine Visa-Karte erforderlich. Anschließend ändern wir in einer Schleife eine zufällig ausgewählte Stelle der Kartennummer (außer der ersten) in eine ebenfalls zufällig bestimmte Ziffer aus dem Bereich »0« bis »9« ab. Das wiederholen wir so lange, bis die abgewandelte Zahl irgendwann passt.






1    Diese Ungenauigkeit ist unter der Bug-ID 4075078 bei Sun gemeldet und immer noch in Bearbeitung. Eine rückwärtskompatible Lösung gibt es aber nicht.

2    Seltsam ist, dass die Entwickler keine Methode regionMatchesIgnoreCase() vorgesehen haben, wo es doch auch equalsIgnoreCase() und compareToIgnoreCase() gibt; wird der Methodenname zu lang? Das sind wieder Geheimnisse, die sich nicht so leicht ergründen. Wie dem auch sei, auch equals IgnoreCase() nutzt intern die regionMatches()-Methode.

3    Ist der BMI > 30, dann lese nicht weiter, gehe später über Los und spaziere Richtung Park. – Nein! Nicht zum Zigarettenholen!

4    Wieder ein Hinweis, dass Visual Basic einfach die bessere Sprache ist ...

5    Mehr deutsche Palindrome gibt es unter http://studserv.stud.uni-hannover.de/~hinze/palindrom.htm. Eine Liste mit englischen Palindromen und ein paar Anagrammen gibt es unter http://freenet. buffalo.edu/~cd431/palindromes.html. Noch mehr unter http://www.vis.colostate.edu/~scriven/ anagrammer.php3.

6    Erstaunlicherweise ergibt sich unter einigen Linux-Versionen ein etwas anderes Bild, denn die Sortierreihenfolge ist dort durch einen Bug falsch.

7    Die Methode hasMoreElemente() ruft direkt hasMoreTokens() auf und wurde nur implementiert, damit ein StringTokenizer auch die Schnittstelle Enumeration implementiert.

8    Veni, Vidi, VISA: I came, I saw, I did a little shopping.

9    Der Name der Diners Karte versteht sich aus der Geschichte: Einen Abend im Februar 1950 vergaß Frank MacNamara seine Brieftasche. Er kam auf die Idee, eine Kreditkarte aus Karton anzubieten. Mit seinen Freunden gründete er am 28.2.1950 den Diners Club, der im Gründerjahr mehr als 10.000 Mitglieder und 1.000 Vertragspartner fasste. So war die erste Kreditkarte geboren.

10    Im Jahre 1958 entschloss sich das internationale Transport-, Reise-, und Finanzierungsunternehmen American Express, eine eigene Karte herauszugeben.

  

Java 2




Copyright © Galileo Press GmbH 2002
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.


[Galileo Computing]

Galileo Press GmbH, Gartenstraße 24, 53229 Bonn, fon: 0228.42150.0, fax 0228.42150.77, info@galileo-press.de