Learn Java

Lesson 52Phase 5: Errors, files and the standard library

Regular expressions: patterns in text

Describing the shape of text with a pattern, then searching, validating, splitting and replacing with Pattern and Matcher.

By Updated 50 min

  • regex
  • Pattern
  • Matcher
  • character class
  • quantifier
  • group

A sign-up form wants a username of 3 to 15 small letters, digits or underscores. A delivery form wants a postcode shaped like SW1A 1AA, and a phone field wants a zero, four digits, a space and six more. A shop wants every price pulled out of a paragraph of text. Each of these jobs is about the shape of some text: does it have the right shape, and where in a longer text does that shape appear? A regular expression is a short pattern that describes a shape like that, and Java can use one to check text, search it, split it and change it.

By the end of this lesson
  • Read and write patterns made of character classes, shorthands such as \d, the dot, quantifiers, anchors, alternation and groups.
  • Put a regex into a Java string, writing every backslash twice.
  • Check a whole string with matches, and search inside a text with a Pattern, a Matcher and a find() loop.
  • Pull pieces out of each match with numbered and named groups.
  • Split and replace text with a regex, and avoid the traps of split(".") and a greedy .*.
  • Compile a pattern once, use flags and Pattern.quote, and read a PatternSyntaxException.
From before
You know the String methods from Strings, StringBuilder, StringBuffer: length, charAt, indexOf, substring and replace. You have also used split(" ") once, to cut a sentence into words in HashMap and HashSet. The backslash escapes such as \n come from variables and types, the import lines from packages and imports, and catching an exception from exception handling. One exercise adds up prices with BigDecimal.

Checking the shape of text#

Start with the username rule: 3 to 15 characters, each one a small letter, a digit or an underscore. With the String methods you already know, the check is a length test and a loop over the characters.

