Regex #

You have thousands of lines of server logs and need to extract all IP addresses. Or you must validate phone number formats from user input before saving to the database. Or you need to convert all DD/MM/YYYY date formats in a document to YYYY-MM-DD. All of these can be solved in a few lines of code with regex. Regex (Regular Expression) is a mini-language for describing text patterns — once you master it, many string processing problems that seemed complex become trivial. Java provides complete regex support through java.util.regex with two main classes: Pattern and Matcher.

Overview #

Before diving into syntax, there are three concepts to understand:

Pattern is the expression that describes what you’re looking for. For example \d+ means “one or more digits”, and [a-z]{3} means “exactly three lowercase letters”.

Compilation is the process of converting a pattern string into a ready-to-use Pattern object. It’s not a cheap operation — always compile once and store the result as a constant if the same pattern is used repeatedly.

Matching is the process of applying a Pattern to a target text via a Matcher object. Matcher stores the current position in the text and provides methods to find, extract, and replace.

flowchart LR
    A["Pattern string\n\"\\d+\""] -->|"Pattern.compile()"| B["Pattern\n(compiled)"]
    B -->|"pattern.matcher(text)"| C["Matcher\n(position in text)"]
    C -->|"find() / matches()"| D["Results:\ngroup(), start(), end()"]
In Java, the backslash \ in strings must be written doubled: \\. So the regex pattern \d (one digit) is written as "\\d" in Java code. This is often a source of confusion at first.

Basic Regex Syntax #

Before using Pattern and Matcher, you need to understand the “language” they use. Regex has a small vocabulary, but its combinations are very powerful.

Literal Characters and the Dot #

a        → matches the letter 'a' literally
abc      → matches the sequence "abc"
.        → matches ANY single character (except newline)
\.       → matches a literal dot (the dot must be escaped)

Character Classes #

A character class is a list or range of characters allowed to match at a certain position.

SyntaxMeaningMatching examples
[abc]One of a, b, or ca, b, c
[^abc]Anything except a, b, cd, 1, !
[a-z]Lowercase a through zm, z
[A-Z]Uppercase A through ZB, X
[0-9]Digits 0 through 93, 9
[a-zA-Z0-9]Letters or digitsg, 7

Character Class Shorthands #

Because some character classes are used very often, regex provides shorthands:

ShorthandEquivalent toMeaning
\d[0-9]A digit
\D[^0-9]Not a digit
\w[a-zA-Z0-9_]A “word” character (letters, digits, underscore)
\W[^a-zA-Z0-9_]Not a word character
\s[ \t\n\r\f]Whitespace (space, tab, newline)
\S[^ \t\n\r\f]Not whitespace

Quantifiers #

Quantifiers determine how many times the previous pattern may appear.

QuantifierMeaningExample
*0 or more\d* matches "", "5", "123"
+1 or more\d+ matches "5", "123" but not ""
?0 or 1 (optional)colou?r matches "color" and "colour"
{n}Exactly n times\d{4} matches "2025"
{n,}n times or more\d{3,} matches "123", "12345"
{n,m}Between n and m times\d{2,4} matches "12", "123", "1234"

By default, quantifiers are greedy — they match as much as possible. Add ? at the end to make them lazy (match as little as possible): .*?, \d+?, \w{2,5}?.

Anchors #

Anchors don’t match characters, but positions in the text.

AnchorPosition matched
^Start of the string (or start of a line in multiline mode)
$End of the string (or end of a line in multiline mode)
\bWord boundary — a position between \w and \W
\BNot a word boundary
^Java        → "Java" only matches if at the start of the string
\.java$      → ".java" only matches if at the end of the string
\bJava\b     → "Java" as a whole word, doesn't match "JavaScript"

Alternation and Groups #

cat|dog      → matches "cat" or "dog"
(cat|dog)s   → matches "cats" or "dogs"
(ha)+        → matches "ha", "haha", "hahaha"
(?:ha)+      → like above but the group isn't captured (non-capturing group)

Pattern and Matcher #

