Недавно я получил свой бесплатный экземпляр книги «Регулярные выражения 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 . |