UsernameCheck.java
1public class Main {
2 public static void main(String[] args) {
3 System.out.println(isValid("sana_99"));
4 System.out.println(isValid("Sana!"));
5 System.out.println(isValid("al"));
6 }
7
8 static boolean isValid(String name) {
9 if (name.length() < 3 || name.length() > 15) {
10 return false;
11 }
12 for (int i = 0; i < name.length(); i++) {
13 char c = name.charAt(i);
14 boolean allowed = (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_';
15 if (!allowed) {
16 return false;
17 }
18 }
19 return true;
20 }
21}
Prints
›true
›false
›false

It prints true, false and false. “Sana!” breaks the rule twice, with a capital S and an exclamation mark, and “al” is too short. The method works, but it takes a dozen lines to say one short rule, and the rule is hard to see among them. A postcode would need a longer method still, with different checks for different parts of the text.

Here is the same check written with a regular expression.

UsernameRegex.java
1public class Main {
2 public static void main(String[] args) {
3 String rule = "[a-z0-9_]{3,15}";
4
5 System.out.println("sana_99".matches(rule));
6 System.out.println("Sana!".matches(rule));
7 System.out.println("al".matches(rule));
8 }
9}
Prints
›true
›false
›false

It prints the same three answers, and the whole rule is now the string [a-z0-9_]{3,15}. A regular expression, or regex for short, is a piece of text that describes a pattern of characters. This one has two parts. [a-z0-9_]means “one character that is a small letter, a digit or an underscore”, and {3,15}means “3 to 15 of those in a row”. The String method matches returns true when the whole string fits the pattern.

The pattern language has only a handful of pieces. The next sections add them one at a time, each with a program to run, and by the end of the page you can read every character of patterns like this one.

Coming from Python or JavaScript?
Python's remodule and JavaScript's regexes use almost the same pattern language, so most of what you know carries over. Two things differ. Java has no regex literal like JavaScript's /\d+/, so a pattern always lives in an ordinary string, and that matters for backslashes, as you will see. And Java's matcheschecks the whole string, like Python's re.fullmatch. Python's re.matchonly checks the start, and JavaScript's test searches anywhere in the text.

Character classes, shorthands and the dot#

Most characters in a pattern stand for themselves. The pattern catmatches the text “cat” and nothing else: a c, then an a, then a t. Patterns get useful when one position may hold one of several characters.

A character class is a list of characters inside square brackets. It matches exactly one character, which must be one of those in the list. [bcr]at matches bat, cat and rat. Inside the brackets you can also write:

  • A range, such as [a-z]: any one small letter from a to z. [A-Z] and [0-9] work the same way, and ranges can sit side by side, so [a-zA-Z0-9] is any one letter or digit.
  • A caret straight after the opening bracket, as in [^0-9]. This is a negated class: it matches any one character that is missing from the list. Here, anything except a digit.
Classes.java
1public class Main {
2 public static void main(String[] args) {
3 System.out.println("cat".matches("cat"));
4 System.out.println("cat".matches("[bcr]at"));
5 System.out.println("hat".matches("[bcr]at"));
6 System.out.println("rat".matches("[a-z]at"));
7 System.out.println("Rat".matches("[a-z]at"));
8 System.out.println("7".matches("[^0-9]"));
9 System.out.println("x".matches("[^0-9]"));
10 }
11}
Prints
›true
›true
›false
›true
›false
›false
›true

It prints true, true, false, true, false, false, true. “hat” fails because h is not in [bcr]. “Rat” fails because [a-z]holds small letters only, and to Java a capital R is a different character. “7” fails [^0-9]because it is a digit, and “x” passes because it is not.

One class, one character
However long the list inside the brackets, a class matches a single character. [a-z] is one character with 26 choices, and [bcr]at always matches exactly three characters.
Quick check
Which of these strings fits [A-Z][0-9] when checked with matches?

Shorthand classes and the dot

A few classes come up so often that they have short names, each written as a backslash and a letter.

ShorthandMatches oneSame as
\ddigit[0-9]
\wword character: a letter, a digit or an underscore[a-zA-Z_0-9]
\swhitespace character: a space, a tab, a line break or one of a few rarer ones[ \t\n\x0B\f\r]
\Dcharacter that is not a digit[^0-9]
\Wcharacter that is not a word character[^\w]
\Scharacter that is not whitespace[^\s]
.character of any kind except a line breakno class needed
A capital letter turns a shorthand around: the capital version matches every character the small one does not.

The dot is the widest of them. c.tmatches cat, cot, c7t and even “c t” with a space in the middle.

The double backslash

Now try \d in a program. The obvious line does not even compile:

Terminal
read along
1$ javac Main.java
2Main.java:3: error: illegal escape character
3 System.out.println("123".matches("\d+"));
4 ^
51 error

Inside a Java string, a backslash starts an escape sequence such as \n for a line break. \dis not one of Java's escape sequences, so the compiler refuses it. To put one real backslash into a string, you write two. The string "\\d" holds two characters, a backslash and a d, and that pair is what the regex engine reads as \d.

Backslashes.java
1public class Main {
2 public static void main(String[] args) {
3 String threeDigits = "\\d\\d\\d";
4 System.out.println(threeDigits);
5 System.out.println("123".matches(threeDigits));
6 System.out.println("12a".matches(threeDigits));
7
8 System.out.println("cat".matches("c.t"));
9 System.out.println("c t".matches("c.t"));
10 System.out.println("c.t".matches("c\\.t"));
11 System.out.println("cat".matches("c\\.t"));
12 }
13}
Prints
›\d\d\d
›true
›false
›true
›true
›true
›false

The first line prints \d\d\d: the string holds three backslashes, though the source code shows six. Then true, and falsefor “12a”, which ends in a letter. The last four lines are about the dot. c.taccepts both “cat” and “c t”. To match a real dot, put a backslash in front of it: the regex c\.t, written "c\\.t"in Java, matches “c.t” and rejects “cat”.

The regex
In your Java code
\d
"\\d"
\d{3}-\d{4}
"\\d{3}-\\d{4}"
\. (a real dot)
"\\."
\\ (a real backslash)
"\\\\"

The characters with a job in a pattern are . * + ? ^ $ | ( ) [ ] { } and the backslash itself. To match one of them as a plain character, put a backslash in front: \$ matches a dollar sign and \( an opening bracket. Java accepts a backslash in front of any punctuation mark, needed or not, so when in doubt you can escape it. A backslash in front of a letter is different: those pairs are reserved for shorthands such as \d, and an unknown one such as \y is an error.

Key idea

A regex goes into Java as an ordinary string, so every backslash in the pattern is written twice. The regex \d+ is the Java string "\\d+".

Quick check
You want the regex \d{3}, three digits, in a Java program. Which line is right?

Saying how many: quantifiers#

So far every piece of a pattern has matched exactly one character. A quantifier goes straight after a piece and says how many times that piece may repeat.

QuantifierHow manyExampleMatches
*zero or moreab*cac, abc, abbbbc
+one or moreab+cabc, abbbbc, but not ac
?zero or one, so the piece is optionalcolou?rcolor, colour
{n}exactly n\d{4}2026
{n,}n or more\d{2,}42, 1234567
{n,m}from n to m[a-z]{3,15}sana, banana
Quantifiers.java
1public class Main {
2 public static void main(String[] args) {
3 System.out.println("ac".matches("ab*c"));
4 System.out.println("ac".matches("ab+c"));
5 System.out.println("abbbc".matches("ab+c"));
6 System.out.println("color".matches("colou?r"));
7 System.out.println("colour".matches("colou?r"));
8 System.out.println("2026".matches("\\d{4}"));
9 System.out.println("202".matches("\\d{4}"));
10 System.out.println("x".matches("[a-z]{3,15}"));
11 }
12}
Prints
›true
›false
›true
›true
›true
›true
›false
›false

It prints true, false, true, true, true, true, false, false. ab*caccepts “ac” because zero b's is allowed, while ab+c needs at least one. “202” is one digit short for \d{4}, and a lone “x” is too short for [a-z]{3,15}.

A quantifier repeats only the piece just before it
ab+means an a followed by one or more b's, so it matches “abbb” and never “abab”. To repeat several characters together, wrap them in parentheses: (ab)+matches “ababab”. Parentheses have a second job too, which the section on groups explains.

Reading a real pattern

You now have enough pieces for the phone and postcode checks from the start of the page. A phone number shaped like 07700 900123 is a zero, four more digits, a space and six digits: 0\d{4} \d{6}. UK postcodes come in more shapes (SW1A 1AA, M1 1AE, B33 8TH), and a common pattern for them is [A-Z]{1,2}\d[A-Z\d]? \d[A-Z]{2}. Read it from left to right:

  1. 1[A-Z]{1,2}: one or two capital letters (SW).
  2. 2\d: a digit (1).
  3. 3[A-Z\d]?: maybe one more capital letter or digit (A). A shorthand can go inside square brackets.
  4. 4A space.
  5. 5\d: a digit (1).
  6. 6[A-Z]{2}: two capital letters (AA).
ShapeCheck.java
1public class Main {
2 public static void main(String[] args) {
3 String phone = "0\\d{4} \\d{6}";
4 String postcode = "[A-Z]{1,2}\\d[A-Z\\d]? \\d[A-Z]{2}";
5
6 System.out.println("07700 900123".matches(phone));
7 System.out.println("7700 900123".matches(phone));
8 System.out.println("SW1A 1AA".matches(postcode));
9 System.out.println("M1 1AE".matches(postcode));
10 System.out.println("SW1A1AA".matches(postcode));
11 }
12}
Prints
›true
›false
›true
›true
›false

It prints true, false, true, true, false. The second phone number has lost its leading zero. “M1 1AE” fits because the optional piece can be absent, and “SW1A1AA” fails because the pattern asks for a space.

A regex checks the shape only
The postcode pattern accepts any text of the right shape, whether or not a building has that postcode, and the phone pattern cannot tell whether a number rings. Checking that a value really exists needs other data. Real UK postcodes also follow a few more rules than this pattern knows.
ExerciseProductCodes.java
A shop's product codes look like ABC-1234: three capital letters, a hyphen and four digits. Write the pattern so that the program prints true, false, false, true.

Finding matches inside text#

matchesanswers one question about a whole string. The shop's other job is different. A menu says “Tea is 3.50, cake is 4.25 and a pot for two is 6.00.”, and you want every price in it. The prices sit somewhere inside the text, and there could be any number of them. For searching, Java has two classes in the package java.util.regex:

  • Pattern holds a regex that Java has compiled: checked for mistakes and turned into a form it can run quickly. You make one with Pattern.compile(regex).
  • Matcher is a pattern at work on one particular text. You get one from the pattern with pattern.matcher(text).

A matcher's find() method searches for the next stretch of the text that fits the pattern, called a match. It returns true when it finds one, and then three methods describe it: group() returns the matched text, start() the index of its first character, and end() the index just after its last character, which is how substring counts too.

Prices.java
1import java.util.regex.Matcher;
2import java.util.regex.Pattern;
3
4public class Main {
5 public static void main(String[] args) {
6 String menu = "Tea is 3.50, cake is 4.25 and a pot for two is 6.00.";
7 Pattern price = Pattern.compile("\\d+\\.\\d{2}");
8 Matcher m = price.matcher(menu);
9
10 while (m.find()) {
11 System.out.println(m.group() + " at " + m.start() + " to " + m.end());
12 }
13 }
14}
Prints
›3.50 at 7 to 11
›4.25 at 21 to 25
›6.00 at 47 to 51

Step through it and read the narration for each find(): it names the text it found and where. The program prints 3.50 at 7 to 11, 4.25 at 21 to 25 and 6.00 at 47 to 51. Each find() carries on from where the last match ended, and once no price is left it returns false and the loop stops. The full stop after 6.00 stays out of the last match, because after \. the pattern asks for exactly two digits.

Key idea

matches asks whether the whole text fits the pattern. find() looks for the next part of the text that fits, and a loop of find() calls visits every match in turn.

Ask find() before you ask group()
group(), start() and end() describe the last successful match. Call them before any find(), or after a find() that returned false, and Java throws an IllegalStateException with the message No match found.
ExerciseReceipt.java
A receipt lists four prices. Finish the program so that it adds every price to total and prints Total: 14.60. new BigDecimal(m.group()) turns a matched price such as “4.50” into an exact amount.

matches or find?

Predict the outputLettersFirst.java
1import java.util.regex.Matcher;
2import java.util.regex.Pattern;
3
4public class Main {
5 public static void main(String[] args) {
6 System.out.println("abc123".matches("[a-z]+"));
7
8 Matcher m = Pattern.compile("[a-z]+").matcher("abc123");
9 System.out.println(m.find());
10 System.out.println(m.group());
11 }
12}

[a-z]+ means one or more small letters. What do the three lines print?

It prints false, true and abc. matchesneeds the pattern to cover all of “abc123”, and [a-z]+ has no way to cover the digits. find()only needs some part of the text to fit, and the first part that does is “abc”.

matches()
find()
The whole text must fit the pattern.
Any part of the text may fit.
On a String, text.matches(regex). On a matcher, m.matches().
On a matcher only: m.find().
Calling it again gives the same answer.
Each call moves on to the next match.
Used to check input: a username, a postcode, a date typed into a form.
Used to search: every price, every date, every word in a text.

Anchors: ^ and $

Sometimes it matters where in the text a match sits. ^ matches at the very start of the text, and $ at the very end. They are called anchors, and they match a position between characters, never a character itself. So ^\d+ finds digits only when they open the text.

Anchors.java
1import java.util.regex.Pattern;
2
3public class Main {
4 public static void main(String[] args) {
5 Pattern atStart = Pattern.compile("^\\d+");
6 Pattern atEnd = Pattern.compile("\\d+$");
7 String[] lines = {"42 apples", "apples 42", "42"};
8
9 for (String line : lines) {
10 boolean starts = atStart.matcher(line).find();
11 boolean ends = atEnd.matcher(line).find();
12 System.out.println(line + ": starts " + starts + ", ends " + ends);
13 }
14 }
15}
Prints
›42 apples: starts true, ends false
›apples 42: starts false, ends true
›42: starts true, ends true

It prints 42 apples: starts true, ends false, then apples 42: starts false, ends true, then 42: starts true, ends true. With matches, anchors add nothing, because matches already needs the whole text to fit. ^\d+$ with find() asks the same question as \d+ with matches.

One symbol, two jobs
Outside square brackets, a caret means “the start of the text”. Straight after an opening bracket it means “none of these”. ^[^0-9] finds a first character that is not a digit.

Try your own patterns

The tester below runs your pattern over your text with Java's own matching rules, the same ones the Run button uses. It marks every match a find() loop would report, lists each one with its start and end, and shows the pattern as you would write it in Java, every backslash doubled. Start from the examples, then change one character at a time: take the \. out of the price pattern, change {2} to {1,2}, or put a ^ at the front.

Regex tester3 matches
Examples

In Java: Pattern.compile("\\d+\\.\\d{2}")

52 of 300 characters. Patterns can be up to 100.

Every match a find() loop reports

Tea is 3.50, cake is 4.25 and a pot for two is 6.00.
  1. 1
    "3.50" start 7, end 11
  2. 2
    "4.25" start 21, end 25
  3. 3
    "6.00" start 47, end 51
matches() on the whole text
false
groupCount()
0

Alternation and groups#

Either one or the other

A vertical bar means “or”. This is called alternation: cat|dog matches cat or dog. The bar splits the whole pattern into two halves, so I like cats|dogs means “I like cats” or “dogs”. To limit the choice to part of the pattern, put that part in parentheses.

Alternation.java
1public class Main {
2 public static void main(String[] args) {
3 System.out.println("dog".matches("cat|dog"));
4 System.out.println("I like dogs".matches("I like cats|dogs"));
5 System.out.println("I like dogs".matches("I like (cats|dogs)"));
6 System.out.println("grey".matches("gr(a|e)y"));
7 }
8}
Prints
›true
›false
›true
›true

It prints true, false, true, true. The second line is the trap: the bar took everything on its left as one choice, so that pattern accepts only “I like cats” or a bare “dogs”. With brackets, (cats|dogs) offers a choice for the last word only, and gr(a|e)y accepts both spellings of grey.

Capturing groups

Parentheses make a group, and a group has a second job: it captures, which means the matcher remembers the text that part of the pattern matched. Groups are numbered by counting opening brackets from the left, starting at 1, and m.group(1) returns what group 1 captured. Group 0 is the whole match, so m.group(0) is the same as m.group(). m.groupCount() says how many groups the pattern has.

DateParts.java
1import java.util.regex.Matcher;
2import java.util.regex.Pattern;
3
4public class Main {
5 public static void main(String[] args) {
6 Pattern date = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
7 Matcher m = date.matcher("Ordered 2026-10-18, delivered 2026-11-02.");
8
9 while (m.find()) {
10 String year = m.group(1);
11 String month = m.group(2);
12 String day = m.group(3);
13 System.out.println(m.group() + " is day " + day + ", month " + month + ", year " + year);
14 }
15 System.out.println("Groups in the pattern: " + m.groupCount());
16 }
17}
Prints
›2026-10-18 is day 18, month 10, year 2026
›2026-11-02 is day 02, month 11, year 2026
›Groups in the pattern: 3

It prints 2026-10-18 is day 18, month 10, year 2026, then 2026-11-02 is day 02, month 11, year 2026, then Groups in the pattern: 3. Watch year, month and day fill up as you step: the three groups take each date apart, and the program can use the parts in any order it likes.

Quick check
The pattern ((\d{2}):(\d{2})) finds a match in “at 09:45”. What does m.group(2) return?

Named groups

Numbers are easy to mix up once a pattern has several groups. You can name a group instead, by writing (?<name>...), and read it back with m.group("name").

NamedGroups.java
1import java.util.regex.Matcher;
2import java.util.regex.Pattern;
3
4public class Main {
5 public static void main(String[] args) {
6 Pattern date = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})");
7 Matcher m = date.matcher("2026-10-18");
8
9 if (m.matches()) {
10 System.out.println("Year: " + m.group("year"));
11 System.out.println("Month: " + m.group("month"));
12 System.out.println("Day: " + m.group("day"));
13 System.out.println("Group 1 is still " + m.group(1));
14 }
15 }
16}
Prints
›Year: 2026
›Month: 10
›Day: 18
›Group 1 is still 2026

