Регулярные выражения в Java (regex)
Вызов "192.168.0.1".split(".") возвращает не четыре числа, а пустой массив длины 0. Причина в том, что split() принимает не обычный текст, а регулярное выражение, и точка в нём означает «любой символ» — разделителем становится вся строка. Чтобы такие сюрпризы не отнимали час отладки, достаточно один раз разобраться в синтаксисе regex.
Регулярные выражения в Java (regex) — это шаблоны для поиска, проверки, замены и разбиения текста, записанные на специальном языке метасимволов. Работа с ними сосредоточена в пакете java.util.regex: класс Pattern хранит скомпилированный шаблон, класс Matcher применяет его к строке. Кроме того, регулярные выражения принимают методы String: matches(), split(), replaceAll() и replaceFirst().
Что такое регулярные выражения в Java
Регулярное выражение описывает не конкретную строку, а целый класс строк: «три цифры», «слово из латинских букв», «имя файла с расширением». Java применяет такой шаблон к тексту и сообщает, совпал ли он со всей строкой или с её частью, где именно и какие фрагменты попали в группы.
Например, есть текст:
Используются файлы file1.doc, file2.txt.
А ещё было бы неплохо обратить внимание на файл file3.img.
Также просмотрите содержимое file4.doc. Из него нужно извлечь все имена файлов: file1.doc, file2.txt, file3.img, file4.doc. Для этого подходит регулярное выражение:
[a-zA-Z0-9]+\.[a-z]{3} Разберём его по частям: [a-zA-Z0-9]+ — одна или более латинских букв или цифр (имя файла), \. — сама точка (без обратного слэша точка означала бы «любой символ»), [a-z]{3} — ровно три строчные латинские буквы (расширение). Цифры в первом наборе обязательны: шаблон [a-zA-Z]+\.[a-z]{3} не найдёт ни одного имени из примера, потому что перед точкой стоит цифра.
В Java-коде обратный слэш внутри строкового литерала удваивается, поэтому \. записывается как "\\.":
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class FileNamesExample {
public static void main(String[] args) {
String text = "Используются файлы file1.doc, file2.txt.\n"
+ "А ещё было бы неплохо обратить внимание на файл file3.img.\n"
+ "Также просмотрите содержимое file4.doc.";
Pattern pattern = Pattern.compile("[a-zA-Z0-9]+\\.[a-z]{3}");
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
System.out.println(matcher.group());
}
}
}
// Вывод:
// file1.doc
// file2.txt
// file3.img
// file4.doc Важно
В этом уроке шаблоны вне Java-кода записаны так, как их видит движок регулярных выражений (\d, \.), а в Java-коде — со вторым обратным слэшем ("\\d", "\\."). Запись "\d" в Java-строке не компилируется: illegal escape character. А чтобы найти в тексте сам обратный слэш, в Java-строке нужно написать четыре: "\\\\".
Pattern и Matcher: как применить regex в Java
Чтобы применить регулярное выражение в Java, шаблон компилируют методом Pattern.compile(), затем вызывают pattern.matcher(text) и получают объект Matcher, который ищет совпадения в конкретной строке. Публичных конструкторов у обоих классов нет: Pattern создаётся только через compile(), Matcher — только через matcher().
Pattern — скомпилированное, неизменяемое представление регулярного выражения. Компиляция проверяет синтаксис (ошибка даёт PatternSyntaxException) и строит внутреннюю структуру для быстрого поиска, поэтому один Pattern выгодно создать один раз (например, в поле static final) и использовать многократно.
Matcher — «искатель», который хранит состояние поиска по одной строке: текущую позицию, найденное совпадение и его группы. Для каждой новой строки создают новый Matcher или вызывают reset(newText).
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class PatternMatcherExample {
public static void main(String[] args) {
Pattern pattern = Pattern.compile("[a-z]+");
Matcher matcher = pattern.matcher("code 2 learn java tutorial");
while (matcher.find()) {
System.out.println(matcher.group() + " [" + matcher.start() + ", " + matcher.end() + ")");
}
}
}
// Вывод:
// code [0, 4)
// learn [7, 12)
// java [13, 17)
// tutorial [18, 26) Шаблон [a-z]+ — это последовательность из одной или более строчных латинских букв, поэтому цифра 2 и пробелы в совпадения не попадают. Метод start() возвращает индекс первого символа совпадения, end() — индекс символа после последнего (как у substring()).
Основные методы класса Pattern:
| Метод | Что делает | Нюанс |
|---|---|---|
Pattern.compile(regex) | Компилирует шаблон | Синтаксическая ошибка — PatternSyntaxException |
Pattern.compile(regex, flags) | Компилирует шаблон с флагами | Флаги объединяются через |, см. раздел Флаги |
pattern.matcher(text) | Создаёт Matcher для строки | Принимает любой CharSequence, в том числе StringBuilder |
Pattern.matches(regex, text) | Проверяет, что вся строка соответствует шаблону | Компилирует шаблон при каждом вызове |
pattern.split(text) | Разбивает строку по совпадениям | splitAsStream() (Java 8) возвращает Stream<String> |
Pattern.quote(s) | Превращает строку в шаблон, совпадающий с ней буквально | Удобно для текста от пользователя |
asPredicate() / asMatchPredicate() | Возвращают Predicate<String> для фильтрации | Первый работает как find() (Java 8), второй — как matches() (Java 11) |
Основные методы класса Matcher:
| Метод | Что делает | Граничный случай |
|---|---|---|
matches() | Шаблон совпадает со всей строкой | "\\d+" на "abc123" — false |
lookingAt() | Шаблон совпадает с началом строки | "\\d+" на "123abc" — true |
find() | Ищет следующее совпадение в любом месте строки | Каждый вызов продолжает с места предыдущего совпадения |
group(), group(n), group("name") | Текст совпадения или группы | До успешного поиска — IllegalStateException: No match found |
start(), end() | Индексы начала и конца (не включительно) совпадения | Тоже требуют успешного поиска |
groupCount() | Количество групп в шаблоне | Группа 0 (всё совпадение) не считается |
replaceAll(r), replaceFirst(r) | Заменяют все или первое совпадение | В замене $1 — ссылка на группу |
results() | Поток всех совпадений Stream<MatchResult> | Java 9+ |
reset(), reset(text) | Сбрасывает позицию поиска, может сменить строку | Позволяет переиспользовать Matcher |
Начиная с Java 9 все совпадения удобно собрать в список одной цепочкой вызовов через results():
import java.util.List;
import java.util.regex.MatchResult;
import java.util.regex.Pattern;
public class ResultsExample {
public static void main(String[] args) {
List<String> numbers = Pattern.compile("\\d+")
.matcher("a1b22c333")
.results()
.map(MatchResult::group)
.toList();
System.out.println(numbers);
}
}
// Вывод: [1, 22, 333] Метод toList() у Stream появился в Java 16; в более ранних версиях используйте collect(Collectors.toList()).
matches(), find() и lookingAt(): в чём разница
matches() возвращает true, только если шаблон описывает всю строку целиком; lookingAt() — если шаблон совпадает с началом строки; find() — если совпадение есть хотя бы где-то внутри. Перепутать их — самая частая причина, почему «правильная» регулярка возвращает false.
import java.util.regex.Pattern;
public class MatchesFindExample {
public static void main(String[] args) {
Pattern pattern = Pattern.compile("\\d+");
for (String s : new String[]{"123", "123abc", "abc123"}) {
System.out.println(s
+ ": matches=" + pattern.matcher(s).matches()
+ ", lookingAt=" + pattern.matcher(s).lookingAt()
+ ", find=" + pattern.matcher(s).find());
}
}
}
// Вывод:
// 123: matches=true, lookingAt=true, find=true
// 123abc: matches=false, lookingAt=true, find=true
// abc123: matches=false, lookingAt=false, find=true | Строка | matches() | lookingAt() | find() |
|---|---|---|---|
"123" | true | true | true |
"123abc" | false | true | true |
"abc123" | false | false | true |
Для каждого метода в примере создаётся новый Matcher: find() продолжает поиск с позиции, на которой закончилось предыдущее совпадение, поэтому повторные вызовы на одном и том же объекте без reset() могут дать другой результат.
Ещё один пример: шаблон a*b (любое количество букв a, затем b):
import java.util.regex.Pattern;
public class StarExample {
public static void main(String[] args) {
Pattern pattern = Pattern.compile("a*b");
System.out.println(pattern.matcher("aaab").matches());
System.out.println(pattern.matcher("b").matches());
System.out.println(pattern.matcher("aaabc").matches());
System.out.println(pattern.matcher("aaabc").find());
}
}
// Вывод:
// true
// true
// false
// true Строка "b" подходит, потому что * допускает ноль повторений. Строка "aaabc" не проходит matches() из-за лишней буквы c в конце, но find() находит в ней фрагмент aaab.
Синтаксис регулярных выражений: метасимволы
Метасимволы — это символы с особым значением в шаблоне: . ^ $ | ( ) [ ] { } \ ? * +. Все остальные символы совпадают сами с собой. Чтобы найти метасимвол как обычный символ, перед ним ставят обратный слэш (\., \$, \() или помещают его в набор [.].
| Метасимвол | Значение | Пример | Совпадает с |
|---|---|---|---|
. | Любой символ, кроме перевода строки (с флагом DOTALL — любой) | j.va | java, j3va |
^ | Начало строки (с флагом MULTILINE — начало каждой строки текста) | ^Java | Java в начале текста |
$ | Конец строки (с MULTILINE — конец каждой строки текста) | \.txt$ | .txt в конце текста |
| | «Или»: альтернатива между подвыражениями | cat|dog | cat или dog |
( ) | Группа: объединяет часть шаблона и запоминает совпадение | (ab)+ | ab, abab |
[ ] | Набор символов: один любой из перечисленных | [0-9] | Одна цифра |
{ } | Точное число повторений | \d{4} | 2026 |
? * + | Квантификаторы: 0 или 1, 0 и более, 1 и более | colou?r | color, colour |
\ | Экранирование метасимвола или начало класса (\d, \s) | \$\d+ | $100 |
Классы символов: \d, \s, \w и наборы [ ]
Класс символов совпадает ровно с одним символом из заданного множества. Набор задаётся в квадратных скобках ([abc], диапазон — [a-c], отрицание — [^abc]), а для частых множеств есть готовые сокращения: \d — цифра, \s — пробельный символ, \w — символ слова.
| Конструкция | Что совпадает | В Java-строке | Пример: да / нет |
|---|---|---|---|
[abc] | Набор: один из символов a, b или c | "[abc]" | b / d |
[a-c] | Диапазон: любой символ от a до c включительно | "[a-c]" | a / z |
[^abc] | Любой символ, кроме перечисленных | "[^abc]" | x / a |
[a-zA-Z0-9] | Латинская буква любого регистра или цифра | "[a-zA-Z0-9]" | Q, 7 / _ |
\d | Цифра, то же что [0-9] | "\\d" | 5 / x |
\D | Любой символ, кроме цифры | "\\D" | x / 5 |
\s | Пробельный символ: пробел, табуляция \t, перевод строки \n, \r, \f, \x0B | "\\s" | пробел / a |
\S | Любой непробельный символ | "\\S" | a / пробел |
\w | Символ слова: латинская буква, цифра или подчёркивание, то же что [a-zA-Z_0-9] | "\\w" | a, _ / я, - |
\W | Любой символ, кроме символа слова | "\\W" | -, я / a |
\b | Граница слова (не символ, а позиция) | "\\bjava\\b" | java в «I like java» / в «javascript» |
\p{L} | Любая буква Юникода, включая кириллицу | "\\p{L}" | я, a / 5 |
Сочетание \s+ (один или более пробельных символов подряд) — самый частый шаблон для разбиения текста на слова и схлопывания лишних пробелов; примеры — в разделах split() и Методы String.
Нюанс кириллицы
По умолчанию \w и \b в Java понимают только латиницу: шаблон \w+ не найдёт слово «Привет». Диапазон [а-я] тоже неполный — буква ё лежит в Юникоде вне его, поэтому пишите [а-яё] или используйте \p{L} и флаг UNICODE_CHARACTER_CLASS (пример — в разделе Флаги Pattern).
Квантификаторы: ?, *, +, {n,m}
Квантификатор указывает, сколько раз должен повториться предшествующий ему элемент: символ, класс символов или группа. По умолчанию квантификаторы жадные — захватывают как можно больше текста; с добавленным ? они становятся ленивыми и захватывают как можно меньше.
| Квантификатор | Сколько раз | Пример | Совпадает с |
|---|---|---|---|
a? | 0 или 1 раз (необязательный элемент) | colou?r | color, colour |
a* | 0 и более раз | ab*c | ac, abc, abbbc |
a+ | 1 и более раз | ab+c | abc, abbbc, но не ac |
a{n} | Ровно n раз | \d{4} | 2026 |
a{n,} | n и более раз | \w{3,} | java, regex, но не go |
a{n,m} | От n до m раз включительно | \d{2,4} | 12, 123, 2026 |
*?, +?, ??, {n,m}? | Ленивые варианты: минимально возможное число повторений | '.*?' | Каждая строка в кавычках отдельно |
*+, ++, ?+ | Притяжательные (possessive): жадные, но без возврата при неудаче | \d++ | Все цифры подряд, без отката |
Что выведет этот код? Шаблоны отличаются одним знаком вопроса:
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class GreedyLazyExample {
public static void main(String[] args) {
String text = "'a' и 'b'";
Matcher greedy = Pattern.compile("'.*'").matcher(text);
while (greedy.find()) {
System.out.println("жадный: " + greedy.group());
}
Matcher lazy = Pattern.compile("'.*?'").matcher(text);
while (lazy.find()) {
System.out.println("ленивый: " + lazy.group());
}
}
}
// Вывод:
// жадный: 'a' и 'b'
// ленивый: 'a'
// ленивый: 'b' Жадный .* сначала захватывает всё до конца строки, а затем отступает назад до последней кавычки — поэтому получается одно длинное совпадение. Ленивый .*? останавливается на первой подходящей кавычке. Того же результата, что у ленивого варианта, можно добиться набором с отрицанием: '[^']*' — он ещё и работает быстрее, потому что не делает откатов.
Альтернация (OR) и группы
Оператор | означает «или»: шаблон Java|Kotlin|Scala совпадает с любым из трёх слов. Круглые скобки ( ) объединяют часть шаблона в группу: они ограничивают действие | и квантификаторов, а текст, совпавший с группой, можно получить через group(n) или подставить в замену как $n.
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class AlternationExample {
public static void main(String[] args) {
Matcher matcher = Pattern.compile("Java|Kotlin|Scala")
.matcher("Kotlin и Scala работают на JVM, как и Java");
while (matcher.find()) {
System.out.println(matcher.group());
}
System.out.println(Pattern.compile("^cat|dog$").matcher("hotdog").find());
System.out.println(Pattern.compile("^(cat|dog)$").matcher("hotdog").find());
}
}
// Вывод:
// Kotlin
// Scala
// Java
// true
// false У | самый низкий приоритет: шаблон ^cat|dog$ читается как «cat в начале строки» или «dog в конце строки», поэтому он находит совпадение в "hotdog". Чтобы альтернатива относилась к обоим якорям, её заключают в скобки: ^(cat|dog)$. Если запоминать совпадение не нужно, используйте незахватывающую группу (?:cat|dog).
Группы нумеруются по открывающим скобкам слева направо, начиная с 1; группа 0 — всё совпадение. С Java 7 группам можно давать имена: (?<year>\d{4}).
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class GroupsExample {
public static void main(String[] args) {
Pattern date = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})");
Matcher matcher = date.matcher("Релиз: 2026-10-16");
if (matcher.find()) {
System.out.println(matcher.group());
System.out.println(matcher.group(1));
System.out.println(matcher.group("day"));
System.out.println(matcher.groupCount());
}
String ru = "2026-10-16".replaceAll("(\\d{4})-(\\d{2})-(\\d{2})", "$3.$2.$1");
System.out.println(ru);
}
}
// Вывод:
// 2026-10-16
// 2026
// 16
// 3
// 16.10.2026 Флаги Pattern: регистр, многострочный режим, кириллица
Флаги меняют поведение всего шаблона: поиск без учёта регистра, многострочный режим для ^ и $, поддержку Юникода. Их передают вторым аргументом Pattern.compile(regex, flags), объединяя через |, или включают прямо в шаблоне встроенной записью вроде (?i).
| Флаг | В шаблоне | Что делает |
|---|---|---|
CASE_INSENSITIVE | (?i) | Поиск без учёта регистра — по умолчанию только для латиницы (US-ASCII) |
UNICODE_CASE | (?u) | Вместе с CASE_INSENSITIVE — регистр по правилам Юникода (кириллица и другие алфавиты) |
MULTILINE | (?m) | ^ и $ совпадают с началом и концом каждой строки текста |
DOTALL | (?s) | Точка совпадает и с переводом строки |
UNICODE_CHARACTER_CLASS | (?U) | \w, \d, \b и другие классы работают по Юникоду (Java 7+); включает UNICODE_CASE |
COMMENTS | (?x) | Пробелы в шаблоне игнорируются, после # — комментарий |
LITERAL | — | Весь шаблон трактуется как обычный текст |
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class FlagsExample {
public static void main(String[] args) {
System.out.println(Pattern.compile("привет", Pattern.CASE_INSENSITIVE)
.matcher("ПРИВЕТ").matches());
System.out.println(Pattern.compile("привет", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE)
.matcher("ПРИВЕТ").matches());
System.out.println("JAVA".matches("(?i)java"));
Matcher ascii = Pattern.compile("\\w+").matcher("Привет, Java");
while (ascii.find()) {
System.out.println("без флага: " + ascii.group());
}
Matcher unicode = Pattern.compile("\\w+", Pattern.UNICODE_CHARACTER_CLASS).matcher("Привет, Java");
while (unicode.find()) {
System.out.println("UNICODE_CHARACTER_CLASS: " + unicode.group());
}
}
}
// Вывод:
// false
// true
// true
// без флага: Java
// UNICODE_CHARACTER_CLASS: Привет
// UNICODE_CHARACTER_CLASS: Java Первая строка вывода — неочевидный момент: флаг CASE_INSENSITIVE сам по себе не сравнивает кириллицу без учёта регистра, для русского текста к нему нужен UNICODE_CASE.
split(): разбиение строки по регулярному выражению
Метод split() разбивает строку на массив подстрок, считая разделителем каждое совпадение с регулярным выражением. Он есть у класса String (str.split(regex)) и у класса Pattern (pattern.split(str)); результат одинаковый, но Pattern удобнее, если один и тот же разделитель используется многократно.
import java.util.Arrays;
import java.util.regex.Pattern;
public class PatternSplitExample {
public static void main(String[] args) {
String text = "java5tiger 77 java6mustang";
Pattern pattern = Pattern.compile("\\d+\\s?");
System.out.println(Arrays.toString(pattern.split(text)));
System.out.println(Arrays.toString(text.split("\\s*\\d+\\s*")));
}
}
// Вывод:
// [java, tiger , java, mustang]
// [java, tiger, java, mustang] Обратите внимание на "tiger " с пробелом в первой строке вывода: шаблон \d+\s? захватывает пробел только после цифр, а пробел перед 77 остаётся в предыдущем элементе. Шаблон \s*\d+\s* забирает пробелы с обеих сторон.
Разбиение по пробелам через \s+ и несколько неочевидных случаев:
import java.util.Arrays;
import java.util.regex.Pattern;
public class SplitEdgeCases {
public static void main(String[] args) {
String words = " Java regex\tэто просто ";
System.out.println(Arrays.toString(words.split("\\s+")));
System.out.println(Arrays.toString(words.strip().split("\\s+")));
String ip = "192.168.0.1";
System.out.println(ip.split(".").length);
System.out.println(Arrays.toString(ip.split("\\.")));
System.out.println(Arrays.toString("a|b|c".split("|")));
System.out.println(Arrays.toString("a|b|c".split(Pattern.quote("|"))));
System.out.println(Arrays.toString("a,b,,".split(",")));
System.out.println(Arrays.toString("a,b,,".split(",", -1)));
}
}
// Вывод:
// [, Java, regex, это, просто]
// [Java, regex, это, просто]
// 0
// [192, 168, 0, 1]
// [a, |, b, |, c]
// [a, b, c]
// [a, b]
// [a, b, , ] | Вызов | Результат | Почему |
|---|---|---|
" a b".split("\\s+") | Первый элемент — пустая строка | Перед ведущими пробелами есть «пустое» слово; уберите их через strip() (Java 11) или trim() |
"1.2.3".split(".") | Пустой массив | Точка — любой символ; все элементы пустые, а хвостовые пустые строки отбрасываются |
"a|b".split("|") | Разбиение по каждому символу | | с пустыми альтернативами совпадает с пустой строкой; нужен "\\|" или Pattern.quote("|") |
"a,b,,".split(",") | [a, b] | Хвостовые пустые строки удаляются; split(",", -1) их сохраняет |
Методы String с регулярными выражениями
Класс String принимает регулярные выражения в четырёх методах: matches(), split(), replaceAll() и replaceFirst(). Метод replace() регулярные выражения не использует и заменяет текст буквально — путаница между replace() и replaceAll() встречается постоянно. Остальные методы String разобраны в уроке Java: класс String и его методы.
| Метод String | Что делает | Эквивалент через Pattern |
|---|---|---|
matches(regex) | Вся строка соответствует шаблону | Pattern.matches(regex, str) |
split(regex), split(regex, limit) | Разбивает строку по совпадениям | Pattern.compile(regex).split(str) |
replaceAll(regex, replacement) | Заменяет все совпадения | Pattern.compile(regex).matcher(str).replaceAll(r) |
replaceFirst(regex, replacement) | Заменяет первое совпадение | ...matcher(str).replaceFirst(r) |
replace(target, replacement) | Заменяет все вхождения буквально, без regex | — |
public class StringRegexExample {
public static void main(String[] args) {
System.out.println("a.b.c".replace(".", "-"));
System.out.println("a.b.c".replaceAll(".", "-"));
System.out.println("a.b.c".replaceAll("\\.", "-"));
System.out.println("[" + " Java regex ".replaceAll("\\s+", " ") + "]");
System.out.println("12345".matches("\\d+"));
System.out.println("Цена: 100".replaceAll("100", "200$"));
}
}
// Вывод:
// a-b-c
// -----
// a-b-c
// [ Java regex ]
// true
// Exception in thread "main" java.lang.IllegalArgumentException: Illegal group reference: group index is missing В строке замены у replaceAll() символ $ обозначает ссылку на группу, а \ — экранирование, поэтому "200$" приводит к исключению. Если замена содержит произвольный текст, пропустите её через Matcher.quoteReplacement("200$") — тогда результат будет Цена: 200$.
Методы String.matches() и replaceAll() компилируют шаблон при каждом вызове. В цикле по тысячам строк вынесите Pattern в поле private static final и используйте pattern.matcher(s).
Примеры: проверка домена и поиск email
На практике регулярные выражения чаще всего используют для двух задач: проверить, что строка целиком имеет нужный формат (matches()), и найти в тексте все фрагменты нужного вида (find() в цикле). Ниже — по примеру на каждую задачу.
Проверка доменного имени
import java.util.regex.Pattern;
public class DomainExample {
private static final Pattern DOMAIN = Pattern.compile(".+\\.(com|ua|ru)");
public static void main(String[] args) {
System.out.println(test("google.com"));
System.out.println(test("reference1.ua"));
System.out.println(test("reference1.org"));
}
public static boolean test(String testString) {
return DOMAIN.matcher(testString).matches();
}
}
// Вывод:
// true
// true
// false Шаблон требует, чтобы строка заканчивалась точкой и одной из трёх зон, перечисленных через |. Это учебная проверка: .+ пропустит и пробелы, например "a b.com". Для настоящих URL надёжнее разбирать строку классом java.net.URI.
Поиск email в тексте
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class EmailExample {
private static final Pattern EMAIL = Pattern.compile("([\\w.+-]+)@([\\w.-]+\\.[a-z]{2,})");
public static void main(String[] args) {
String text = "Адреса эл. почты: [email protected], [email protected]!";
Matcher matcher = EMAIL.matcher(text);
while (matcher.find()) {
System.out.println("e-mail: " + matcher.group()
+ ", логин: " + matcher.group(1)
+ ", домен: " + matcher.group(2));
}
}
}
// Вывод:
// e-mail: [email protected], логин: ivanov, домен: gmail.com
// e-mail: [email protected], логин: sidorov, домен: bsu.by Группа 1 захватывает логин (буквы, цифры, _ . + -), группа 2 — домен с зоной из двух и более латинских букв. Полная проверка email по стандарту RFC 5322 регулярным выражением практически нечитаема, поэтому в реальных проектах regex используют как первичный фильтр, а адрес подтверждают письмом.
Типичные промахи при работе с regex
Большинство ошибок с регулярными выражениями в Java не ловится компилятором: шаблон — это строка, и проблема проявляется только при выполнении — исключением, неверным результатом или резким падением скорости. Частые случаи и исправления:
| Код | Что произойдёт | Как исправить |
|---|---|---|
Pattern.compile("\d+") | Ошибка компиляции: illegal escape character | Удвоить слэш: "\\d+" |
Pattern.compile("[a-z") | PatternSyntaxException: Unclosed character class near index 3 | Закрыть скобку: "[a-z]" |
"abc123".matches("\\d+") | false, хотя цифры в строке есть | Для поиска внутри строки — matcher.find() |
matcher.group() без вызова find() | IllegalStateException: No match found | Сначала if (matcher.find()) или matches() |
"1.2.3".split(".") | Пустой массив | split("\\.") или Pattern.quote(".") |
replaceAll("100", "200$") | IllegalArgumentException: Illegal group reference | Matcher.quoteReplacement("200$") |
Вложенные квантификаторы вроде "(a+)+b" | Риск катастрофического отката (ReDoS): на длинной строке без совпадения число вариантов перебора растёт экспоненциально | Не вкладывать квантификаторы друг в друга ("a+b"), использовать притяжательные ++ |
s.matches(regex) в цикле | Шаблон компилируется на каждой итерации | private static final Pattern + matcher(s) |
Часто задаваемые вопросы
Потокобезопасны ли Pattern и Matcher?
Pattern неизменяем и потокобезопасен: один объект в поле static final можно использовать из любого количества потоков. Matcher хранит состояние поиска и потокобезопасным не является — каждый поток должен создавать свой через pattern.matcher(text).
Как проверить, что строка содержит только цифры?
str.matches("[0-9]+") вернёт true только для непустой строки из цифр; для пустой строки результат false, а шаблон [0-9]* пропустил бы и её. Если строку потом нужно превратить в число, учтите, что Integer.parseInt() бросит NumberFormatException для слишком длинных чисел, даже если regex их пропустил.
Как одним регулярным выражением проверить сложность пароля?
С помощью опережающих проверок (lookahead) (?=...): они проверяют условие, не сдвигая позицию. password.matches("(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).{8,}") требует минимум 8 символов, хотя бы одну цифру, одну строчную и одну заглавную латинскую букву. Есть и ретроспективные проверки: (?<=...) и (?<!...).
Можно ли в Java не удваивать обратный слэш в шаблоне?
Нет. Сырые строковые литералы (JEP 326) планировались в Java 12, но были отозваны. Текстовые блоки из Java 15 тоже обрабатывают escape-последовательности, поэтому в них слэш удваивается так же. Уменьшить их число помогают наборы ([.] вместо \\.) и Pattern.quote().
Практика по теме
Закрепите урок на задачах — у каждой есть уровень и оценка времени.
- Заменить каждое второе вхождение строки
- Простейшее регулярное выражение
- Регулярное выражение, описывающее ссылку
- Регулярное выражение для нахождения Java X
- Найти количество слов
Видео объяснение
Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.
Комментарии