Недавно я получил свой бесплатный экземпляр книги «Регулярные выражения Java 9» от Анубхавы Шриваставы, изданной Packt. Книга является хорошим руководством и введением для всех, кто хочет узнать, что такое регулярные выражения, и начать с нуля. Тем, кто знает, как использовать регулярные выражения в книге, все еще может быть интересно повторить знания и углубиться в моркомплексные функции, такие как утверждения нулевой длины, обратные ссылки и тому подобное.
В этой статье я остановлюсь на функциях регулярных выражений, которые характерны для Java 9 и не были доступны в более ранней версии JDK. Там не так много, хотя.
Модуль регулярных выражений Java 9
JDK в Java 9 разделен на модули. Можно с полным основанием ожидать появления нового модуля для обработки пакетов и классов регулярных выражений. На самом деле нет ни одного. Модуль java.base является модулем по умолчанию, от которого по умолчанию зависят все остальные модули, и, следовательно, классы экспортируемых пакетов всегда доступны в приложениях Java. Пакет регулярных выражений java.util.regex экспортируется этим модулем. Это делает разработку немного проще: нет необходимости явно «требовать» модуля, если мы хотим использовать регулярные выражения в нашем коде. Кажется, что регулярные выражения настолько важны для Java, что они включены в базовый модуль.
Классы регулярных выражений
Пакет java.util.regex содержит классы
-
MatchResult -
Matcher -
Patternи -
PatternSyntaxException
Единственный класс, который изменил API — это Matcher .
Изменения в классе Matcher
Класс Matcher добавляет пять новых методов. Четыре из них являются перегруженной версией уже существующих методов. Эти:
-
appendReplacement -
appendTail -
replaceAll -
replaceFirst -
results
Первые четыре существуют в более ранних версиях, и есть только изменение типов аргументов (в конце концов, это означает, что перегрузка означает).
appendReplacement / Tail
В случае appendReplacement и appendTail единственное отличие состоит в том, что аргумент также может быть StringBuilder а не только StringBuffer . Учитывая, что StringBuilder появился в Java 1.5 примерно 13 лет назад, никто не должен говорить, что это невнимательный поступок.
Интересно, как appendReplacement онлайн-версия API JDK документирует поведение appendReplacement для аргумента appendReplacement . Более старый аргументированный метод StringBuffer явно документирует, что строка замены может содержать именованные ссылки, которые будут заменены соответствующей группой. Аргументированная версия StringBuilder пропускает это. Документация выглядит как копирование / вставка, а затем редактируется. Текст заменяет «буфер» на «строитель» и т. Д., А текст, документирующий названную ссылочную функцию, удаляется.
Я попробовал функциональность, используя Java 9 build160, и результат для этих двух версий метода одинаков. Это не должно вызывать удивления, так как исходный код двух методов одинаков, простой копирование / вставка в JDK за исключением типа аргумента.
Кажется, что вы можете использовать
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
@Test public void testAppendReplacement() { Pattern p = Pattern.compile("cat(?<plural>z?s?)"); //Pattern p = Pattern.compile("cat(z?s?)"); Matcher m = p.matcher("one catz two cats in the yard"); StringBuilder sb = new StringBuilder(); while (m.find()) { m.appendReplacement(sb, "dog${plural}"); //m.appendReplacement(sb, "dog$001"); } m.appendTail(sb); String result = sb.toString(); assertEquals("one dogz two dogs in the yard", result); } |
обе закомментированные строки или строка над каждой. Документация, однако, говорит только о пронумерованных ссылках.
replaceAll / Первый
Это также «старый» метод, который заменяет сопоставленные группы некоторыми новыми строками. Единственная разница между старой версией и новой заключается в том, как предоставляется строка замены. В старой версии строка была задана как String рассчитанная до вызова метода. В новой версии строка предоставляется в виде Function<MatchResult,String> . Эта функция вызывается для каждого результата матча, и строка замены может быть вычислена на лету.
Зная, что класс Function был введен только 3 года назад в Java 8, его новое использование в регулярных выражениях может показаться незначительным. Или, может быть … может быть, мы должны рассматривать это как намек на то, что через десять лет, когда классу Fuction будет 13 лет, у нас все еще будет Java 9?
Давайте углубимся в эти два метода. (На самом деле только для replaceAll потому что replaceFirst — это то же самое, за исключением того, что он заменяет только первую подобранную группу.) Я попытался создать несколько не совсем сложных примеров, когда такое использование может быть полезным.
Первый пример взят из документации JDK:
|
1
2
3
4
5
6
7
|
@Test public void demoReplaceAllFunction() { Pattern pattern = Pattern.compile("dog"); Matcher matcher = pattern.matcher("zzzdogzzzdogzzz"); String result = matcher.replaceAll(mr -> mr.group().toUpperCase()); assertEquals("zzzDOGzzzDOGzzz", result); } |
Это не слишком сложно и показывает функциональность. Использование лямбда-выражения абсолютно адекватно. Я не могу представить себе более простой способ прописать строчную букву константы «собака». Возможно, только написание «СОБАКА». Хорошо, я просто шучу. Но на самом деле этот пример слишком прост. Это нормально для документации, когда что-либо более сложное отвлекает читателя от функциональности документированного метода. На самом деле: не ожидайте менее сложных примеров в JavaDoc. В нем описывается, как использовать API, а не то, почему API был создан таким образом.
Но здесь и сейчас мы рассмотрим несколько более сложных примеров. Мы хотим заменить в строке символы # на цифры 1, 2, 3 и так далее. Строка содержит пронумерованные элементы, и в случае, если мы вставляем новый в строку, мы не хотим изменять нумерацию вручную. Иногда мы группируем два элемента, и в этом случае мы пишем ## а затем мы просто хотим пропустить серийный номер для следующего # . Поскольку у нас есть модульное тестирование, код описывает функциональность лучше, чем я могу выразить словами:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
@Test public void countSampleReplaceAllFunction() { AtomicInteger counter = new AtomicInteger(0); Pattern pattern = Pattern.compile("#+"); Matcher matcher = pattern.matcher("# first item\n" + "# second item\n" + "## third and fourth\n" + "## item 5 and 6\n" + "# item 7"); String result = matcher.replaceAll(mr -> "" + counter.addAndGet(mr.group().length())); assertEquals("1 first item\n" + "2 second item\n" + "4 third and fourth\n" + "6 item 5 and 6\n" + "7 item 7", result); } |
Лямбда-выражение, переданное на replaceAll получает счетчик и вычисляет следующее значение. Если мы использовали один # то он увеличивает его на 1, если мы использовали два, затем добавляет два к счетчику и так далее. Поскольку лямбда-выражение не может изменить значение переменной в окружающей среде (переменная должна быть фактически конечной), счетчик не может быть переменной типа int или Integer . Нам нужен объект, который содержит значение int и может быть изменен. AtomicInteger — это именно так, даже если мы не используем его атомарную особенность.
Следующий пример идет еще дальше и выполняет некоторые математические вычисления. Он заменяет любое форматированное число с плавающей запятой в строке на его значение синуса. Таким образом, это исправляет наше предложение, поскольку sin (pi) даже не близко к pi, что не может быть точно выражено здесь. Это довольно близко к нулю:
|
1
2
3
4
5
6
7
|
@Test public void calculateSampleReplaceAllFunction() { Pattern pattern = Pattern.compile("\\d+(?:\\.\\d+)?(?:[Ee][+-]?\\d{1,2})?"); Matcher matcher = pattern.matcher("The sin(pi) is 3.1415926"); String result = matcher.replaceAll(mr -> "" + (Math.sin(Double.parseDouble(mr.group())))); assertEquals("The sin(pi) is 5.3589793170057245E-8", result); } |
Мы также немного поиграемся с этим расчетом для демонстрации последнего метода в нашем списке, который является совершенно новым в классе Matcher .
Потоковые результаты ()
Новый метод results() возвращает поток результатов сопоставления. Чтобы быть более точным, он возвращает Stream объектов MatchResult . В приведенном ниже примере мы используем его, чтобы собрать любое отформатированное число с плавающей запятой из строки и вывести их синусоидальное значение через запятую:
|
01
02
03
04
05
06
07
08
09
10
11
|
@Test public void resultsTest() { Pattern pattern = Pattern.compile("\\d+(?:\\.\\d+)?(?:[Ee][+-]?\\d{1,2})?"); Matcher matcher = pattern.matcher("Pi is around 3.1415926 and not 3.2 even in Indiana"); String result = String.join(",", matcher .results() .map(mr -> "" + (Math.sin(Double.parseDouble(mr.group())))) .collect(Collectors.toList())); assertEquals("5.3589793170057245E-8,-0.058374143427580086", result); } |
Резюме
Новые методы регулярных выражений, представленные в Java 9 JDK, существенно не отличаются от уже доступных. Они аккуратны и удобны, а в некоторых ситуациях могут облегчить программирование. Там нет ничего, что не могло быть введено в более ранней версии. Это просто способ Java сделать такие изменения в JDK медленными и продуманными. Ведь именно поэтому мы любим Java, не так ли?
Весь код вставки из IDE может быть найден и загружен из следующего списка
| Ссылка: | Новые возможности Regex в Java 9 от нашего партнера по JCG Питера Верхаса из блога Java Deep . |