It prints Year: 2026, Month: 10, Day: 18 and Group 1 is still 2026: a named group keeps its number too. This program calls the matcher's own matches(), which checks the whole text like the String method does, and once it returns true the groups can be read.

Group names are letters and digits only
A group name must start with a letter, and may contain only the letters A to Z and a to z and the digits 0 to 9. An underscore is not allowed, so (?<first_name>\w+) throws a PatternSyntaxException. Write firstName instead.
Put the lines in orderScores.java
Put the lines in order so that the program prints Ada scored 91, Grace scored 88 and Alan scored 79, one per line.

Tap a line, then tap where it should go. Or use the arrows.

Splitting text#

You have used split to cut a sentence at its spaces. Its argument was a regex all along, which makes it far more flexible than it looked. To see the pieces, these programs print each array with Arrays.toString, from java.util.Arrays, which lists an array's items between square brackets.

SplitWords.java
1import java.util.Arrays;
2
3public class Main {
4 public static void main(String[] args) {
5 String messy = "one two three";
6 System.out.println(Arrays.toString(messy.split(" ")));
7 System.out.println(Arrays.toString(messy.split("\\s+")));
8
9 String fruit = "apples, pears ,plums; figs";
10 System.out.println(Arrays.toString(fruit.split("\\s*[,;]\\s*")));
11 }
12}
Prints
›[one, , two, , , three]
›[one, two, three]
›[apples, pears, plums, figs]

