Skip to main content
Java intermediate Lesson 16 of 58

Regular Expressions in Java

Learn Java regex with Pattern and Matcher — character classes, groups, named groups, lookahead, lookbehind, and real-world patterns.

Pattern and Matcher Basics

Java regex lives in java.util.regex. The two core classes are:

  • Pattern — compiled representation of a regex (thread-safe, reuse it)
  • Matcher — engine that applies a Pattern to a specific input string (not thread-safe)
import java.util.regex.*;

public class RegexBasics {
    public static void main(String[] args) {

        // Compile once, reuse many times
        Pattern digits = Pattern.compile("\\d+");

        // find() — search anywhere in the string
        Matcher m1 = digits.matcher("Order #1042 contains 3 items");
        while (m1.find()) {
            System.out.println("Found: " + m1.group() + " at " + m1.start());
        }
        // Found: 1042 at 7
        // Found: 3 at 26

        // matches() — entire input must match
        System.out.println(digits.matcher("12345").matches()); // true
        System.out.println(digits.matcher("123x5").matches()); // false

        // Convenience methods on String
        System.out.println("hello123".matches("[a-z]+\\d+")); // true
        System.out.println("hello world".replaceAll("\\s+", "_")); // hello_world
        System.out.println("a,b,,c".split(",").length);  // 4 (note empty token)
    }
}

Regex Syntax Reference

Character Classes

.        any character except newline (unless DOTALL mode)
\d       digit: [0-9]
\D       non-digit: [^0-9]
\w       word char: [a-zA-Z0-9_]
\W       non-word char
\s       whitespace: [ \t\r\n\f]
\S       non-whitespace
[abc]    a, b, or c
[^abc]   anything except a, b, c
[a-z]    range: lowercase letter
[a-zA-Z] letter (upper or lower)

Quantifiers

*        0 or more (greedy)
+        1 or more (greedy)
?        0 or 1 (greedy)
{n}      exactly n
{n,}     n or more
{n,m}    between n and m (inclusive)
*?       0 or more (lazy — minimal match)
+?       1 or more (lazy)
??       0 or 1 (lazy)

Anchors and Boundaries

^        start of string (or line in MULTILINE mode)
$        end of string (or line in MULTILINE mode)
\b       word boundary
\B       non-word boundary
\A       start of entire input
\Z       end of entire input (before optional trailing newline)

Greedy vs Lazy

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

// Greedy — matches as much as possible
Pattern greedy = Pattern.compile("<.+>");
Matcher mg = greedy.matcher(html);
if (mg.find()) System.out.println(mg.group());
// <b>bold</b> and <i>italic</i>  — too much!

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

Capturing Groups

Parentheses (...) create a capturing group. Access matches with group(n):

import java.util.regex.*;

public class GroupsDemo {
    public static void main(String[] args) {

        // Date parsing: yyyy-MM-dd
        Pattern date = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
        Matcher m = date.matcher("Event on 2024-07-15 starts at 09:00");

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

        // Iterating all matches
        Pattern price = Pattern.compile("\\$(\\d+\\.\\d{2})");
        Matcher pm = price.matcher("Cart: $12.99 + $3.50 + $0.99");
        double total = 0;
        while (pm.find()) {
            total += Double.parseDouble(pm.group(1));
        }
        System.out.printf("Total: $%.2f%n", total); // Total: $17.48
    }
}

Named Groups

Named groups (?<name>...) make patterns more readable and robust to reordering:

import java.util.regex.*;

public class NamedGroups {
    public static void main(String[] args) {

        // Log line: [2024-07-15 14:32:01] ERROR UserService: User 42 not found
        Pattern log = Pattern.compile(
            "\\[(?<date>\\d{4}-\\d{2}-\\d{2}) (?<time>\\d{2}:\\d{2}:\\d{2})\\]" +
            " (?<level>\\w+) (?<logger>[\\w.]+): (?<message>.+)"
        );

        String line = "[2024-07-15 14:32:01] ERROR UserService: User 42 not found";
        Matcher m = log.matcher(line);

        if (m.matches()) {
            System.out.println("Date:    " + m.group("date"));    // 2024-07-15
            System.out.println("Time:    " + m.group("time"));    // 14:32:01
            System.out.println("Level:   " + m.group("level"));   // ERROR
            System.out.println("Logger:  " + m.group("logger"));  // UserService
            System.out.println("Message: " + m.group("message")); // User 42 not found
        }

        // Named group in replacement
        Pattern ssn = Pattern.compile("(?<first>\\d{3})-(?<mid>\\d{2})-(?<last>\\d{4})");
        String masked = ssn.matcher("SSN: 123-45-6789")
            .replaceAll("***-**-${last}");
        System.out.println(masked); // SSN: ***-**-6789
    }
}

Lookahead and Lookbehind

These assert context without consuming characters:

import java.util.regex.*;
import java.util.*;

public class LookaroundDemo {

