Классы Pattern и Matcher в Java
Компиляция паттернов и поиск совпадений в Java с помощью классов Pattern и Matcher.
Регулярные выражения в Java находятся в пакете java.util.regex, и почти весь этот пакет состоит из двух классов: Pattern и Matcher. Pattern — это скомпилированное регулярное выражение, то есть правило. Matcher — это движок, который применяет это правило к входным данным и сообщает о найденных совпадениях. Разделение на два класса позволяет скомпилировать выражение один раз и повторно использовать его для тысяч входных строк — именно это отличает быстрый код с регулярными выражениями от медленного.
В этой главе рассматриваются компиляция Pattern, управление Matcher, ключевое различие между find() и matches(), захватывающие и именованные группы, а также флаги. Если вы только начинаете знакомство с синтаксисом, сначала прочитайте главы введение в регулярные выражения и синтаксис регулярных выражений.
Pattern: компилировать один раз, использовать всегда
Pattern.compile(String regex) разбирает синтаксис регулярного выражения и возвращает неизменяемый потокобезопасный объект Pattern. Компиляция — это ресурсоёмкий шаг, поэтому его следует выполнять один раз — как правило, в поле static final — и использовать результат совместно. Вспомогательные методы String (matches, replaceAll, split) перекомпилируют паттерн при каждом вызове, что приемлемо для разового использования, но расточительно в цикле.
// Good: compile once, reuse
private static final Pattern EMAIL =
Pattern.compile("[\\w.+-]+@[\\w.-]+\\.[a-z]{2,}");
// Wasteful in a loop: String.matches recompiles every iteration
for (String s : lines) {
if (s.matches("[\\w.+-]+@[\\w.-]+\\.[a-z]{2,}")) { /* ... */ }
}Обратите внимание на двойные обратные косые черты: \\w в исходном коде Java соответствует токену регулярного выражения \w, поскольку обратная косая черта сначала должна пройти через экранирование строк Java.
Matcher: движок с состоянием
Pattern не хранит входные данные и позицию — он содержит только правило. Вызов pattern.matcher(input) создаёт объект Matcher, привязанный к входным данным; Matcher хранит всё изменяемое состояние: текущую позицию поиска, границы последнего совпадения и захваченные группы. Поскольку он содержит состояние, Matcher не является потокобезопасным — каждому потоку следует создавать собственный экземпляр.
| Метод | Что делает |
|---|---|
matches() | Проверяет, соответствует ли весь ввод паттерну |
lookingAt() | Проверяет, соответствует ли ввод паттерну начиная с начала (не обязательно до конца) |
find() | Находит следующее совпадение в любом месте ввода; возвращает true и продвигается вперёд |
group() / group(n) | Возвращает всё совпадение или захватывающую группу n |
start() / end() | Индекс первого символа совпадения и индекс, следующий за последним |
replaceAll(repl) | Заменяет каждое совпадение, используя обратные ссылки $1, $2 на группы |
reset() | Перематывает matcher на позицию ноль (опционально с новыми входными данными) |
find() против matches(): самая распространённая ошибка
matches() привязан ко всей строке — он возвращает true только если паттерн охватывает весь ввод целиком. find() работает как сканер: он ищет паттерн в любом месте и может вызываться многократно для обхода всех вхождений.
Pattern p = Pattern.compile("\\d+");
System.out.println(p.matcher("abc123").matches()); // false — whole string isn't digits
System.out.println(p.matcher("abc123").find()); // true — found "123" inside
Matcher m = p.matcher("a1 b22 c333");
while (m.find()) {
System.out.println(m.group() + " @ " + m.start()); // 1@1, 22@4, 333@8
}Частая ошибка — вызов group() до успешного выполнения matches()/find(): это приводит к IllegalStateException, поскольку совпадения ещё нет.
Захватывающие группы, именованные группы и замена
Скобки в регулярном выражении создают захватывающие группы, пронумерованные слева направо, начиная с 1 (группа 0 — это всё совпадение целиком). Java также поддерживает именованные группы с синтаксисом (?<name>...), к которым обращаются через group("name") — это гораздо читабельнее, чем счёт скобок. В строках замены $1 и ${name} вставляют захваченные группой данные. Глава группы в регулярных выражениях подробнее рассматривает незахватывающие группы и обратные ссылки.
Pattern date = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})");
Matcher m = date.matcher("2024-11-30");
if (m.matches()) {
System.out.println(m.group("year")); // 2024
System.out.println(m.group(3)); // 30 (third numbered group)
}
// Reformat using back-references
System.out.println("2024-11-30".replaceFirst(
"(\\d{4})-(\\d{2})-(\\d{2})", "$3/$2/$1")); // 30/11/2024Флаги: без учёта регистра, многострочный режим и другие
Флаги передаются вторым аргументом в Pattern.compile и объединяются через |. Наиболее используемые: CASE_INSENSITIVE, MULTILINE (чтобы ^/$ соответствовали концам строк), и DOTALL (чтобы . соответствовал символу новой строки). Те же флаги можно задать встроенно: (?i), (?m), (?s). Полный список и описание компромиссов смотрите в главе флаги регулярных выражений.
Pattern p = Pattern.compile("error", Pattern.CASE_INSENSITIVE);
System.out.println(p.matcher("FATAL ERROR").find()); // true
// Equivalent inline form:
Pattern.compile("(?i)error");Практический пример: разбор строки лога с одним скомпилированным паттерном
Эта программа компилирует паттерн даты один раз и испытывает Matcher в различных режимах — сканирует все даты в строке с помощью find(), сравнивает find() с matches(), переформатирует текст через обратные ссылки на группы, разбивает строку по пробелам и извлекает значения из именованных групп.
Что можно извлечь из результата выполнения:
find()— сканер с памятью. Циклwhile (m.find())нашёл обе даты — по индексу6и23— потому что каждый вызов продолжает поиск с позиции, где закончилось предыдущее совпадение. Именно так перечисляются все вхождения, и именно поэтому итоговый счётчик оказался равен2.matches()работает по принципу «всё или ничего».matches whole log?вернулfalse, потому что даты вложены в другой текст, тогда какmatches one date?вернулtrue, потому что"2024-01-15"является всей входной строкой. Используйтеfind()для поиска,matches()для проверки.- К пронумерованным группам можно обращаться в процессе поиска. Внутри цикла
m.group(1)вернул только четырёхзначный год (2024), аm.group()вернул всю дату — группа0— это совпадение целиком, группы1..n— захваченные скобками фрагменты слева направо. - Обратные ссылки перегруппируют текст.
replaceAll("$3/$2/$1")превратил каждую дату видаYYYY-MM-DDвDD/MM/YYYY, результат:start 15/01/2024 build 30/11/2024 done— движок подставил каждую захваченную группу в шаблон замены. - Именованные группы читаются как поля. Разбивка строки
"a b c"по\s+свернула последовательности пробелов в3части, а именованный паттерн позволил получитьaliceиw3docs.comчерезnm.group("user")иnm.group("host")по имени, а не по ненадёжным номерам позиций.