Splitting at a single space leaves an empty string wherever two spaces sat side by side: [one, , two, , , three]. Splitting at \s+, one or more whitespace characters, treats each run of spaces as one cut and gives [one, two, three]. The last line cuts at a comma or a semicolon together with any spaces around it, \s*[,;]\s*, and prints [apples, pears, plums, figs].

Three traps

Because the argument is a regex, a character with a job in patterns does that job, whatever the text looks like. Decide what this prints before you run it.

Predict the outputVersion.java
1public class Main {
2 public static void main(String[] args) {
3 String version = "21.0.2";
4 String[] parts = version.split(".");
5 System.out.println(parts.length);
6 }
7}

A program splits a version number at its dots. What does it print?

It prints 0, the length of an empty array. The dot means any character, so every character of “21.0.2” is a cut, and every piece between two cuts is an empty string. split then drops the empty strings at the end of the result, and here that is all of them.

SplitTraps.java
1import java.util.Arrays;
2
3public class Main {
4 public static void main(String[] args) {
5 System.out.println(Arrays.toString("21.0.2".split(".")));
6 System.out.println(Arrays.toString("21.0.2".split("\\.")));
7
8 System.out.println(Arrays.toString("red|green|blue".split("|")));
9 System.out.println(Arrays.toString("red|green|blue".split("\\|")));
10
11 System.out.println(Arrays.toString("Ada,91,,".split(",")));
12 System.out.println(Arrays.toString("Ada,91,,".split(",", -1)));
13 }
14}
Prints
›[]
›[21, 0, 2]
›[r, e, d, |, g, r, e, e, n, |, b, l, u, e]
›[red, green, blue]
›[Ada, 91]
›[Ada, 91, , ]

