Parsing text in Java comes down to four tools: split to cut a string into pieces, String.join to glue pieces back together, the regular expression classes for anything more structured, and the parse methods for turning text into numbers. The two split behaviours that surprise people - dropped trailing empties and the fact that the separator is a regular expression - are worth learning before you rely on them.
How does split actually behave?
split takes a regular expression and returns an array of the matching pieces. The pattern can be as simple as a comma or as complex as you like, and the second argument controls how many pieces you get.
import java.util.Arrays;
public class SplitTraps {
public static void main(String[] args) {
String csv = "a,b,,c";
System.out.println("default : " + Arrays.toString(csv.split(",")));
System.out.println("limit -1 : " + Arrays.toString(csv.split(",", -1)));
System.out.println("limit 2 : " + Arrays.toString(csv.split(",", 2)));
System.out.println("trailing : " + Arrays.toString("a,b,,".split(",")));
System.out.println("trailing kept : " + Arrays.toString("a,b,,".split(",", -1)));
String dotted = "2026.07.14";
System.out.println("escaped dot : " + Arrays.toString(dotted.split("\\.")));
System.out.println("any char : " + Arrays.toString("a1b22c".split("[0-9]+")));
}
}default : [a, b, , c]
limit -1 : [a, b, , c]
limit 2 : [a, b,,c]
trailing : [a, b]
trailing kept : [a, b, , ]
escaped dot : [2026, 07, 14]
any char : [a, b, c]Four behaviours to internalise. First, trailing empty strings are dropped unless you pass a negative limit. Second, empty fields in the middle are kept, so "a,b,,c" yields four elements with an empty one in the middle. Third, a positive limit caps the number of pieces, and the last piece holds the entire remainder of the string - that is how you split a line into "everything before the first comma" and "everything after". Fourth, the separator is a regular expression, so a literal dot must be written "\." or it matches any character at all.
How do you join pieces back together?
String.join is the mirror of split. It takes a separator and a sequence of pieces - an array, a list, or a series of individual values - and returns the combined text. There is also a joining() collector for streams, which comes in handy when the pieces come from a pipeline.
For tokenising text where the separator runs and empty fields do not matter, StringTokenizer is the older tool that still fits. It never returns empty tokens, and it can also report the delimiter positions, which is occasionally handy.
import java.util.StringTokenizer;
public class JoinAndTokenize {
public static void main(String[] args) {
String[] parts = { "alpha", "beta", "gamma" };
System.out.println("join : " + String.join(" -> ", parts));
System.out.println("join chars : " + String.join("-", "a", "b", "c"));
System.out.println("join list : " + String.join(", ", java.util.List.of("x", "y")));
StringTokenizer tokenizer = new StringTokenizer("red, green,,blue");
System.out.println("token count : " + tokenizer.countTokens());
while (tokenizer.hasMoreTokens()) {
System.out.println("token [" + tokenizer.nextToken() + "]");
}
StringTokenizer csvTokenizer = new StringTokenizer("a,,b", ",");
while (csvTokenizer.hasMoreTokens()) {
System.out.println("csv token [" + csvTokenizer.nextToken() + "]");
}
}
}join : alpha -> beta -> gamma
join chars : a-b-c
join list : x, y
token count : 2
token [red,]
token [green,,blue]
csv token [a]
csv token [b]Compare the two tokenisers in the output. The default StringTokenizer skipped the empty field between the commas, while the one constructed with an explicit delimiter character kept the structure of the input but still did not produce an empty token. If you need to know that a field was empty, use split with a negative limit.
When do you need the Pattern and Matcher classes?
When your text has structure - dates, identifiers, key-value pairs - a regular expression is the right tool, and the Pattern and Matcher classes let you work with one step by step. A pattern is a compiled expression, a matcher is a cursor over a string, and groups capture the parts you care about.
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class PatternMatcher {
public static void main(String[] args) {
Pattern pattern = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcher matcher = pattern.matcher("due 2026-07-14 ok");
if (matcher.find()) {
System.out.println("year : " + matcher.group(1));
System.out.println("month : " + matcher.group(2));
System.out.println("day : " + matcher.group(3));
}
System.out.println("matches whole : " + Pattern.matches("\\d+", "12345"));
System.out.println("matches letters : " + Pattern.matches("\\d+", "12a45"));
System.out.println("replace all : " + "order-1 and order-2".replaceAll("order-\\d+", "order-N"));
}
}year : 2026
month : 07
day : 14
matches whole : true
matches letters : false
replace all : order-N and order-NGroup zero is always the entire match. Groups one and above are the parts you marked with parentheses, which is how the date example pulls the year, month and day out separately. Two static shortcuts save typing: Pattern.matches answers a yes-or-no question about the whole string, and String.replaceAll runs a pattern-based replacement for you.
How do you turn text into numbers safely?
Integer.parseInt and Double.parseDouble do the work and throw NumberFormatException when the text is not a number. Whether you catch that exception or check the format first is a design decision - but never let it escape into a crash for input that came from a user.
public class NumberParsing {
public static void main(String[] args) {
String input = " 1234 ";
int value = Integer.parseInt(input.trim());
System.out.println("parsed : " + value);
double ratio = Double.parseDouble("0.75");
System.out.println("ratio : " + ratio);
try {
Integer.parseInt("12a");
} catch (NumberFormatException e) {
System.out.println("bad number : " + e.getMessage());
}
System.out.println("safe parse : " + tryParse("42"));
System.out.println("safe parse : " + tryParse("forty-two"));
String csv = "name,age\nana,31";
for (String line : csv.lines().toList()) {
String[] cols = line.split(",");
System.out.println("line " + cols.length + " cols, first = " + cols[0]);
}
}
static int tryParse(String text) {
try {
return Integer.parseInt(text.trim());
} catch (NumberFormatException e) {
return -1;
}
}
}parsed : 1234
ratio : 0.75
bad number : For input string: "12a"
safe parse : 42
safe parse : -1
line 2 cols, first = name
line 2 cols, first = anaThe tryParse helper shows the pattern most production code ends up with: trim, attempt, and return a sensible default when the text does not fit. The last part of the program shows lines(), which turns a multi-line string into a stream of lines - a neat way to process a file's contents or a block of text without a manual line-by-line loop.
Which approach should you use?
| Input shape | Tool | Notes |
|---|---|---|
| One delimiter, fields may be empty | split(regex, -1) | Keeps empty and trailing fields |
| One delimiter, empties irrelevant | split(regex) | Trailing empties are dropped |
| Split into two parts | split(regex, 2) | Second part holds the remainder |
| Structured text | Pattern and Matcher | Groups capture the fields |
| Rejoining pieces | String.join | Works on arrays, lists and varargs |
| Text to number | parseInt in a try block | Or validate the format first |
One performance note that matters in practice: compiling a regular expression is not free, so build the Pattern once as a constant and reuse it, rather than calling String.matches inside a loop over thousands of lines.
The rules worth keeping
- The separator in
splitis a regular expression, so escape literal dots. - Pass
-1as the limit when empty trailing fields matter. - Use a positive limit to split into "head" and "rest" in one call.
- Reuse a compiled
Patterninstead of recompiling per line. - Always handle
NumberFormatExceptionfor text that came from outside your program.
Try it yourself: write a small parser for a log line of the form 2026-07-14 09:12:31 WARN disk=92% msg="cache miss". Use a regular expression with named groups for the timestamp, the level and the message, split the key-value pairs on the first equals sign, and print each part on its own line. Then feed it a malformed line and make sure your code reports a clean error instead of throwing.