2009-06-12 9 views
5

Zachęcony this oraz fakt mam miliardy ciąg do analizowania, próbowałem zmodyfikować mój kod do zaakceptowania StringTokenizer zamiast string []Replikacja String.split z StringTokenizer

jedyne co pozostało między mną a coraz to pyszne wzrost wydajności x2 jest fakt, że gdy robisz

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

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

Jak mogę osiągnąć podobne rezultaty z StringTokenizer? Czy są na to szybsze sposoby?

Odpowiedz

12

Czy tylko tokenizujesz przecinki? Jeśli tak, to piszę własny tokenizer - może to być nawet bardziej efektywne niż bardziej ogólny moduł StringTokenizer, który może szukać wielu tokenów, i możesz sprawić, że będzie się zachowywał jak chcesz. W przypadku tak prostego zastosowania może to być prosta implementacja.

Jeśli byłoby to przydatne, można nawet zaimplementować Iterable<String> i uzyskać wsparcie dla ulepszonej pętli z silnym pisaniem zamiast z obsługą Enumeration dostarczoną przez StringTokenizer. Daj mi znać, jeśli potrzebujesz pomocy w kodowaniu takiej bestii - to naprawdę nie powinno być zbyt trudne.

Dodatkowo, spróbowałbym przeprowadzić testy wydajności rzeczywistych danych przed zbytnim skokiem w stosunku do istniejącego rozwiązania. Czy masz pojęcie, ile czasu na wykonanie jest faktycznie spędził w String.split? Wiem, że masz wiele ciągów do przeanalizowania, ale jeśli robisz z nimi coś znaczącego, spodziewałbym się, że będzie to znacznie ważniejsze niż podział.

+1

+1, Lubię to pomysł wdrożenia Iterable ! – coobird

+0

Dzięki Jon, ręcznie parsowałem (używając wielu indeksów) i teraz jest to x4 szybciej! – Dani

2

W zależności od tego, jaki rodzaj ciągów potrzebujesz do tokenizacji, możesz na przykład napisać własny rozdzielacz oparty na String.indexOf(). Można również utworzyć wielordzeniowe rozwiązanie, aby jeszcze bardziej zwiększyć wydajność, ponieważ tokenizacja łańcuchów jest niezależna od siebie. Praca nad partiami - powiedzmy - 100 ciągów na rdzeń. Rób String.split() lub cokolwiek innego.

-1

Jeśli dane wejściowe są ustrukturyzowane, można przejrzeć kompilator JavaCC. Generuje klasę Java czytającą twoje dane wejściowe. to będzie wyglądać następująco:

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

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


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

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

Zamiast StringTokenizer, można spróbować klasę StrTokenizer z Apache Commons Lang, który cytuję:

Ta klasa może podzielić ciąg na wiele mniejszych łańcuchów. Ma on podobną funkcję do StringTokenizera, jednak oferuje znacznie większą kontrolę i elastyczność, w tym implementację interfejsu ListIterator.

Puste tokeny mogą zostać usunięte lub zwrócone jako puste.

To brzmi jak potrzeba, tak myślę?

4

Uwaga: Po wykonaniu kilku szybkich testów porównawczych skaner okazał się około cztery razy wolniejszy niż String.split. Dlatego nie należy używać skanera.

(Wyjeżdżam stanowisko aż do zarejestrowania faktu, że skaner jest zły pomysł w tym przypadku (przeczytaj jak. Nie downvote mnie za sugerowanie skaner, ...))

Zakładając używasz Java 1.5 lub wyższej, spróbuj Scanner, który realizuje Iterator<String>, jak to się dzieje:

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

daje:

dog 

cat 
+2

Uważam, że skaner używa wewnętrznego regex, więc OP może nie uzyskać poprawy wydajności, której szukają. Warto spróbować jednak z odpowiednim testem porównawczym :) –

+2

Szybka ankieta wydajności daje mi 47 ms dla StringTokenizer, 625 ms dla String.split i 2235 ms dla skanera. Dlatego wycofuję moją sugestię. Nie używaj skanera, jest on ohydnie wolny. – Zarkonnen

1

Można zrobić coś takiego. Nie jest doskonały, ale może ci pomóc.

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

Jeśli to możliwe można ommit rzeczą listy i bezpośrednio coś zrobić podciągu:

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