Read the output in pairs, each trap followed by its fix:

  • split(".") gives []. Escape the dot, split("\\."), to get [21, 0, 2]. split("[.]") works too, because inside square brackets a dot is a plain dot.
  • split("|")cuts between every pair of characters. The bar means “or”, here with an empty pattern on each side, and an empty pattern fits at every position in the text, so every position becomes a cut. split("\\|") cuts at the real bars.
  • The spreadsheet row Ada,91,, has two empty cells at the end, and split(",") drops them: [Ada, 91]. Pass a negative limit, split(",", -1), to keep every piece: [Ada, 91, , ]. A cut at the very start is different: it does leave an empty first piece, so ",a,b".split(",") gives [, a, b].
Key idea

split, replaceAll, replaceFirst and matches all read their first argument as a regex. A dot, a bar or a bracket in it does its regex job unless you escape it.

Replacing text#

replaceAll(regex, replacement) returns a copy of the string with every match of the regex replaced, and replaceFirst replaces only the first match. Both take a regex. The replace method from the strings lesson looks for plain text instead.

Replace.java
1public class Main {
2 public static void main(String[] args) {
3 String phone = "077 00-900 123";
4 System.out.println(phone.replaceAll("\\D", ""));
5
6 String spaced = "too many spaces";
7 System.out.println(spaced.replaceAll("\\s+", " "));
8
9 System.out.println("a.b.c".replace(".", "-"));
10 System.out.println("a.b.c".replaceAll(".", "-"));
11 System.out.println("a.b.c".replaceFirst("\\.", "-"));
12 }
13}
Prints
›07700900123
›too many spaces
›a-b-c
›-----
›a-b.c