Pattern and Matcher are the two main classes in java.util.regex. All serious regex operations in Java go through them.

Basic Usage #

import java.util.regex.Pattern;
import java.util.regex.Matcher;

String text = "I have 3 cats and 12 fish.";
String pattern = "\\d+"; // one or more digits

// Compile the pattern — do it once, store as a constant if used repeatedly
Pattern pattern = Pattern.compile(pattern);

// Create a Matcher for the target text
Matcher matcher = pattern.matcher(text);

// find() looks for the next match, returns true if found
while (matcher.find()) {
    System.out.println("Found: " + matcher.group());
    System.out.println("  position: " + matcher.start() + " - " + matcher.end());
}
// Output:
// Found: 3
//   position: 10 - 11
// Found: 12
//   position: 22 - 24

matches() vs find() vs lookingAt() #

The three main Matcher methods behave differently and often confuse beginners.

Pattern p = Pattern.compile("\\d+");

// matches(): the ENTIRE text must match the pattern — rarely used directly
Matcher m1 = p.matcher("123");
System.out.println(m1.matches()); // true — all of "123" is digits

Matcher m2 = p.matcher("123abc");
System.out.println(m2.matches()); // false — there's "abc" after

// find(): search for a match ANYWHERE in the text — most commonly used
Matcher m3 = p.matcher("abc 123 def");
System.out.println(m3.find()); // true — "123" found in the middle

// lookingAt(): matches from the START of the text, but doesn't have to reach the end
Matcher m4 = p.matcher("123abc");
System.out.println(m4.lookingAt()); // true — starts with digits

Matcher m5 = p.matcher("abc123");
System.out.println(m5.lookingAt()); // false — doesn't start with digits

Compilation Flags #

Flags change the pattern matching behavior.

// CASE_INSENSITIVE: ignore letter case
Pattern pInsensitive = Pattern.compile("java", Pattern.CASE_INSENSITIVE);
System.out.println(pInsensitive.matcher("JAVA").matches()); // true
System.out.println(pInsensitive.matcher("Java").matches()); // true

// MULTILINE: ^ and $ match at the start/end of each line, not just the whole string
String multiline = "line 1\nline 2\nline 3";
Pattern pMulti = Pattern.compile("^line", Pattern.MULTILINE);
Matcher mMulti = pMulti.matcher(multiline);
int count = 0;
while (mMulti.find()) count++;
System.out.println(count); // 3 — found at the start of every line

// DOTALL: the dot (.) also matches newlines
Pattern pDotall = Pattern.compile("a.b", Pattern.DOTALL);
System.out.println(pDotall.matcher("a\nb").matches()); // true

// Combine multiple flags with bitwise OR
Pattern pCombined = Pattern.compile("java", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);

// Flags can also be written inline in the pattern
Pattern pInline = Pattern.compile("(?i)java"); // equivalent to CASE_INSENSITIVE

Capture Groups #

Capture groups are parts of the pattern wrapped in parentheses (). Besides structuring the pattern, groups capture the matching text so it can be accessed separately.

Numbered Groups #

// Pattern: (year)-(month)-(day)
Pattern date = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcher m = date.matcher("Birth date: 1995-07-20");

if (m.find()) {
    System.out.println("Full match: " + m.group(0)); // 1995-07-20
    System.out.println("Year:  " + m.group(1)); // 1995
    System.out.println("Month:  " + m.group(2)); // 07
    System.out.println("Day:   " + m.group(3)); // 20
}

Named Groups #

Named groups are easier to read and don’t depend on ordering — pattern changes don’t change how you access the groups.

// Named group syntax: (?<groupName>pattern)
Pattern date = Pattern.compile(
    "(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})"
);
Matcher m = date.matcher("Date: 2025-08-17");

if (m.find()) {
    System.out.println("Year: " + m.group("year")); // 2025
    System.out.println("Month: " + m.group("month")); // 08
    System.out.println("Day:  " + m.group("day"));  // 17
}

Backreferences #

Backreferences let you refer back to text already captured by a previous group within the same pattern.

