Почему большинство обработок строк в Java на основе regexp?

В Java существует набор методов, что все имеют отношение к управлению Строками. Самым простым примером является String.split ("что-то") метод.

Теперь фактическое определение многих из тех методов - то, что они все берут регулярное выражение в качестве своего входного параметра (параметров). Который делает затем все очень мощные стандартные блоки.

Теперь существует два эффекта, которые Вы будете видеть во многих из тех методов:

  1. Они перекомпилировали выражение каждый раз, когда метод вызывается. Как таковой они налагают влияние производительности.
  2. Я нашел, что в большинстве "реальных" ситуаций эти методы называют с "фиксированными" текстами. Наиболее распространенное использование метода разделения еще хуже: это обычно называют с единственным символом (обычно '', a''; или '& ') для разделения.

Таким образом, это не только, что методы по умолчанию мощны, они также кажутся подавленными для того, для чего они на самом деле используются. Внутренне мы разработали "fastSplit" метод, который разделяет на фиксированных строках. Я записал тест дома для наблюдения, насколько быстрее я мог сделать это, если бы это, как было известно, было единственным символом. Оба значительно быстрее, чем "стандартный" метод разделения.

Таким образом, я задавался вопросом: почему был Java API, выбранный способ, которым это теперь? Что серьезное основание состояло в том, чтобы пойти для этого вместо того, чтобы иметь что-то как разделение (символ) и разделить (Строка) и splitRegex (Строка)??


Обновление: Я хлопнул вместе несколько вызовов для наблюдения, сколько времени различные способы разделить строку возьмут.

Краткое изложение: Это имеет большое значение!

Я сделал 10 000 000 повторений для каждого тестового сценария, всегда с помощью входа

"aap,noot,mies,wim,zus,jet,teun" 

и всегда с помощью'', или"", как аргумент разделения.

Это - то, что я вошел в свою систему Linux (это - поле Atom D510, таким образом, это немного медленно):

fastSplit STRING
Test  1 : 11405 milliseconds: Split in several pieces
Test  2 :  3018 milliseconds: Split in 2 pieces
Test  3 :  4396 milliseconds: Split in 3 pieces

homegrown fast splitter based on char
Test  4 :  9076 milliseconds: Split in several pieces
Test  5 :  2024 milliseconds: Split in 2 pieces
Test  6 :  2924 milliseconds: Split in 3 pieces

homegrown splitter based on char that always splits in 2 pieces
Test  7 :  1230 milliseconds: Split in 2 pieces

String.split(regex)
Test  8 : 32913 milliseconds: Split in several pieces
Test  9 : 30072 milliseconds: Split in 2 pieces
Test 10 : 31278 milliseconds: Split in 3 pieces

String.split(regex) using precompiled Pattern
Test 11 : 26138 milliseconds: Split in several pieces 
Test 12 : 23612 milliseconds: Split in 2 pieces
Test 13 : 24654 milliseconds: Split in 3 pieces

StringTokenizer
Test 14 : 27616 milliseconds: Split in several pieces
Test 15 : 28121 milliseconds: Split in 2 pieces
Test 16 : 27739 milliseconds: Split in 3 pieces

Поскольку Вы видите, что это имеет большое значение, если у Вас есть много "фиксированных символьных" разделений, чтобы сделать.

Дать Вам парней некоторое понимание; я в настоящее время нахожусь в файлах журнала Apache и арене Hadoop с данными большого веб-сайта. Таким образом мне этот материал действительно имеет значение :)

Что-то, во что я не включил здесь, является сборщиком "мусора". Насколько я могу сказать компиляцию регулярного выражения в Pattern/Matcher/.. выделит много объектов, которые должны быть собраны некоторое время. Таким образом, возможно, в конечном счете различия между этими версиями еще больше.... или меньше.

Мои заключения до сих пор:

  • Только оптимизируйте это, если у Вас есть МНОГО строк для разделения.
  • Если Вы используете regex методы, всегда предварительно компилируют, если Вы неоднократно используете тот же шаблон.
  • Забудьте (устаревший) StringTokenizer
  • Если Вы хотите разделить на единственном символе, затем используют пользовательский метод, особенно если только необходимо разделить его на определенное количество частей (как... 2).

P.S. Я даю Вам все свое разделение собственной разработки символьными методами для проигрывания с (в соответствии с лицензией, которую все на этом сайте подпадает под :)). Я никогда полностью протестировал их.. все же. Весело провести время.

private static String[]
        stringSplitChar(final String input,
                        final char separator) {
    int pieces = 0;

    // First we count how many pieces we will need to store ( = separators + 1 )
    int position = 0;
    do {
        pieces++;
        position = input.indexOf(separator, position + 1);
    } while (position != -1);

    // Then we allocate memory
    final String[] result = new String[pieces];

    // And start cutting and copying the pieces.
    int previousposition = 0;
    int currentposition = input.indexOf(separator);
    int piece = 0;
    final int lastpiece = pieces - 1;
    while (piece < lastpiece) {
        result[piece++] = input.substring(previousposition, currentposition);
        previousposition = currentposition + 1;
        currentposition = input.indexOf(separator, previousposition);
    }
    result[piece] = input.substring(previousposition);

    return result;
}

