2.5Ausgaben formatieren
Ausgaben sind in Programmen alltäglich, wobei wir hierunter nicht nur die Konsolenausgaben verstehen wollen, sondern alle Umwandlungen in ein lesbares Format. In diesem Kapitel geht es um die diversen Formatierungsklassen zum Aufbereiten von Strings.
2.5.1Die Formatter-Klasse *
Die String.format(…)-Methode und prinf(…)-Methoden der Ein-/Ausgabeklassen übernehmen die Aufbereitung nicht selbst, sondern delegieren sie an die Klasse java.util.Formatter. Das ist auch der Grund, warum die Dokumentation für die Formatspezifizierer nicht etwa an String.format(…) hängt, sondern an Formatter.
Konstruktor vom Formatter
Die Klasse Formatter hat eine beeindruckende Anzahl an Konstruktoren:
Formatter()
Formatter(Appendable a)
Formatter(Appendable a, Locale l)
Formatter(File file)
Formatter(File file, String csn)
Formatter(File file, String csn, Locale l)
Formatter(Locale l)
Formatter(OutputStream os)
Formatter(OutputStream os, String csn)
Formatter(OutputStream os, String csn, Locale l)
Formatter(PrintStream ps)
Formatter(String fileName)
Formatter(String fileName, String csn)
Formatter(String fileName, String csn, Locale l)
Wird nicht der Standard-Konstruktor eingesetzt, schreibt der Formatter in die angegebene Quelle. Daher ist die Klasse schön für das Schreiben von Texten in Dateien geeignet. Formatter implementiert Closeable, ist also auch AutoCloseable. Ein Beispiel zum Schreiben in Dateien:
Listing 2.16WriteFormattedStringToFile.java, main()
StandardCharsets.ISO_8859_1.name() ) ) {
for ( int i = 0; i < 10; i++ )
out.format( "%02d%n", i );
}
catch ( FileNotFoundException | UnsupportedEncodingException e ) {
e.printStackTrace();
}
format(…)-Aufrufe optimieren
Ein Blick auf die Methode format(…) der Klasse String verrät, wie der Formatter ins Spiel kommt:
Listing 2.17java.lang.String, format()
return new Formatter().format( format, args ).toString();
}
Ein Formatter übernimmt zwei Aufgaben. Er übernimmt zum einen die tatsächliche Formatierung, und zum anderen gibt er die formatierten Ausgaben an ein Ziel weiter. Wird der Formatter mit dem Standard-Konstruktor aufgerufen, so baut er selbst das Ausgabeziel aus einem StringBuilder auf, den folgende format(…)-Aufrufe dann füllen. toString() vom Formatter ist so implementiert, dass es auf dem Ausgabeziel (also in unserem Fall dem StringBuilder) toString() aufruft.
Das Wissen um diesen Mechanismus ist für die Optimierung wichtig, um nicht zu viele Zwischenobjekte zu erzeugen. So führt die Schleife
for ( double d = 0; d <= 1; d += 0.1 ) {
String s = String.format( "%.1f%n", d );
sb.append( s );
}
System.out.println( sb ); // 0,1 0,2 … 1,0
zu:
for ( double d = 0; d <= 1; d += 0.1 ) {
String s = new Formatter().format( "%.1f%n", d ).toString();
sb.append( s );
}
System.out.println( sb ); // 0,1 0,2 … 1,0
Bei jedem Schleifendurchlauf wird also ein neuer Formatter aufgebaut. Intern entsteht damit ein neuer StringBuilder als Ziel für die formatierten Strings und schlussendlich über toString() ein String-Objekt. Nicht zu vergessen sind die internen char-Felder und die automatische Speicherbereinigung, die die Objekte wieder wegräumen muss.
Würden wir gleich das Ziel angeben, so könnte das viel effizienter werden. Dazu wird nicht der Standard-Konstruktor von Formatter eingesetzt, der das Ziel mit einem neuen StringBuilder vorbestimmt, sondern ein eigenes Zielobjekt, das unser StringBuilder sein kann (es ist alles erlaubt, was vom Typ Appendable ist). Optimiert folgt somit:
Listing 2.18FormatterDemo.java, main()
Formatter formatter = new Formatter( sb );
for ( double d = 0; d <= 1; d += 0.1 )
formatter.format( "%.1f%n", d );
System.out.println( formatter ); // 0,1 0,2 … 1,0
Wir weisen in der Schleife den Formatter an, die Formatierung vorzunehmen. Da dieser mit dem Ziel StringBuilder aufgebaut wurde, füllen die Zahlen nach und nach unseren StringBuilder. Temporäre Zwischenobjekte werden so minimiert. Zum Schluss wird der Formatter nach dem Ergebnis gefragt.
Formattable und formatTo(…)
Der Formatspezifizierer %s kann auf jedem Argumenttyp angewendet werden, denn durch die Varargs werden auch primitive Elemente zu Wrapper-Objekten, die eine toString()-Methode haben. Nun kann es aber sein, dass toString() besonders implementiert werden muss und nicht unbedingt die Zeichenkette liefert, die für die Ausgabe gewünscht ist. Für diesen Fall berücksichtigt der Formatter einen besonderen Typ. Implementiert die Klasse die besondere Schnittstelle java.util.Formattable, so ruft der Formatter nicht die toString()-Methode auf, sondern formatTo(Formatter formatter, int flags, int width, int precision). Die API-Dokumentation liefert ein Beispiel.
2.5.2Formatieren mit Masken *
Oftmals unterscheidet sich bei grafischen Oberflächen die Darstellung von Daten von dem tiefer liegenden Datenmodell. Während ein Datum zum Beispiel intern als große Zahl vorliegt, soll der Anwender sie in der gewünschten Landessprache sehen können. Bei einigen Ausgaben kommen Trennzeichen in die Ausgabe, um sie für den Leser besser verständlich zu machen. Eine IP-Adresse enthält Punkte an ganz bestimmten Stellen, eine Telefonnummer trennt die Vorwahl vom Rest ab, und die Segmente eines Datums trennen in der Regel die Zeichen »/« oder »-«.
Für Formatierungen, bei denen ein Original-String in einen Ausgabe-String konvertiert wird und dabei neue Zeichen zur Ausgabe eingefügt werden, bietet die Java-API eine Klasse javax.swing.text.MaskFormatter. Die Swing-Klasse hilft bei der Formatierung und dem Parsen:
Listing 2.19MaskFormatterDemo.java, main()
mf.setValueContainsLiteralCharacters( false );
String valueToString = mf.valueToString( "12031973" );
System.out.println( valueToString ); // 12-03-1973
Object stringToValue = mf.stringToValue( valueToString );
System.out.println( stringToValue ); // 12031973
Der Konstruktor von MaskFormatter bekommt ein Muster, wobei es Platzhalter gibt. Das Sternchen * steht für ein Zeichen. Die Methode valueToString(…) bringt einen String in das Muster. Der gemusterte String wandelt stringToValue() wieder in das Original um.
Das Schöne ist, dass die Musterdefinitionen aus einer externen Quelle stammen können, ohne den Programmcode mit speziellen Formatierungsanweisungen zu verschmutzen. Neben * gibt es weitere Platzhalter, die erlaubte Zeichen eingrenzen, sodass bei der Umwandlung mit valueToString() eine ParseException ausgelöst wird, wenn das Zeichen nicht im Format des Musterplatzhalters ist.
Musterzeichen | Steht für |
---|---|
* | jedes Zeichen |
# | eine Zahl, wie Character.isDigit(…) sie testet |
? | ein Zeichen nach Character.isLetter(…) |
A | ein Zeichen oder eine Ziffer, also Character.isLetter(…) oder Character.isDigit(…) |
U | ein Zeichen nach Character.isLetter(…), aber konvertiert in einen Großbuchstaben |
L | ein Zeichen nach Character.isLetter(…), aber konvertiert in einen Kleinbuchstaben |
H | ein Hexadezimalzeichen (0–9, a–f oder A–F) |
' | einen ausmaskierten und nicht interpretierten Bereich |
Tabelle 2.11Musterplatzhalter
Weitere Möglichkeiten der Klasse beschreibt die API-Dokumentation.
2.5.3Format-Klassen
Die Methode format(…) formatiert Zahlen, Datumswerte und sonstige Ausgaben und benötigt wegen ihrer Komplexität eine Beschreibung von mehreren Bildschirmseiten. Dabei gibt es noch eine andere Möglichkeit, für unterschiedliche Typen von zu formatierenden Werten eigene Klassen zu haben:
Die Klassen haben gemeinsam, dass sie die abstrakte Klasse Format erweitern und so eine gemeinsame Schnittstelle haben. Jede dieser Klassen implementiert auf jeden Fall die Methode format(…) zur Ausgabe und zum Parsen, also zur Konvertierung vom String in das Zielobjekt, die Methode parseObject(…).
Zwei Gründe sprechen für den Einsatz der Format-Klassen:
Es gibt in String zwar eine format(…)-Methode, aber keine parseXXX(…)-Methode.
Die Format-Klassen liefern mit statischen getXXXInstance(…)-Methoden vordefinierte Format-Objekte, die übliche Standardausgaben erledigen, etwa gerundete Ganzzahlen, Prozente oder unterschiedlich genaue Datums-/Zeitangaben.
Das folgende Beispiel zeigt einige Anwendungen zum zweiten Punkt.
Ergebnis | Formatiert mit |
---|---|
02.09.2005 | DateFormat.getDateInstance().format( new Date() ) |
15:25:16 | DateFormat.getTimeInstance().format( new Date() ) |
02.09.2005 15:25:16 | DateFormat.getDateTimeInstance().format( new Date() ) |
12.345,679 | NumberFormat.getInstance().format( 12345.6789 ) |
12.345,68 € | NumberFormat.getCurrencyInstance().format( 12345.6789 ) |
12 % | NumberFormat.getPercentInstance().format( 0.123 ) |
Tabelle 2.12Formatobjekte im Einsatz
Abbildung 2.3DateFormat, MessageFormat und NumberFormat erweitern die abstrakte Klasse Format. Die Unterklassen übernehmen die Ein-/Ausgabe für Datumsangaben, für allgemeine Programmmeldungen und für Zahlen.
Beim Einsatz von DateFormat.getDateInstance().format(date) berücksichtigt die Methode korrekt je nach Land die Reihenfolge von Tag, Monat und Jahr und das Trennzeichen. Bei einem String.format(…) über %t müssten die drei Segmente je nach Sprache in die richtige Reihenfolge gebracht werden, sodass die Variante über DateFormat besser ist.
[»]Hinweis
NumberFormat stellt die Zahlen nicht in Exponentialschreibweise dar, und standardmäßig ist die Anzahl an Nachkommastellen beschränkt:
System.out.println( NumberFormat.getInstance().format( 2E-30 ) );
Die Ausgabe ist:
0
implements Serializable, Cloneable
String format(Object obj)
Formatiert das Objekt obj und gibt eine Zeichenkette zurück.abstract StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos)
Formatiert ein Objekt und hängt den Text an den angegebenen StringBuffer an (eine Methode mit StringBuilder gibt es nicht). 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.Object parseObject(String source)
Analysiert den Text von Anfang an.abstract Object parseObject(String source, ParsePosition pos)
Der Text wird ab der Stelle pos umgewandelt. Konnte parseObject(…) die Zeichenkette nicht zurückübersetzen, so folgt eine ParseException. parseObject(String, ParsePosition) verändert das ParsePosition-Objekt nicht und gibt die null-Referenz zurück.Object clone()
Gibt eine Kopie zurück.
Die Mehrzahl der Format-Unterklassen implementiert statische Fabrikmethoden der Art:
static XXXFormat getYYYInstance()
Liefert ein Formatierungsobjekt mit den Formatierungsregeln für das voreingestellte Land.static XXXFormat getYYYInstance(Locale l)
Liefert ein Formatierungsobjekt mit den Formatierungsregeln für das angegebene Land. So erlauben es die Unterklassen von Format dem Benutzer auch, weitere Objekte zu erzeugen, die an die speziellen Sprachbesonderheiten der Länder angepasst sind.
2.5.4Zahlen, Prozente und Währungen mit NumberFormat und DecimalFormat formatieren *
NumberFormat widmet sich der Ausgabe von Zahlen. Dabei unterstützt die Klasse vier Typen von Ausgaben, für die es jeweils eine statische Fabrikmethode gibt.
extends Format
static NumberFormat getNumberInstance()
Liefert den einfachen Formatierer für Zahlen.static NumberFormat getIntegerInstance()
Liefert einen Formatierer, der den Nachkommateil abschneidet und rundet.static NumberFormat getPercentInstance()
Liefert einen Formatierer, der Fließkommazahlen über die format(…)-Methode im Bereich von 0 bis 1 annimmt und dann als Prozentzahl formatiert. Nachkommastellen werden abgeschnitten.static NumberFormat getCurrencyInstance()
Liefert einen Formatierer für Währungen, der ein Währungszeichen zur Ausgabe hinzufügt.
Die genannten vier statischen Methoden gibt es jeweils in der parameterlosen Variante und in der Variante mit einem Locale-Objekt, um etwa das Währungszeichen oder das Kommazeichen anzupassen.
Dezimalzahlformatierung mit DecimalFormat
DecimalFormat ist eine Unterklasse von NumberFormat und ermöglicht individuellere Anpassungen an die Ausgabe. Dem Konstruktor kann ein Formatierungs-String übergeben werden, 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 2.20DecimalFormatDemo.java, main()
DecimalFormat df = new DecimalFormat( "###,##0.00" );
System.out.println( df.format( d ) ); // 12.345,68
Der Formatierungs-String kann eine Menge von Formatierungsanweisungen vertragen; im Beispiel kommen #, 0 und das Komma vor. Die beiden wichtigen Symbole sind jedoch 0 und #. Beide repräsentieren Ziffern. Der Unterschied tritt erst dann zu Tage, wenn weniger Zeichen zum Formatieren da sind, als im Formatierungs-String genannt werden.
Symbol | Bedeutung |
---|---|
0 | Repräsentiert eine Ziffer – ist die Stelle nicht belegt, wird eine Null angezeigt. |
# | Repräsentiert eine Ziffer – ist die Stelle nicht belegt, bleibt sie leer, damit führende Nullen und unnötige Nullen hinter dem Komma nicht angezeigt werden. |
. | Dezimaltrenner, trennt Vor- und Nachkommastellen. |
, | Gruppiert die Ziffern (eine Gruppe ist so groß wie der Abstand von |
; | Trennzeichen; links davon steht das Muster für positive Zahlen, rechts davon das Muster für negative Zahlen. |
- | das Standardzeichen für das Negativpräfix |
% | Die Zahl wird mit 100 multipliziert und als Prozentwert ausgewiesen. |
\u2030 | Die Zahl wird mit 1.000 multipliziert und als Promillewert ausgezeichnet. |
\u00A4 | nationales Währungssymbol (€ für Deutschland) |
\u00A4\u00A4 | internationales Währungssymbol (EUR für Deutschland) |
X | Alle anderen Zeichen – symbolisch X – können ganz normal benutzt werden. |
' | Ausmaskieren von speziellen Symbolen im Präfix oder Suffix |
Tabelle 2.13Formatierungsanweisungen für DecimalFormat
Hier sehen wir ein Beispiel für die Auswirkungen der Formatanweisungen auf einige Zahlen:
Format | Eingabezahl | Ergebnis |
---|---|---|
0000 | 12 | 0012 |
0000 | 12,5 | 0012 |
0000 | 1234567 | 1234567 |
## | 12 | 12 |
## | 12.3456 | 12 |
## | 123456 | 123456 |
.00 | 12.3456 | 12,35 |
.00 | .3456 | ,35 |
0.00 | .789 | 0,79 |
#.000000 | 12.34 | 12,340000 |
,### | 12345678.901 | 12.345.679 |
#.#;(#.#) | 12345678.901 | 12345678,9 |
#.#;(#.#) | –12345678.901 | (12345678,9) |
,###.## \u00A4 | 12345.6789 | 12.345,68 € |
,#00.00 \u00A4\u00A4 | –12345678.9 | –12.345.678,90 EUR |
,#00.00 \u00A4\u00A4 | 0.1 | 00,10 EUR |
Tabelle 2.14Beispiel für verschiedene Formatanweisungen
Währungen angeben und die Klasse Currency
Die NumberFormat-Klasse liefert mit getCurrencyInstance() ein Format-Objekt, das neben der Dezimalzahl auch noch ein Währungssymbol mit anzeigt. So liefert NumberFormat.getCurrencyInstance().format(12345.6789) dann 12.345,68 €, also automatisch mit einem Euro-Zeichen. Dass es ein Euro-Zeichen ist und kein Yen-Symbol, liegt einfach daran, dass Java standardmäßig das eingestellte Land »sieht« und daraus die Währung ableitet. Wenn wir explizit den Formatter mit einem Land initialisieren, etwa wie in
System.out.println( frmt1.format( 12345.6789 ) ); // 12345,68 €
so ist die Währung automatisch Euro (denn Frankreich nutzt den Euro); schreiben wir DecimalFormat.getCurrencyInstance(Locale.JAPAN), ist sie Yen, und wir bekommen ¥12,346 (es gibt standardmäßig keine Nachkommastellen beim Yen). Locale-Objekte repräsentieren immer eine Sprachregion.
DecimalFormat bzw. schon die Oberklasse NumberFormat ermöglicht die explizite Angabe der Währung. In der Java-Bibliothek wird sie durch die Klasse java.util.Currency repräsentiert. NumberFormat liefert mit getCurrency() die eingestellte Currency, die zur Formatierung verwendet wird, und setCurrency() setzt sie neu. Das löst Szenarios, in denen etwa ein Euro-Zeichen die Währung darstellt, aber die Zahlenformatierung englisch ist, wie die folgenden Zeilen zeigen:
frmt.setCurrency( Currency.getInstance( "EUR" ) );
System.out.println( frmt.format( 12345.6789 ) ); // EUR12,345.68
Die Currency-Klasse bietet drei statische Methoden, die Currency-Objekte liefern. Da ist zum einen getAvailableCurrencies(), was ein Set<Currency> liefert, und zum anderen gibt es die beiden Fabrikfunktionen getInstance(Locale locale) und getInstance(String currencyCode). Currency-Objekte besitzen eine ganze Reihe von Objektfunktionen, die etwa den ISO-4217-Währungscode liefern oder den ausgeschriebenen Währungsnamen (und das auch noch in verschiedenen Sprachen, wenn gewünscht).
Folgendes Programm geht über alle Währungen und gibt die zentralen Informationen aus:
System.out.printf( "%s, %s, %s (%s)%n",
currency.getCurrencyCode(),
currency.getSymbol(),
currency.getDisplayName(),
currency.getDisplayName(Locale.ENGLISH) );
}
Wir bekommen dann mehr als 200 Ausgaben, und die Ausgabe beginnt mit:
IQD, IQD, Irak Dinar (Iraqi Dinar)
GHS, GHS, Ghana Cedi (Ghana Cedi)
AFN, AFN, Afghani (Afghani)
MUR, MUR, Mauritius Rupie (Mauritius Rupee)
SGD, SGD, Singapur Dollar (Singapore Dollar)
…
2.5.5MessageFormat und Pluralbildung mit ChoiceFormat
MessageFormat ist eine besondere Unterklasse von Format, die für die Formulierung von Texten gedacht ist. Insbesondere bei internationalisierten Ausgaben kommt die Klasse zum Einsatz. Die Klasse ist in etwa mit den Formatierungen über printf(…) vergleichbar, nur werden bei MessageFormat die Platzhalter immer per Index in geschweiften Klammern angesprochen.
[zB]Beispiel
Formuliere einen Nachrichten-String mit drei Feldern:
double sum = 1234534534;
String s = MessageFormat.format( "{0} Auto(s) verkauft am {1,date} zum "+
"Gesamtpreis von {2,number,currency}.",
soldCars, new Date(), sum );
System.out.println( s );
Die Ausgabe ist:
»10 Auto(s) verkauft am 01.07.2011 zum Gesamtpreis von 1.234.534.534,00 €.«
Sie ist automatisch lokalisiert, die Sprache lässt sich jedoch wieder als Locale-Objekt übergeben.
ChoiceFormat
Eine besondere Möglichkeit ist die Verbindung von MessageFormat und ChoiceFormat, um das Problem zu lösen, das unser Beispiel im Fall von verkauften Autos mit »Auto(s)« löst. Im Deutschen ist die Pluralbildung anspruchsvoll, da es »0 Autos, 1 Auto, 2 Autos, 3 Autos« usw. heißt, aber nur »0 Koffer, 1 Koffer, 2 Koffer, …«. Das in Software zu modellieren ist nicht ganz einfach, aber mit ChoiceFormat lässt es sich lösen. Dem Konstruktor werden zum Generieren der Ausgabe zwei Felder mitgegeben: Ein Limit-Array kodiert Bereiche, und ein zweites Feld enthält die zugeordneten Elemente für den Bereich.
[zB]Beispiel
Löse das Problem mit »0 Autos, 1 Auto, 2 Autos« usw.:
double[] limits = { 0., 1., 2. };
String[] formats = { "Autos", "Auto", "Autos" };
ChoiceFormat choices = new ChoiceFormat( limits, formats );
formatter.setFormatByArgumentIndex( 1, choices );
int size = 4;
Object[] params = { size, size };
System.out.println( formatter.format( params ) ); // Du hast 4 Autos verkauft.
Das Feld {0., 1., 2.} interpretiert sich so: Liegt der Wert zwischen größer gleich 0 und echt kleiner 1 (also bei Ganzzahlen ist er effektiv 0), wird das erste Element des Feldes {"Autos", "Auto", "Autos"}, also »Autos«, gewählt. Liegt es zwischen größer gleich 1 und echt kleiner 2, dann ist es das »Auto«. Alles, was echt größer 2 ist, wird auf »Autos« abgebildet. Für die 2 im Feld ist Math.nextUp(1.) eine Alternative, und wenn auf »Komma-Autos« gewechselt wird, ist es gleich korrekt, denn es heißt zwar »1 Auto« aber »1,5 Autos«. (Genau genommen sind es sogar »1,0 Autos«, wodurch die ganze Pluralbildung wegfällt.)
Die API-Dokumentation der Klasse ChoiceFormat gibt weitere Beispiele und zeigt insbesondere, wie sich die Bereiche auch in den Strings selbst kodieren lassen, was für externe Übersetzungsdateien optimal ist.