Регулярные выражения в Java (regex) - Вопросы

Всего: 6 вопросов

1. 

Как применить регулярное выражение в Java с помощью классов Pattern и Matcher?

Шаблон компилируют методом Pattern.compile(regex), затем вызывают pattern.matcher(text) и получают Matcher для конкретной строки. Публичных конструкторов у обоих классов нет. Синтаксическая ошибка в шаблоне обнаруживается при компиляции и приводит к PatternSyntaxException.

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class DigitsFinder {
    private static final Pattern DIGITS = Pattern.compile("\\d+");

    public static void main(String[] args) {
        Matcher m = DIGITS.matcher("a1b22");
        while (m.find()) {
            System.out.println(m.group() + " [" + m.start() + ", " + m.end() + ")");
        }
    }
}
// Вывод:
// 1 [1, 2)
// 22 [3, 5)

Pattern неизменяем и потокобезопасен, поэтому его создают один раз (например, в поле static final) и переиспользуют. Matcher хранит состояние поиска — текущую позицию, последнее совпадение и его группы — и потокобезопасным не является: для новой строки создают новый Matcher или вызывают reset(text). Проверку выполняет один из трёх методов: matches() — шаблон совпадает со всей строкой, lookingAt() — с её началом, find() — ищет следующее совпадение в любом месте. Методы String.matches(), split() и replaceAll() компилируют шаблон заново при каждом вызове.

2. 

Что такое метасимволы в регулярных выражениях и почему в Java-строке обратный слэш удваивается?

Метасимволы — символы с особым значением в шаблоне: . ^ $ | ( ) [ ] { } \ ? * +. Остальные символы совпадают сами с собой. Чтобы найти метасимвол буквально, перед ним ставят обратный слэш (\., \$, \(), помещают его в набор ([.]) или пропускают строку через Pattern.quote().

Обратный слэш одновременно служит escape-символом строковых литералов Java, поэтому шаблон \d в коде записывают как "\\d", а \. — как "\\.". Запись "\d" не компилируется (illegal escape character), а чтобы найти в тексте сам обратный слэш, в Java-строке нужно четыре: "\\\\". Сырых строк в Java нет, а текстовые блоки (Java 15) тоже обрабатывают escape-последовательности.

System.out.println("java".matches("j.va"));            // true: точка — любой символ
System.out.println("java".matches("j\\.va"));          // false: \. — сама точка
System.out.println("j.va".matches("j\\.va"));          // true
System.out.println("C:\\dir".replaceAll("\\\\", "/")); // C:/dir
3. 

Какие классы символов есть в регулярных выражениях Java: \d, \s, \w и наборы [ ]?

Класс символов совпадает ровно с одним символом из множества. Наборы в квадратных скобках: [abc] — один из перечисленных символов, [a-c] — диапазон, [^abc] — любой символ, кроме перечисленных. Готовые сокращения: \d — цифра ([0-9]), \s — пробельный символ (пробел, \t, \n, \r, \f, \x0B), \w — символ слова ([a-zA-Z_0-9]). Заглавные \D, \S, \W означают отрицание. \b — граница слова (позиция, а не символ), \p{L} — любая буква Юникода.

System.out.println("a1 b2".replaceAll("[^a-z]", ""));  // ab
System.out.println("Привет".matches("\\w+"));          // false
System.out.println("Привет".matches("\\p{L}+"));       // true

По умолчанию \w и \b понимают только латиницу, а диапазон [а-я] не включает букву ё. Для русского текста пишите [а-яё], используйте \p{L} или флаг UNICODE_CHARACTER_CLASS.

4. 

Какие квантификаторы есть в Java regex и чем жадный квантификатор отличается от ленивого?

Квантификатор задаёт число повторений предшествующего элемента (символа, класса или группы): ? — 0 или 1 раз, * — 0 и более, + — 1 и более, {n} — ровно n, {n,} — n и более, {n,m} — от n до m включительно. Например, colou?r совпадает с color и colour, а \d{2,4} — с 12 и 2026, но не со строкой 20261 целиком.

По умолчанию квантификаторы жадные: захватывают максимум текста и отступают назад только при неудаче. С добавленным ? (*?, +?, {n,m}?) они становятся ленивыми и берут минимум. С добавленным + (*+, ++) — притяжательными: жадными, но без отката.

Matcher greedy = Pattern.compile("'.*'").matcher("'a' и 'b'");
greedy.find();
System.out.println(greedy.group());   // 'a' и 'b'

Matcher lazy = Pattern.compile("'.*?'").matcher("'a' и 'b'");
lazy.find();
System.out.println(lazy.group());     // 'a'

Тот же результат, что у ленивого варианта, даёт набор с отрицанием '[^']*' — он работает быстрее, потому что не делает откатов.

5. 

Как в Java regex включить поиск без учёта регистра и другие флаги Pattern?

Флаги передают вторым аргументом Pattern.compile(regex, flags), объединяя через |, или включают прямо в шаблоне встроенной записью вроде (?i). Основные флаги: CASE_INSENSITIVE ((?i)) — без учёта регистра; UNICODE_CASE ((?u)) — регистр по правилам Юникода; MULTILINE ((?m)) — ^ и $ срабатывают на каждой строке текста; DOTALL ((?s)) — точка совпадает и с переводом строки; UNICODE_CHARACTER_CLASS ((?U)) — \w, \d, \b работают по Юникоду; LITERAL — весь шаблон трактуется как обычный текст.

System.out.println(Pattern.compile("привет", Pattern.CASE_INSENSITIVE)
        .matcher("ПРИВЕТ").matches());                          // false
System.out.println(Pattern.compile("привет", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE)
        .matcher("ПРИВЕТ").matches());                          // true
System.out.println("JAVA".matches("(?i)java"));                  // true
System.out.println(Pattern.compile("^\\w+$", Pattern.MULTILINE)
        .matcher("one\ntwo").results().count());               // 2

Неочевидный момент: CASE_INSENSITIVE сам по себе сравнивает без учёта регистра только латиницу (US-ASCII), поэтому для русского текста к нему нужен UNICODE_CASE.

6. 

Какие исключения и ошибки чаще всего возникают при работе с регулярными выражениями в Java?

Шаблон — это обычная строка, поэтому почти все ошибки проявляются только при выполнении:

Pattern.compile("\d+") с одним слэшем — ошибка компиляции Java illegal escape character; нужно "\\d+".

Pattern.compile("[a-z") — PatternSyntaxException: Unclosed character class: синтаксис шаблона проверяется при компиляции Pattern.

matcher.group(), start() или end() до успешного find() или matches() — IllegalStateException: No match found.

В строке замены replaceAll() символ $ — ссылка на группу, а \ — экранирование. Замена "200$" бросает IllegalArgumentException: Illegal group reference, а "$200" читается как ссылка на группу 2 — IndexOutOfBoundsException: No group 2. Произвольный текст пропускайте через Matcher.quoteReplacement().

Matcher m = Pattern.compile("\\d+").matcher("abc123");
// m.group();  // IllegalStateException: No match found
if (m.find()) {
    System.out.println(m.group());                             // 123
}
System.out.println("Цена: 100".replaceAll("100",
        Matcher.quoteReplacement("200$")));                    // Цена: 200$

Без исключения, но с неверным результатом: "abc123".matches("\\d+") возвращает false (нужен find()), а вложенные квантификаторы вроде (a+)+b на длинной строке без совпадения вызывают катастрофический откат (ReDoS).

Страница 1 из 1