private static String[]
        stringSplitChar(final String input,
                        final char separator,
                        final int maxpieces) {
    if (maxpieces <= 0) {
        return stringSplitChar(input, separator);
    }
    int pieces = maxpieces;

    // Then we allocate memory
    final String[] result = new String[pieces];

    // And start cutting and copying the pieces.
    int previousposition = 0;
    int currentposition = input.indexOf(separator);
    int piece = 0;
    final int lastpiece = pieces - 1;
    while (currentposition != -1 && piece < lastpiece) {
        result[piece++] = input.substring(previousposition, currentposition);
        previousposition = currentposition + 1;
        currentposition = input.indexOf(separator, previousposition);
    }
    result[piece] = input.substring(previousposition);

    // All remaining array elements are uninitialized and assumed to be null
    return result;
}

private static String[]
        stringChop(final String input,
                   final char separator) {
    String[] result;
    // Find the separator.
    final int separatorIndex = input.indexOf(separator);
    if (separatorIndex == -1) {
        result = new String[1];
        result[0] = input;
    }
    else {
        result = new String[2];
        result[0] = input.substring(0, separatorIndex);
        result[1] = input.substring(separatorIndex + 1);
    }
    return result;
}
42
задан Niels Basjes 31 July 2010 в 06:05
поделиться

9 ответов

Обратите внимание, что regex не нужно перекомпилировать каждый раз. Из Javadoc:

Вызов этого метода в виде str.split(regex, n) дает тот же результат, что и выражение

Pattern.compile(regex).split(str, n) 

То есть, если вас беспокоит производительность, вы можете предварительно скомпилировать шаблон и затем использовать его повторно:

Pattern p = Pattern.compile(regex);
...
String[] tokens1 = p.split(str1); 
String[] tokens2 = p.split(str2); 
...

вместо

String[] tokens1 = str1.split(regex);
String[] tokens2 = str2.split(regex);
...

Я считаю, что основная причина такого дизайна API - удобство. Поскольку регулярные выражения включают в себя все "фиксированные" строки/заголовки, это упрощает API, позволяя иметь один метод вместо нескольких. А если кто-то беспокоится о производительности, то регулярное выражение все равно может быть прекомпилировано, как показано выше.

Мое ощущение (которое я не могу подкрепить никакими статистическими данными), что в большинстве случаев String.split() используется в контексте, где производительность не является проблемой. Например, это одноразовое действие, или разница в производительности пренебрежимо мала по сравнению с другими факторами. IMO редки случаи, когда вы разделяете строки с помощью одного и того же регекса тысячи раз в узком цикле, где оптимизация производительности действительно имеет смысл.

Было бы интересно увидеть сравнение производительности реализации регекс-матчера с фиксированными строками/заголовками по сравнению с матчером, специализированным для них. Разница может оказаться недостаточно большой, чтобы оправдать отдельную реализацию.

12
ответ дан 27 November 2019 в 00:00
поделиться

Я бы не сказал, что большинство операций со строками в Java основаны на регулярных выражениях. На самом деле мы говорим только о split и replaceAll / replaceFirst . Но я согласен, это большая ошибка.

Помимо уродства, когда функция низкоуровневого языка (строки) становится зависимой от функции более высокого уровня (регулярное выражение), это также неприятная ловушка для новых пользователей, которые, естественно, могут предположить, что метод с сигнатурой String.replaceAll (String, String) будет функцией замены строки. Код, написанный в соответствии с этим предположением, будет выглядеть так, как будто он работает, пока не появится специальный символ регулярного выражения, после чего у вас появятся запутанные, трудно отлаживаемые (и, возможно, даже важные с точки зрения безопасности) ошибки.

Забавно, что язык, который может быть настолько педантично строгим в отношении набора текста, сделал небрежную ошибку, рассматривая строку и регулярное выражение как одно и то же. Менее забавно то, что по-прежнему нет встроенного метода для замены или разделения простой строки. Вы должны использовать замену регулярного выражения на строку Pattern.quote d. И вы получаете это только с Java 5 и новее. Безнадежно.

@ Тим Пицкер:

Существуют ли другие языки, которые делают то же самое?

Строки JavaScript частично смоделированы на основе Java, а также беспорядок в случае replace () . Передавая строку, вы получаете замену простой строки, но она заменяет только первое совпадение, что бывает редко.Чтобы получить замену всего, вам необходимо передать объект RegExp с флагом / g , что опять же имеет проблемы, если вы хотите создать его динамически из строки (нет встроенный метод RegExp.quote в JS). К счастью, split () полностью основан на строках, поэтому вы можете использовать идиому:

s.split(findstr).join(replacestr)

Плюс, конечно, Perl делает абсолютно все с регулярным выражением, потому что это просто извращение.

(Это комментарий больше, чем ответ, но он слишком велик для одного. Почему Java сделала это? Не знаю, они сделали много ошибок в первые дни. Некоторые из них с тех пор было исправлено. Я подозреваю, что если бы они подумали добавить функциональность регулярных выражений в поле с пометкой Pattern еще в 1.0, дизайн String был бы более понятным.)

12
ответ дан 27 November 2019 в 00:00
поделиться

Я подозреваю, что причина, по которой такие вещи, как String # split (String) , используют регулярное выражение под капотом, заключается в том, что он включает меньше постороннего кода в библиотеке классов Java. Конечный автомат, возникающий в результате разделения на что-то вроде , или пространство, настолько прост, что вряд ли будет значительно медленнее для выполнения, чем статически реализованный эквивалент с использованием StringCharacterIterator .

Кроме того, статически реализованное решение усложнило бы оптимизацию времени выполнения с помощью JIT, потому что это был бы другой блок кода, который также требует анализа горячего кода. Регулярное использование существующих алгоритмов Pattern в библиотеке означает, что они более вероятны для JIT-компиляции.

1
ответ дан 27 November 2019 в 00:00
поделиться

Ответ на ваш вопрос заключается в том, что API ядра Java сделал это неправильно. Для повседневной работы вы можете подумать об использовании CharMatcher библиотек Guava, который прекрасно восполняет пробелы.

0
ответ дан 27 November 2019 в 00:00
поделиться

Я полагаю, что веская причина в том, что они могут просто переложить ответственность на метод regex, который делает всю настоящую тяжелую работу для всех строковых методов. Полагаю, они решили, что если у них уже есть работающее решение, то с точки зрения разработки и поддержки менее эффективно изобретать колесо для каждого метода работы со строками.

2
ответ дан 27 November 2019 в 00:00
поделиться

При рассмотрении класса Java String использование регулярного выражения кажется разумным, и есть альтернативы, если регулярное выражение нежелательно:

http://java.sun.com/javase/6/docs/api/java /lang/String.html

булевы совпадения (String regex) - Регулярное выражение кажется подходящим, в противном случае вы могли бы просто использовать equals

String replaceAll / replaceFirst (String regex, String replace) - Есть эквиваленты, которые вместо этого принимают CharSequence, предотвращая регулярное выражение.

String [] split (String regex, int limit) - Мощное, но дорогостоящее разделение, вы можете использовать StringTokenizer для разделения по токенам.

Я видел единственные функции, которые использовали регулярные выражения.

Редактировать: Увидев, что StringTokenizer является устаревшим, я бы воспользовался ответом Петера Торёка для предварительной компиляции регулярного выражения для разделения вместо использования токенизатора.

1
ответ дан 27 November 2019 в 00:00
поделиться

Очень хороший вопрос ..

Я полагаю, когда дизайнеры сели посмотреть на это (и, кажется, не очень долго), они подошли к этому с точки зрения того, что он должен быть спроектирован так, чтобы соответствовать как можно большему количеству различных возможностей. возможный. Регулярные выражения предлагают такую ​​гибкость.

Они не думали об эффективности. Для этого доступен Процесс сообщества Java .

Приходилось ли вам использовать класс java.util.regex.Pattern, в котором вы компилируете выражение один раз, а затем используете его для разных строк.

Pattern exp = Pattern.compile(":");
String[] array = exp.split(sourceString1);
String[] array2 = exp.split(sourceString2);
1
ответ дан 27 November 2019 в 00:00
поделиться

... почему Java API был выбран таким, какой он есть сейчас?

Краткий ответ: это не так. Никто никогда не решал отдавать предпочтение методам регулярных выражений перед методами без регулярных выражений в String API, просто так получилось.

Я всегда понимал, что разработчики Java сознательно свели к минимуму методы обработки строк, чтобы избежать раздувания API. Но когда в JDK 1.4 появилась поддержка регулярных выражений, им, конечно, пришлось добавить некоторые удобные методы в API String.

Итак, теперь пользователи сталкиваются с выбором между чрезвычайно мощными и гибкими методами регулярных выражений и базовыми методами, которые всегда предлагала Java.

0
ответ дан 27 November 2019 в 00:00
поделиться

Интересное обсуждение!

Изначально Java не задумывалась как язык пакетного программирования. Таким образом, API из коробки больше настроен на выполнение одной «замены», одного «синтаксического анализа» и т. Д., За исключением инициализации приложения, когда можно ожидать, что приложение будет анализировать кучу файлов конфигурации.

Следовательно, оптимизация этих API была принесена в жертву ради простоты IMO. Но вопрос поднимает важный момент. Стремление Python к тому, чтобы регулярное выражение отличалось от нерегулярного в своем API, проистекает из того факта, что Python также может использоваться как отличный язык сценариев. В UNIX исходные версии fgrep также не поддерживали регулярное выражение.

Я был вовлечен в проект, в котором нам нужно было проделать некоторый объем работы с ETL на java. В то время я помню, что придумал оптимизацию, о которой вы упомянули в своем вопросе.

2
ответ дан 27 November 2019 в 00:00
поделиться
Другие вопросы по тегам:

Похожие вопросы: