The Guava library, for me, is hands down the best Java utility library out there. It is a shining example of clean code and API design, both in its implementation and documentation. If you’re not already using it in your Java projects – you absolutely should be! Check out the wiki here to get started.
In this series I want to highlight different pieces of Guava that I have found particularly useful. Often there’s a Guava utility that fits your need perfectly, so becoming familiar with the different features of the library can be extremely helpful!
Introduction
Whether you’re parsing input data or formatting output data, String manipulation is one of the most common operations you’ll encounter as a developer. The JDK provides some limited utilities for basic operations, but Guava’s Splitter
and Joiner
help fill in the gaps tremendously.
Splitter
If you’re lucky enough to have not encountered the quirks of Java’s String.split() method, I would encourage you to avoid it in all circumstances. If you have Error Prone set up in your project, it will even throw a warning if you try to use it!
Quirks aside, String.split()
returns an array which is rarely the most convenient format for the client. It also only accepts regular expressions on which to split, which in many cases involves compiling your input into a Pattern
under the hood. This is often way more expensive than you need if all you want to do is split based on a plain String
literal. Fortunately, Guava has our back with an easy to use alternative that allows complete control over the behavior and return type of the split operation while avoiding any unnecessary computations.
Splitter instances do exactly what you think – they split String
s. You can get the output as an Iterable
, List
, or Stream
depending your needs.
String csvData = "One,Two,Three";
String[] badExample = csvData.split(","); // don't use this
// Iterable
for (String s : Splitter.on(',').split(csvData))
{
...
}
// List
List<String> strings = Splitter.on(',').splitToList(csvData);
// Stream
Splitter.on(',').splitToStream(csvData)
...
Splitter
instances are immutable and thread-safe, so you can safely store them as static final
constants to avoid repeatedly creating instances.
Customization
The real power of the Splitter
class comes in the many ways in which instances can be customized. The Splitter
wiki details these methods, which I’ve included below.
Method | Description | Example |
---|---|---|
omitEmptyStrings() |
Automatically omits empty strings from the result. | Splitter.on(',').omitEmptyStrings().split("a,,c,d") returns "a", "c", "d" |
trimResults() |
Trims whitespace from the results; equivalent to trimResults(CharMatcher.WHITESPACE) . |
Splitter.on(',').trimResults().split("a, b, c, d") returns "a", "b", "c", "d" |
trimResults(CharMatcher) |
Trims characters matching the specified CharMatcher from results. |
Splitter.on(',').trimResults(CharMatcher.is('_')).split("_a ,_b_ ,c__") returns "a ", "b_ ", "c" |
limit(int) |
Stops splitting after the specified number of strings have been returned. | Splitter.on(',').limit(3).split("a,b,c,d") returns "a", "b", "c,d" |
These modifications can be invoked in any order.
Remember that because Splitter
s are immutable, you must store a reference to the Splitter
returned from one of these methods. In other words, do this:
private static final Splitter MY_SPLITTER = Splitter.on(',').trimResults().omitEmptyStrings();
And not this:
Splitter splitter = Splitter.on('/');
splitter.trimResults(); // does nothing! IntelliJ will flag the ignored result
return splitter.split("wrong / wrong / wrong");
Similar to what you see with the Builder pattern or Java Streams, this is a great example of a fluent API.
Creation
In addition to Splitter.on(char)
and Splitter.on(String)
, there are several other useful factories to be aware of. Some of them are tailored to very specific applications, but you never know when one might come in handy. (table copied from the wiki)
Method | Description | Example |
---|---|---|
Splitter.on(CharMatcher) |
Split on occurrences of any character in some category. | Splitter.on(CharMatcher.BREAKING_WHITESPACE) Splitter.on(CharMatcher.anyOf(";,.")) |
Splitter.on(Pattern) Splitter.onPattern(String) |
Split on a regular expression. | Splitter.onPattern("\r?\n") |
Splitter.fixedLength(int) |
Splits strings into substrings of the specified fixed length. The last piece can be smaller than length but will never be empty. |
Splitter.fixedLength(3) |
Joiner
Going the opposite direction, there’s Joiner. Again, Joiner
does exactly what you’d expect it to – it joins Strings. Once you’re comfortable with the Splitter API, using Joiner will feel instantly familiar.
In its basic form, Joiner
will accept inputs in array, Iterable
, or varargs format.
// array
String[] strings = {"one", "two", "three"};
String joined = Joiner.on(',').join(strings);
// Iterable
List<String> strings = List.of("one", "two", "three");
String joined = Joiner.on(',').join(strings);
// varargs
String joined = Joiner.on(',').join("one", "two", "three");
Object
s used as inputs to Joiner
s will be converted to String
s using their toString
methods.
Like Splitter
, Joiner
instances are always immutable, thread-safe, and usable as a static final
constant.
Customization
The modifiers available for Joiner
s dictate the behavior for null inputs.
Method | Description | Example |
---|---|---|
skipNulls() |
Automatically skips over any provided null elements. | Joiner.on("; ").skipNulls().join("Harry", null, "Ron", "Hermione") returns "Harry; Ron; Hermione" |
useForNull(String) |
Automatically substitutes the given string for any provided null elements. | Joiner.on("; ").useForNull("Ginny").join("Harry", null, "Ron", "Hermione") returns "Harry; Ginny; Ron; Hermione" |
Note that if neither skipNulls()
nor useForNull(String)
is specified, the joining methods will throw a NullPointerException
if any given element is null.
JDK Alternatives
Java 8 added a similar utility to Joiner
with the String.join()
method. This method will also accept array, Iterable
, or varargs inputs.
// array
String[] strings = {"one", "two", "three"};
String joined = String.join(",", strings);
// Iterable
List<String> strings = List.of("one", "two", "three");
String joined = String.join(",", strings);
// varargs
String joined = String.join(",", "one", "two", "three");
The only thing you lose by not using Joiner
here is control over behavior with null inputs – the String "null" is always added. And before you scream that there’s no Object creation involved with the static String.join() method, there actually is under the hood in the form of StringJoiner instances.
Collectors.joining()
also uses the StringJoiner
class in the JDK to do its job, but it is designed to work with Streams in a way that Guava’s Joiner cannot.
String joined = List.of("one", "two", "three").stream()
.map(String::toUpperCase)
.collect(joining(","));
System.out.println(joined); // prints "ONE,TWO,THREE"
Note that it is customary and wise to statically import all members of Collectors
because it makes stream pipelines more readable (EJ 46).
Use with Maps
Splitter
and Joiner
have close cousins for use with serializing and deserializing maps, MapSplitter
and MapJoiner
. Let’s say you have a cache or some other Map
that contains data you want to write out to a file or some other location. MapJoiner
can help you easily serialize your Map
in whatever format you desire.
Map<String, String> data = Map.of("name", "Michael", "role", "admin", "city", "Nashville", "state", "TN");
String serializedForm = Joiner.on('\n').withKeyValueSeparator(" == ").join(data);
This is what our serialized form looks like:
state == TN
name == Michael
city == Nashville
role == admin
Clean and human-readable. Not bad for one line of code!
Going the other direction is just as easy using MapSplitter
:
Map<String, String> myData = Splitter.on('\n').withKeyValueSeparator(" == ").split(serializedForm);
This isn’t the stuff you’ll use every day, but it’s so clean and satisfying when you can put it to use. You’ll likely impress your teammates as well!