String building and formatting in Java

What would you answer if I asked you how many ways there are to build/format strings in Java? Maybe you could answer there are a few. No, it’s wrong! There are really many ways to build and format strings, at least up to Java 21. In this article, I will provide a good overview of all the known string building and formatting features.

Introduction

In Java, String is an immutable type. Once you have a String object, you cannot change its state. Over time, since the first version of Java, many classes and methods have been added to the Java Standard Edition framework to build and format strings in complex ways. The following sections describe all the available techniques so you can learn them and choose the best one, depending on your requirements.

The StringBuffer class

StringBuffer is a class that has existed since the first version of Java. This class represents a simple buffer of characters that can grow (or shrink) as necessary. It does not provide any sophisticated formatting features. You can only append, insert, delete, modify, or replace the characters in the buffer. Finally, you can call the toString() method to get the definitive String object.

In practice, something like:

String name = "John";
int year = 1985;

// s = "Name: John, 1985"
String s = new StringBuffer().append("Name: ").append(name).append(", ").append(year).toString();

All the modification methods return the reference to the StringBuffer object under construction, so you can use the so-called method-chaining technique, as shown above.

There is an important thing to know and remember about StringBuffer: it is a thread-safe class. All the methods are synchronized on a lock object that is the instance of StringBuffer. For this reason, in modern Java code, using StringBuffer is generally not recommended in favor of the more recent StringBuilder class.

One nice characteristic of StringBuffer is that you can preallocate the buffer to a specific capacity to avoid intermediate reallocations. If, for example, you know that your final string will be around 200 characters, you can do new StringBuffer(200). By default, StringBuffer has an initial capacity of 16 characters.

The StringBuilder class (JDK 5+)

StringBuilder is a class that has been introduced in JDK 5. The purpose is substantially similar to StringBuffer, and the two classes are functionally equivalent. Indeed, StringBuilder has the same API as StringBuffer (same method names with the same meaning and behavior). In the following example code, only the class name is different from the previous example code.

String name = "John";
int year = 1985;

// s = "Name: John, 1985"
String s = new StringBuilder().append("Name: ").append(name).append(", ").append(year).toString();

However, StringBuilder has a noteworthy difference since it is not thread-safe. Most of the time, you don’t need the thread safety provided by StringBuffer, and thus, StringBuilder is generally preferred to avoid synchronization costs.

String concatenation

String concatenation is the most basic and straightforward way to compose strings. For example:

String name = "John";
int year = 1985;
String s = "Name: " + name + ", " + year;

In the Java language, the + operator performs a string concatenation if at least one of the two operands is a String. The code generated by the compiler depends on the Java version:

  • Before JDK 5, StringBuffer is used to build the string.
  • Since JDK 5 StringBuilder is used instead of StringBuffer to build the string.
  • Since JDK 9 string concatenation takes advantage of the invokedynamic bytecode instruction (Java 7+) using makeConcat/makeConcatWithConstants of StringConcatFactory as bootstrap methods (note: this is an advanced concept! See the JEP 280: Indify String Concatenation).

The Formatter class (JDK 5+)

The JDK 5 introduced a new and exciting string formatting feature. It allows you to format strings similarly to the famous printf function of the “C” language. Formatting works by defining a format string that contains one or more format specifiers like %s, %d, %f, %x, etc. If you know the printf function of the “C” (or an equivalent version in another language), you should have a good idea of this feature. The Java implementation, however, is much more sophisticated and robust.

This new type of formatting is managed by Formatter, which is the core “engine” class that interprets and handles all the format specifiers. The Formatter class is rarely directly used since there are other more practical “cover” methods in the framework (see next section).

Formatter is rather general because it can write to many destinations like files, output streams (java.io.OutputStream), java.io.PrintStream, and any Appendable object. The class has several constructors that you can choose from to specify a destination. Remember also that Formatter implements Closeable and AutoCloseable, so it’s crucial to close the Formatter, especially in the case of files and streams. The best way to ensure this is using the try-with-resource construct introduced in Java 7.

The following is a basic example that uses Formatter directly to write to a file.

