Недавно мы столкнулись с феноменом, о котором мы абсолютно не знали: вы можете уничтожить любую Java IDE, а также любой процесс Java с помощью простого регулярного выражения…
Еще в университете меня учили, что регулярные выражения, которые называются регулярными грамматиками или грамматиками типа 3, всегда оказываются конечным автоматом и поэтому могут обрабатываться за линейное время (длина ввода удваивается, время обработки удваивается). Однако это верно только для «здравомыслящих» выражений. Регулярное выражение также может привести к недетерминированному автомату конечных состояний, и все может быть очень плохо.
Рассмотрим выражение: (0 *) * A Это будет любое количество нулей, за которым следует заглавная буква A. Теперь, если вы используете Matcher.find () для этого выражения, все в порядке, если во входных данных есть совпадение , Однако, если вы вызовете это, указав «00000000000000000000» в качестве входных данных, ваша программа будет зависать (как и консоль регулярных выражений в Eclipse или IntelliJ и в каждой (основанной на Java) онлайн-службе регулярных выражений).
То, что на первый взгляд выглядит как бесконечный цикл, превращается в катастрофическое возвращение назад . Это в основном означает, что устройство сопоставления обнаруживает, что в конце ввода не было найдено A. Теперь внешний квантификатор переходит на шаг назад — внутренний вперед и снова — безрезультатно. Таким образом, устройство сопоставления шаг за шагом повторяет все комбинации, чтобы найти совпадение. В конечном итоге он вернется (без совпадения), но сложность (и, следовательно, время выполнения) этого является потенциальной (добавление одного символа к входу удваивает время выполнения). Подробное описание можно найти здесь: катастрофический откат
Вот некоторые показатели времени выполнения, которые я измерил (которые почти точно удваиваются для каждого добавленного символа):
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 | 0000000000: 0.1ms00000000000: 0.2ms000000000000: 0.7ms0000000000000: 1.3ms00000000000000: 1.7ms000000000000000: 3.5ms0000000000000000: 7.2ms00000000000000000: 13.9ms000000000000000000: 27.5ms0000000000000000000: 55.5ms00000000000000000000: 113.0ms000000000000000000000: 226.4ms0000000000000000000000: 439.1ms00000000000000000000000: 886.0ms | 
Небольшое замечание: для таких микро-тестов вам всегда нужно «прогреть» JVM, так как JIT HotSpot в какой-то момент подключится и оптимизирует код. Поэтому первый запуск выглядит так:
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 | 0000000000: 6.8ms00000000000: 11.8ms000000000000: 25.5ms0000000000000: 39.5ms00000000000000: 6.3ms   <- JIT jumped inand started to translate000000000000000: 5.4ms     to native code.0000000000000000: 7.1ms00000000000000000: 14.2ms000000000000000000: 26.8ms0000000000000000000: 54.4ms00000000000000000000: 109.6ms000000000000000000000: 222.1ms0000000000000000000000: 439.2ms00000000000000000000000: 885.6ms | 
Итак, что здесь на вынос? Если вы используете серверное приложение или что-то критическое, используемое многими пользователями, не позволяйте им вводить регулярные выражения, если вы действительно им не доверяете. Существуют реализации регулярных выражений, которые обнаруживают эту проблему и отменяют ее, но Java (до JDK 8) этого не делает.
Примечание. Вы можете проверить это с помощью своей локальной среды IDE или небольшой Java-программы, но, пожалуйста, не начинайте выбивать все веб-сайты тестировщиков regex. Эти ребята предоставляют хороший инструмент бесплатно, так что это было бы совершенно несправедливо.
Вот крошечный тест, который я использовал:
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 | publicclassTest {    publicstaticvoidmain(String[] args) {        for(intruns = 0; runs < 2; runs++) {            Pattern pattern = Pattern.compile("(0*)*A");            // Run from 5 to 25 characters            for(intlength = 5; length < 25; length++) {                // Build input of specified length                String input = "";                for(inti = 0; i < length; i++) { input += "0"; }                               // Measure the average duration of two calls...                 longstart = System.nanoTime();                for(inti = 0; i < 2; i++) {                    pattern.matcher(input).find();                }                System.out.println(input + ": "                       + ((System.nanoTime() - start) / 2000000d)                        + "ms");            }        }    }} |