Regex

August 12, 20264 min readUpdated 8/20/2026

A regular expression describes a pattern of text. Java's support lives in java.util.regex, and the hardest part is not the syntax — it is knowing when a regex is the right tool and keeping the ones you write readable.

The three ways in

class Demo {
    void run() {
        String text = "order-1099";

        // 1. String methods — convenient, recompile the pattern every call
        System.out.println(text.matches("order-\\d+"));            // true
        System.out.println(text.replaceAll("\\d+", "#"));          // order-#
        System.out.println("a1b2c3".split("\\d").length);          // 3

        // 2. Pattern + Matcher — compile once, reuse
        Pattern pattern = Pattern.compile("order-(\\d+)");
        Matcher matcher = pattern.matcher(text);
        if (matcher.matches()) {
            System.out.println(matcher.group(1));                  // 1099
        }
    }
}

Compile the pattern once if it is used more than occasionally. A Pattern is immutable and thread-safe, so it belongs in a static final field; a Matcher is neither and must be created per use.

Note matches() requires the whole string to match, while find() looks for a match anywhere. Confusing the two is the most common first mistake.

The syntax you will actually use

PatternMatches
.any character except a newline
\d \w \sdigit, word character, whitespace
\D \W \Sthe negation of each
[abc] [^abc] [a-z]one of, none of, a range
* + ?zero or more, one or more, zero or one
{2} {2,} {2,5}exactly, at least, between
^ $start and end of input
\ba word boundary
(...) (?:...)a capturing group, a non-capturing group
|either side

In Java every backslash is doubled, because the string literal consumes one before the regex engine sees it. \d in a pattern is "\\d" in source — and it is the single most common source of confusion when copying a pattern from elsewhere.

Text blocks do not change this; they are not raw strings.

Groups

class Demo {
    void run() {
        Pattern pattern = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
        Matcher matcher = pattern.matcher("due 2026-08-20 exactly");

        if (matcher.find()) {
            System.out.println(matcher.group());      // 2026-08-20 — group 0 is the whole match
            System.out.println(matcher.group(1));     // 2026
            System.out.println(matcher.group(3));     // 20
            System.out.println(matcher.start());      // 4 — where it matched
        }
    }
}

Numbered groups are fragile: insert a group at the front and every number shifts. Name them instead, which is both safer and self-documenting:

class Demo {
    void run() {
        Pattern pattern = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})");
        Matcher matcher = pattern.matcher("2026-08-20");

        if (matcher.matches()) {
            System.out.println(matcher.group("year") + "/" + matcher.group("month"));  // 2026/08
        }
    }
}

Finding every match

class Demo {
    void run() {
        Pattern pattern = Pattern.compile("\\b\\w+@\\w+\\.\\w+\\b");
        String text = "mail ana@example.com or bo@test.org today";

        // Iterating
        Matcher matcher = pattern.matcher(text);
        while (matcher.find()) {
            System.out.println(matcher.group());
        }

        // Or as a stream, since Java 9
        System.out.println(pattern.matcher(text).results()
                .map(MatchResult::group)
                .toList());                    // [ana@example.com, bo@test.org]
    }
}

Greedy, and the bug it causes

class Demo {
    void run() {
        String html = "<b>bold</b> and <i>italic</i>";

        Matcher greedy = Pattern.compile("<(.+)>").matcher(html);
        if (greedy.find()) {
            System.out.println(greedy.group());   // the ENTIRE string — .+ took everything
        }

        Matcher lazy = Pattern.compile("<(.+?)>").matcher(html);
        if (lazy.find()) {
            System.out.println(lazy.group());     // <b> — stops at the first >
        }
    }
}

Quantifiers are greedy by default: they take as much as possible and give back only if the match fails. Adding ? makes them lazy. When a pattern matches far more than you intended, this is nearly always why.

Catastrophic backtracking

Worth knowing by name, because it turns a validator into a denial-of-service vector. A pattern with nested quantifiers can take exponential time on input that almost matches:

class Demo {
    void run() {
        // (a+)+ against a long run of a's followed by something else:
        // the engine tries every way of splitting the a's before giving up.
        Pattern dangerous = Pattern.compile("(a+)+b");

        // Safe: no nested quantifier, one way to match
        Pattern safe = Pattern.compile("a+b");
        System.out.println(safe.matcher("aaab").matches());   // true
    }
}

The rule: avoid a quantifier applied to a group that already contains one. If a regex is validating user input, either keep it simple or bound the input length first.

Replacing with a computed value

Two replacement methods are worth knowing beyond the literal form. Backreferences let the replacement reuse captured groups, and Matcher.replaceAll can take a function:

class Demo {
    void run() {
        // $1, $2 refer to captured groups; ${name} to named ones
        System.out.println("2026-08-20".replaceAll(
                "(\\d{4})-(\\d{2})-(\\d{2})", "$3/$2/$1"));      // 20/08/2026

        // A function, when the replacement has to be computed — Java 9+
        Pattern pattern = Pattern.compile("\\d+");
        System.out.println(pattern.matcher("a1 b22 c333")
                .replaceAll(m -> String.valueOf(m.group().length())));   // a1 b2 c3
    }
}

One trap in the literal form: $ and \ are special in the replacement string too. If the replacement comes from user input or a variable, wrap it in Matcher.quoteReplacement, or a stray $1 in the data becomes a group reference. The same applies to patterns — Pattern.quote escapes a string you want matched literally.

Useful flags

class Demo {
    void run() {
        System.out.println(Pattern.compile("hello", Pattern.CASE_INSENSITIVE)
                .matcher("HELLO").matches());                         // true

        // MULTILINE: ^ and $ match at each line, not just the whole input
        System.out.println(Pattern.compile("^b", Pattern.MULTILINE)
                .matcher("a\nb").find());                             // true

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

When not to use a regex

Regex is the wrong tool more often than it is the right one:

  • Do not parse HTML, XML or JSON with it. They are not regular languages. Use a parser.
  • Do not validate email addresses with it. The correct pattern is thousands of characters. Check for an @ with something on both sides and send a confirmation email.
  • Prefer plain String methods when they fit — contains, startsWith and split on a literal are clearer and faster than a pattern.

When you do write one, name it and comment it. A static final Pattern called ORDER_ID with a line explaining the format is maintainable; the same expression inline is a puzzle for whoever reads it next.

Next

Database access is next — JDBC, and the things that matter regardless of which framework sits on top of it.