Предлагаем вашему вниманию перевод краткого руководства по регулярным выражениям в языке Java, написанного Джеффом Фрисеном (Jeff Friesen) для сайта javaworld. Для простоты чтения мы разделили статью на несколько частей. Данная часть — заключительная.
Регулярные выражения в Java, часть 5 - 1
Регулярные выражения в Java, часть 1 Регулярные выражения в Java, часть 2 Регулярные выражения в Java, часть 3 Регулярные выражения в Java, часть 4

Использование регулярных выражений для лексического анализа

Еще более полезное приложение регулярных выражений – библиотека допускающего многократное использование кода для выполнения лексического анализа, ключевой компонент любого компилятора или ассемблера. В этом случае входной поток символов группируется в выходной поток маркеров (token) – имен для последовательностей символов, обладающих общим значением. Например, наткнувшись во входном потоке на последовательность символов c, o, u, n, t, e, r, лексический анализатор может вывести маркер ID (идентификатор). Соответствующая маркеру последовательность символов называется лексемой (lexeme).
Еще о маркерах и лексемах
Такие маркеры, как ID, могут соответствовать многим последовательностям символов. В случае подобных маркеров, соответствующая маркеру фактическая лексема тоже необходима компилятору, ассемблеру или другой утилите, которой требуется лексический анализ. Для маркеров же, которые представляют одну конкретную последовательность символов, как маркер PLUS, соответствующий только символу +, фактическая лексема не требуется, поскольку её можно [однозначно] определить по маркеру.
Регулярные выражения гораздо эффективнее лексических анализаторов на основе [конечного] состояния, которые необходимо писать вручную и обычно нельзя использовать повторно. В качестве примера лексического анализатора на основе регулярных выражений можно привести JLex, лексический генератор для языка Java, использующий регулярные выражения для задания правил разбиения входного потока данных на маркеры. Еще один пример – Lexan.

Знакомимся с Lexan

Lexan – допускающая многократное использование Java-библиотека, предназначенная для лексического анализа. Она основана на коде из серии сообщений из блога Пишем синтаксический анализатор на языке Java веб-сайта Cogito Learning. Библиотека состоит из следующих классов, которые находятся в пакете ca.javajeff.lexan, включенном в загружаемый код данной статьи:
  • Lexan: лексический анализатор;
  • LexanException: исключение, генерируемое в конструкторе класса Lexan;
  • LexException: исключение, генерируемое в случае обнаружения неправильного синтаксиса при лексическом анализе;
  • Token: название с атрибутом-регулярным выражением;
  • TokLex: пара маркер/лексема.
Конструктор Lexan(java.lang.Class tokensClass) создает новый лексический анализатор. Для него требуется один аргумент в виде объекта класса java.lang.Class, соответствующий классу констант типа static Token. При помощи API Reflection, конструктор читает все константы Token в массив значений Token[]. Если констант Token нет, генерируется исключение LexanException.
Регулярные выражения в Java, часть 5 - 2
Класс Lexan также предоставляет следующие два метода:
  • Метод List getTokLexes() возвращает список Token этого лексического анализатора;
  • Метод void lex(String str) выполняет лексический анализ входной строки [с помещением результата] в список значений типа TokLex. В случае обнаружения символа, не соответствующего ни одному из шаблонов массива Token[], генерируется исключение LexException.
В классе LexanException методов нет, он использует для возврата сообщения исключения унаследованный метод getMessage(). В отличие от него, класс LexException предоставляет следующие методы:
  • Метод int getBadCharIndex() возвращает позицию символа, не соответствующего ни одному из шаблонов маркеров.
  • Метод String getText() возвращает анализировавшийся при генерации исключения текст.
Класс Token переопределяет метод toString() для возврата названия маркера. Он также предоставляет метод String getPattern(), возвращающий атрибут регулярного выражения маркера. Класс TokLex предоставляет метод Token getToken(), который возвращает его маркер. Он также предоставляет метод String getLexeme(), который возвращает его лексему.

Демонстрация работы библиотеки Lexan

Для демонстрации работы библиотеки Lexan я написал приложение LexanDemo. Оно состоит из классов LexanDemo, BinTokens, MathTokens и NoTokens. Исходный код приложения LexanDemo приведен в листинге 2. Листинг 2. Демонстрация работы библиотеки Lexan
import ca.javajeff.lexan.Lexan;
import ca.javajeff.lexan.LexanException;
import ca.javajeff.lexan.LexException;
import ca.javajeff.lexan.TokLex;

