If you’ve used C/C++ I/O in the past, it may have occurred to you that C’s printf is pretty convenient for formatting output.  So you may have asked yourself:  “Is there anything like printf in Java?  How do I gain greater control of formatting my output?”

Before version 5 (1.5) of Java, it wasn’t easy.  Java provided a set of classes to allow you to format messages, but they were (and still are) not the easiest things to use.

Fortunately, Java 5/1,5 added the ability to format messages in a way that is much more compatible with C/C++’s printf, that was also available in many other languages.  It is much easier to use, and many people are already familiar with the principles of printf formatting.

The Format Family

For some time, Java has provided the ability to format messages.  It uses the following classes to achieve this functionality:

  • Format
    • MessageFormat
    • NumberFormat
      • DecimalFormat
      • ChoiceFormat
    • DateFormat

These classes are part of the java.text package, which is primarily used for Internationalization. The subject of Internationalization (commonly called “I18N”) is a very complex one, and we won’t be covering it in detail here. Instead, we’ll just give some examples of how to do the equivalent of some printf operations (albeit in a more convoluted fashion).

MessageFormat

MessageFormat provides a means to produce concatenated messages in natural language-neutral way. You use this to construct messages displayed for end users.  MessageFormat takes a set of objects, formats them, then inserts the formatted strings into the pattern at the appropriate places.

For example:

package inputOutput;

import java.text.MessageFormat;
import java.util.GregorianCalendar;

public class FormatText
{
    public static void main(String[] args)
    {
        String pattern = 
         "On {2}, a {0} destroyed {1} houses and caused {3} of damage.";
        MessageFormat msgFormat = new MessageFormat(pattern);
        Object[] msgArgs =
        { 
            "hurricane", 
            new Integer(99), 
            new GregorianCalendar(1999, 0, 1).getTime(),
            new Double(10E7)
        };
        System.out.println(msgFormat.format(msgArgs));
    }
}

which outputs:

On 1/1/99 12:00 AM, a hurricane destroyed 99 houses and caused 100,000,000 of damage.

Explicit Formats

We can change the way each object is displayed in the formatted string, by:

  • Adding placeholder information to the string itself, or
  • Calling the appropriate setFormat or setFormats method
Using the setFormat method

Here’s how to do it by calling setFormat methods:

package inputOutput;

import java.text.MessageFormat;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.util.GregorianCalendar;

public class FormatText
{
    public static void main(String[] args)
    {
        String pattern = 
         "On {2}, a {0} destroyed {1} houses and caused {3} of damage.";
        MessageFormat msgFormat = new MessageFormat(pattern);
        msgFormat.setFormat(0, DateFormat.getDateInstance(DateFormat.LONG));
        msgFormat.setFormat(3, NumberFormat.getCurrencyInstance());
        Object[] msgArgs =
        { 
            "hurricane", 
            new Integer(99), 
            new GregorianCalendar(1999, 0, 1).getTime(),
            new Double(10E7)
        };
        System.out.println(msgFormat.format(msgArgs));
    }
}

which produces:

On January 1, 1999, a hurricane destroyed 99 houses and caused $100,000,000.00 of damage.
Using the setFormats method

We could have accomplished the same thing as follows:

package inputOutput;

import java.text.MessageFormat;
import java.text.DateFormat;
import java.text.Format;
import java.text.NumberFormat;
import java.util.GregorianCalendar;

public class FormatText
{
    public static void main(String[] args)
    {
        String pattern = 
         "On {2}, a {0} destroyed {1} houses and caused {3} of damage.";
        MessageFormat msgFormat = new MessageFormat(pattern);
        Format argFormats[] =
        {
            DateFormat.getDateInstance(DateFormat.LONG),
            null,
            null,
            NumberFormat.getCurrencyInstance()
        };
        msgFormat.setFormats(argFormats);
        Object[] msgArgs =
        { 
            "hurricane", 
            new Integer(99), 
            new GregorianCalendar(1999, 0, 1).getTime(),
            new Double(10E7)
        };
        System.out.println(msgFormat.format(msgArgs));
    }
}

You can repeat the same argument, but with a different format:

package inputOutput;

import java.text.MessageFormat;
import java.text.DateFormat;
import java.text.Format;
import java.util.GregorianCalendar;

public class FormatText
{
    public static void main(String[] args)
    {
        String pattern = 
         "On {2}, a {0} touched down at {2} and destroyed {1} houses.";
        MessageFormat msgFormat = new MessageFormat(pattern);
        Format argFormats[] =
        {
            DateFormat.getDateInstance(DateFormat.LONG),
            null,
            DateFormat.getTimeInstance(DateFormat.SHORT),
            null
        };
        msgFormat.setFormats(argFormats);
        Object[] msgArgs =
        { 
            "tornado", 
            new Integer(99), 
            new GregorianCalendar(1999, 0, 1).getTime()
        };
        System.out.println(msgFormat.format(msgArgs));
    }
}

which outputs:

On January 1, 1999, a tornado touched down at 12:00 AM and destroyed 99 houses.

Note: The argument formats correspond to the position of the placeholder, while the message arguments correspond to the number of the placeholder.

Specifying Formats in the Placeholder

