Подтвердить что ты не робот

Почему большинство строковых манипуляций в Java основано на regexp?

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

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

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

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

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

Поэтому мне было интересно: почему 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

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

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

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

Мои выводы до сих пор:

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

P.S. Я даю вам все мои родные раскол с помощью методов 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;
}
4b9b3361

Ответ 1

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

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

Ответ 2

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

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

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

@Tim Pietzcker:

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

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

s.split(findstr).join(replacestr)

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

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

Ответ 3

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

Ответ 4

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

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

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

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

Ответ 5

При взгляде на класс 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 устарел, я отложил ответ Péter Török, чтобы предварительно скомпилировать регулярное выражение для split вместо использования токенизатора.

Ответ 6

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

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

Ответ 7

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

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

Они не думали с точки зрения эффективности. Существует Java Community Process, чтобы повысить это.

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

Pattern exp = Pattern.compile(":");
String[] array = exp.split(sourceString1);
String[] array2 = exp.split(sourceString2);

Ответ 8

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

Ответ 9

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

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

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

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