public final class LexanDemo
{
   public static void main(String[] args)
   {
      lex(MathTokens.class, " sin(x) * (1 + var_12) ");
      lex(BinTokens.class, " 1 0 1 0 1");
      lex(BinTokens.class, "110");
      lex(BinTokens.class, "1 20");
      lex(NoTokens.class, "");
   }

   private static void lex(Class tokensClass, String text)
   {
      try
      {
         Lexan lexan = new Lexan(tokensClass);
         lexan.lex(text);
         for (TokLex tokLex: lexan.getTokLexes())
            System.out.printf("%s: %s%n", tokLex.getToken(),
                              tokLex.getLexeme());
      }
      catch (LexanException le)
      {
         System.err.println(le.getMessage());
      }
      catch (LexException le)
      {
         System.err.println(le.getText());
         for (int i = 0; i < le.getBadCharIndex(); i++)
            System.err.print("-");
         System.err.println("^");
         System.err.println(le.getMessage());
      }
      System.out.println();
   }
}
Метод main() из листинга 2 вызывает утилиту lex() для демонстрации лексического анализа при помощи Lexan. При каждом вызове этому методу передается класс маркеров в объекте Class и строка, анализ которой необходимо выполнить. Метод lex() сначала создает объект класса Lexan, передавая объект Class конструктору класса Lexan. А затем он вызывает метод lex() класса Lexan для этой строки. В случае успешного выполнения лексического анализа, с целью возврата списка объектов TokLex вызывается метод getTokLexes() класса Lexan. Для каждого из этих объектов вызывается его метод getToken() класса TokLex для возврата маркера и его метод getLexeme() для возврата лексемы. Оба значения выводятся в стандартный поток вывода. В случае неудачи лексического анализа генерируется и обрабатывается соответствующим образом одно из исключений LexanException или LexException. Для краткости, рассмотрим, из составляющих это приложение, только класс MathTokens. В листинге 3 показан его исходный код. Листинг 3. Описание набора маркеров для небольшого математического языка
import ca.javajeff.lexan.Token;

public final class MathTokens
{
   public final static Token FUNC = new Token("FUNC", "sin|cos|exp|ln|sqrt");
   public final static Token LPAREN = new Token("LPAREN", "\\(");
   public final static Token RPAREN = new Token("RPAREN", "\\)");
   public final static Token PLUSMIN = new Token("PLUSMIN", "[+-]");
   public final static Token TIMESDIV = new Token("TIMESDIV", "[*/]");
   public final static Token CARET = new Token("CARET", "\\^");
   public final static Token INTEGER = new Token("INTEGER", "[0-9]+");
   public final static Token ID = new Token("ID", "[a-zA-Z][a-zA-Z0-9_]*");
}
Из листинга 3 видно, что класс MathTokens описывает последовательность констант типа Token. Каждой из них присваивается значение объекта Token. Конструктор этого объекта получает строку-название маркера, вместе с регулярным выражением, описывающим все строки символов, относящихся к этому маркеру. Для ясности, желательно, чтобы строковое название маркера совпадало с названием константы, но это не обязательно.
Регулярные выражения в Java, часть 5 - 3
Позиция константы Token в списке маркеров важна. Расположенные выше в списке константы Token имеют приоритет перед расположенными ниже. Например, встретив sin, Lexan выбирает маркер FUNC вместо ID. Если бы маркер ID предшествовал маркеру FUNC, то был бы выбран он.

Компиляция и запуск приложения LexanDemo

Загружаемый код для данной статьи включает архив lexan.zip, содержащий все файлы дистрибутива Lexan. Распакуйте этот архив и перейдите в подкаталог demos корневого каталога lexan. Если вы используете Windows, выполните следующую команду для компиляции файлов исходного кода демо-приложения:
javac -cp ..\library\lexan.jar *.java
В случае успешной компиляции выполните следующую команду для запуска демо-приложения:
java -cp ..\library\lexan.jar;. LexanDemo
Вы должны увидеть следующие результаты:
FUNC: sin
LPAREN: (
ID: x
RPAREN: )
TIMESDIV: *
LPAREN: (
INTEGER: 1
PLUSMIN: +
ID: var_12
RPAREN: )
ONE: 1
ZERO: 0
ONE: 1
ZERO: 0
ONE: 1
ONE: 1
ONE: 1
ZERO: 0
1 20
--^
Неожиданный символ во входном тексте: 20
Сообщение Неожиданный символ во входном тексте: 20 возникает в результате генерации исключения LexanException, вызванного тем, что в классе BinTokens не описана константа Token со значением 2 в качестве регулярного выражения. Обратите внимание на то, что обработчик исключений вывел полученную в результате лексического анализа текста позицию неподходящего символа. Сообщение маркеры отсутствуют получается в результате генерации исключения LexException, вызванного тем, что в классе NoTokens не описано никаких констант Token.

