Java 25 output formatting

Java's printf formatting syntax is still useful in Java 25, but small console programs no longer need to rely on System.out.printf() for formatted output.

Java 25 includes the java.lang.IO class for simple console input and output. Combine IO.println() with String.formatted() and you get a modern alternative that keeps the familiar Formatter syntax:

void main() {
    var name = "Cameron";
    var site = "TheServerSide";

    IO.println(
        "I like the articles %s writes on %S."
            .formatted(name, site)
    );
}

The output is:

I like the articles Cameron writes on THESERVERSIDE.

This article uses Java 25 compact source files and IO.println() for most examples. The underlying format specifiers are the same ones used by printf.

printf vs. String.formatted in Java 25

The traditional approach is still valid:

System.out.printf(
    "Hello %s, you have %,d messages.%n",
    "Cameron",
    12345
);

A modern Java 25 alternative is:

void main() {
    var message = "Hello %s, you have %,d messages."
        .formatted("Cameron", 12345);

    IO.println(message);
}

String.formatted() creates the formatted String, while IO.println() handles console output. This cleanly separates formatting from I/O.

Java format specifier syntax

The general Formatter syntax is:

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

The conversion character determines how the corresponding argument is formatted.

Specifier Purpose
%sString representation
%SUppercase String representation
%dDecimal integer
%oOctal integer
%xHexadecimal integer
%fDecimal floating-point value
%eScientific notation
%cCharacter
%bBoolean representation
%%Literal percent sign
%nPlatform line separator

Format Strings

Use %s for normal String output and %S for uppercase output:

void main() {
    var language = "Java";
    var version = "twenty five";

    IO.println(
        "%s %S".formatted(language, version)
    );
}
{% highlight text %} Java TWENTY FIVE {% endhighlight %} {% raw %}

String width and alignment

Specify a minimum field width with a number. Strings are right-aligned by default:

void main() {
    IO.println("|%15s|".formatted("Java"));
    IO.println("|%-15s|".formatted("Java"));
}
{% highlight text %} | Java| |Java | {% endhighlight %} {% raw %}

The - flag left-aligns the value.

String precision

For Strings, precision limits the maximum number of characters written:

void main() {
    IO.println("%.5s".formatted("JavaScript"));
}
{% highlight text %} JavaS {% endhighlight %} {% raw %}

Format integers

Use %d with integral values such as byte, short, int and long.

void main() {
    int above = -98765;
    long below = 54321L;

    IO.println(
        "%,d :: %+,d".formatted(above, below)
    );
}

With a U.S. formatting locale:

-98,765 :: +54,321

The original version of this example claimed that %,d :: %d would produce zero padding and a plus sign. It does not. Those behaviors require explicit flags.

Integer flags

