JAVA / DATES, NUMBERS, REGEX AND CORE LIBRARIES
Regular expressions for validation and extraction
Use matches() for whole-string validation and find()/results() to pull values out of text, avoiding Java's anchor, Unicode and recompile traps.
What you will learn
- Validate with matches(): it must cover the whole input, so no ^ or $ is needed.
- Extract with find() in a loop or results(), reading group(n), start() and end().
- Anchor by hand with \A and \z; $ also matches before a final newline.
- Keep Pattern in a static final field; build a new Matcher per input, never share one.
Understanding Regular expressions for validation and extraction
Java splits a regex into two objects for a reason. Pattern is the compiled program: immutable, thread-safe and comparatively expensive to build. Matcher is a cursor over one specific input, holding a current position and the boundaries of the last successful attempt. Validation and extraction are that same program asked two different questions: matches() succeeds only if the match spans the entire input region, while find() scans forward, stops at the first position where the program succeeds, and leaves start(), end() and the captured text behind for you to read.
Because matches() is implicitly anchored at both ends, a validation pattern needs no ^ or $ at all, and bolting them on invites a Java-specific hole: in default mode $ matches at the end of the input and also immediately before a single line terminator at the end. So the pattern ^\d{3}$ used with find() cheerfully accepts "123" with a trailing newline, which is exactly the shape you get from a sloppily read line of a file. When you genuinely need explicit anchors, use \A and \z, which mean the start of input and the very end of input with no newline exception.
Extraction patterns should lean on the fixed text that surrounds the value rather than on the value's shape alone: a literal prefix like ORD- costs nothing and lets the engine reject non-candidates almost immediately, whereas open-ended shapes such as (\d+)+ or .*(\d+).* force the backtracking engine to try an enormous number of ways to split the input before admitting defeat. Remember too that a regex only ever proves shape, never meaning: \d{4}-\d{2}-\d{2} happily accepts 2026-02-31. Confirm the shape with the regex, then hand the text to a real parser such as Integer.parseInt or LocalDate.parse and let it reject impossible values.
Compilation cost is the other thing beginners overlook. String.matches(regex) compiles a fresh Pattern on every single call, so inside a loop over ten thousand rows you pay for ten thousand compilations of an identical pattern. Hoist the Pattern into a static final field and create a cheap Matcher per input; the Pattern can be shared across threads, the Matcher cannot, because it stores per-input match state.
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Main {
// Compiled once: Pattern is immutable and shareable, Matcher is not.
private static final Pattern ORDER_ID =
Pattern.compile("ORD-(\\d{4})-([A-Z]{2}\\d{3})");
static boolean isValid(String s) {
return ORDER_ID.matcher(s).matches(); // must cover the whole input
}
public static void main(String[] args) {
String[] candidates = {"ORD-2026-AB123", "ord-2026-AB123", "see ORD-2026-AB123 now"};
for (String c : candidates) {
boolean somewhere = ORDER_ID.matcher(c).find();
System.out.println("[" + c + "] matches=" + isValid(c) + " find=" + somewhere);
}
Matcher m = ORDER_ID.matcher("ORD-2026-AB123 and ORD-2025-ZZ001");
while (m.find()) {
System.out.println("at " + m.start() + ".." + m.end()
+ " year=" + m.group(1) + " code=" + m.group(2));
}
}
}matches() asks whether the entire input is the pattern while find() asks where the pattern occurs inside it, and every validation-versus-extraction decision follows from that difference.
Worked examples
Why ^ and $ are not a validity test
Shows that a trailing newline slips past ^...$ with find(), but not past matches() or \A...\z.
import java.util.regex.Pattern;
public class Main {
public static void main(String[] args) {
Pattern loose = Pattern.compile("^\\d{3}$");
Pattern strict = Pattern.compile("\\A\\d{3}\\z");
String input = "123\n"; // a line read without trimming
System.out.println("loose find : " + loose.matcher(input).find());
System.out.println("loose matches : " + loose.matcher(input).matches());
System.out.println("strict find : " + strict.matcher(input).find());
System.out.println("strict matches: " + strict.matcher(input).matches());
}
}Example explained
Line 1The Java literal "^\\d{3}$" is the four-token regex ^\d{3}$; the doubled backslash belongs to the string escape, not to the pattern.
Line 2loose find is true because $ matches both at the end of input and directly before a final line terminator, so the engine matches indices 0 to 3 and simply leaves the newline unread.
Line 3loose matches is false because matches() additionally requires the match to end at the end of the input region, and the newline is still unconsumed there.
Line 4\z means the very end of input with no newline exception, so the strict pattern reports false either way, which is the answer a validator should give.
Pulling every match out with results()
Turns repeated find() calls into a stream of MatchResult snapshots to extract selected fields.
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class Main {
private static final Pattern ENTRY = Pattern.compile("(\\d{3}) (\\d+)ms");
public static void main(String[] args) {
String log = "GET /a 200 12ms, GET /b 404 3ms, POST /c 500 87ms";
List<String> problems = ENTRY.matcher(log).results()
.filter(r -> !r.group(1).equals("200"))
.map(r -> "status " + r.group(1) + " after " + r.group(2) + "ms")
.collect(Collectors.toList());
System.out.println(problems);
System.out.println("total entries: " + ENTRY.matcher(log).results().count());
}
}Example explained
Line 1results() drives find() lazily and hands you an immutable MatchResult per match, so each element keeps its own captured text instead of being overwritten by the next iteration.
Line 2The filter compares group(1) as a String: the engine only ever produces text, so numeric interpretation is your job.
Line 3A second matcher is built for the count because results() does not reset the matcher, and a spent one would report no further matches.
Line 4Matcher.results() requires Java 9 or later; on Java 8 write while (m.find()) and read the groups inside the loop body.
What \d actually accepts
Demonstrates that \d is ASCII-only by default while Integer.parseInt is not, so validator and parser can disagree.
import java.util.regex.Pattern;
public class Main {
public static void main(String[] args) {
String eastern = "\u0661\u0662\u0663"; // Arabic-Indic digits for 123
Pattern unicodeDigits = Pattern.compile("\\d+", Pattern.UNICODE_CHARACTER_CLASS);
System.out.println("ascii digits : " + eastern.matches("\\d+"));
System.out.println("unicode flag : " + unicodeDigits.matcher(eastern).matches());
System.out.println("range 0-9 : " + eastern.matches("[0-9]+"));
System.out.println("parseInt : " + Integer.parseInt(eastern));
}
}Example explained
Line 1By default \d is exactly [0-9], so these code points are rejected even though a human and the JDK both read them as digits.
Line 2UNICODE_CHARACTER_CLASS redefines \d as any Unicode decimal digit, and the same input then validates.
Line 3An explicit [0-9] range is unaffected by that flag, which is why you should write it when you truly mean ASCII only.
Line 4Integer.parseInt goes through Character.digit, which accepts those code points, so the string your regex called invalid still parses to 123.
Important notes
Every regex backslash is doubled in Java source: the pattern \d is written "\\d", and "\d" is not even a legal string literal.
One Pattern can be shared by any number of threads, but a Matcher holds the position and groups of a single match attempt, so never cache one in a static field.
Common mistakes
Validating with find() instead of matches(): Pattern.compile("\\d{5}").matcher("zip=90210!").find() is true, so "zip=90210!" is accepted as a postal code and the junk reaches the database.
Calling group(1) after a match attempt without checking the boolean: a failed matches() or find() clears the match state, so the next group() call throws IllegalStateException: No match found, usually only on the bad input you were trying to reject.
Nesting an unbounded quantifier, as in ^(\d+)+$: on a 30-character line of digits ending in a letter, the engine tries exponentially many ways to split the digits and the thread appears to hang instead of returning false.
Try it yourself
Change, predict, then run
Write static boolean isSku(String s) that accepts exactly three uppercase letters, a hyphen, then 4 to 6 digits, using one static final Pattern and matches(), and print the result for "KEY-1234", "key-1234", "KEY-12" and "xx KEY-1234 xx". Then add a find() loop over "KEY-1234 and BOX-987654" that prints each SKU together with its start offset.
Open the Java workspaceCheck your understanding
You compile Pattern.compile("^\\d{4}$") and call find() on the string "1234" followed by a newline character. What happens, and why?
- It throws PatternSyntaxException, because ^ and $ are only legal when the MULTILINE flag is set.
- It returns false, because the input contains a character the pattern does not describe.
- It returns true, because $ can also match immediately before a line terminator at the end of input, so find() with ^...$ is not a validity test.
- It returns true, because find() looks for a substring anywhere and ignores anchors completely.
Show answer
In default mode $ matches at the end of the input and also just before a single trailing line terminator, so the engine matches indices 0 to 4 and find() succeeds even though the string is not four digits; matches() returns false on the same input because it must consume the newline too, and \A\d{4}\z fails as well. The last option is tempting but wrong: find() honours anchors fully, which is exactly why swapping ^ and $ for \A and \z changes the answer.