// \1 refers to the text captured by the first group
// This pattern matches repeated words ("the the", "is is")
Pattern repeat = Pattern.compile("\\b(\\w+)\\s+\\1\\b");
Matcher m = repeat.matcher("this is is an example of a word that that repeats");

while (m.find()) {
    System.out.println("Repeated word: " + m.group(1));
}
// Output:
// Repeated word: is
// Repeated word: that

Lookahead and Lookbehind #

Lookahead and lookbehind are zero-width assertions — they check the context around the current position without being included in the match result.

Lookahead #

// Positive lookahead (?=...): matches only if followed by a specific pattern
// Find numbers followed by " USD"
Pattern price = Pattern.compile("\\d+(?= USD)");
Matcher m = price.matcher("Price: 500 USD and 300 EUR");
while (m.find()) {
    System.out.println(m.group()); // 500 (only the one followed by USD)
}

// Negative lookahead (?!...): matches only if NOT followed by a specific pattern
// Find numbers NOT followed by " USD"
Pattern notUSD = Pattern.compile("\\d+(?! USD)");
Matcher m2 = notUSD.matcher("Price: 500 USD and 300 EUR");
// will match "50" (part of 500) and "300" (followed by EUR)

Lookbehind #

// Positive lookbehind (?<=...): matches only if preceded by a specific pattern
// Find numbers preceded by "Rp"
Pattern rupiah = Pattern.compile("(?<=Rp)\\d+");
Matcher m = rupiah.matcher("Price: Rp50000 and USD300");
while (m.find()) {
    System.out.println(m.group()); // 50000
}

// Negative lookbehind (?<!...): matches only if NOT preceded by a specific pattern
Pattern notRupiah = Pattern.compile("(?<!Rp)\\d+");

Text Replacement #

Matcher provides methods to replace matching text — either all occurrences or one by one with custom logic.

replaceAll and replaceFirst #

// replaceAll via String (shortcut, no need to create a Pattern/Matcher)
String text = "Phone: 0812-3456-7890 and 0856-1234-5678";
String censored = text.replaceAll("\\d", "*");
System.out.println(censored);
// Phone: ****-****-**** and ****-****-****

// replaceFirst: only replace the first occurrence
String onlyFirst = text.replaceFirst("\\d+", "XXXX");
System.out.println(onlyFirst);
// Phone: XXXX-3456-7890 and 0856-1234-5678

// Use group references in the replacement: $1, $2, etc.
String date = "Date: 17/08/2025";
// Convert DD/MM/YYYY format → YYYY-MM-DD
String iso = date.replaceAll("(\\d{2})/(\\d{2})/(\\d{4})", "$3-$2-$1");
System.out.println(iso); // Date: 2025-08-17

Replacement with Custom Logic #

// Java 9+: replaceAll(Function<MatchResult, String>) for custom logic
Pattern numbers = Pattern.compile("\\d+");
Matcher m = numbers.matcher("5 apples, 12 mangoes, 3 oranges");

// Double all the numbers
String result = m.replaceAll(mr -> String.valueOf(Integer.parseInt(mr.group()) * 2));
System.out.println(result);
// 10 apples, 24 mangoes, 6 oranges

Splitting with Regex #

String.split() accepts a regex as the delimiter, far more flexible than literal-character splitting.

Splitting with Patterns #

// Split by one or more whitespace characters
String sentence = "  This   is   a   text   with   double   spaces  ";
String[] words = sentence.trim().split("\\s+");
System.out.println(words.length); // 6
// [This, is, a, text, with, double]

// Split by commas with optional spaces
String csv = "Budi, Ani,Citra ,Doni";
String[] names = csv.split("\\s*,\\s*");
// [Budi, Ani, Citra, Doni] — spaces around commas are removed too

// Split by several delimiters at once
String mixed = "one;two,three|four";
String[] parts = mixed.split("[;,|]");
// [one, two, three, four]

// Limit the number of parts with the limit parameter
String log = "ERROR:NullPointerException:line 42:long detail";
String[] parts = log.split(":", 3); // max 3 parts
// [ERROR, NullPointerException, line 42:long detail]

