Running on Java 22-ea+27-2262 (Preview)
Home of The JavaSpecialists' Newsletter

312Alternate String Formatting with #

Author: Dr Heinz M. KabutzDate: 2023-10-24Java Version: 17Category: Tips and Tricks
 

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.

Alternate String Formatting with #

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 :-)

 

Comments

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)

When you load these comments, you'll be connected to Disqus. Privacy Statement.

Related Articles

Browse the Newsletter Archive

About the Author

Heinz Kabutz Java Conference Speaker

Java Champion, author of the Javaspecialists Newsletter, conference speaking regular... About Heinz

Superpack '23

Superpack '23 Our entire Java Specialists Training in one huge bundle more...

Free Java Book

Dynamic Proxies in Java Book
Java Training

We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.

Java Consulting

We can help make your Java application run faster and trouble-shoot concurrency and performance bugs...