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
- The StringBuffer class
- The StringBuilder class (JDK 5+)
- String concatenation
- The Formatter class (JDK 5+)
- The format method of String and other classes (JDK 5+)
- The formatted method of String (JDK 15+)
- The MessageFormat and ChoiceFormat classes (JDK 1.1+)
- The String Templates feature (JDK 21+)
- String joining (JDK 8+)
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+) usingmakeConcat
/makeConcatWithConstants
ofStringConcatFactory
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 format
ted
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 01#1 notification
means “1 notification” when the value is 11<{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(", ", "[", "]"));