Another way of controlling the format is to specify the formats in the placeholder itself:

package inputOutput;

import java.text.MessageFormat;
import java.util.GregorianCalendar;

public class FormatText
{
    public static void main(String[] args)
    {
        String pattern = 
         "On {2,date,long}, a {0} touched down at {2,time,short}, " +
         "destroyed {1} houses, and caused {3,number,currency} of damage.";
        MessageFormat msgFormat = new MessageFormat(pattern);
        Object[] msgArgs =
        { 
            "tornado", 
            new Integer(99), 
            new GregorianCalendar(1999, 0, 1).getTime(),
            new Double(10E7)
        };
        System.out.println(msgFormat.format(msgArgs));
    }
}

which produces:

On January 1, 1999, a tornado touched down at 12:00 AM, destroyed 99 houses, and caused $100,000,000.00 of damage.
Choice Formats

Consider the following modification of the above program:

package inputOutput;

import java.text.MessageFormat;
import java.util.GregorianCalendar;

public class FormatText
{
    public static void main(String[] args)
    {
        String pattern = 
         "On {2,date,long}, a {0} " +
         "destroyed {1} houses, and caused {3,number,currency} of damage.";
        MessageFormat msgFormat = new MessageFormat(pattern);
        Object[] msgArgs =
        { 
            "earthquake", 
            new Integer(1), 
            new GregorianCalendar(1999, 0, 1).getTime(),
            new Double(10E7)
        };
        System.out.println(msgFormat.format(msgArgs));
    }
}

This outputs the string:

On January 1, 1999, a earthquake destroyed 1 houses, and caused $100,000,000.00 of damage.

which should, to be grammatically correct, read:

On January 1, 1999, an earthquake destroyed 1 house, and caused $100,000,000.00 of damage.

In other words, we have to deal with plurals correctly, as well as ensuring that parts of speech match properly (such as indefinite articles, etc.). This can be more important and more complex in other languages such as German or French, where articles must match the gender and context of a noun.

ChoiceFormat was designed to let you solve this problem. You must specify:

  • An array of limits
  • An array of format strings

Here’s an example of how to solve the above problem:

package inputOutput;

import java.text.ChoiceFormat;
import java.text.MessageFormat;
import java.util.GregorianCalendar;

public class FormatText
{
    public static void main(String[] args)
    {
        double[] limits = {1, 1, 2};
        String[] formatStrings = 
		{"no houses", "one house", 
                    "{1,number,integer} houses"};
        ChoiceFormat choiceFormat = 
			new ChoiceFormat(limits, formatStrings);
        String pattern = 
         "On {2,date,long}, {0} " +
         "destroyed {1}, and caused {3,number,currency} of damage.";
        MessageFormat msgFormat = new MessageFormat(pattern);
        msgFormat.setFormat(2, choiceFormat);
        Object[] msgArgs =
        { 
            "an earthquake", 
            new Integer(1), 
            new GregorianCalendar(1999, 0, 1).getTime(),
            new Double(10E7)
        };
        System.out.println(msgFormat.format(msgArgs));
    }
}

which outputs:

On January 1, 1999, an earthquake destroyed one house, and caused $100,000,000.00 of damage.

Or, we can embed the same choices into the placeholders themselves:

package inputOutput;

import java.text.MessageFormat;
import java.util.GregorianCalendar;

public class FormatText
{
    public static void main(String[] args)
    {
        String pattern = 
         "On {2,date,long}, {0} destroyed " +
         "{1,choice,0#no houses|1#one house|2#{1,number,integer} houses}, " +
         "and caused {3,number,currency} of damage.";
        MessageFormat msgFormat = new MessageFormat(pattern);
        Object[] msgArgs =
        { 
            "an earthquake", 
            new Integer(1), 
            new GregorianCalendar(1999, 0, 1).getTime(),
            new Double(10E7)
        };
        System.out.println(msgFormat.format(msgArgs));
    }
}

This is actually far preferred, because it means that we can remove the message(s) from the program, and provide separate sets of messages for each language/locale, and have the separated messages contain all the necessary instructions to format the messages correctly in each language.  That way, the program does not have special code for each language.

Believe me, this is very important when you are writing code that will have to be internationalized (globalization is here, already!)

Summary of Patterns

Summary of MessageFormat Patterns

Here is the grammar for MessageFormat Patterns:

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