\D matches every character that is not a digit, and replacing each one with nothing leaves only the digits: 07700900123. \s+ squeezes every run of spaces into a single space. The last three lines show what the regex changes. replace swaps the real dots and prints a-b-c. replaceAll(".", "-") swaps every character, since the dot matches anything, and prints -----. replaceFirst with an escaped dot changes only the first dot: a-b.c.

Groups in the replacement

In the replacement text, $1 stands for whatever group 1 captured, $2 for group 2, and so on. That lets you rearrange the parts of every match. Here, dates written year first become day first:

ReformatDates.java
1public class Main {
2 public static void main(String[] args) {
3 String log = "Shipped 2026-10-18, delivered 2026-10-21.";
4
5 String byNumber = log.replaceAll("(\\d{4})-(\\d{2})-(\\d{2})", "$3/$2/$1");
6 System.out.println(byNumber);
7
8 String byName = log.replaceAll("(?<y>\\d{4})-(?<m>\\d{2})-(?<d>\\d{2})", "${d}.${m}.${y}");
9 System.out.println(byName);
10 }
11}
Prints
›Shipped 18/10/2026, delivered 21/10/2026.
›Shipped 18.10.2026, delivered 21.10.2026.

It prints Shipped 18/10/2026, delivered 21/10/2026. The replacement $3/$2/$1 puts group 3, the day, first. A named group goes into a replacement as ${name}, and the second line prints Shipped 18.10.2026, delivered 21.10.2026.

A dollar sign in the replacement is special
Because $ starts a group reference, "Price: 5".replaceAll("\\d+", "$10") does not put in $10. Java reads $1 as group 1, finds no such group and throws IndexOutOfBoundsException: No group 1. For a real dollar sign, write "\\$10" in the Java string. For replacement text that you did not write yourself, such as text a user typed, wrap it in Matcher.quoteReplacement(...), which makes every $ and backslash in it plain.

Greedy and reluctant

Here is a common first try at removing HTML tags from a bit of text. A tag starts with < and ends with >, so <.*> looks as if it matches one tag.

Predict the outputStripTags.java
1public class Main {
2 public static void main(String[] args) {
3 String html = "Hello <b>world</b>!";
4 System.out.println(html.replaceAll("<.*>", ""));
5 }
6}

What does this print?