Flag Meaning Example
-Left-align%-10d
+Always show sign%+d
0Zero-pad field%010d
,Locale-sensitive grouping%,d
(Negative values in parentheses%(d
spaceLeading space for positive values% d

Decimal, octal and hexadecimal

void main() {
    int value = 123456789;

    IO.println("Decimal: %,d".formatted(value));
    IO.println("Octal:   %o".formatted(value));
    IO.println("Hex:     %x".formatted(value));
}
{% highlight text %} Decimal: 123,456,789 Octal: 726746425 Hex: 75bcd15 {% endhighlight %} {% raw %}
Java integer printf example

Java Formatter patterns provide width, alignment, grouping and padding options for integer values.

Format double and float values

Use %f for floating-point values. Precision controls the number of digits after the decimal point.

void main() {
    double top = 1234.12345;
    float bottom = 1234.12345f;

    IO.println(
        "%+,.3f :: %,.5f"
            .formatted(top, bottom)
    );
}

Typical U.S.-locale output is:

+1,234.123 :: 1,234.12341

The float output illustrates floating-point precision. A Java float cannot exactly represent all of the decimal digits in 1234.12345f.

The first pattern, %+,.3f, means:

  • + always displays the sign;
  • , requests locale-sensitive grouping;
  • .3 displays three digits after the decimal point; and
  • f selects decimal floating-point formatting.

Scientific notation

Use %e instead of %f for scientific notation:

void main() {
    double top = 1234.12345;
    float bottom = 1234.12345f;

    IO.println(
        "%+.3e :: %.5e"
            .formatted(top, bottom)
    );
}
{% highlight text %} +1.234e+03 :: 1.23412e+03 {% endhighlight %} {% raw %}

Do not combine the grouping flag , with scientific notation. Formatter does not support that combination and throws a FormatFlagsConversionMismatchException.

Java printf format output

Width, precision and Formatter flags can produce aligned floating-point output.

Format char and boolean values

Use %c for characters and %b for Boolean-style output. Their uppercase forms, %C and %B, uppercase the formatted result.

void main() {
    boolean flag = false;
    char lower = 'a';
    char unicode = '\u0077';

    IO.println(
        "%B :: %c :: %C"
            .formatted(flag, lower, unicode)
    );
}
{% highlight text %} FALSE :: a :: W {% endhighlight %} {% raw %}

The original code assigned a floating-point literal to a char, which does not compile. The corrected example uses actual character values.

Modern date and time formatting

Formatter supports legacy %t date/time conversions, but modern Java applications should normally use the java.time API and DateTimeFormatter.

For example:

void main() {
    var now = LocalDateTime.now();

    var formatter = DateTimeFormatter.ofPattern(
        "EEE MMM d, uuuu HH:mm:ss"
    );

    IO.println(now.format(formatter));
}

A result might look like:

Fri Aug 14, 2026 14:30:45

This is clearer and less error-prone than repeating a single date object several times in a Formatter call.

Time zones require a zone-aware type

A LocalDateTime deliberately contains no time-zone information. If you need a UTC offset or zone name, use a type such as ZonedDateTime:

void main() {
    var now = ZonedDateTime.now();

    var formatter = DateTimeFormatter.ofPattern(
        "uuuu-MM-dd HH:mm:ss z XXX"
    );

    IO.println(now.format(formatter));
}

This fixes another common mistake in older examples: attempting to format a timezone from LocalDateTime, which has no zone or offset.

Legacy %t date and time conversions

If you are maintaining existing Formatter code, common date/time conversions include:

Specifier Meaning
%tHHour, 00 through 23
%tMMinute
%tSSecond
%tAFull weekday name
%taAbbreviated weekday
%tBFull month name
%tbAbbreviated month
%tdDay of month
%tYFour-digit year
%tyTwo-digit year
%tT24-hour time as HH:MM:SS
%tzNumeric UTC offset when the value contains zone information

Format a console table with Java 25 IO

Width specifiers are especially useful for aligned console tables.

record Primitive(
    String category,
    String name,
    int bits
) {}

void main() {
    var types = List.of(
        new Primitive("Floating", "double", 64),
        new Primitive("Floating", "float", 32),
        new Primitive("Integral", "long", 64),
        new Primitive("Integral", "int", 32),
        new Primitive("Integral", "char", 16),
        new Primitive("Integral", "short", 16),
        new Primitive("Integral", "byte", 8),
        new Primitive("Boolean", "boolean", 1)
    );

    IO.println("--------------------------------");
    IO.println(" Java's Primitive Types");
    IO.println("--------------------------------");
    IO.println(
        "| %-10s | %-8s | %4s |"
            .formatted("CATEGORY", "NAME", "BITS")
    );
    IO.println("--------------------------------");

    for (var type : types) {
        IO.println(
            "| %-10s | %-8s | %04d |"
                .formatted(
                    type.category(),
                    type.name(),
                    type.bits()
                )
        );
    }

    IO.println("--------------------------------");
}

The output is:

--------------------------------
 Java's Primitive Types
--------------------------------
| CATEGORY   | NAME     | BITS |
--------------------------------
| Floating   | double   | 0064 |
| Floating   | float    | 0032 |
| Integral   | long     | 0064 |
| Integral   | int      | 0032 |
| Integral   | char     | 0016 |
| Integral   | short    | 0016 |
| Integral   | byte     | 0008 |
| Boolean    | boolean  | 0001 |
--------------------------------
Table with Java printf example

Formatter width, alignment and padding rules make fixed-width console tables straightforward.

Use Locale when formatting must be deterministic

Grouping separators, decimal separators and some uppercase conversions depend on the default formatting locale.

If a test or generated file requires predictable U.S. formatting, use String.format() with an explicit locale:

void main() {
    var value = 1234567.89;

    var output = String.format(
        Locale.US,
        "%,.2f",
        value
    );

    IO.println(output);
}
{% highlight text %} 1,234,567.89 {% endhighlight %} {% raw %}

Java 25 formatting cheat sheet

Goal Pattern
String%s
Uppercase String%S
Decimal integer%d
Grouped integer%,d
Hexadecimal integer%x
Octal integer%o
Two decimal places%.2f
Scientific notation%e
Character%c
Boolean%b
Minimum width 15%15s
Left alignment%-15s
Zero padding%010d
Always show sign%+d

The modern Java 25 approach is straightforward: keep using Formatter-style patterns where they make output clearer, create the String with formatted() or String.format(), and send the finished value to IO.println().

System.out.printf() remains valid, but String.formatted() plus IO.println() gives small Java 25 programs cleaner syntax and separates formatting from console output.