Notes:

  • If there is no elementFormat, then the argument must be a String, which is substituted.
  • If there is no dateTimeStyle or numberStyle, then the default format is used (for example, NumberFormat.getInstance, DateFormat.getTimeInstance, or DateFormat.getInstance).
  • In strings, single quotes can be used to quote the “{” (curly brace) if necessary. A real single quote is represented by '' (two successive single quotes). Inside a messageFormatElement, quotes are not removed. For example, {1,number,$'#',##} will produce a number format with the pound-sign quoted, with a result such as: “$#31,45“.
  • If a pattern is used, then unquoted braces in the pattern, if any, must match: that is, ab {0} de and ab '}' de are OK, but ab {0'}' de and ab } de are not.
  • The argument is a number from 0 to 9, which corresponds to the arguments presented in an array to be formatted.
  • It is ok to have unused arguments in the array. With missing arguments or arguments that are not of the right class for the specified format, a ParseException is thrown. First, format checks to see if a Format object has been specified for the argument with the setFormats method. If so, then format uses that Format object to format the argument. Otherwise, the argument is formatted based on the object’s type:
    • If the argument is a Number, then format uses NumberFormat.getInstance to format the argument; 
    • if the argument is a Date, then format uses DateFormat.getDateTimeInstance to format the argument.
    • Otherwise, it uses the toString method.

Summary of ChoiceFormat

ChoiceFormat allows you to attach a format to a range of numbers. It is generally used in a MessageFormat for handling plurals. The choice is specified with an ascending list of doubles, where each item specifies a half-open interval up to the next item:

X matches j if and only if limit[j] <= X < limit[j+1] 

If there is no match, then either the first or last index is used, depending on whether the number (X) is too low or too high.

Formatting Numbers

When you use MessageFormat and provide a number as one of the arguments, you must specify an object, not a primitive type.

The Number class hierarchy is as follows:

  • Number
    • Byte
    • Double
    • Float
    • Integer
    • Long
    • Short
    • BigInteger
    • BigDecimal

The family of classes used to format numbers is:

  • Format
    • NumberFormat
      • DecimalFormat
      • ChoiceFormat

Using NumberFormat

NumberFormat is the abstract base class for all number formats. This class provides the interface for formatting and parsing numbers. NumberFormat also provides methods for determining which locales have number formats, and what their names are. NumberFormat helps you to format and parse numbers for any locale. Your code can be completely independent of the locale conventions for decimal points, thousands-separators, or even the particular decimal digits used, or whether the number format is even decimal.

To format a number for the current Locale, use one of the factory class methods:

myString = NumberFormat.getInstance().format(myNumber);

If you are formatting multiple numbers, it is more efficient to get the format and use it multiple times so that the system doesn’t have to fetch the information about the local language and country conventions multiple times.

NumberFormat nf = NumberFormat.getInstance();
for (int i = 0; i < a.length; ++i) 
{
     output.println(nf.format(myNumber[i]) + "; ");
}

To format a number for a different Locale, specify it in the call to getInstance.

NumberFormat nf = NumberFormat.getInstance(Locale.FRENCH);

You can also use a NumberFormat to parse numbers:

myNumber = nf.parse(myString);

Use getInstance or getNumberInstance to get the normal number format. Use getCurrencyInstance to get the currency number format. And use getPercentInstance to get a format for displaying percentages. With this format, a fraction like 0.53 is displayed as 53%.

You can also control the display of numbers with such methods as setMinimumFractionDigits. If you want even more control over the format or parsing, or want to give your users more control, you can try casting the NumberFormat you get from the factory methods to a DecimalFormat. This will work for the vast majority of locales; just remember to put it in a try block in case you encounter an unusual one.

NumberFormat and DecimalFormat are designed such that some controls work for formatting and others work for parsing. The following is the detailed description for each these control methods,

  • setParseIntegerOnly : only affects parsing, e.g. if true, “3456.78” -> 3456 (and leaves the parse position just after index 6) if false, “3456.78” -> 3456.78 (and leaves the parse position just after index 8) This is independent of formatting. If you want to not show a decimal point where there might be no digits after the decimal point, use setDecimalSeparatorAlwaysShown.
  • setDecimalSeparatorAlwaysShown : only affects formatting, and only where there might be no digits after the decimal point, such as with a pattern like “#,##0.##“, e.g., if true, 3456.00 -> “3,456.” if false, 3456.00 -> “3456” This is independent of parsing. If you want parsing to stop at the decimal point, use setParseIntegerOnly.

You can also use forms of the parse and format methods with ParsePosition and FieldPosition to allow you to:

  • progressively parse through pieces of a string
  • align the decimal point and other areas For example, you can align numbers in two ways:
    1. If you are using a monospaced font with spacing for alignment, you can pass the FieldPosition in your format call, with field = INTEGER_FIELD. On output, getEndIndex will be set to the offset between the last character of the integer and the decimal. Add (desiredSpaceCount - getEndIndex) spaces at the front of the string.
    2. If you are using proportional fonts, instead of padding with spaces, measure the width of the string in pixels from the start to getEndIndex. Then move the pen by (desiredPixelWidth - widthToAlignmentPoint) before drawing the text. It also works where there is no decimal, but possibly additional characters at the end, e.g. with parentheses in negative numbers: “(12)” for -12.

Using DecimalFormat

DecimalFormat is a concrete subclass of NumberFormat for formatting decimal numbers. This class allows for a variety of parameters, and localization to Western, Arabic, or Indic numbers. Normally, you get the proper NumberFormat for a specific locale (including the default locale) using one of NumberFormat‘s factory methods such as getInstance. You may then modify it from there (after testing to make sure it is a DecimalFormat, of course!)

Special cases:

  • NaN (IEEE floating point “not-a-number”) is formatted as a single character, typically \uFFFD.
  • +/- Infinity is formatted as a single character, typically \u221E, plus the positive and negative pre/suffixes.

Note: this class is designed for common users; for very large or small numbers, use a format that can express exponential values.

The following shows the structure of the pattern:

pattern    := subpattern{;subpattern}
 
subpattern := {prefix}integer{.fraction}{suffix}
 
prefix     := '\\u0000'..'\\uFFFD' - specialCharacters
suffix     := '\\u0000'..'\\uFFFD' - specialCharacters
integer    := '#'* '0'* '0'
fraction   := '0'* '#'* 

Notation:
  X*       0 or more instances of X
  (X | Y)  either X or Y.
  X..Y     any character from X up to Y, inclusive.
  S - T    characters in S, except those in T  

The first subpattern is for positive numbers. The second (optional) subpattern is for negative numbers. (In both cases, ‘,‘ can occur inside the integer portion — it is just too messy to indicate in BNF — Backus–Naur form, used above.)

Here are the special characters used in the parts of the subpattern:

SymbolMeaning
0a digit
#a digit, zero shows as absent
.placeholder for decimal separator
,placeholder for grouping separator.
;separates formats.
default negative prefix.
%multiply by 100 and show as percentage
multiply by 1000 and show as per mille
¤currency sign; replaced by currency symbol; if doubled, replaced by international currency symbol. If present in a pattern, the monetary decimal separator is used instead of the decimal separator.
Xany other characters can be used in the prefix or suffix
used to quote special characters in a prefix or suffix.

Here are some examples of typical format string usage:

Format StringMeaningValueOutput String
“##0.00”At least one digit before the decimal point, and two after.1234.567
.256
1234.56
0.25
“#.000”Possibly no digits before the point, three after.1234.567
.256
1234.567
.256
“,###”Use thousands separator and no decimal places.1234.567
.256
1,234
0
“0.00;0.00-“Show negative numbers with sign on the right.-27.527.5-

Notes

  • If there is no explicit negative subpattern, – is prefixed to the positive form. That is, “0.00” alone is equivalent to “0.00;-0.00“.
  • Illegal patterns, such as “#.#.#” or mixing ‘_‘ and ‘*‘ in the same pattern, will cause an IllegalArgumentException to be thrown. From the message of IllegalArgumentException, you can find the place in the string where the error occurred.
  • The grouping separator is commonly used for thousands, but in some countries for ten-thousands. The interval is a constant number of digits between the grouping characters, such as 100,000,000 or 1,0000,0000. If you supply a pattern with multiple grouping characters, the interval between the last one and the end of the integer is the one that is used. So “#,##,###,####” == “######,####” == “##,####,####“.
  • When calling DecimalFormat.parse(String, ParsePosition) and parsing fails, a null object will be returned. The unchanged parse position also reflects that an error has occurred during parsing. When calling the convenient method DecimalFormat.parse(String) and parsing fails, a ParseException will be thrown.
  • This class only handles localized digits where the 10 digits are contiguous in Unicode, from 0 to 9. Other digits sets (such as superscripts) would need a different subclass.
DecimalFormat Example

Here’s an example using DecimalFormat:

package inputOutput;
 
 import java.util.Locale;
 import java.text.DecimalFormat;
 import java.text.NumberFormat;
 import java.text.ParseException;
 
 public class FormatDecimal
 {
    public static void main(String[] args)
    {
        // Gather the available locales
        Locale[] locales = NumberFormat.getAvailableLocales();

        double myNumber = -1234.56;
        NumberFormat form;

        // Print out a number with the locale number, currency
        // and percent format for each locale we can.
        for (int j = 0; j < 3; ++j) 
        {
            System.out.println("FORMAT");
            for (int i = 0; i < locales.length; ++i) 
            {
                if (locales[i].getCountry().length() == 0) 
                {
                    // skip language-only
                    continue;
                }
                System.out.print(locales[i].getDisplayName());
                switch (j) 
                {
                  default:
                    form = NumberFormat.getInstance(locales[i]); 
                    break;
                  case 1:
                    form = NumberFormat.getCurrencyInstance(locales[i]); 
                    break;
                  case 0:
                    form = NumberFormat.getPercentInstance(locales[i]); 
                    break;
                }
                try 
                {
                    System.out.print(": " + 
                                     ((DecimalFormat)form).toPattern() +
                                     " -> " + form.format(myNumber));
                } 
                catch (IllegalArgumentException iae) 
                { }
                try 
                {
                    System.out.println(" -> " + 
                                       form.parse(
                                            form.format(myNumber)));
                } 
                catch (ParseException pe) 
                { }
            }
        }
    }
 }

which, on my Microsoft Windows system, prints out a very long list:

FORMAT
Arabic (Egypt): #,##0% -> -123,456% -> -1234.56
Byelorussian (Belarus): #,##0% -> -123 456% -> -1234.56
Bulgarian (Bulgaria): #,##0% -> -123 456% -> -1234.56
Catalan (Spain): #,##0% -> -123.456% -> -1234.56
Czech (Czech Republic): #,##0% -> -123.456% -> -1234.56
Danish (Denmark): #,##0% -> -123.456% -> -1234.56
German (Germany): #,##0% -> -123.456% -> -1234.56
German (Germany,Euro): #,##0% -> -123.456% -> -1234.56
German (Austria): #,##0% -> -123.456% -> -1234.56
German (Austria,Euro): #,##0% -> -123.456% -> -1234.56
German (Switzerland): #,##0% -> -123'456% -> -1234.56
German (Luxembourg): #,##0% -> -123.456% -> -1234.56
German (Luxembourg,Euro): #,##0% -> -123.456% -> -1234.56
Greek (Greece): #,##0% -> -123.456% -> -1234.56
English (United States): #,##0% -> -123,456% -> -1234.56
English (Australia): #,##0% -> -123,456% -> -1234.56
English (Canada): #,##0% -> -123,456% -> -1234.56
English (United Kingdom): #,##0% -> -123,456% -> -1234.56
English (Ireland): #,##0% -> -123,456% -> -1234.56
...
Japanese (Japan): #,##0.### -> -1,234.56 -> -1234.56
Korean (South Korea): #,##0.### -> -1,234.56 -> -1234.56
Lithuanian (Lithuania): #,##0.## -> -1.234,56 -> -1234.56
Latvian (Lettish) (Latvia): #,##0.### -> -1 234,56 -> -1234.56
Macedonian (Macedonia): #,##0.###;(#,##0.###) -> (1.234,56) -> -1234.56
Dutch (Netherlands): #,##0.### -> -1.234,56 -> -1234.56
Dutch (Netherlands,Euro): #,##0.### -> -1.234,56 -> -1234.56
Dutch (Belgium): #,##0.### -> -1.234,56 -> -1234.56
Dutch (Belgium,Euro): #,##0.### -> -1.234,56 -> -1234.56
Norwegian (Norway,B): #,##0.### -> -1 234,56 -> -1234.56
Norwegian (Norway,NY): #,##0.### -> -1 234,56 -> -1234.56
Polish (Poland): #,##0.### -> -1 234,56 -> -1234.56
Portuguese (Portugal): #,##0.### -> -1.234,56 -> -1234.56
Portuguese (Portugal,Euro): #,##0.### -> -1.234,56 -> -1234.56
Portuguese (Brazil): #,##0.### -> -1.234,56 -> -1234.56
Romanian (Romania): #,##0.### -> -1.234,56 -> -1234.56
Russian (Russian Federation): #,##0.### -> -1 234,56 -> -1234.56
Serbo-Croatian (Yugoslavia): #,##0.### -> -1.234,56 -> -1234.56
Slovak (Slovakia): #,##0.### -> -1 234,56 -> -1234.56
Slovenian (Slovenia): #,##0.### -> -1.234,56 -> -1234.56
Albanian (Albania): #,##0.### -> -1.234,56 -> -1234.56
Serbian (Yugoslavia): #,##0.### -> -1 234,56 -> -1234.56
Swedish (Sweden): #,##0.### -> -1 234,56 -> -1234.56
Thai (Thailand): #,##0.### -> -1,234.56 -> -1234.56
Turkish (Turkey): #,##0.### -> -1.234,56 -> -1234.56
Ukrainian (Ukraine): #,##0.### -> -1.234,56 -> -1234.56
Chinese (China): #,##0.### -> -1,234.56 -> -1234.56
Chinese (Taiwan): #,##0.### -> -1,234.56 -> -1234.56

Using MessageFormat

However, for most purposes, you can use MessageFormat to format numbers:

package inputOutput;
 
 import java.text.MessageFormat;
 
 public class FormatNumber
 {
    public static void main(String[] args)
    {
        Customer[] cust = 
        {
            new Customer(1, "Heather Gilmartin", 200.00),
            new Customer(2, "Bill Gates", -3000000.00),
            new Customer(500, "Crocodile Dundee", 1000.00)
        };
        for (int i = 0; i < cust.length; i++)
        {
            printCustomerStatus(cust[i]);
        }
    }
    
    static void printCustomerStatus(Customer cust)
    {
        String pattern = 
            "Customer {0,number,000}, {1}, has a balance of " +
            "{2,number,¤###,##0.00;(¤###,##0.00)}"; //}";
        MessageFormat msgFormat = new MessageFormat(pattern);
        Object[] msgArgs = 
        {
            new Integer(cust.getId()),
            cust.getName(),
            new Double(cust.getBalance())
        };
        System.out.println(msgFormat.format(msgArgs));
    }
    
    static class Customer
    {
        Customer(int id, String name, double balance)
        {
            m_id = id;
            m_name = name;
            m_balance = balance;
        }
        
        int     getId()     { return m_id; }
        String  getName()   { return m_name; }
        double  getBalance(){ return m_balance; }
        
        private int     m_id;
        private String  m_name;
        private double  m_balance;
    }
 }

which produces the following output:

Customer 001, Heather Gilmartin, has a balance of $200.00
Customer 002, Bill Gates, has a balance of ($3,000,000.00)
Customer 500, Crocodile Dundee, has a balance of $1,000.00

The printf Family

In Java 5, they finally added a printf functionality similar to that of C/C++ and other languages.  It was difficult to do that before Java 5 because there was no support for variable-length argument lists — they added support for that in Java 5.

So, here’s how printf and family work…

Classes Involved

Here are the players in this new printf game:

The class java.io.PrintStream has added new methods:public PrintStream printf(String format, Object… args)public PrintStream printf(Locale l, String format, Object… args)public PrintStream format(String format, Object… args)public PrintStream format(Locale l, String format, Object… args)
The class java.io.PrintWriter has added new methods:public PrintWriter printf(String format, Object… args)public PrintWriter printf(Locale l, String format, Object… args)public PrintWriter format(String format, Object… args)public PrintWriter format(Locale l, String format, Object… args)
The class java.lang.String has added new methods:public static String format(String format, Object… args)public static String format(Locale l, String format, Object… args)

The work is actually performed by class java.util.Formatter, which is documented (for Java 5.0) here. Almost the entire javadoc for class Formatter is the specification for how such formatting is supposed to work, so it’s worth a close read.

Notice that each of the above methods employs the new (as of Java 5/1.5) variable number of arguments syntax.

In each of the above methods, the parameter, format, is known as a Format String, which may contain zero or more Format Specifiers.

What’s a Format Specifier?  Read on…

Format Specifiers

What’s a Format Specifier?  Here’s the scoop:

A call to one of the methods in the printf family involves passing in a Format String, which contains:

  • Normal text

plus

  • Zero or more Format Specifiers

In most cases, a Format Specifier is associated with one of the arguments passed in after the Format String argument.  A Format Specifier, as its name suggests, specifies the output format for how that argument’s value should be displayed.

General, Character and Numeric Type Format Specifiers

The format specifiers for general, character, and numeric types have the following syntax:

%[argument_index$][flags][width][.precision]conversion

The optional argument_index is a decimal integer indicating the position of the argument in the argument list. The first argument is referenced by “1$”, the second by “2$”, etc.

The optional flags is a set of characters that modify the output format. The set of valid flags depends on the conversion.

The optional width is a non-negative decimal integer indicating the minimum number of characters to be written to the output.

The optional precision is a non-negative decimal integer usually used to restrict the number of characters. The specific behavior depends on the conversion.

The required conversion is a character indicating how the argument should be formatted. The set of valid conversions for a given argument depends on the argument’s data type.

Date and Time Format Specifiers

The format specifiers for types which are used to represents dates and times have the following syntax:

%[argument_index$][flags][width]conversion

The optional argument_indexflags and width are defined as above.

The required conversion is a two character sequence. The first character is ‘t’ or ‘T’. The second character indicates the format to be used. These characters are similar to but not completely identical to those defined by GNU date and POSIX strftime(3c).

Non-Argument Format Specifiers

The format specifiers which do not correspond to arguments have the following syntax:

%[flags][width]conversion

The optional flags and width is defined as above.

The required conversion is a character indicating content to be inserted in the output.

Some Examples

Here are some examples of how to use printf and family.

We start out simple, and build up…

Using Plain print & println

First, let’s take a look at what we get if we just use System.out.print[ln] calls.

For example, let’s produce a table from some employee data.  

Our first attempt will be pretty crude:

package inputOutput;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * Class to produce a table of information about customers
 */
public class EmployeeTablePrinter1
{
  public static void main(String[] args)
  {
    List<Employee1> employees = new ArrayList<Employee1>();
    
    // Populate list with Employees
    employees.add( new Employee1(1, "Bloggs, Fred", "10 Firebird Crescent", 
                                new Date(1976 - 1900, 5 - 1, 20), 40000.00 ) );
    employees.add( new Employee1(2, "Charles, Cheryl", "5 Lattice Lane",
                                new Date(1989 - 1900, 4 - 1, 3), 50000.00 ) );
    employees.add( new Employee1(3, "Romney, Titus", "42 St. Martins Rd.",
                                new Date(1973 - 1900, 11 - 1, 19), 45000.00 ) );
    employees.add( new Employee1(4, "Clinton, Burton", "567 Nebula Circle",
                                new Date(1964 - 1900, 1 - 1, 29), 100000.00 ) );
    employees.add( new Employee1(5, "McDuff, Hazel", "1 Church Row",
                                new Date(1985 - 1900, 3 - 1, 6), 55000.00 ) );
    employees.add( new Employee1(6, "Frazier, Bill", "92 S. Main St.",
                                new Date(1980 - 1900, 12 - 1, 5), 70000.00 ) );
    employees.add( new Employee1(7, "Zambroski, Heather", "589 Trumpington Place",
                                new Date(1959 - 1900, 4 - 1, 3), 50000.00 ) );
    
    // Print out the table of employees
    Employee1.printHeader();
    for (Employee1 emp : employees)
    {
      emp.print();
    }
  }
}

/**
 * Class to represent an employee
 */
class Employee1
{
  Employee1(int id, String name, String address, Date dob, double salary)
  {
    m_id = id;
    m_name = name;
    m_address = address;
    m_dob = dob;
    m_salary = salary;
  }
  
  static void printHeader()
  {
    System.out.print("|");
    System.out.print("ID" + "|");
    System.out.print("Name" + "|");
    System.out.print("Address" + "|");
    System.out.print("Date of Birth" + "|");
    System.out.println("Salary" + "|");  }
  
  void print()
  {
    System.out.print("|");
    System.out.print(m_id + "|");
    System.out.print(m_name + "|");
    System.out.print(m_address + "|");
    System.out.print(m_dob + "|");
    System.out.println(m_salary + "|");
  }
  
  private int     m_id;
  private String  m_name;
  private String  m_address;
  private Date    m_dob;
  private double  m_salary;
}

Notes:

  • I have used the Date class.  The particular constructor for Date that I used is officially deprecated.  However, I am using it because it is the simplest form to use for this example (Calendar, and GregorianCalendar are far too complex!)
  • Other annoyances with the Date class include:
    • The year must be specified from a base of 1900
    • The month number is zero-based

When run, this program produces the following output:

|ID|Name|Address|Date of Birth|Salary|
|1|Bloggs, Fred|10 Firebird Crescent|Thu May 20 00:00:00 EDT 1976|40000.0|
|2|Charles, Cheryl|5 Lattice Lane|Mon Apr 03 00:00:00 EDT 1989|50000.0|
|3|Romney, Titus|42 St. Martins Rd.|Mon Nov 19 00:00:00 EST 1973|45000.0|
|4|Clinton, Burton|567 Nebula Circle|Wed Jan 29 00:00:00 EST 1964|100000.0|
|5|McDuff, Hazel|1 Church Row|Wed Mar 06 00:00:00 EST 1985|55000.0|
|6|Frazier, Bill|92 S. Main St.|Fri Dec 05 00:00:00 EST 1980|70000.0|
|7|Zambroski, Heather|589 Trumpington Place|Fri Apr 03 00:00:00 EST 1959|50000.0|

Not exactly the best format one could hope for, right?

Fixing Some Problems

What are the problems?  Well, here are some:

  1. The most obvious is that the lines that are intended to separate the data items don’t line up.
  2. The default formatting of the date of birth is likely to be gobbledygook to most people.  Besides, we don’t want the time of birth in there!
  3. Conventionally, monetary amounts, like salary, are presented with a fixed, two digit, size for cents (the fractional part).  Also, wouldn’t we like to know about monetary units?  Is the value in dollars or euros or pounds, or what?  And wouldn’t it be nicer to use commas, so we can read it more easily ($40,000.00, for example)?
  4. Perhaps we would expect an ID number to include some number of leading zeros — say, 0001, instead of simply 1.  It’s conventional to make an ID be of fixed size by inserting those leading zeros.

Let’s see if we can fix the last three of these, by switching to using some printf features:

package inputOutput;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * Class to produce a table of information about customers
 */
public class EmployeeTablePrinter2
{
  public static void main(String[] args)
  {
    List<Employee2> employees = new ArrayList<Employee2>();
    
    // Populate list with Employees
    employees.add( new Employee2(1, "Bloggs, Fred", "10 Firebird Crescent", 
                                new Date(1976 - 1900, 5 - 1, 20), 40000.00 ) );
    employees.add( new Employee2(2, "Charles, Cheryl", "5 Lattice Lane",
                                new Date(1989 - 1900, 4 - 1, 3), 50000.00 ) );
    employees.add( new Employee2(3, "Romney, Titus", "42 St. Martins Rd.",
                                new Date(1973 - 1900, 11 - 1, 19), 45000.00 ) );
    employees.add( new Employee2(4, "Clinton, Burton", "567 Nebula Circle",
                                new Date(1964 - 1900, 1 - 1, 29), 100000.00 ) );
    employees.add( new Employee2(5, "McDuff, Hazel", "1 Church Row",
                                new Date(1985 - 1900, 3 - 1, 6), 55000.00 ) );
    employees.add( new Employee2(6, "Frazier, Bill", "92 S. Main St.",
                                new Date(1980 - 1900, 12 - 1, 5), 70000.00 ) );
    employees.add( new Employee2(7, "Zambroski, Heather", "589 Trumpington Place",
                                new Date(1959 - 1900, 4 - 1, 3), 50000.00 ) );
    
    // Print out the table of employees
    Employee2.printHeader();
    for (Employee2 emp : employees)
    {
      emp.print();
    }
  }
}

/**
 * Class to represent an employee
 */
class Employee2
{
  Employee2(int id, String name, String address, Date dob, double salary)
  {
    m_id = id;
    m_name = name;
    m_address = address;
    m_dob = dob;
    m_salary = salary;
  }
  
  static void printHeader()
  {
    System.out.printf("|ID|Name|Address|Date of Birth|Salary|\n");
  }
  
  void print()
  {
    System.out.printf("|%04d|%s|%s|%tF|$%,.2f|\n", 
                      m_id, m_name, m_address, m_dob, m_salary);
  }
  
  private int     m_id;
  private String  m_name;
  private String  m_address;
  private Date    m_dob;
  private double  m_salary;
}

which now produces the following:

|ID|Name|Address|Date of Birth|Salary|
|0001|Bloggs, Fred|10 Firebird Crescent|1976-05-20|$40,000.00|
|0002|Charles, Cheryl|5 Lattice Lane|1989-04-03|$50,000.00|
|0003|Romney, Titus|42 St. Martins Rd.|1973-11-19|$45,000.00|
|0004|Clinton, Burton|567 Nebula Circle|1964-01-29|$100,000.00|
|0005|McDuff, Hazel|1 Church Row|1985-03-06|$55,000.00|
|0006|Frazier, Bill|92 S. Main St.|1980-12-05|$70,000.00|
|0007|Zambroski, Heather|589 Trumpington Place|1959-04-03|$50,000.00|

Notes:

  • The ID values are all now 4 characters wide, with leading zeros
  • The date format is more readable (I chose ISO date format — YYYY-MM-DD — because US date formatting conventions conflict with those used elsewhere in the world.  But you can always change it to your own taste.)
  • The salary now specifies that it is in dollars, and always shows two decimal places and is comma-separated.
Width, Spacing & Justification

Let’s finally fix those spacing and alignment problems.

Here are my final changes:

package inputOutput;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * Class to produce a table of information about customers
 */
public class EmployeeTablePrinter3
{
  public static void main(String[] args)
  {
    List<Employee3> employees = new ArrayList<Employee3>();
    
    // Populate list with Employees
    employees.add( new Employee3(1, "Bloggs, Fred", "10 Firebird Crescent", 
                                new Date(1976 - 1900, 5 - 1, 20), 40000.00 ) );
    employees.add( new Employee3(2, "Charles, Cheryl", "5 Lattice Lane",
                                new Date(1989 - 1900, 4 - 1, 3), 50000.00 ) );
    employees.add( new Employee3(3, "Romney, Titus", "42 St. Martins Rd.",
                                new Date(1973 - 1900, 11 - 1, 19), 45000.00 ) );
    employees.add( new Employee3(4, "Clinton, Burton", "567 Nebula Circle",
                                new Date(1964 - 1900, 1 - 1, 29), 100000.00 ) );
    employees.add( new Employee3(5, "McDuff, Hazel", "1 Church Row",
                                new Date(1985 - 1900, 3 - 1, 6), 55000.00 ) );
    employees.add( new Employee3(6, "Frazier, Bill", "92 S. Main St.",
                                new Date(1980 - 1900, 12 - 1, 5), 70000.00 ) );
    employees.add( new Employee3(7, "Zambroski, Heather", "589 Trumpington Place",
                                new Date(1959 - 1900, 4 - 1, 3), 50000.00 ) );
    
    // Print out the table of employees
    Employee3.printHeader();
    for (Employee3 emp : employees)
    {
      emp.print();
    }
  }
}

/**
 * Class to represent an employee
 */
class Employee3
{
  Employee3(int id, String name, String address, Date dob, double salary)
  {
    m_id = id;
    m_name = name;
    m_address = address;
    m_dob = dob;
    m_salary = salary;
  }
  
  static void printHeader()
  {
    System.out.println(ROW_SEPARATOR);
    System.out.printf("| %-4s | %-20s | %-25s | %-14s | %-13s |\n",
                      "ID", "Name", "Address", "Date of Birth", "Salary");
    System.out.println(ROW_SEPARATOR);
  }
  
  void print()
  {
    System.out.printf("| %04d | %-20s | %-25s | %-14tF | $%,12.2f |\n", 
                      m_id, m_name, m_address, m_dob, m_salary);
    System.out.println(ROW_SEPARATOR);
  }
  
  private int     m_id;
  private String  m_name;
  private String  m_address;
  private Date    m_dob;
  private double  m_salary;
  
  static private String ROW_SEPARATOR = 
 "+------+----------------------+---------------------------+----------------+---------------+";
}

which produces the following output:

+------+----------------------+---------------------------+----------------+---------------+
| ID   | Name                 | Address                   | Date of Birth  | Salary        |
+------+----------------------+---------------------------+----------------+---------------+
| 0001 | Bloggs, Fred         | 10 Firebird Crescent      | 1976-05-20     | $   40,000.00 |
+------+----------------------+---------------------------+----------------+---------------+
| 0002 | Charles, Cheryl      | 5 Lattice Lane            | 1989-04-03     | $   50,000.00 |
+------+----------------------+---------------------------+----------------+---------------+
| 0003 | Romney, Titus        | 42 St. Martins Rd.        | 1973-11-19     | $   45,000.00 |
+------+----------------------+---------------------------+----------------+---------------+
| 0004 | Clinton, Burton      | 567 Nebula Circle         | 1964-01-29     | $  100,000.00 |
+------+----------------------+---------------------------+----------------+---------------+
| 0005 | McDuff, Hazel        | 1 Church Row              | 1985-03-06     | $   55,000.00 |
+------+----------------------+---------------------------+----------------+---------------+
| 0006 | Frazier, Bill        | 92 S. Main St.            | 1980-12-05     | $   70,000.00 |
+------+----------------------+---------------------------+----------------+---------------+
| 0007 | Zambroski, Heather   | 589 Trumpington Place     | 1959-04-03     | $   50,000.00 |
+------+----------------------+---------------------------+----------------+---------------+

While far from ideal, it certainly beats our original, crude attempt!

Notes:

  • I added a single space on either side of the data item to prevent it from being forced up against the column separator.
  • I set minimum widths for the fields, to allow each column to be a standard width, including the header.
  • I forced left alignment on all the header items, the names, addresses and dates of birth.  The default alignment is right aligned, and that is most natural for the salary column, where you want the decimal points to align correctly.  
  • Note however, that right alignment causes a slight problem with the salary format: it causes the dollar sign to be separated from the amount.  If you wanted to cause the dollar sign to be right up against the amount, then we could do that, but we’d have to do two levels of formatting.