Performance and Anti-Patterns #

Carelessly written regex can be very slow or even hang the program. Some common anti-patterns you should avoid.

Recompiling Inside a Loop #

// ANTI-PATTERN: Pattern.compile() called on every iteration — very expensive
List<String> emails = getEmailList();
for (String email : emails) {
    if (Pattern.compile("^[\\w.-]+@[\\w.-]+\\.[a-zA-Z]{2,}$").matcher(email).matches()) {
        // process...
    }
}

// CORRECT: compile once as a static constant
private static final Pattern EMAIL_PATTERN =
    Pattern.compile("^[\\w.-]+@[\\w.-]+\\.[a-zA-Z]{2,}$");

for (String email : emails) {
    if (EMAIL_PATTERN.matcher(email).matches()) {
        // process...
    }
}

Catastrophic Backtracking #

// ANTI-PATTERN: patterns with nested quantifiers and overlapping alternation
// (a+)+ or (a|aa)+ on long input can run exponentially
Pattern dangerous = Pattern.compile("(a+)+b");
// "aaaaaaaaaaaaaaaaac" will hang the program due to unbounded backtracking

// CORRECT: avoid nested quantifiers, use atomic groups or possessive quantifiers
// Or use a more specific pattern
Pattern safe = Pattern.compile("a+b"); // simple, no nesting

Greedy vs Lazy — Choose the Right One #

String html = "<b>bold text</b> and <i>italic</i>";

// ANTI-PATTERN: greedy matches TOO MUCH
Pattern greedy = Pattern.compile("<.+>");
Matcher m1 = greedy.matcher(html);
if (m1.find()) System.out.println(m1.group());
// Output: <b>bold text</b> and <i>italic</i>  ← too wide!

// CORRECT: lazy matches as little as possible
Pattern lazy = Pattern.compile("<.+?>");
Matcher m2 = lazy.matcher(html);
while (m2.find()) System.out.println(m2.group());
// Output:
// <b>
// </b>
// <i>
// </i>

Real-World Cases #

Email Validation #

private static final Pattern EMAIL =
    Pattern.compile("^[\\w.+-]+@[\\w-]+\\.[a-zA-Z]{2,}(?:\\.[a-zA-Z]{2,})?$");

public static boolean isValidEmail(String email) {
    if (email == null) return false;
    return EMAIL.matcher(email).matches();
}

System.out.println(isValidEmail("[email protected]"));      // true
System.out.println(isValidEmail("[email protected]"));       // true
System.out.println(isValidEmail("not-an-email"));           // false
System.out.println(isValidEmail("@no-user.com"));       // false

Indonesian Phone Number Validation #

// Format: 08XX-XXXX-XXXX or +628XX-XXXX-XXXX (hyphens optional)
private static final Pattern ID_PHONE = Pattern.compile(
    "^(\\+62|0)8[1-9][0-9][-\\s]?[0-9]{3,4}[-\\s]?[0-9]{3,4}$"
);

public static boolean isValidPhone(String number) {
    if (number == null) return false;
    return ID_PHONE.matcher(number.trim()).matches();
}

System.out.println(isValidPhone("081234567890"));   // true
System.out.println(isValidPhone("0812-3456-7890")); // true
System.out.println(isValidPhone("+628****7890")); // true
System.out.println(isValidPhone("1234567890"));     // false

Extracting All URLs from Text #

private static final Pattern URL = Pattern.compile(
    "https?://[\\w.-]+(?:/[\\w./?=%&+-]*)?"
);

public static List<String> extractURLs(String text) {
    List<String> urls = new ArrayList<>();
    Matcher m = URL.matcher(text);
    while (m.find()) {
        urls.add(m.group());
    }
    return urls;
}

String article = "Visit https://java.unisbadri.com and https://docs.oracle.com/javase/ for references.";
List<String> found = extractURLs(article);
found.forEach(System.out::println);
// https://java.unisbadri.com
// https://docs.oracle.com/javase/

Extracting All IP Addresses from Logs #

