Перейти к основному содержимому

Руководство по API регулярных выражений Java

· 20 мин. чтения

1. Обзор

В этой статье мы обсудим Java Regex API и способы использования регулярных выражений в языке программирования Java.

В мире регулярных выражений существует множество различных вариантов, таких как grep, Perl, Python, PHP, awk и многие другие.

Это означает, что регулярное выражение, работающее в одном языке программирования, может не работать в другом. Синтаксис регулярных выражений в Java больше всего похож на синтаксис Perl.

2. Настройка

Чтобы использовать регулярные выражения в Java, нам не нужна специальная настройка. JDK содержит специальный пакет java.util.regex , полностью посвященный операциям с регулярными выражениями. Нам нужно только импортировать его в наш код.

Кроме того, класс java.lang.String также имеет встроенную поддержку регулярных выражений, которую мы обычно используем в нашем коде.

3. Пакет регулярных выражений Java

Пакет java.util.regex состоит из трех классов: Pattern, Matcher и PatternSyntaxException :

  • Объект шаблона представляет собой скомпилированное регулярное выражение. Класс Pattern не предоставляет общедоступных конструкторов. Чтобы создать шаблон, мы должны сначала вызвать один из его общедоступных статических методов компиляции , который затем вернет объект Pattern . Эти методы принимают регулярное выражение в качестве первого аргумента.
  • Объект Matcher интерпретирует шаблон и выполняет операции сопоставления с входной строкой . Он также не определяет общедоступных конструкторов. Мы получаем объект Matcher , вызывая метод matcher для объекта Pattern .
  • Объект PatternSyntaxException — это непроверенное исключение, указывающее на синтаксическую ошибку в шаблоне регулярного выражения.

Мы подробно рассмотрим эти классы; однако мы должны сначала понять, как строится регулярное выражение в Java.

Если вы уже знакомы с регулярными выражениями из другой среды, вы можете найти определенные отличия, но они минимальны.

4. Простой пример

Начнем с простейшего варианта использования регулярного выражения. Как мы отмечали ранее, когда регулярное выражение применяется к строке, оно может совпадать ноль или более раз.

Наиболее простой формой сопоставления шаблонов, поддерживаемой API java.util.regex , является сопоставление строкового литерала . Например, если регулярное выражение — foo , а входная строкаfoo , совпадение будет успешным, поскольку строки идентичны:

@Test
public void givenText_whenSimpleRegexMatches_thenCorrect() {
Pattern pattern = Pattern.compile("foo");
Matcher matcher = pattern.matcher("foo");

assertTrue(matcher.find());
}

Сначала мы создаем объект Pattern , вызывая его статический метод компиляции и передавая ему шаблон, который мы хотим использовать.

Затем мы создаем объект Matcher , вызывая метод сопоставления объекта Pattern и передавая ему текст, который мы хотим проверить на совпадения. ``

После этого вызываем метод find в объекте Matcher.

Метод find продолжает продвигаться по входному тексту и возвращает true для каждого совпадения, поэтому мы также можем использовать его для определения количества совпадений:

@Test
public void givenText_whenSimpleRegexMatchesTwice_thenCorrect() {
Pattern pattern = Pattern.compile("foo");
Matcher matcher = pattern.matcher("foofoo");
int matches = 0;
while (matcher.find()) {
matches++;
}

assertEquals(matches, 2);
}

Поскольку мы будем запускать больше тестов, мы можем абстрагироваться от логики поиска совпадений в методе runTest :

public static int runTest(String regex, String text) {
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text);
int matches = 0;
while (matcher.find()) {
matches++;
}
return matches;
}

Когда мы получаем 0 совпадений, тест должен провалиться, иначе он должен пройти.

5. Мета-персонажи

Метасимволы влияют на способ сопоставления шаблона, добавляя логику к шаблону поиска. Java API поддерживает несколько метасимволов, самым простым из которых является точка «.». который соответствует любому символу:

@Test
public void givenText_whenMatchesWithDotMetach_thenCorrect() {
int matches = runTest(".", "foo");

assertTrue(matches > 0);
}

Учитывая предыдущий пример, где регулярное выражение foo соответствовало тексту foo , а также foofoo два раза. Если бы мы использовали метасимвол точки в регулярном выражении, мы бы не получили два совпадения во втором случае:

@Test
public void givenRepeatedText_whenMatchesOnceWithDotMetach_thenCorrect() {
int matches= runTest("foo.", "foofoo");

assertEquals(matches, 1);
}

Обратите внимание на точку после foo в регулярном выражении. Сопоставитель сопоставляет каждый текст, которому предшествует foo , поскольку последняя часть с точкой означает любой последующий символ. Таким образом, после нахождения первого foo остальные рассматриваются как любой символ. Вот почему есть только один матч.

API поддерживает несколько других метасимволов <([{\^-=$!|]})?*+.> , которые мы рассмотрим далее в этой статье.

6. Классы персонажей

Просматривая официальную спецификацию класса Pattern , мы обнаружим сводки поддерживаемых конструкций регулярных выражений. В классах персонажей у нас есть около 6 конструкций.

6.1. ИЛИ класс

Построен как [abc] . Любой из элементов в наборе соответствует:

@Test
public void givenORSet_whenMatchesAny_thenCorrect() {
int matches = runTest("[abc]", "b");

assertEquals(matches, 1);
}

Если они все встречаются в тексте, то каждый сопоставляется отдельно без учета порядка:

@Test
public void givenORSet_whenMatchesAnyAndAll_thenCorrect() {
int matches = runTest("[abc]", "cab");

assertEquals(matches, 3);
}

Их также можно чередовать как часть String . В следующем примере, когда мы создаем разные слова, чередуя первую букву с каждым элементом набора, все они совпадают:

@Test
public void givenORSet_whenMatchesAllCombinations_thenCorrect() {
int matches = runTest("[bcr]at", "bat cat rat");

assertEquals(matches, 3);
}

6.2. НО класс

Приведенный выше набор инвертируется добавлением каретки в качестве первого элемента:

@Test
public void givenNORSet_whenMatchesNon_thenCorrect() {
int matches = runTest("[^abc]", "g");

assertTrue(matches > 0);
}

Другой случай:

@Test
public void givenNORSet_whenMatchesAllExceptElements_thenCorrect() {
int matches = runTest("[^bcr]at", "sat mat eat");

assertTrue(matches > 0);
}

6.3. Класс диапазона

Мы можем определить класс, который указывает диапазон, в который должен попадать совпадающий текст, используя дефис (-), аналогичным образом мы также можем отрицать диапазон.

Соответствующие прописные буквы:

@Test
public void givenUpperCaseRange_whenMatchesUpperCase_
thenCorrect() {
int matches = runTest(
"[A-Z]", "Two Uppercase alphabets 34 overall");

assertEquals(matches, 2);
}

Соответствующие строчные буквы:

@Test
public void givenLowerCaseRange_whenMatchesLowerCase_
thenCorrect() {
int matches = runTest(
"[a-z]", "Two Uppercase alphabets 34 overall");

assertEquals(matches, 26);
}

Соответствие как прописным, так и строчным буквам:

@Test
public void givenBothLowerAndUpperCaseRange_
whenMatchesAllLetters_thenCorrect() {
int matches = runTest(
"[a-zA-Z]", "Two Uppercase alphabets 34 overall");

assertEquals(matches, 28);
}

Соответствие заданному диапазону чисел:

@Test
public void givenNumberRange_whenMatchesAccurately_
thenCorrect() {
int matches = runTest(
"[1-5]", "Two Uppercase alphabets 34 overall");

assertEquals(matches, 2);
}

Соответствие другому диапазону чисел:

@Test
public void givenNumberRange_whenMatchesAccurately_
thenCorrect2(){
int matches = runTest(
"3[0-5]", "Two Uppercase alphabets 34 overall");

assertEquals(matches, 1);
}

6.4. Союз Класс

Объединенный класс символов является результатом объединения двух или более классов символов:

@Test
public void givenTwoSets_whenMatchesUnion_thenCorrect() {
int matches = runTest("[1-3[7-9]]", "123456789");

assertEquals(matches, 6);
}

Приведенный выше тест будет соответствовать только 6 из 9 целых чисел, потому что набор объединения пропускает 4, 5 и 6.

6.5. Класс пересечения

Подобно классу объединения, этот класс является результатом выбора общих элементов между двумя или более наборами. Чтобы применить пересечение, мы используем && :

@Test
public void givenTwoSets_whenMatchesIntersection_thenCorrect() {
int matches = runTest("[1-6&&[3-9]]", "123456789");

assertEquals(matches, 4);
}

Мы получаем 4 совпадения, потому что пересечение двух множеств имеет только 4 элемента.

6.6. Класс вычитания

Мы можем использовать вычитание для отрицания одного или нескольких классов символов, например, для сопоставления набора нечетных десятичных чисел:

@Test
public void givenSetWithSubtraction_whenMatchesAccurately_thenCorrect() {
int matches = runTest("[0-9&&[^2468]]", "123456789");

assertEquals(matches, 5);
}

Совпадут только 1,3,5,7,9 .

7. Предопределенные классы символов

API регулярных выражений Java также принимает предопределенные классы символов. Некоторые из приведенных выше классов символов могут быть выражены в более короткой форме, что делает код менее интуитивным. Одним из особых аспектов Java-версии этого регулярного выражения является escape-символ.

Как мы увидим, большинство символов начинаются с обратной косой черты, которая в Java имеет особое значение. Чтобы они были скомпилированы классом Pattern , необходимо экранировать начальную обратную косую черту, т. е . \d становится \\d .

Совпадающие цифры, эквивалентные [0-9] :

@Test
public void givenDigits_whenMatches_thenCorrect() {
int matches = runTest("\\d", "123");

assertEquals(matches, 3);
}

Соответствие не цифрам, эквивалентным [^0-9] :

@Test
public void givenNonDigits_whenMatches_thenCorrect() {
int mathces = runTest("\\D", "a6c");

assertEquals(matches, 2);
}

Соответствие пробелу:

@Test
public void givenWhiteSpace_whenMatches_thenCorrect() {
int matches = runTest("\\s", "a c");

assertEquals(matches, 1);
}

Соответствие небелому пространству:

@Test
public void givenNonWhiteSpace_whenMatches_thenCorrect() {
int matches = runTest("\\S", "a c");

assertEquals(matches, 2);
}

Соответствие символу слова, эквивалентному [a-zA-Z_0-9] :

@Test
public void givenWordCharacter_whenMatches_thenCorrect() {
int matches = runTest("\\w", "hi!");

assertEquals(matches, 2);
}

Соответствие символу, не являющемуся словом:

@Test
public void givenNonWordCharacter_whenMatches_thenCorrect() {
int matches = runTest("\\W", "hi!");

assertEquals(matches, 1);
}

8. Квантификаторы

API регулярных выражений Java также позволяет нам использовать квантификаторы. Это позволяет нам дополнительно настроить поведение сопоставления, указав количество вхождений для сопоставления.

Чтобы сопоставить текст ноль или один раз, мы используем ? квантификатор:

@Test
public void givenZeroOrOneQuantifier_whenMatches_thenCorrect() {
int matches = runTest("\\a?", "hi");

assertEquals(matches, 3);
}

В качестве альтернативы мы можем использовать синтаксис скобок, также поддерживаемый API регулярных выражений Java:

@Test
public void givenZeroOrOneQuantifier_whenMatches_thenCorrect2() {
int matches = runTest("\\a{0,1}", "hi");

assertEquals(matches, 3);
}

В этом примере представлена концепция совпадений нулевой длины. Так получилось, что если порог совпадения квантификатора равен нулю, он всегда соответствует всему тексту, включая пустую строку в конце каждого ввода. Это означает, что даже если ввод пуст, он вернет одно совпадение нулевой длины.

Это объясняет, почему мы получаем 3 совпадения в приведенном выше примере, несмотря на то, что у нас есть строка S длины два. Третье совпадение — пустая строка нулевой длины .

Чтобы сопоставить текст ноль или неограниченное количество раз, мы используем квантор *, он просто похож на ?:

@Test
public void givenZeroOrManyQuantifier_whenMatches_thenCorrect() {
int matches = runTest("\\a*", "hi");

assertEquals(matches, 3);
}

Поддерживаемая альтернатива:

@Test
public void givenZeroOrManyQuantifier_whenMatches_thenCorrect2() {
int matches = runTest("\\a{0,}", "hi");

assertEquals(matches, 3);
}

Квантификатор с разницей равен +, он имеет порог совпадения 1. Если требуемая строка вообще не встречается, совпадения не будет, даже строки нулевой длины :

@Test
public void givenOneOrManyQuantifier_whenMatches_thenCorrect() {
int matches = runTest("\\a+", "hi");

assertFalse(matches);
}

Поддерживаемая альтернатива:

@Test
public void givenOneOrManyQuantifier_whenMatches_thenCorrect2() {
int matches = runTest("\\a{1,}", "hi");

assertFalse(matches);
}

Как и в Perl и других языках, синтаксис фигурных скобок может использоваться для многократного сопоставления заданного текста:

@Test
public void givenBraceQuantifier_whenMatches_thenCorrect() {
int matches = runTest("a{3}", "aaaaaa");

assertEquals(matches, 2);
}

В приведенном выше примере мы получаем два совпадения, поскольку совпадение происходит только в том случае, если a появляется три раза подряд. Однако в следующем тесте мы не получим совпадения, поскольку текст появляется только два раза подряд:

@Test
public void givenBraceQuantifier_whenFailsToMatch_thenCorrect() {
int matches = runTest("a{3}", "aa");

assertFalse(matches > 0);
}

Когда мы используем диапазон в фигурной скобке, совпадение будет жадным, начиная с верхнего конца диапазона:

@Test
public void givenBraceQuantifierWithRange_whenMatches_thenCorrect() {
int matches = runTest("a{2,3}", "aaaa");

assertEquals(matches, 1);
}

Мы указали не менее двух вхождений, но не более трех, поэтому вместо этого мы получаем одно совпадение, когда сопоставитель видит один aaa и одинокий a , которые не могут быть сопоставлены.

Однако API позволяет нам указать ленивый или неохотный подход, чтобы сопоставитель мог начинать с нижнего конца диапазона, и в этом случае сопоставлялись два вхождения как aa и aa :

@Test
public void givenBraceQuantifierWithRange_whenMatchesLazily_thenCorrect() {
int matches = runTest("a{2,3}?", "aaaa");

assertEquals(matches, 2);
}

9. Захват групп

API также позволяет нам обрабатывать несколько символов как единое целое посредством захвата групп .

Он будет прикреплять номера к группам захвата и разрешать обратные ссылки с использованием этих номеров.

В этом разделе мы увидим несколько примеров того, как использовать группы захвата в Java regex API.

Давайте используем группу захвата, которая соответствует только тогда, когда входной текст содержит две цифры рядом друг с другом:

@Test
public void givenCapturingGroup_whenMatches_thenCorrect() {
int maches = runTest("(\\d\\d)", "12");

assertEquals(matches, 1);
}

Число, прикрепленное к приведенному выше совпадению, равно 1 , используя обратную ссылку, чтобы сообщить сопоставителю, что мы хотим сопоставить другое вхождение совпадающей части текста. Таким образом, вместо:

@Test
public void givenCapturingGroup_whenMatches_thenCorrect2() {
int matches = runTest("(\\d\\d)", "1212");

assertEquals(matches, 2);
}

Там, где есть два отдельных совпадения для ввода, мы можем иметь одно совпадение, но распространить одно и то же совпадение регулярного выражения на всю длину ввода, используя обратную ссылку:

@Test
public void givenCapturingGroup_whenMatchesWithBackReference_
thenCorrect() {
int matches = runTest("(\\d\\d)\\1", "1212");

assertEquals(matches, 1);
}

Где нам пришлось бы повторять регулярное выражение без обратной ссылки для достижения того же результата:

@Test
public void givenCapturingGroup_whenMatches_thenCorrect3() {
int matches = runTest("(\\d\\d)(\\d\\d)", "1212");

assertEquals(matches, 1);
}

Точно так же для любого другого количества повторений обратная ссылка может заставить сопоставитель рассматривать ввод как одно совпадение:

@Test
public void givenCapturingGroup_whenMatchesWithBackReference_
thenCorrect2() {
int matches = runTest("(\\d\\d)\\1\\1\\1", "12121212");

assertEquals(matches, 1);
}

Но если вы измените даже последнюю цифру, совпадение не удастся:

@Test
public void givenCapturingGroupAndWrongInput_
whenMatchFailsWithBackReference_thenCorrect() {
int matches = runTest("(\\d\\d)\\1", "1213");

assertFalse(matches > 0);
}

Важно не забывать об обратной косой черте, это очень важно в синтаксисе Java.

10. Сопоставители границ

API регулярных выражений Java также поддерживает сопоставление границ. Если нас волнует, где именно во входном тексте должно произойти совпадение, то это то, что мы ищем. В предыдущих примерах все, о чем мы заботились, было найдено совпадение или нет.

Чтобы соответствовать только тому, когда требуемое регулярное выражение истинно в начале текста, мы используем знак вставки ^.

Этот тест не пройден, так как в начале можно найти текстовую собаку :

@Test
public void givenText_whenMatchesAtBeginning_thenCorrect() {
int matches = runTest("^dog", "dogs are friendly");

assertTrue(matches > 0);
}

Следующий тест не пройдет:

@Test
public void givenTextAndWrongInput_whenMatchFailsAtBeginning_
thenCorrect() {
int matches = runTest("^dog", "are dogs are friendly?");

assertFalse(matches > 0);
}

Чтобы соответствовать только тогда, когда требуемое регулярное выражение истинно в конце текста, мы используем символ доллара $. Совпадение будет найдено в следующем случае:

@Test
public void givenText_whenMatchesAtEnd_thenCorrect() {
int matches = runTest("dog$", "Man's best friend is a dog");

assertTrue(matches > 0);
}

И здесь совпадений не будет:

@Test
public void givenTextAndWrongInput_whenMatchFailsAtEnd_thenCorrect() {
int matches = runTest("dog$", "is a dog man's best friend?");

assertFalse(matches > 0);
}

Если мы хотим совпадения только тогда, когда требуемый текст находится на границе слова, мы используем регулярное выражение \\b в начале и в конце регулярного выражения:

Пространство – это граница слова:

@Test
public void givenText_whenMatchesAtWordBoundary_thenCorrect() {
int matches = runTest("\\bdog\\b", "a dog is friendly");

assertTrue(matches > 0);
}

Пустая строка в начале строки также является границей слова:

@Test
public void givenText_whenMatchesAtWordBoundary_thenCorrect2() {
int matches = runTest("\\bdog\\b", "dog is man's best friend");

assertTrue(matches > 0);
}

Эти тесты пройдены, потому что начало String , а также пробел между одним текстом и другим отмечают границу слова, однако следующий тест показывает обратное:

@Test
public void givenWrongText_whenMatchFailsAtWordBoundary_thenCorrect() {
int matches = runTest("\\bdog\\b", "snoop dogg is a rapper");

assertFalse(matches > 0);
}

Символы из двух слов, появляющиеся в строке, не отмечают границу слова, но мы можем сделать так, чтобы она проходила, изменив конец регулярного выражения для поиска границы, отличной от слова:

@Test
public void givenText_whenMatchesAtWordAndNonBoundary_thenCorrect() {
int matches = runTest("\\bdog\\B", "snoop dogg is a rapper");
assertTrue(matches > 0);
}

11. Методы классов шаблонов

Ранее мы создавали объекты Pattern только базовым способом. Однако у этого класса есть другой вариант метода компиляции , который принимает набор флагов вместе с аргументом регулярного выражения, влияющим на способ сопоставления шаблона.

Эти флаги представляют собой просто абстрактные целочисленные значения. Давайте перегрузим метод runTest в тестовом классе, чтобы он мог принимать флаг в качестве третьего аргумента:

public static int runTest(String regex, String text, int flags) {
pattern = Pattern.compile(regex, flags);
matcher = pattern.matcher(text);
int matches = 0;
while (matcher.find()){
matches++;
}
return matches;
}

В этом разделе мы рассмотрим различные поддерживаемые флаги и способы их использования.

Pattern.CANON_EQ

Этот флаг включает каноническую эквивалентность. Когда указано, два символа будут считаться соответствующими тогда и только тогда, когда совпадают их полные канонические разложения.

Рассмотрим символ Unicode с ударением é . Его составная кодовая точка — u00E9 . Однако в Unicode также есть отдельная кодовая точка для составных символов e , u0065 и острого ударения, u0301 . В этом случае составной характер u00E9 неотличима от двухсимвольной последовательности u0065ты 0301 .

По умолчанию сопоставление не учитывает каноническую эквивалентность:

@Test
public void givenRegexWithoutCanonEq_whenMatchFailsOnEquivalentUnicode_thenCorrect() {
int matches = runTest("\u00E9", "\u0065\u0301");

assertFalse(matches > 0);
}

Но если мы добавим флаг, то тест пройдёт:

@Test
public void givenRegexWithCanonEq_whenMatchesOnEquivalentUnicode_thenCorrect() {
int matches = runTest("\u00E9", "\u0065\u0301", Pattern.CANON_EQ);

assertTrue(matches > 0);
}

Pattern.CASE_INSENSITIVE

Этот флаг разрешает сопоставление независимо от регистра. По умолчанию при сопоставлении учитывается регистр:

@Test
public void givenRegexWithDefaultMatcher_whenMatchFailsOnDifferentCases_thenCorrect() {
int matches = runTest("dog", "This is a Dog");

assertFalse(matches > 0);
}

Таким образом, используя этот флаг, мы можем изменить поведение по умолчанию:

@Test
public void givenRegexWithCaseInsensitiveMatcher
_whenMatchesOnDifferentCases_thenCorrect() {
int matches = runTest(
"dog", "This is a Dog", Pattern.CASE_INSENSITIVE);

assertTrue(matches > 0);
}

Мы также можем использовать эквивалентное встроенное выражение флага для достижения того же результата:

@Test
public void givenRegexWithEmbeddedCaseInsensitiveMatcher
_whenMatchesOnDifferentCases_thenCorrect() {
int matches = runTest("(?i)dog", "This is a Dog");

assertTrue(matches > 0);
}

Выкройка.КОММЕНТАРИИ

Java API позволяет включать комментарии, используя # в регулярном выражении. Это может помочь в документировании сложных регулярных выражений, которые могут быть неочевидны для другого программиста.

Флаг комментариев заставляет сопоставитель игнорировать любые пробелы или комментарии в регулярном выражении и рассматривать только шаблон. В режиме сопоставления по умолчанию следующий тест не пройден:

@Test
public void givenRegexWithComments_whenMatchFailsWithoutFlag_thenCorrect() {
int matches = runTest(
"dog$ #check for word dog at end of text", "This is a dog");

assertFalse(matches > 0);
}

Это связано с тем, что сопоставитель будет искать во входном тексте все регулярное выражение, включая пробелы и символ #. Но когда мы используем флаг, он будет игнорировать лишние пробелы, и каждый текст, начинающийся с #, будет рассматриваться как комментарий, который следует игнорировать для каждой строки:

@Test
public void givenRegexWithComments_whenMatchesWithFlag_thenCorrect() {
int matches = runTest(
"dog$ #check end of text","This is a dog", Pattern.COMMENTS);

assertTrue(matches > 0);
}

Для этого также существует альтернативное встроенное выражение флага:

@Test
public void givenRegexWithComments_whenMatchesWithEmbeddedFlag_thenCorrect() {
int matches = runTest(
"(?x)dog$ #check end of text", "This is a dog");

assertTrue(matches > 0);
}

Выкройка.DOTALL

By default, when we use the dot “.” expression in regex, we are matching every character in the input String until we encounter a new line character.

Using this flag, the match will include the line terminator as well. We will understand better with the following examples. These examples will be a little different. Since we are interested in asserting against the matched String , we will use matcher ‘s group method which returns the previous match.

First, we will see the default behavior:

@Test
public void givenRegexWithLineTerminator_whenMatchFails_thenCorrect() {
Pattern pattern = Pattern.compile("(.*)");
Matcher matcher = pattern.matcher(
"this is a text" + System.getProperty("line.separator")
+ " continued on another line");
matcher.find();

assertEquals("this is a text", matcher.group(1));
}

As we can see, only the first part of the input before the line terminator is matched.

Now in dotall mode, the entire text including the line terminator will be matched:

@Test
public void givenRegexWithLineTerminator_whenMatchesWithDotall_thenCorrect() {
Pattern pattern = Pattern.compile("(.*)", Pattern.DOTALL);
Matcher matcher = pattern.matcher(
"this is a text" + System.getProperty("line.separator")
+ " continued on another line");
matcher.find();
assertEquals(
"this is a text" + System.getProperty("line.separator")
+ " continued on another line", matcher.group(1));
}

We can also use an embedded flag expression to enable dotall mode:

@Test
public void givenRegexWithLineTerminator_whenMatchesWithEmbeddedDotall
_thenCorrect() {

Pattern pattern = Pattern.compile("(?s)(.*)");
Matcher matcher = pattern.matcher(
"this is a text" + System.getProperty("line.separator")
+ " continued on another line");
matcher.find();

assertEquals(
"this is a text" + System.getProperty("line.separator")
+ " continued on another line", matcher.group(1));
}

Pattern.LITERAL

When in this mode, matcher gives no special meaning to any metacharacters, escape characters or regex syntax. Without this flag, the matcher will match the following regex against any input String :

@Test
public void givenRegex_whenMatchesWithoutLiteralFlag_thenCorrect() {
int matches = runTest("(.*)", "text");

assertTrue(matches > 0);
}

This is the default behavior we have been seeing in all the examples. However, with this flag, no match will be found, since the matcher will be looking for (.*) instead of interpreting it:

@Test
public void givenRegex_whenMatchFailsWithLiteralFlag_thenCorrect() {
int matches = runTest("(.*)", "text", Pattern.LITERAL);

assertFalse(matches > 0);
}

Now if we add the required string, the test will pass:

@Test
public void givenRegex_whenMatchesWithLiteralFlag_thenCorrect() {
int matches = runTest("(.*)", "text(.*)", Pattern.LITERAL);

assertTrue(matches > 0);
}

There is no embedded flag character for enabling literal parsing.

Pattern.MULTILINE