String name = "John";
int year = 1985;

try (Formatter fmt = new Formatter("person.txt", "UTF-8")) {    // try-with-resource
    fmt.format("Name: %s%n", name);
    fmt.format("Year: %d%n", year);
}

Final note: the Javadoc of Formatter contains the complete and detailed description of all the format specifiers. I suggest you read this documentation to get familiar with the various specifiers.

Notes on StringBuffer/StringBuilder

In the StringBuffer section, I said that it doesn’t have any fancy formatting features. This is true since there are no format methods in StringBuffer (or StringBuilder). However, as I said before, Formatter can write to any Appendable object. StringBuffer and StringBuilder are two implementations (among others) of Appendable. Thus, you can also do the following things, for example:

// Formatter implicitly uses a StringBuilder
String s = new Formatter().format("User %s has [%07d] points", user, points).toString();

Or:

// Formatter uses an explicit StringBuilder
String s = new Formatter(new StringBuilder(30)).format("User %s has [%07d] points", user, points).toString();

The only disadvantage of using Formatter with a StringBuilder in these ways is that an IDE may complain that the Formatter is not closed. But since the destination is a StringBuilder, there is nothing to really “close”.

The format method of String and other classes (JDK 5+)

Since JDK 5, there are two format methods in the String class:

public static String format(String format, Object... args)
public static String format(Locale l, String format, Object... args)

These static methods are simple “cover” methods to use the Formatter engine (see previous section) when you only need to create a formatted string. They can be used, for example, in the following way:

String user = "Jane";
int points = 326;

// s = "User Jane has [0000326] points"
String s = String.format("User %s has [%07d] points", user, points);

Note that the new formatting feature is not only present in the String class since there are other “cover” methods in a few other I/O classes:

  • java.io.Console
  • java.io.PrintStream
  • java.io.PrintWriter

In these classes, you will find that there are two types of methods named format and printf. Beware, they are practically the same thing! Indeed, printf calls the format method, and there are no other differences. The printf method exists only as an aid for developers with a background in the “C” language.

In the end, if you already have a Console, PrintStream, or PrintWriter, you don’t need to use Formatter directly. There is, in particular, a well-known PrintStream object, the System.out constant, used to write to standard output. So, instead of using the traditional print/println methods, you can format a text in the following way:

String user = "Jane";
int points = 326;

System.out.printf("User %s has [%07d] points", user, points);   // or System.out.format

The formatted method of String (JDK 15+)

In JDK 15, a new method named formatted has been added to the String class:

public String formatted(Object... args)

This method offers a simple stylistic variation on the string formatting feature. In practice, instead of writing:

String s = String.format("User %s has [%07d] points", user, points);

You can write:

String s = "User %s has [%07d] points".formatted(user, points);

The latter version is a little less verbose and more legible. It is also conceptually more logical since you invoke the formatted method on the format string instead of using a static method.

The MessageFormat and ChoiceFormat classes (JDK 1.1+)

MessageFormat and ChoiceFormat are two classes that have existed since JDK 1.1. They reside in the java.text package dedicated to general text, number, and message formatting and parsing.

MessageFormat, in particular, produces text messages typically displayed for the end user. The usage of MessageFormat is rare for what I have always seen. Most often, it is generally used in conjunction with resource bundles for the application internationalization/localization.

MessageFormat offers some distinctive features you will not find in the other methods described in this article. Firstly, it can format numbers, dates, and times. Furthermore, it can also format choices. A choice is a particular case where a phrase has various forms that depend on the value of a parameter. This is typically used to compose phrases with a singular/plural form or possibly more forms.

Consider, for example, the following message forms:

  • You have no notifications.
  • You have 1 notification.
  • You have 4 notifications.

Instead of writing your logic from scratch to select the proper form, you can delegate this task to MessageFormat, which in turn delegates to ChoiceFormat. Let’s see how in the following example code:

String msg = "You have {0,choice,0#no notifications|1#1 notification|1<{0,number,integer} notifications}.";
MessageFormat mf = new MessageFormat(msg);