За кулисами

Lexan использует в качестве своего "движка" класс Lexan. Взгляните на реализацию этого класса в листинге 4 и отметьте вклад регулярных выражений в обеспечение возможности повторного использования "движка". Листинг 4. Создание архитектуры лексического анализатора на основе регулярных выражений
package ca.javajeff.lexan;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;

/**
 *  Лексический анализатор. Этот класс можно использовать для
 *  преобразования входного потока символов в выходной поток маркеров.
 *
 *  @Автор Джефф Фризен
 */

public final class Lexan
{
   private List tokLexes;

   private Token[] values;

   /**
    *  Инициализируем лексический анализатор набором объектов Token.
    *
    *  @параметры tokensClass – объект Class класса, содержащего
    *       набор объектов Token
    *
    *  @генерирует исключение LexanException в случае невозможности
    *			формирования объекта Lexan, возможно, из-за отсутствия объектов
    *			Token в классе
    */

   public Lexan(Class tokensClass) throws LexanException
   {
      try
      {
         tokLexes = new ArrayList<>();
         List _values = new ArrayList<>();
         Field[] fields = tokensClass.getDeclaredFields();
         for (Field field: fields)
            if (field.getType().getName().equals("ca.javajeff.lexan.Token"))
               _values.add((Token) field.get(null));
         values = _values.toArray(new Token[0]);
         if (values.length == 0)
            throw new LexanException("маркеры отсутствуют");
      }
      catch (IllegalAccessException iae)
      {
         throw new LexanException(iae.getMessage());
      }

   /**
    * Получаем список TokLex'ов этого лексического анализатора.
    *
    *  @возвращает список TokLex'ов
    */

   public List getTokLexes()
   {
      return tokLexes;
   }

   /**
    *  Выполняет лексический анализ входной строки [с помещением
    *  результата] в список TokLex'ов.
    *
    *  @параметры str – строка, подвергаемая лексическому анализу
    *
    *  @генерирует исключение LexException: во входных данных обнаружен
    *  			неожиданный символ
    */

   public void lex(String str) throws LexException
   {
      String s = new String(str).trim(); // удалить ведущие пробелы
      int index = (str.length() - s.length());
      tokLexes.clear();
      while (!s.equals(""))
      {
         boolean match = false;
         for (int i = 0; i < values.length; i++)
         {
            Token token = values[i];
            Matcher m = token.getPattern().matcher(s);
            if (m.find())
            {
               match = true;
               tokLexes.add(new TokLex(token, m.group().trim()));
               String t = s;
               s = m.replaceFirst("").trim(); // удалить ведущие пробелы
               index += (t.length() - s.length());
               break;
            }
         }
         if (!match)
            throw new LexException("Неожиданный символ во входном тексте: "
												+ s, str, index);
      }
   }
}
Код метода lex() основан на коде, приведенном в сообщении блога "Пишем синтаксический анализатор на языке Java: Генератор маркеров" веб-сайта Cogito Learning. Прочитайте это сообщение, чтобы узнать больше о том, как Lexan использует API Regex для компиляции кода.

Заключение

Регулярные выражения – полезный инструмент, который может пригодиться любому разработчику. API Regex языка программирования Java упрощает их использование в приложениях и библиотеках. Теперь, когда у вас уже есть базовое представление о регулярных выражениях и этом API, загляните в документацию SDK java.util.regex, чтобы узнать еще больше о регулярных выражениях и дополнительных методах API Regex.

Что еще почитать:

Популярно о лямбда-выражениях в Java. С примерами и задачами. Часть 1

Создание простейшего веб-проекта в IntelliJ Idea Enterprise. Пошагово, с картинками

Лучшие книги для подготовки к экзамену OCAJP8 (1Z0-808) по Java 8