By default ^ and $ metacharacters match absolutely at the beginning and at the end respectively of the entire input String . The matcher disregards any line terminators:

@Test
public void givenRegex_whenMatchFailsWithoutMultilineFlag_thenCorrect() {
int matches = runTest(
"dog$", "This is a dog" + System.getProperty("line.separator")
+ "this is a fox");

assertFalse(matches > 0);
}

The match fails because the matcher searches for dog at the end of the entire String but the dog is present at the end of the first line of the string.

However, with the flag, the same test will pass since the matcher now takes into account line terminators. So the String dog is found just before the line terminates, hence success:

@Test
public void givenRegex_whenMatchesWithMultilineFlag_thenCorrect() {
int matches = runTest(
"dog$", "This is a dog" + System.getProperty("line.separator")
+ "this is a fox", Pattern.MULTILINE);

assertTrue(matches > 0);
}

Here is the embedded flag version:

@Test
public void givenRegex_whenMatchesWithEmbeddedMultilineFlag_
thenCorrect() {
int matches = runTest(
"(?m)dog$", "This is a dog" + System.getProperty("line.separator")
+ "this is a fox");

assertTrue(matches > 0);
}

12. Matcher Class Methods

In this section, we will look at some useful methods of the Matcher class. We will group them according to functionality for clarity.

12.1. Index Methods

Index methods provide useful index values that show precisely where the match was found in the input String . In the following test, we will confirm the start and end indices of the match for dog in the input String :

@Test
public void givenMatch_whenGetsIndices_thenCorrect() {
Pattern pattern = Pattern.compile("dog");
Matcher matcher = pattern.matcher("This dog is mine");
matcher.find();

assertEquals(5, matcher.start());
assertEquals(8, matcher.end());
}

12.2. Study Methods

Study methods go through the input String and return a boolean indicating whether or not the pattern is found. Commonly used are matches and lookingAt methods.

The matches and lookingAt methods both attempt to match an input sequence against a pattern. The difference, is that matches requires the entire input sequence to be matched, while lookingAt does not.

Both methods start at the beginning of the input String :

@Test
public void whenStudyMethodsWork_thenCorrect() {
Pattern pattern = Pattern.compile("dog");
Matcher matcher = pattern.matcher("dogs are friendly");

assertTrue(matcher.lookingAt());
assertFalse(matcher.matches());
}

The matches method will return true in a case like so:

@Test
public void whenMatchesStudyMethodWorks_thenCorrect() {
Pattern pattern = Pattern.compile("dog");
Matcher matcher = pattern.matcher("dog");

assertTrue(matcher.matches());
}

12.3. Replacement Methods

Replacement methods are useful to replace text in an input string. The common ones are replaceFirst and replaceAll .

The replaceFirst and replaceAll methods replace the text that matches a given regular expression. As their names indicate, replaceFirst replaces the first occurrence, and replaceAll replaces all occurrences:

@Test
public void whenReplaceFirstWorks_thenCorrect() {
Pattern pattern = Pattern.compile("dog");
Matcher matcher = pattern.matcher(
"dogs are domestic animals, dogs are friendly");
String newStr = matcher.replaceFirst("cat");

assertEquals(
"cats are domestic animals, dogs are friendly", newStr);
}

Replace all occurrences:

@Test
public void whenReplaceAllWorks_thenCorrect() {
Pattern pattern = Pattern.compile("dog");
Matcher matcher = pattern.matcher(
"dogs are domestic animals, dogs are friendly");
String newStr = matcher.replaceAll("cat");

assertEquals("cats are domestic animals, cats are friendly", newStr);
}

The replaceAll method allows us to substitute all matches with the same replacement. If we want to replace matches on a case by basis, we'd need a token replacement technique .

13. Conclusion

In this article, we have learned how to use regular expressions in Java and also explored the most important features of the java.util.regex package.

Полный исходный код проекта, включая все используемые здесь примеры кода, можно найти в проекте GitHub .