Репликация String.split с помощью StringTokenizer - PullRequest
5 голосов
/ 12 июня 2009

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

Единственное, что осталось от меня и такого восхитительного прироста производительности x2, это то, что когда вы делаете

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

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

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

Ответы [ 9 ]

12 голосов
/ 12 июня 2009

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

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

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

10 голосов
/ 12 июня 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.

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

4 голосов
/ 12 июня 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
2 голосов
/ 12 июня 2009

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

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

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

Звучит так, как тебе нужно, я думаю?

2 голосов
/ 12 июня 2009

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

1 голос
/ 12 июня 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;
}

Если возможно, вы можете опустить объект 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 ++, но я этого не сделал не делайте этого здесь, потому что я считаю этот плохой стиль.

0 голосов
/ 22 ноября 2012

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

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

0 голосов
/ 12 июня 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 экранированных запятых) к тому времени, когда вы Если бы вы допустили это, вы, вероятно, в конечном итоге потеряете часть выигрыша в скорости.

В конце концов, это, вероятно, не стоит беспокоиться.

0 голосов
/ 12 июня 2009

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

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

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


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

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