Laden...
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. By the way, at 17:20 into my talk, I give away a gift of my Data Structures Course. However, that coupon has expired. Here is another coupon, only for you my newsletter subscriber and only until the end of October 2023. Please don't share that link - it is only for those receiving my newsletter via email. When you sign up for the free course, you can opt in to receive "marketing emails" on Teachable. Don't worry, you won't get a gazillion emails on that platform - only a couple per year for special occasions :-)
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); } // Don't change anything above this line public static void main(String... args) { // TODO: Change this method so that the converted String // becomes "Bear" without any command line flags (thus // no deep reflection on String) String result = convert(new 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 0xCAFEBABEOf 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 0XCAFEBABEIn 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); } // Don't change anything above this line public static void main(String... args) { class Bear extends Bull implements java.util.Formattable { public void formatTo(java.util.Formatter formatter, int flags, int width, int precision) { formatter.format("Bear"); } } String result = convert(new Bear()); 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 :-)
Java Specialists Superpack '23 Our entire Java Specialists Training in One Huge BundleLaden...
Laden...