Статьи

Новые возможности Regex в Java 9

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