w moim systemie ostatnia metoda jest szybsza niż StringTokenizer roztworu, ale może chcesz przetestować jak to działa dla ciebie. (Oczywiście możesz uczynić tę metodę nieco krótszą, omijając {} sekundy, podczas gdy wyglądasz i oczywiście możesz użyć pętli for zamiast zewnętrznej pętli while i włączając w to ostatnie i ++, ale nie zrobiłem tego t zrobić to tutaj, ponieważ uważam, że zły styl.

0

Dobrze, najszybszy rzeczą, jaką można zrobić byłoby ręcznie przechodzić ciąg, np

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

To (nieformalny test) wygląda na coś dwa razy tak szybko jak split. Jednak jest to trochę niebezpieczne do iterowania w ten sposób, na przykład będzie się łamać na przecinkach z przecinkiem, a jeśli w końcu będziesz musiał sobie z tym poradzić (ponieważ twoja lista miliardów ciągów ma 3 przecinki z przecinkiem) przed upływem czasu Za to prawdopodobnie stracisz część korzyści prędkości.

Ostatecznie prawdopodobnie nie warto się tym przejmować.

10

Po majstrowaniu przy klasie StringTokenizer, nie mogłem znaleźć sposobu na spełnienie wymagań dotyczących zwrotu ["dog", "", "cat"].

Ponadto, klasa StringTokenizer jest pozostawiona tylko ze względu na kompatybilność, a użycie String.split jest zabezpieczone. Od specyfikacji API dla StringTokenizer:

StringTokenizer jest klasa dziedzictwo że zostaje zachowana dla kompatybilności powodów chociaż jego stosowanie jest zniechęcony w nowym kodem. Zaleca się, aby każdy, kto szukał tej funkcji , używał zamiast tego pakietu split lub .

Ponieważ problem jest rzekomo słabym wynikiem metody String.split, musimy znaleźć alternatywę.

Uwaga: Mówię „podobno słabe działanie”, ponieważ trudno jest ustalić, że każdy przypadek użycia będzie skutkować StringTokenizer są lepsze metody String.split. Ponadto, w wielu przypadkach, chyba że tokenizacja strun jest rzeczywiście wąskim gardłem aplikacji określonej przez właściwe profilowanie, uważam, że w ostateczności będzie to przedwczesna optymalizacja, jeśli w ogóle. Byłbym skłonny powiedzieć napisać kod, który jest znaczący i łatwy do zrozumienia, przed wyruszeniem na optymalizację.

Teraz, z obecnych wymagań, prawdopodobnie toczenia własnego tokenizera nie byłoby zbyt trudne.

Roll nasz własny tokenzier!

Oto prosty tokenizer, który napisałem. Należy zauważyć, że nie istnieją żadne optymalizacje prędkości, ani nie ma błędów kontrole, aby zapobiec dzieje poza końcem napisu - to szybkie i-brudny realizacja:

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 zajmie String tokenize i String jako ogranicznik, i użyj metody String.indexOf, aby wykonać wyszukiwanie ograniczników. Tokeny są produkowane metodą String.substring.

Podejrzewam, że można poprawić wydajność, pracując na łańcuchu na poziomie char[], a nie na poziomie . Ale zostawię to jako ćwiczenie dla czytelnika.

Klasa realizuje również Iterable i Iterator w celu skorzystania z for-each pętli konstruktu, który został wprowadzony w Java 5. StringTokenizer jest Enumerator i nie obsługuje for-each konstrukcję.

Czy to jest szybsze?

Aby dowiedzieć się, czy jest to szybciej, napisałem program do porównywania prędkości w czterech następujących metod:

  1. Zastosowanie StringTokenizer.
  2. Użycie nowego MyTokenizer.
  3. Korzystanie z String.split.
  4. Używanie prekompilowanego wyrażenia regularnego przez Pattern.compile.

W czterech metodach ciąg "dog,,cat" został rozdzielony na tokeny. Chociaż StringTokenizer jest uwzględnione w porównaniu, należy zauważyć, że nie zwróci on pożądanego wyniku z ["dog", "", "cat].

Tokenowanie powtórzono w sumie 1 milion razy, aby dać wystarczająco dużo czasu, aby zauważyć różnicę w metodach.

kod wykorzystywany do prostej odniesienia był następujący:

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

Efekty

Testy przeprowadzano stosując Java SE 6 (build 1.6.0_12-B04), a wyniki następujące:

 
        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 

Więc, jak widać z ograniczonego badania i tylko pięciu seriach przesunięcie StringTokenizer rzeczywiście c ome się najszybciej, ale MyTokenizer zajął drugie miejsce.Następnie, String.split był najwolniejszy, a prekompilowany wyrażenie regularne było nieco szybsze niż metoda split.

Podobnie jak w przypadku każdego małego testu porównawczego, prawdopodobnie nie jest on bardzo reprezentatywny dla rzeczywistych warunków, więc wyniki należy pobrać za pomocą ziarna (lub kopca) soli.

+0

Myślę, że ta metoda powinna wyglądać następująco: public String next() { if (nextIndex == -1) nextIndex = s.length(); String token = s.substring (curIndex, nextIndex); curIndex = nextIndex + delim.length(); token zwrotu; } –

0

Polecam Google Guava Splitter.
porównałem go z coobird testu i mam następujące wyniki:

StringTokenizer 104
Google guawy Splitter 142
String.split 446
regexp 299