It prints Hello !, and the word world goes with the tags. Quantifiers are greedy: .* first takes as many characters as it can, all the way to the end of the text, then gives them back one at a time until the rest of the pattern can match. The first > it meets while backing up is the last one in the text, so the single match runs from the first < to the last >: <b>world</b>.

Put a ? after a quantifier and it becomes reluctant (some people say lazy). It takes as few characters as it can, and takes one more only when the rest of the pattern fails. <.*?> stops at the first > after each <.

Tags.java
1import java.util.regex.Matcher;
2import java.util.regex.Pattern;
3
4public class Main {
5 public static void main(String[] args) {
6 String html = "Hello <b>world</b>!";
7
8 Matcher greedy = Pattern.compile("<.*>").matcher(html);
9 while (greedy.find()) {
10 System.out.println("greedy: " + greedy.group());
11 }
12
13 Matcher reluctant = Pattern.compile("<.*?>").matcher(html);
14 while (reluctant.find()) {
15 System.out.println("reluctant: " + reluctant.group());
16 }
17
18 System.out.println(html.replaceAll("<.*?>", ""));
19 }
20}
Prints
›greedy: <b>world</b>
›reluctant: <b>
›reluctant: </b>
›Hello world!

Step through it and compare the narration of the two loops. The greedy matcher finds one match, <b>world</b>. The reluctant one finds two, <b> and </b>, and removing them leaves Hello world!. The Greedy and Reluctant examples in the tester show the same difference on a longer text.

Key idea

*, +, ? and {n,m} are greedy: they match as much as they can. A ? after one, as in .*?, makes it match as little as it can.

Often clearer: say what cannot appear
<[^>]*> also matches exactly one tag: a <, any characters except >, then a >. It cannot run past the end of a tag, whatever comes after it. Real web pages are harder, with tags inside tags and > signs inside quotes, so programs that read whole pages use an HTML parser library instead of a regex.

Patterns in real programs#

Compile once, use many times

String.matches is handy, but every call compiles its regex from scratch. When a program checks many strings against the same pattern, such as every username in a list of sign-ups, compile the pattern once and keep it. A static final field from static and final is the usual place.

SignUps.java
1import java.util.regex.Pattern;
2
3public class Main {
4 static final Pattern USERNAME = Pattern.compile("[a-z0-9_]{3,15}");
5
6 public static void main(String[] args) {
7 String[] signups = {"sana_99", "Ravi", "x", "grace_h", "ada.l"};
8
9 for (String name : signups) {
10 if (USERNAME.matcher(name).matches()) {
11 System.out.println(name + " is fine");
12 } else {
13 System.out.println(name + " is not allowed");
14 }
15 }
16 }
17}
Prints
›sana_99 is fine
›Ravi is not allowed
›x is not allowed
›grace_h is fine
›ada.l is not allowed

It prints sana_99 is fine, Ravi is not allowed, x is not allowed, grace_h is fine and ada.l is not allowed. The pattern is compiled once, when the class is set up, and each name gets a fresh Matcher of its own.

Flags

A flag changes how a whole pattern matches. You pass flags as a second argument to Pattern.compile. The one you will use most is Pattern.CASE_INSENSITIVE, which lets letters match in either case.

AnyCase.java
1import java.util.regex.Matcher;
2import java.util.regex.Pattern;
3
4public class Main {
5 public static void main(String[] args) {
6 String text = "Java, JAVA and java are the same word here.";
7
8 System.out.println("Exact case: " + count(Pattern.compile("java"), text));
9 System.out.println("Any case: " + count(Pattern.compile("java", Pattern.CASE_INSENSITIVE), text));
10 System.out.println("JaVa".matches("(?i)java"));
11 }
12
13 static int count(Pattern pattern, String text) {
14 Matcher m = pattern.matcher(text);
15 int found = 0;
16 while (m.find()) {
17 found++;
18 }
19 return found;
20 }
21}
Prints
›Exact case: 1
›Any case: 3
›true

It prints Exact case: 1, Any case: 3 and true. The last line switches the flag on another way: (?i) at the start of the regex itself. That form also works in String.matches, split and replaceAll, which have no place for a flags argument.

FlagInside the regexWhat it changes
CASE_INSENSITIVE(?i)Letters match in either case. Only the English letters a to z, unless you add UNICODE_CASE as well.
MULTILINE(?m)^ and $ match at the start and end of every line, as well as of the whole text.
DOTALL(?s)The dot matches line breaks too.
COMMENTS(?x)Spaces in the pattern are ignored and # starts a comment, so a long pattern can be spread out and explained.
To switch on two flags, join them with a single bar, Java's bitwise or: Pattern.CASE_INSENSITIVE | Pattern.MULTILINE.

Searching for text with special characters

