Репликация String.split с помощью StringTokenizer

Воодушевленный этим, а также тем фактом, что мне нужно проанализировать миллиарды строк, я попытался изменить мой код, чтобы он принимал StringTokenizer вместо String[]

Единственное, что осталось между мной и получением восхитительного прироста производительности в 2 раза, это тот факт, что когда вы делаете

"dog,,cat".split(",")
//output: ["dog","","cat"]

StringTokenizer("dog,,cat")
// nextToken() = "dog"
// nextToken() = "cat"

Как я могу добиться аналогичных результатов с помощью StringTokenizer? Есть ли более быстрые способы сделать это?


person Dani    schedule 12.06.2009    source источник


Ответы (9)


Вы на самом деле токенизируете только запятые? Если это так, я бы написал свой собственный токенизатор - он вполне может оказаться даже более эффективным, чем StringTokenizer более общего назначения, который может искать несколько токенов, и вы можете заставить его вести себя так, как вам хочется. Для такого простого варианта использования это может быть простая реализация.

Если бы это было полезно, вы могли бы даже реализовать Iterable<String> и получить улучшенную поддержку цикла for со строгой типизацией вместо поддержки Enumeration, предоставляемой StringTokenizer. Дайте мне знать, если вам нужна помощь в кодировании такого зверя - это действительно не должно быть слишком сложно.

Кроме того, я бы попробовал провести тесты производительности на ваших реальных данных, прежде чем слишком далеко отходить от существующего решения. Вы хоть представляете, сколько времени вашего выполнения на самом деле тратится на String.split? Я знаю, что вам нужно разобрать много строк, но если вы впоследствии будете делать с ними что-то существенное, я ожидаю, что это будет гораздо важнее, чем разбиение.

person Jon Skeet    schedule 12.06.2009
comment
Спасибо, Джон, я вручную создал синтаксический анализ (используя множество indexof), и теперь он в 4 раза быстрее! - person Dani; 12.06.2009

Поработав с классом StringTokenizer, я смог не найти способ удовлетворить требования по возврату ["dog", "", "cat"].

Кроме того, класс StringTokenizer оставлен только из соображений совместимости, а использование класса String.split приветствуется. Из спецификации API для StringTokenizer:

StringTokenizer — это устаревший класс, который сохраняется по соображениям совместимости, хотя его использование в новом коде не рекомендуется. Всем, кто ищет эту функциональность, рекомендуется вместо этого использовать метод split из String или пакет java.util.regex.

Поскольку проблема заключается в предположительно низкой производительности String.split, нам нужно найти альтернативу.

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

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

Создайте собственный токензиер!

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

class MyTokenizer implements Iterable<String>, Iterator<String> {
  String delim = ",";
  String s;
  int curIndex = 0;
  int nextIndex = 0;
  boolean nextIsLastToken = false;

  public MyTokenizer(String s, String delim) {
    this.s = s;
    this.delim = delim;
  }

  public Iterator<String> iterator() {
    return this;
  }

  public boolean hasNext() {
    nextIndex = s.indexOf(delim, curIndex);

    if (nextIsLastToken)
      return false;

    if (nextIndex == -1)
      nextIsLastToken = true;

    return true;
  }

  public String next() {
    if (nextIndex == -1)
      nextIndex = s.length();

    String token = s.substring(curIndex, nextIndex);
    curIndex = nextIndex + 1;

    return token;
  }

  public void remove() {
    throw new UnsupportedOperationException();
  }
}

MyTokenizer возьмет String для токенизации и String в качестве разделителя и будет использовать метод String.indexOf для поиска разделителей. Токены производятся методом String.substring.

Я подозреваю, что могут быть некоторые улучшения производительности при работе со строкой на уровне char[], а не на уровне String. Но я оставлю это в качестве упражнения для читателя.

Класс также реализует Iterable и Iterator, чтобы воспользоваться преимуществами конструкции цикла for-each, которая был введен в Java 5. StringTokenizer является Enumerator и не поддерживает конструкцию for-each.

Это быстрее?

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

  1. Использование StringTokenizer.
  2. Использование нового MyTokenizer.
  3. Использование String.split.
  4. Использование предварительно скомпилированного регулярного выражения с помощью Pattern.compile.

В четырех методах строка "dog,,cat" была разделена на токены. Хотя StringTokenizer включено в сравнение, следует отметить, что оно не вернет желаемый результат ["dog", "", "cat].

Токенизация повторялась в общей сложности 1 миллион раз, чтобы было достаточно времени, чтобы заметить разницу в методах.

Код, используемый для простого теста, был следующим:

long st = System.currentTimeMillis();
for (int i = 0; i < 1e6; i++) {
  StringTokenizer t = new StringTokenizer("dog,,cat", ",");
  while (t.hasMoreTokens()) {
    t.nextToken();
  }
}
System.out.println(System.currentTimeMillis() - st);

st = System.currentTimeMillis();
for (int i = 0; i < 1e6; i++) {
  MyTokenizer mt = new MyTokenizer("dog,,cat", ",");
  for (String t : mt) {
  }
}
System.out.println(System.currentTimeMillis() - st);

st = System.currentTimeMillis();
for (int i = 0; i < 1e6; i++) {
  String[] tokens = "dog,,cat".split(",");
  for (String t : tokens) {
  }
}
System.out.println(System.currentTimeMillis() - st);

st = System.currentTimeMillis();
Pattern p = Pattern.compile(",");
for (int i = 0; i < 1e6; i++) {
  String[] tokens = p.split("dog,,cat");
  for (String t : tokens) {
  }
}
System.out.println(System.currentTimeMillis() - st);

Результаты

Тесты проводились с использованием Java SE 6 (сборка 1.6.0_12-b04), и результаты были следующими:

                   Run 1    Run 2    Run 3    Run 4    Run 5
                   -----    -----    -----    -----    -----
StringTokenizer      172      188      187      172      172
MyTokenizer          234      234      235      234      235
String.split        1172     1156     1171     1172     1156
Pattern.compile      906      891      891      907      906

Итак, как видно из ограниченного тестирования и всего пяти прогонов, StringTokenizer действительно оказался самым быстрым, но MyTokenizer занял второе место. Тогда String.split был самым медленным, а предварительно скомпилированное регулярное выражение было немного быстрее, чем метод split.

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

person coobird    schedule 12.06.2009
comment
Я думаю, что этот метод должен быть следующим: public String next() { if (nextIndex == -1) nextIndex = s.length(); Строковый токен = s.substring(curIndex, nextIndex); curIndex = nextIndex + delim.length(); токен возврата; } - person Juan Carlos Blanco Martínez; 21.07.2009

Примечание. Проведя несколько быстрых тестов, Scanner оказался примерно в четыре раза медленнее, чем String.split. Следовательно, не используйте Сканер.

(Я оставляю пост, чтобы отметить тот факт, что Сканер — плохая идея в данном случае. (Читайте как: не минусуйте меня за предложение Сканера, пожалуйста...))

Если вы используете Java 1.5 или выше, попробуйте Сканер, реализующий Iterator<String>, как это бывает:

Scanner sc = new Scanner("dog,,cat");
sc.useDelimiter(",");
while (sc.hasNext()) {
    System.out.println(sc.next());
}

дает:

dog

cat
person Zarkonnen    schedule 12.06.2009
comment
Я считаю, что Scanner использует регулярное выражение внутри, поэтому OP может не получить желаемого повышения производительности. Однако стоит попробовать, с подходящим тестом :) - person Jon Skeet; 12.06.2009
comment
Быстрый опрос производительности дает мне 47 мс для StringTokenizer, 625 мс для String.split и 2235 мс для Scanner. Поэтому я отказываюсь от своего предложения. Не используйте сканер, он ужасно медленный. - person Zarkonnen; 12.06.2009

В зависимости от того, какие строки вам нужно токенизировать, вы можете написать свой собственный сплиттер, например, на основе String.indexOf(). Вы также можете создать многоядерное решение для дальнейшего повышения производительности, поскольку токенизация строк не зависит друг от друга. Работайте с партиями, скажем, по 100 строк на ядро. Выполните String.split() или что-то еще.

person akarnokd    schedule 12.06.2009

Вместо StringTokenizer вы можете попробовать класс StrTokenizer из Apache Commons Lang, который я цитирую:

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

Пустые токены могут быть удалены или возвращены как null.

Это звучит как то, что вам нужно, я думаю?

person skaffman    schedule 12.06.2009

Вы могли бы сделать что-то подобное. Это не идеально, но может сработать для вас.

public static List<String> find(String test, char c) {
    List<String> list = new Vector<String>();
    start;
    int i=0;
    while (i<=test.length()) {
        int start = i;
        while (i<test.length() && test.charAt(i)!=c) {
            i++;
        }
        list.add(test.substring(start, i));
        i++;
    }
    return list;
}

Если возможно, вы можете опустить элемент списка и напрямую сделать что-то с подстрокой:

public static void split(String test, char c) {
    int i=0;
    while (i<=test.length()) {
        int start = i;
        while (i<test.length() && test.charAt(i)!=c) {
            i++;
        }
        String s = test.substring(start,i);
         // do something with the string here
        i++;
    }
}

В моей системе последний метод работает быстрее, чем решение StringTokenizer, но вы можете проверить, как он работает для вас. (Конечно, вы могли бы сделать этот метод немного короче, опустив {} во втором цикле while, и, конечно, вы могли бы использовать цикл for вместо внешнего цикла while и включить в него последний i++, но я этого не сделал. Я не делаю этого здесь, потому что считаю это плохим стилем.

person user121391    schedule 12.06.2009

Ну, самое быстрое, что вы могли бы сделать, это вручную пройти по строке, например.

List<String> split(String s) {
        List<String> out= new ArrayList<String>();
           int idx = 0;
           int next = 0;
        while ( (next = s.indexOf( ',', idx )) > -1 ) {
            out.add( s.substring( idx, next ) );
            idx = next + 1;
        }
        if ( idx < s.length() ) {
            out.add( s.substring( idx ) );
        }
               return out;
    }

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

В конце концов, наверное, не стоит заморачиваться.

person Steve B.    schedule 12.06.2009

Я бы порекомендовал Google Guava Splitter.
Я сравнил его с тестом coobird и получил следующие результаты:

StringTokenizer 104
Google Guava Splitter 142
String.split 446
регулярное выражение 299

person oshai    schedule 21.11.2012

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

TOKEN { <CAT: "cat"> , <DOG:"gog"> }

input: (cat() | dog())*


cat: <CAT>
   {
   animals.add(new Animal("Cat"));
   }

dog: <DOG>
   {
   animals.add(new Animal("Dog"));
   }
person Pierre    schedule 12.06.2009