How do split and String.join work in Java, and what are the subtle differences in how they treat the separator?
They are mirror images, with one asymmetry that catches everyone out. String.join takes a separator and a sequence of pieces and produces one String. String.split takes a pattern and a String and produces the pieces. The asymmetry is that split takes a regular expression while join takes a literal separator, so a dot in the separator behaves completely differently in the two methods.
Inside split, a dot matches any character. To split on a literal dot you must escape it, or wrap it in Pattern.quote. The same applies to a pipe, which is a regex alternation operator. A comma, a space and a hyphen are not metacharacters, so they work unescaped and are usually what you want.
The second subtlety is empty strings. By default, split removes trailing empty strings produced by the split, so "a,b,,".split(",") does not end with an empty piece. Passing a negative limit keeps every piece. A leading empty string is kept by default, because it is not trailing.
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
System.out.println("join : " + String.join(", ", "a", "b", "c"));
System.out.println("join list : " + String.join("-", Arrays.asList("x", "y", "z")));
// split takes a regex, so a dot means "any character".
System.out.println("split on dot : " + Arrays.toString("a.b.c".split("\\.")));
System.out.println("split on pipe : " + Arrays.toString("a|b|c".split("\\|")));
// Trailing empty strings are removed unless you pass a negative limit.
System.out.println("split default : " + Arrays.toString("a,b,,c,".split(",")));
System.out.println("split limit -1 : " + Arrays.toString("a,b,,c,".split(",", -1)));
// A leading empty string is kept by default.
System.out.println("leading comma : " + Arrays.toString(",a,b".split(",")));
}
}The output makes the behaviours concrete: the default split drops the trailing empty piece while the negative limit keeps every piece including the trailing empty one, and the leading comma produces an empty first element.
One more thing to know about split: it has a fast path. If the pattern is a single character that is not a regex metacharacter, the JVM uses a specialised implementation that avoids compiling a pattern. That is why split(",") is markedly faster than split(",\\s*"), and it is also why splitting on a single metacharacter such as split("|") behaves strangely — the pipe is alternation with an empty left side, so it matches at every position.