    public static void main(String[] args) {

        // Positive lookahead (?=...) — match X only if followed by Y
        // Find numbers followed by "px"
        Pattern pxLook = Pattern.compile("\\d+(?=px)");
        Matcher m1 = pxLook.matcher("font-size: 16px; margin: 8px; opacity: 0.5");
        List<String> pxValues = new ArrayList<>();
        while (m1.find()) pxValues.add(m1.group());
        System.out.println(pxValues); // [16, 8]

        // Negative lookahead (?!...) — match X only if NOT followed by Y
        Pattern notPx = Pattern.compile("\\d+(?!\\.\\d|px)\\b");
        // matches integers not followed by .digit or px

        // Positive lookbehind (?<=...) — match X only if preceded by Y
        Pattern afterDollar = Pattern.compile("(?<=\\$)\\d+\\.\\d{2}");
        Matcher m2 = afterDollar.matcher("Price: $29.99, Discount: $5.00");
        while (m2.find()) System.out.println(m2.group()); // 29.99  5.00

        // Negative lookbehind (?<!...) — match X only if NOT preceded by Y
        Pattern notAfterMinus = Pattern.compile("(?<!-)\\b\\d+\\b");
        // matches numbers not preceded by a minus sign
    }
}

Common Real-World Patterns

import java.util.regex.*;

public class CommonPatterns {

    // Email — simplified but practical
    private static final Pattern EMAIL =
        Pattern.compile("^[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}$");

    // URL
    private static final Pattern URL =
        Pattern.compile("https?://[\\w\\-]+(\\.[\\w\\-]+)+(/[\\w\\-._~:/?#\\[\\]@!$&'()*+,;=%]*)?");

    // IPv4 address
    private static final Pattern IPV4 =
        Pattern.compile("^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$");

    // ISO date
    private static final Pattern ISO_DATE =
        Pattern.compile("^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$");

    // Password: 8+ chars, at least one uppercase, one digit, one special char
    private static final Pattern PASSWORD =
        Pattern.compile("^(?=.*[A-Z])(?=.*\\d)(?=.*[!@#$%^&*]).{8,}$");

    // Credit card (basic — 13-19 digits, optional spaces/dashes)
    private static final Pattern CREDIT_CARD =
        Pattern.compile("^[\\d \\-]{13,19}$");

    public static boolean isValidEmail(String s)    { return EMAIL.matcher(s).matches(); }
    public static boolean isValidIpv4(String s)     { return IPV4.matcher(s).matches(); }
    public static boolean isValidIsoDate(String s)  { return ISO_DATE.matcher(s).matches(); }
    public static boolean isStrongPassword(String s){ return PASSWORD.matcher(s).matches(); }

    public static void main(String[] args) {
        System.out.println(isValidEmail("[email protected]")); // true
        System.out.println(isValidEmail("not-an-email"));      // false
        System.out.println(isValidIpv4("192.168.1.1"));        // true
        System.out.println(isValidIsoDate("2024-13-01"));      // false (month 13)
        System.out.println(isStrongPassword("Passw0rd!"));     // true
        System.out.println(isStrongPassword("password"));      // false
    }
}

replaceAll with Logic (Java 9+)

Matcher.replaceAll(Function<MatchResult, String>) lets you compute replacements dynamically:

import java.util.regex.*;

// Replace each number with its square
String input = "values: 3 and 5 and 12";
String result = Pattern.compile("\\d+")
    .matcher(input)
    .replaceAll(mr -> {
        int n = Integer.parseInt(mr.group());
        return String.valueOf(n * n);
    });
System.out.println(result); // values: 9 and 25 and 144

// Redact credit card numbers in logs
String log = "Charged card 4111-1111-1111-1234 for $99.00";
String redacted = Pattern.compile("\\b\\d{4}[- ]?\\d{4}[- ]?\\d{4}[- ]?\\d{4}\\b")
    .matcher(log)
    .replaceAll(mr -> "****-****-****-" + mr.group().replaceAll("[^\\d]", "").substring(12));
System.out.println(redacted); // Charged card ****-****-****-1234 for $99.00

Flags

// Case-insensitive
Pattern p1 = Pattern.compile("hello", Pattern.CASE_INSENSITIVE);
// or inline: Pattern.compile("(?i)hello")

// Multiline — ^ and $ match start/end of each line
Pattern p2 = Pattern.compile("^\\w+", Pattern.MULTILINE);

// Dotall — . matches newlines too
Pattern p3 = Pattern.compile("<div>.+?</div>", Pattern.DOTALL);

// Combine flags with |
Pattern p4 = Pattern.compile("^hello", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);

Frequently Asked Questions

What is the difference between matches() and find()?
String.matches() (and Matcher.matches()) requires the entire string to match the pattern. Matcher.find() scans for a match anywhere within the string. For example, pattern '\d+' with find() matches '42' in 'price: 42', but matches() requires the entire input to be digits.
Why do Java regex patterns need double backslashes?
Java string literals use backslash as an escape character, so '\' in a string produces a single backslash. Regex also uses backslash for special sequences (\d, \w, \s). To get a literal backslash in the regex, you write '\\' in a Java string. Using raw text blocks (Java 15+) does not help here because \d still needs '\\d' in a text block.
How do I make a regex case-insensitive?
Either add the flag (?i) inside the pattern: '(?i)hello', or pass Pattern.CASE_INSENSITIVE to Pattern.compile(): Pattern.compile('hello', Pattern.CASE_INSENSITIVE).
What is a capturing group vs a non-capturing group?
A capturing group (parentheses: (abc)) captures the matched text and makes it accessible via Matcher.group(n). A non-capturing group (?:abc) groups for alternation or quantifiers without capturing — it has no group number and is slightly faster.