Abstract: The printf() method in Java borrows heavily from C, including the alternate flag #. But %s works a bit differently in Java. In this newsletter we explore this a bit more deeply.
Welcome to the 312th edition of The Java(tm) Specialists' Newsletter, which I started writing at 30,000 feet en route to Devoxx Belgium. I first spoke there 17 years ago, when it was still Javapolis. It is one of the most difficult conferences to get in to, both as a speaker and as an attendee. Seats sell out in seconds. A good opening line is "so how did you get a ticket?" As always, an excellent Java conference. Very good talks, great community spirit. Thanks Stephan!
My talk was about IntelliJ. A surprisingly popular talk, maybe because it is so practical? I even got to demo some new AI powered refactoring. Unfortunately the video is a bit dark, so turn your monitor on bright.
javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.
Let's begin with a puzzle. We need to make
convert(Bull)
return "Bear"
instead of "Bull"
. How can we do that?
public class OverridingFinalToStringPuzzle { private static class Bull { public final String toString() { return "Bull"; } } public static String convert(Bull bull) { return "%s".formatted(bull); } public static void main(String... args) { // Don't change anything above this line // TODO: Change this method so that the converted String // becomes "Bear" without any command line flags (thus // no deep reflection on String) Bull bull = new Bull(); // Don't change anything below this line String result = convert(bull); if (!result.equals("Bear")) throw new AssertionError("Should be \"Bear\""); System.out.println("All good!"); } }
After sending my newsletter on AccessFlag Set for Modifiers,
one of my readers asked why I had written
String.format("0x%04x", val)
instead of
String.format("%#04x", val)
. I had never seen the
#
modifier in String formatting. Have you?
But wait, there's more. We also have a
java.util.Formattable
interface for special
String formatting as an alternative to toString()
.
Both the #
and this interface have been in Java
since version 5. Go figure!
Let's begin with the #
alternate form modifier.
The most useful application of this is when we want to format
octal or hexadecimal numbers, like so:
public class PrintingNumbersWithHash { public static void main(String... args) { System.out.format("0%o%n", 01234); System.out.format("%#o%n", 01234); System.out.format("%#x%n", 0xcafebabe); System.out.format("%#X%n", 0xcafebabe); System.out.format("0x%x%n", 0xcafebabe); System.out.format("0x%X%n", 0xcafebabe); } }
We notice that for octal, the # prepends the output with a
0
, and for hexadecimal with 0x
.
Unfortunately, the x
is uppercased when we use
"%#X"
. I prefer writing hexadecimal numbers as
either 0xCAFEBABE
or as 0xcafebabe
,
but never as 0XCAFEBABE
. Searching through
OpenJDK, I found only a few places with the 0X
prefix, but tens of thousands with 0x
.
01234 01234 0xcafebabe 0XCAFEBABE 0xcafebabe 0xCAFEBABE
Of course we can blame K&R for this strange behavior, which we also see in C:
#include <stdio.h> int main() { printf("%#x\n", 0xcafebabe); printf("%#X\n", 0xcafebabe); }
0xcafebabe 0XCAFEBABE
In order to keep Java as similar as possible to C, Java formatted the hexadecimal numbers the same. I wasn't paying attention when I learned C in 1991.
In Java, the alternate form can also be used together with
the "%s"
formatter. I always thought that
"%s"
always calls toString()
on the
parameter. But this is not the case. If the parameter
implements the java.util.Formattable
interface,
then that will be used instead. Let's look at the example of
StockName
. It has a toString()
method, as well as formatTo()
, which includes
information about the flags (ALTERNATE, LEFT_JUSTIFY, UPPERCASE),
the width and the precision. We also have a formatter, that
we can use to direct output to. The formatter can also tell
us what Locale we are writing to.
import java.util.Formattable; import java.util.Formatter; import java.util.Locale; import static java.util.FormattableFlags.ALTERNATE; import static java.util.FormattableFlags.LEFT_JUSTIFY; import static java.util.FormattableFlags.UPPERCASE; public record StockName(String symbol, String companyName, String frenchCompanyName) implements Formattable { @Override public String toString() { return String.format("%s - %s", symbol, companyName); } @Override public void formatTo(Formatter formatter, int flags, int width, int precision) { var alternate = (flags & ALTERNATE) == ALTERNATE; var leftJustify = (flags & LEFT_JUSTIFY) == LEFT_JUSTIFY; var uppercase = (flags & UPPERCASE) == UPPERCASE; var name = chooseName(alternate, precision, formatter.locale()); name = applyPrecision(name, precision); name = applyWidthAndJustification(name, leftJustify, width); name = applyUppercase(name, uppercase); formatter.format(name); } private String chooseName(boolean alternate, int precision, Locale locale) { if (alternate) return symbol; if (precision != -1 && precision < 10) return symbol; if (locale.equals(Locale.FRANCE)) return frenchCompanyName; return companyName; } private static String applyPrecision(String name, int precision) { if (precision == -1 || name.length() < precision) { return name; } else if (precision > 0) { return name.substring(0, precision - 1) + '*'; } else { return ""; } } private static String applyWidthAndJustification( String name, boolean leftJustify, int width) { if (width <= name.length()) return name; var spaces = " ".repeat(width - name.length()); return leftJustify ? name + spaces : spaces + name; } private static String applyUppercase(String name, boolean uppercase) { return uppercase ? name.toUpperCase() : name; } }
Here is some test code that demonstrates how this StockName could be used:
import org.junit.jupiter.api.Test; import java.util.Formatter; import java.util.Locale; import static org.junit.jupiter.api.Assertions.assertEquals; public class StockNameTest { @Test public void testStockName() { var sn = new StockName("HUGE", "Huge Fruit, Inc.", "Fruit Titanesque, Inc."); test("HUGE - Huge Fruit, Inc.", "%s", sn.toString()); test("Huge Fruit, Inc.", "%s", sn); test("HUGE FRUIT, INC.", "%S", sn); test("HUGE", "%#s", sn); test("HUGE ", "%-10.8s", sn); test(" HUGE", "%10.8s", sn); test(" ", "%10.0s", sn); test("HU*", "%.3s", sn); test("Huge Fruit,*", "%.12s", sn); test(" Fruit Titanesque, Inc.", Locale.FRANCE, "%25s", sn); test("HUGE", Locale.FRANCE, "%#s", sn); } private static void test(String expected, String format, Object arg) { test(expected, Locale.US, format, arg); } private static void test(String expected, Locale locale, String format, Object arg) { var formatter = new Formatter(); var formatted = formatter.format(locale, format, arg); assertEquals(expected, formatted.toString()); System.out.println("\"" + formatted + "\""); } }
By now, you have perhaps figured out the puzzle at the
beginning of this newsletter? Since toString()
of our Bull
class is final
,
we cannot override it. However, we can make a subclass
Bear
that also implements
Formattable
, like so:
public class OverridingFinalToStringSolution { private static class Bull { public final String toString() { return "Bull"; } } public static String convert(Bull bull) { return "%s".formatted(bull); } public static void main(String... args) { // Don't change anything above this line class Bear extends Bull implements java.util.Formattable { public void formatTo(java.util.Formatter formatter, int flags, int width, int precision) { formatter.format("Bear"); } } Bull bull = new Bear(); // Don't change anything below this line String result = convert(bull); if (!result.equals("Bear")) throw new AssertionError("Should be \"Bear\""); System.out.println("All good!"); } }
Pure magic.
King regards
Heinz
P.S. Our Mastering Virtual Threads in Java Course is now available as an in-house course for 10 or more programmers, presented either virtually or in-person. Please contact me via email, on my website or via WhatsApp.
P.P.S. And be sure to wish Kirk Pepperdine a happy birthday :-)
We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)
We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.