W3docs

Классы 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(), переформатирует текст через обратные ссылки на группы, разбивает строку по пробелам и извлекает значения из именованных групп.

java— editable, runs on the server

Что можно извлечь из результата выполнения:

  • 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") по имени, а не по ненадёжным номерам позиций.

Практика

Практика
Вы вызываете 'pattern.matcher(input)', чтобы получить Matcher, и сразу же вызываете 'matcher.group()' без предварительного вызова 'find()' или 'matches()'. Что произойдёт?
Вы вызываете 'pattern.matcher(input)', чтобы получить Matcher, и сразу же вызываете 'matcher.group()' без предварительного вызова 'find()' или 'matches()'. Что произойдёт?
Was this page helpful?