Почему большинство строковых манипуляций в Java основано на регулярных выражениях? - PullRequest
42 голосов
/ 29 июля 2010

В Java есть множество методов, которые все имеют отношение к манипулированию строками.Простейшим примером является метод String.split («что-то»).

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

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

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

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

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


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

Краткое резюме: это большая разница!

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

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

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

Это то, что я получил в своей системе 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 / .. выделит много объектов, которые нужно собрать некоторое время.Так что, возможно, в конечном итоге различия между этими версиями будут еще больше ... или меньше.

Мои выводы на данный момент:

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

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

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;
}

Ответы [ 9 ]

12 голосов
/ 29 июля 2010

Обратите внимание, что регулярное выражение не нужно каждый раз перекомпилировать.Из 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 голосов
/ 29 июля 2010

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

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

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

@ Тим Пицкер:

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

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

s.split(findstr).join(replacestr)

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

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

2 голосов
/ 03 августа 2010

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

Изначально Java не был задуман как язык пакетного программирования.Таким образом, API из коробки больше настроены на выполнение одной «замены», одного «разбора» и т. Д., За исключением инициализации приложения, когда можно ожидать, что приложение будет анализировать несколько файлов конфигурации.оптимизация этих API была принесена в жертву в алтаре простоты ИМО.Но вопрос поднимает важный вопрос.Желание Python отличать регулярное выражение от не-регулярного выражения в его API связано с тем фактом, что Python также может использоваться в качестве превосходного языка сценариев.В UNIX исходные версии fgrep также не поддерживали регулярное выражение.

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

2 голосов
/ 29 июля 2010

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

1 голос
/ 29 июля 2010

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

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

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

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

Pattern exp = Pattern.compile(":");
String[] array = exp.split(sourceString1);
String[] array2 = exp.split(sourceString2);
1 голос
/ 29 июля 2010

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

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

1 голос
/ 29 июля 2010

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

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

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

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

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

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

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

0 голосов
/ 31 июля 2010

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

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

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

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

0 голосов
/ 29 июля 2010

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

...