Java · lesson 16 of 66
Beginner
17- 1 Introduction to Java
- 2 Setting Up Java
- 3 Data Types in Java
- 4 Operators in Java
- 5 Variables and Operators in Java
- 6 Control Flow in Java
- 7 Arrays in Java
- 8 Functions (Methods) in Java
- 9 Strings in Java
- 10 Methods in Java
- 11 Java OOP — Complete Guide
- 12 Introduction to Object-Oriented Programming in Java
- 13 Encapsulation in Java
- 14 Inheritance in Java
- 15 Polymorphism in Java
- 17 Abstraction in Java
- 18 Error Types in Java
Intermediate
38- 16 Regular Expressions in Java
- 19 Interfaces in Java — A Deep Dive
- 20 Abstract Classes in Java
- 21 Method Overloading in Java
- 22 Composition vs Inheritance in Java
- 23 Generics in Java
- 27 Exception Handling in Java
- 28 Java Streams API
- 29 Collections Framework in Java
- 30 Optional in Java
- 31 File Handling in Java
- 32 Records in Java
- 33 Java 8 Features
- 34 Sealed Classes and Interfaces in Java
- 35 Multithreading in Java
- 37 JDBC — Database Access in Java
- 39 Maven — Java Build and Dependency Management
- 42 Gradle — Modern Java Build Tool
- 44 Unit Testing with JUnit 5 and Mockito
- 45 Logging in Java with SLF4J and Logback
- 49 Spring Boot — Introduction
- 50 Dependency Injection in Spring Boot
- 51 Java Project Ideas
- 52 Building REST APIs with Spring Boot
- 53 Validation in Spring Boot
- 54 Spring Data JPA — Database Access
- 55 Exception Handling in Spring Boot
- 56 Spring Security — Authentication and Authorization
- 57 JWT Authentication in Spring Boot
- 58 Dockerizing Spring Boot Applications
- 59 Java Enums: Beyond Simple Constants
- 60 Inner and Anonymous Classes in Java
- 61 Java Generics: Bounded Types, Wildcards, and Generic Methods
- 62 Java Date/Time API: LocalDate, LocalDateTime, and Duration
- 63 Java NIO.2: Paths, Files, and WatchService
- 64 Java Annotations: Custom Types and Meta-Information
- 65 Java Reflection: Dynamic Type Inspection and Method Invocation
- 66 Java Localization: ResourceBundle and Locale Support
Advanced
11Regular Expressions in Java
Learn Java regex by running it: Pattern and Matcher, find vs matches, greedy vs lazy, groups, named groups, lookarounds, flags and computed replacements.
By SkillByExample · Updated
Java regex lives in java.util.regex. A Pattern is a compiled, immutable, thread-safe regex;
a Matcher applies it to one input and holds the search state. Every example is a single file run
with the JDK 21 source launcher (java File.java).
find() versus matches()
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegexBasics {
public static void main(String[] args) {
Pattern digits = Pattern.compile("\\d+");
Matcher m = digits.matcher("Order #1042 contains 3 items");
while (m.find())
System.out.println("found " + m.group() + " at " + m.start() + "-" + m.end());
System.out.println(digits.matcher("12345").matches());
System.out.println(digits.matcher("123x5").matches());
System.out.println(digits.matcher("123x5").lookingAt());
System.out.println("hello world".replaceAll("\\s+", "_"));
System.out.println(java.util.Arrays.toString("a,b,,c,,".split(",")));
}
}
$ java RegexBasics.java
found 1042 at 7-11
found 3 at 21-22
true
false
true
hello_world
[a, b, , c]
find walks through the input one match at a time; matches demands the whole string fit, and
lookingAt only the start. Note split: the empty token between b and c survives, but trailing
empty strings are silently dropped — pass a negative limit, split(",", -1), to keep them.
Syntax at a glance
| Syntax | Meaning | Syntax | Meaning |
|---|---|---|---|
. | any char except newline | * + ? | 0+, 1+, 0-1 (greedy) |
\d \w \s | digit, word char, whitespace | {n,m} | between n and m |
\D \W \S | their negations | *? +? | lazy versions |
[abc] [^abc] [a-z] | set, negated set, range | ^ $ | start / end (of line in MULTILINE) |
(...) (?:...) | capturing / non-capturing | \b | word boundary |
In Java source every backslash is doubled: the regex \d+ is written "\\d+".
Greedy versus lazy
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class GreedyLazy {
public static void main(String[] args) {
String html = "<b>bold</b> and <i>italic</i>";
Matcher greedy = Pattern.compile("<.+>").matcher(html);
if (greedy.find()) System.out.println("greedy: " + greedy.group());
Matcher lazy = Pattern.compile("<.+?>").matcher(html);
while (lazy.find()) System.out.println("lazy: " + lazy.group());
}
}
$ java GreedyLazy.java
greedy: <b>bold</b> and <i>italic</i>
lazy: <b>
lazy: </b>
lazy: <i>
lazy: </i>
.+ grabs as much as it can and then backs off only far enough to find a final > — the last
one, so it swallows the whole string. .+? stops at the first > that lets the match succeed,
giving one tag per match.
Capturing groups
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class GroupsDemo {
public static void main(String[] args) {
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("whole: " + m.group(0) + ", groups: " + m.groupCount());
System.out.println("year " + m.group(1) + ", month " + m.group(2) + ", day " + m.group(3));
}
Matcher pm = Pattern.compile("\\$(\\d+\\.\\d{2})").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);
System.out.println("2024-07-15".replaceAll("(\\d{4})-(\\d{2})-(\\d{2})", "$3/$2/$1"));
}
}
$ java GroupsDemo.java
whole: 2024-07-15, groups: 3
year 2024, month 07, day 15
total: $17.48
15/07/2024
Group 0 is always the whole match; groups 1..n are numbered by their opening parenthesis. The
price pattern escapes $ because a bare $ means end-of-input, and captures only the digits. In a
replacement string, $1, $2, $3 refer back to the groups, which reorders the date.
Named groups
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class NamedGroups {
static final Pattern LOG = Pattern.compile(
"\\[(?<date>[\\d-]+) (?<time>[\\d:]+)\\] (?<level>\\w+) (?<logger>[\\w.]+): (?<message>.+)");
public static void main(String[] args) {
Matcher m = LOG.matcher("[2024-07-15 14:32:01] ERROR UserService: User 42 not found");
if (m.matches()) {
System.out.println("level = " + m.group("level"));
System.out.println("logger = " + m.group("logger"));
System.out.println("message = " + m.group("message"));
}
Pattern ssn = Pattern.compile("(?<first>\\d{3})-(?<mid>\\d{2})-(?<last>\\d{4})");
System.out.println(ssn.matcher("SSN: 123-45-6789").replaceAll("***-**-${last}"));
}
}
$ java NamedGroups.java
level = ERROR
logger = UserService
message = User 42 not found
SSN: ***-**-6789
(?<name>...) lets code ask for group("level") instead of counting parentheses, so adding a group
later does not silently shift the numbers. Replacements refer to a named group as ${name}.
Lookahead and lookbehind
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class LookaroundDemo {
static void show(String label, String regex, String input) {
Matcher m = Pattern.compile(regex).matcher(input);
StringBuilder sb = new StringBuilder();
while (m.find()) sb.append(m.group()).append(' ');
System.out.println(label + ": " + sb.toString().strip());
}
public static void main(String[] args) {
show("followed by px", "\\d+(?=px)", "font-size: 16px; margin: 8em; top: 4px");
show("not followed by px", "\\b\\d+(?!\\d|px)", "font-size: 16px; margin: 8em; top: 4px");
show("after $", "(?<=\\$)\\d+\\.\\d{2}", "Price: $29.99, Fee: 5.00, Discount: $5.00");
show("no minus before", "(?<!-)\\b\\d+", "temps: -3 12 -7 25");
}
}
$ java LookaroundDemo.java
followed by px: 16 4
not followed by px: 8
after $: 29.99 5.00
no minus before: 12 25
Lookarounds check context without consuming it, so px and $ are tested but never appear in the
match. (?=...)/(?!...) look ahead, (?<=...)/(?<!...) look behind. The \d inside the
second lookahead matters: without it \d+ would backtrack and match just the 1 of 16, because
1 is followed by 6, not by px.
Validation patterns and flags
import java.util.regex.Pattern;
public class CommonPatterns {
static final Pattern EMAIL = Pattern.compile("[\\w.%+-]+@[\\w.-]+\\.[a-zA-Z]{2,}");
static final Pattern ISO_DATE = Pattern.compile("\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])");
static final Pattern PASSWORD = Pattern.compile("(?=.*[A-Z])(?=.*\\d)(?=.*[!@#$%^&*]).{8,}");
public static void main(String[] args) {
System.out.println(EMAIL.matcher("alice@example.com").matches() + " " + EMAIL.matcher("not-an-email").matches());
System.out.println(ISO_DATE.matcher("2024-12-01").matches() + " " + ISO_DATE.matcher("2024-13-01").matches());
System.out.println(PASSWORD.matcher("Passw0rd!").matches() + " " + PASSWORD.matcher("password").matches());
Pattern hello = Pattern.compile("^hello", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
System.out.println(hello.matcher("Hi\nHELLO there").find());
System.out.println("Hi\nHELLO there".matches("(?is).*hello.*"));
}
}
$ java CommonPatterns.java
true false
true false
true false
true
true
With matches() the pattern is anchored at both ends, so no ^/$ is needed. The password rule
uses three lookaheads to require one of each character kind anywhere, then .{8,} for length.
MULTILINE lets ^ match after the newline and CASE_INSENSITIVE accepts HELLO; the inline
form (?is) turns on case-insensitivity and DOTALL (. matches \n) where no flags argument exists.
Computed replacements
import java.util.regex.Pattern;
public class ReplaceWithLogic {
public static void main(String[] args) {
String squared = Pattern.compile("\\d+")
.matcher("values: 3 and 5 and 12")
.replaceAll(mr -> String.valueOf(Integer.parseInt(mr.group()) * Integer.parseInt(mr.group())));
System.out.println(squared);
String log = "Charged card 4111-1111-1111-1234 for $99.00";
String redacted = Pattern.compile("\\b(?:\\d{4}[- ]?){3}(\\d{4})\\b")
.matcher(log)
.replaceAll(mr -> "****-****-****-" + mr.group(1));
System.out.println(redacted);
System.out.println("cost: 5".replaceAll("\\d", "\\$1"));
}
}
$ java ReplaceWithLogic.java
values: 9 and 25 and 144
Charged card ****-****-****-1234 for $99.00
cost: $1
replaceAll(Function<MatchResult,String>) (Java 9+) computes each replacement from the match. In
a plain replacement string, $ and \ are special, so a literal dollar sign must be written
\\$ — which is why the last line prints the literal text $1 rather than a group reference.
Wrap replacement text that comes from user input with Matcher.quoteReplacement.
Practice
- Extract every hashtag (
#followed by word characters) from a tweet, without the#.
Solution
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Hashtags {
public static void main(String[] args) {
Matcher m = Pattern.compile("#(\\w+)").matcher("Loving #Java21 and #regex! #100DaysOfCode");
List<String> tags = new ArrayList<>();
while (m.find()) tags.add(m.group(1));
System.out.println(tags);
}
}
$ java Hashtags.java
[Java21, regex, 100DaysOfCode]
- Convert
camelCaseNamestosnake_casewith onereplaceAlland a lookahead.
Solution
public class SnakeCase {
public static void main(String[] args) {
for (String s : new String[] {"camelCaseNames", "userId", "parseHTTPResponse"})
System.out.println(s + " -> " + s.replaceAll("(?<=[a-z0-9])(?=[A-Z])", "_").toLowerCase());
}
}
$ java SnakeCase.java
camelCaseNames -> camel_case_names
userId -> user_id
parseHTTPResponse -> parse_httpresponse
The pattern matches the empty position between a lowercase letter and an uppercase one, so the
replacement inserts _ without consuming any characters. HTTPResponse stays one word because
there is no lowercase letter between its capitals.
- Parse
key=valuepairs separated by;(with optional spaces) into aLinkedHashMapusing named groups.
Solution
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class KeyValues {
public static void main(String[] args) {
Pattern kv = Pattern.compile("(?<key>\\w+)\\s*=\\s*(?<value>[^;]+?)\\s*(?:;|$)");
Matcher m = kv.matcher("host = db.local; port=5432 ;user= admin");
Map<String, String> map = new LinkedHashMap<>();
while (m.find()) map.put(m.group("key"), m.group("value"));
System.out.println(map);
}
}
$ java KeyValues.java
{host=db.local, port=5432, user=admin}