private static final Pattern IP = Pattern.compile(
    "\\b(?:(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\b"
);

String log = """
    2025-08-17 ERROR request from 192.168.1.10 failed
    2025-08-17 INFO  access from 10.0.0.254 succeeded
    2025-08-17 WARN  attempt from 999.999.999.999 rejected
    """;

Matcher m = IP.matcher(log);
while (m.find()) {
    System.out.println("IP: " + m.group());
}
// IP: 192.168.1.10
// IP: 10.0.0.254
// (999.999.999.999 doesn't match — values exceed 255)

Reformatting Dates #

// Convert all DD/MM/YYYY dates to YYYY-MM-DD in a document
private static final Pattern ID_DATE =
    Pattern.compile("(?<day>\\d{2})/(?<month>\\d{2})/(?<year>\\d{4})");

public static String reformat(String text) {
    return ID_DATE.matcher(text)
        .replaceAll(mr -> mr.group("year") + "-" + mr.group("month") + "-" + mr.group("day"));
}

String document = "The contract was signed on 17/08/2025 and valid until 31/12/2027.";
System.out.println(reformat(document));
// The contract was signed on 2025-08-17 and valid until 2027-12-31.

Quick Symbol Reference #

SymbolMeaning
.Any character (except newline)
\dDigit [0-9]
\DNot a digit
\wWord character [a-zA-Z0-9_]
\WNot a word character
\sWhitespace
\SNot whitespace
\bWord boundary
^Start of string
$End of string
*0 or more
+1 or more
?0 or 1
{n,m}n to m times
[abc]One of a, b, c
[^abc]Not a, b, or c
(abc)Capture group
(?:abc)Non-capturing group
(?<name>abc)Named group
a|ba or b
(?=...)Positive lookahead
(?!...)Negative lookahead
(?<=...)Positive lookbehind
(?<!...)Negative lookbehind

When to Use Regex #

Use REGEX when:
  ✓ The pattern you're searching for can't be expressed with ordinary String operations
  ✓ You need complex format validation (email, phone, IP, URL)
  ✓ You need to extract many matches from long text
  ✓ You need to replace text with custom patterns (reformatting dates, censoring data)
  ✓ You need to split with varying delimiters

Avoid REGEX when:
  ✗ Ordinary String operations are enough — contains(), startsWith(), indexOf() are faster
  ✗ Validating HTML or XML — use a dedicated parser, regex isn't suited for this
  ✗ Very complex, deeply layered patterns — consider a parser or state machine
  ✗ High maintainability is required — long regex is hard to read and maintain

Performance tips:
  ✓ Always store Pattern.compile() as a static final constant
  ✓ Use non-capturing groups (?:...) if you don't need access to the group
  ✓ Avoid nested quantifiers like (a+)+ which can cause catastrophic backtracking
  ✓ Use the ^ and $ anchors when validating an entire string

Summary #

  • Pattern and Matcher are the main pairPattern.compile(pattern) compiles the pattern once, pattern.matcher(text) creates an object that operates on the target text.
  • Compile patterns once, store as a constantPattern.compile() is expensive. Don’t call it inside loops. Use private static final Pattern.
  • find() vs matches()find() searches for matches anywhere in the text (most commonly used), matches() requires the entire text to match the pattern.
  • Double backslash in Java — the \d pattern in regex is written "\\d" in a Java String. \b is written "\\b". This is a very common source of confusion.
  • Capture groups — wrap parts of the pattern in () to access them via group(n). Use named groups (?<name>pattern) and group("name") for more readable code.
  • Greedy vs lazy — greedy quantifiers (*, +) match as much as possible. Add ? (*?, +?) for lazy, which matches as little as possible. Choose based on what you want to capture.
  • replaceAll with group references$1, $2 in the replacement string refer to capture groups. Useful for reformatting text like DD/MM/YYYY → YYYY-MM-DD.
  • Avoid catastrophic backtracking — nested quantifiers like (a+)+ on non-matching input can run exponentially. Write specific patterns and avoid overlapping alternation.

← Previous: Date & Time   Next: Build Tools →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact