JAVA / DATES, NUMBERS, REGEX AND CORE LIBRARIES
Splitting, replacing and matching with regex groups
Use capturing groups to split, rewrite and reorder text with $1, ${name}, backreferences and split's limit rules.
What you will learn
- $1 and ${name} in a replacement string reorder, duplicate or drop captured text
- Pass -1 to split to keep trailing empty fields; a positive limit caps the count
- \1 belongs inside the pattern, $1 only inside a replacement string
- Compute replacements with a find() loop plus appendReplacement and appendTail
Understanding Splitting, replacing and matching with regex groups
Parentheses in a Java pattern do two jobs: they group for quantifiers and they capture. Capturing groups are numbered by the order of their opening parenthesis, and naming one with (?<m>...) adds an alias without removing the number, so group(2) and group("m") can be the same slot. That number is your only handle on the matched fragment, and it is read from three separate places: \1 inside the pattern itself, $1 inside a replacement string, and group(1) or start(1) from Java code. Mixing up which syntax belongs where is the most common reason regex code compiles and then produces nonsense.
A replacement string is not literal text. Matcher scans it for $ and \ before inserting anything, so $1 and ${name} pull in captured text, \$ inserts a real dollar sign, and a stray $ arriving from user data becomes a group reference that fails at runtime. That little parser cannot do arithmetic or call methods, so when the new text depends on the old text you drive the loop yourself: find() to locate each match, appendReplacement to copy the untouched text between matches and add your computed string, appendTail for whatever follows the last match.
split works from the other side of the same match: you describe the separators and get back the gaps between them, never the separators themselves, no matter how many groups the separator pattern contains. The limit argument controls the tail: the default 0 quietly deletes empty fields at the end, a negative limit keeps every field, and a positive n applies the pattern at most n-1 times so the remainder arrives intact in the last element. Because the argument is a regex, ordinary looking characters such as . | + ( must be escaped or wrapped in Pattern.quote or you split on the wrong thing.
Groups are what let one pattern both find structure and rebuild it, which is why splitting, replacing and matching are the same skill viewed from three angles.
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegexGroups {
public static void main(String[] args) {
String log = "2026-09-03 ERROR disk=/dev/sda1 free=12%";
Pattern p = Pattern.compile("(?<y>\\d{4})-(?<m>\\d{2})-(?<d>\\d{2})");
Matcher m = p.matcher(log);
if (m.find()) {
System.out.println("group(0) = " + m.group(0));
System.out.println("y/m/d = " + m.group("y") + " " + m.group("m") + " " + m.group("d"));
System.out.println("start(2) = " + m.start(2) + ", end(2) = " + m.end(2));
}
System.out.println(p.matcher(log).replaceFirst("${d}/${m}/${y}"));
String[] parts = log.split(" ", 3);
System.out.println(Arrays.toString(parts));
System.out.println("parts.length = " + parts.length);
}
}A capturing group is one numbered slot whose contents are addressed by three separate mini-languages: \1 inside the pattern, $1 inside a replacement, and group(1) in Java code.
Worked examples
Replacement text you have to compute
Doubles every number in a key:value string, which no replacement string can do on its own.
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RaisePrices {
public static void main(String[] args) {
String csv = "widget:4;gizmo:12;bolt:7";
Matcher m = Pattern.compile("(\\w+):(\\d+)").matcher(csv);
StringBuilder out = new StringBuilder();
while (m.find()) {
int doubled = Integer.parseInt(m.group(2)) * 2;
m.appendReplacement(out, m.group(1) + "=" + doubled);
}
m.appendTail(out);
System.out.println(out);
}
}Example explained
Line 1appendReplacement first copies the text between the previous match and this one, which is why the ; separators survive without being part of the pattern.
Line 2group(2) is a String, so parseInt is needed before the multiplication; a replacement string like "$1=$2" can only move text, never change it.
Line 3appendTail is not optional: with input "widget:4;bolt:7;end" the trailing ";end" would be dropped if you forgot it.
Line 4The StringBuilder overload requires Java 9 or later; older code passes a StringBuffer instead.
What split does with empty fields
Shows how the limit argument changes the number of fields returned for the same input.
import java.util.Arrays;
public class SplitEdges {
public static void main(String[] args) {
String row = ",a,,b,,";
System.out.println(Arrays.toString(row.split(",")) + " length " + row.split(",").length);
System.out.println(Arrays.toString(row.split(",", -1)) + " length " + row.split(",", -1).length);
System.out.println(Arrays.toString("a,b,c".split(",", 2)));
}
}Example explained
Line 1The leading empty field is kept because the comma at index 0 is a one-character match, not a zero-width one.
Line 2The one-argument form uses limit 0, which strips only the empty fields at the end, so a CSV row silently loses its last two columns.
Line 3limit -1 applies the pattern as many times as possible and returns all six fields, which is what fixed-column data needs.
Line 4A positive limit of 2 allows one match, so "b,c" stays glued together in the final element.
Backreference in the pattern, $1 in the replacement
Collapses accidentally doubled words using \1 to match repetition and $1 to rebuild the text.
public class DoubledWords {
public static void main(String[] args) {
String text = "the the quick brown brown fox";
System.out.println(text.replaceAll("\\b(\\w+)(\\s+\\1\\b)+", "$1"));
System.out.println("the the".matches("(\\w+) \\1"));
System.out.println("she sells".matches("(\\w+) \\1"));
}
}Example explained
Line 1\1 inside the pattern means "the same characters group 1 just matched", something no character class can express.
Line 2$1 in the replacement writes that captured word back exactly once, so "the the" becomes "the".
Line 3Because \s+ sits inside the repeated group, the two spaces in "brown brown" are part of the match and disappear with it.
Line 4"she sells" fails not because of anchoring alone but because the backreference demands identical text, and "sells" is not "she".
Important notes
Group numbers follow the opening parentheses, so wrapping an existing group in a new pair shifts $1 to $2 and changes the output without any compile error; named groups survive that edit.
String.replace takes literal text while String.replaceAll takes a regex plus the replacement mini-language, and both recompile the pattern on every call, so hoist a Pattern out of loops.
Common mistakes
Using a backreference where a group reference belongs: "abc".replaceAll("(b)", "\\1") returns "a1c", because the replacement parser reads the backslash as an escape and keeps the literal digit instead of the captured text.
Splitting on a metacharacter as if it were plain text: "1.2.3".split(".") matches every character, so all fields are empty, the trailing empties are removed, and you get a zero-length array; use split("\\.") or Pattern.quote(".").
Pasting user-supplied text into replaceAll: a value like "$7 off" is parsed as a reference to group 7 and throws IndexOutOfBoundsException: No group 7 at runtime, so wrap it in Matcher.quoteReplacement first.
Try it yourself
Change, predict, then run
Start from String row = "ada lovelace;grace hopper;;alan turing;", split it with limit -1 and print the length, then print each field passed through replaceAll("(\\w+) (\\w+)", "$2, $1"). Repeat the split without the limit and explain the two fields that disappeared.
Open the Java workspaceCheck your understanding
"a,b,,".split(",") returns 2 elements, but "a,b,,".split(",", -1) returns 4. Which rule explains the difference?
- split collapses runs of consecutive separators, so ,, counts as one, and a negative limit turns that collapsing off.
- Empty strings can never appear in a split result unless the limit is negative.
- With limit 0 the trailing empty fields are discarded, while a negative limit applies the pattern as many times as possible and keeps every field.
- The one-argument form is capped at two fields, and the limit argument raises that cap.
Show answer
The one-argument form uses limit 0, which still matches every separator and then removes only the empty strings at the end of the array; a negative limit skips that trimming. The collapsing answer also predicts 2 for this input, but it is wrong: ",a,,b".split(",") returns [, a, , b], so leading and interior empty fields are kept even with limit 0.