System.out.println(mf.format(new Object[] { 0 }));   // You have no notifications.
System.out.println(mf.format(new Object[] { 1 }));   // You have 1 notification.
System.out.println(mf.format(new Object[] { 4 }));   // You have 4 notifications.

As you can see, the pattern syntax could be more intuitive. You have to specify that the parameter {0} handles a choice, and then you have to separate the various subparts using the pipe (“|”) character. Thus, you have three subparts in the above pattern:

  • 0#no notifications means “no notifications” when the value is 0
  • 1#1 notification means “1 notification” when the value is 1
  • 1<{0,number,integer} notifications} means “n notifications” when n > 1

The String Templates feature (JDK 21+)

String Templates are an entirely new way to format strings introduced in Java/JDK 21 and described by the JEP-430.

Since Java 21, the compiler recognizes a new construct called string template. A string template resembles a literal string but contains one or more embedded expressions enclosed by \{ and }. Each expression can reference any variable visible at that point and can also use complex expressions and method calls.

The string template is interpolated by a string template processor represented by the StringTemplate.Processor interface, which is specifically a functional interface. The interpolation is triggered by “invoking” the string template on a reference to a template processor; see the following example:

String user = "John";
List<String> userHobbies = Arrays.asList("skiing", "dancing");

// s = "User John has 2 hobbies"
String s = STR."User \{user} has \{userHobbies.size()} hobbies";

STR is the reference to a predefined template processor that is implicitly imported in every Java compilation unit. The Java Platform provides another processor named FMT, which must be explicitly imported.

The FMT processor is related to the string formatting feature defined by the java.util.Formatter class. Thus, you can reuse the same format specifiers you may already know. Beware that specifiers must immediately precede the \{ of an embedded expression, as in the following example:

import static java.util.FormatProcessor.FMT;
// ....

String user = "Jane";
int points = 326;

// s = "User Jane has [0000326] points"
String s = FMT."User %s\{user} has [%07d\{points}] points";

As of Java 21, String Templates is a preview feature; thus, it is not yet definitive and permanent. By the way, I discovered there is already another enhancement proposal, the JEP-459, representing a second step on this feature.

String joining (JDK 8+)

Joining strings is not precisely like formatting, as you saw previously, but it is, anyway, a form of string building. So, it can certainly be listed here.

String joining was officially introduced in JDK 8 with the StringJoiner class. This is a simple class you can instantiate by passing a delimiter and, optionally, a prefix+suffix. Then, you can add as many char sequences as you want. The class produces the final string by adding the prefix, the suffix, and delimiters as necessary. For example:

StringJoiner joiner = new StringJoiner(", ", "(", ")");
joiner.add("one");
joiner.add("two");
joiner.add("three");

// s = "(one, two, three)"
String s = joiner.toString();

There are two other parts of the Java SE framework that offer the possibility of joining strings: the String class and some collectors in the Collectors class.

String has two static methods for quick string joining:

public static String join(CharSequence delimiter, CharSequence... elements)
public static String join(CharSequence delimiter, Iterable<? extends CharSequence> elements)

If all you have is an array, an Iterable, or a known bunch of strings, these two methods are more than sufficient. For example:

// s = "one, two, three"
String s = String.join(", ", "one", "two", "three");
List<String> strings = Arrays.asList("one", "two", "three");

// s = "one, two, three"
String s = String.join(", ", strings);

Finally, the Java Stream API provides some specific collectors in the Collectors class:

public static Collector<CharSequence,?,String> joining()
public static Collector<CharSequence,?,String> joining(CharSequence delimiter)
public static Collector<CharSequence,?,String> joining(CharSequence delimiter, CharSequence prefix, CharSequence suffix)

These collectors are helpful when strings (in general, anything that is a CharSequence) come from a Stream, as in the following example:

int[] nums = { 5, 10, 15, 20 };

// s = "[25, 100, 225, 400]"
String s = Arrays.stream(nums)
        .map(n -> n * n)
        .mapToObj(String::valueOf)
        .collect(Collectors.joining(", ", "[", "]"));

Similar Posts