Say a search box lets people type what to look for. If someone searches for 1+1, using their text as a regex goes wrong, because + is a quantifier. Pattern.quote turns any text into a pattern that matches exactly that text.

Search.java
1import java.util.regex.Matcher;
2import java.util.regex.Pattern;
3
4public class Main {
5 public static void main(String[] args) {
6 String text = "Total: 1+1=2, not 11=2";
7 String search = "1+1";
8
9 Matcher asRegex = Pattern.compile(search).matcher(text);
10 while (asRegex.find()) {
11 System.out.println("as a regex: " + asRegex.group() + " at " + asRegex.start());
12 }
13
14 Matcher quoted = Pattern.compile(Pattern.quote(search)).matcher(text);
15 while (quoted.find()) {
16 System.out.println("quoted: " + quoted.group() + " at " + quoted.start());
17 }
18 System.out.println(Pattern.quote(search));
19 }
20}
Prints
›as a regex: 11 at 18
›quoted: 1+1 at 7
›\Q1+1\E

As a regex, 1+1 means one or more 1s followed by another 1, so it finds the 11 at index 18 and misses the real “1+1”: as a regex: 11 at 18. Quoted, it finds quoted: 1+1 at 7. The last line shows how quote works. It wraps the text in \Q and \E, and everything between those two markers counts as plain text.

When the pattern is broken

A regex can contain mistakes, just as code can: a bracket that never closes, a * with nothing before it to repeat, a range written backwards. Java finds them when it compiles the pattern and throws a PatternSyntaxException. It is an unchecked exception, so the compiler does not make you catch it, and every method that takes a regex can throw it, matches, split and replaceAll included.

BrokenPatterns.java
1import java.util.regex.Pattern;
2import java.util.regex.PatternSyntaxException;
3
4public class Main {
5 public static void main(String[] args) {
6 String[] attempts = {"[a-z", "*.txt", "\\d{2,1}", "\\d{1,2}"};
7
8 for (String regex : attempts) {
9 try {
10 Pattern.compile(regex);
11 System.out.println(regex + " is a valid pattern");
12 } catch (PatternSyntaxException e) {
13 System.out.println(e.getMessage());
14 }
15 System.out.println();
16 }
17 }
18}
Prints
›Unclosed character class near index 3
›[a-z
› ^
›
›Dangling meta character '*' near index 0
›*.txt
›^
›
›Illegal repetition range near index 6
›\d{2,1}
› ^
›
›\d{1,2} is a valid pattern
›

Each message names the problem, gives the index in the pattern where Java gave up, and draws a caret under that spot. The first one, for example, is Unclosed character class near index 3, with the caret under the z. *.txt is a typical slip: it is how you ask a file manager for every text file, but in a regex a quantifier needs something in front of it to repeat. The regex for that job is .*\.txt. The Broken example in the tester shows the same message, and changes as you fix the pattern.

Every match as a stream
Matcher.results() hands you all the matches at once as a stream, and Pattern.splitAsStream splits a text into one. Streams are the subject of the next lesson, Streams: processing collections step by step.

Try it

Answer each one in your head or on paper first, then open the answer.

1
Write the Java string for the regex \d{3}-\d{4}, and give a piece of text it matches.
2
Why does "Room 42".matches("\\d+") return false, when find() with the same pattern on the same text returns true?
3
What does "a1b22c333".split("\\d+") return? Think about the end of the string.
4
The pattern (\w+)@(\w+)\.com searches “Write to ada@example.com today”. After one find(), what do m.group(1), m.group(2) and m.group() return?
5
A file row reads Bob,,. Why does split(",") give one piece, and how do you get all three?
6
Write a pattern that matches one HTML tag without a reluctant quantifier.

Practice

Work through these on your own. Each one builds on this page.

1
Print every hashtag in the post Learning #java today, then #regex and #coffee, one per line. A hashtag is a # followed by one or more word characters.
2
Turn “Lovelace, Ada; Hopper, Grace” into “Ada Lovelace; Grace Hopper” with a single call to replaceAll.
3
Write a method boolean isStrong(String password) that returns true only when the password has at least 8 characters, at least one digit and at least one capital letter. Use three small patterns with find(), one for each rule, instead of one long regex, and test it on five passwords of your own.
4
Find doubled words, such as “the the”, in a sentence. Look up backreferences in the Pattern documentation: inside a pattern, \1 matches the same text that group 1 just captured.
5
A log file has lines such as 2026-10-18 09:15 ERROR Disk full. Write a pattern with the named groups date, time, level and message, then print the time and message of only the ERROR lines from a few log lines of your own.

Further reading#

Spotted a mistake, or a part that did not make sense? Report it by email