Scanner vs. StringTokenizer vs. String.Split


155

Właśnie dowiedziałem się o klasie Scanner w Javie i teraz zastanawiam się, jak porównuje / konkuruje z StringTokenizer i String.Split. Wiem, że StringTokenizer i String.Split działają tylko na ciągach znaków, więc dlaczego miałbym chcieć używać Scanner for a String? Czy Scanner ma być po prostu kompleksowym rozwiązaniem do rozdzielania?

Odpowiedzi:


240

Zasadniczo są to konie na kursy.

  • Scannerjest przeznaczony do przypadków, w których musisz przeanalizować ciąg, wyciągając dane różnych typów. Jest bardzo elastyczny, ale prawdopodobnie nie zapewnia najprostszego interfejsu API do prostego pobierania tablicy ciągów oddzielonych określonym wyrażeniem.
  • String.split()i Pattern.split()podam prostą składnię do zrobienia tego drugiego, ale to właściwie wszystko, co robią. Jeśli chcesz przeanalizować otrzymane ciągi lub zmienić ogranicznik w połowie, w zależności od konkretnego tokenu, nie pomogą Ci w tym.
  • StringTokenizerjest jeszcze bardziej restrykcyjny String.split(), a także nieco bardziej skomplikowany w użyciu. Zasadniczo jest przeznaczony do wyciągania tokenów oddzielonych ustalonymi podciągami. Z powodu tego ograniczenia jest około dwa razy szybszy niż String.split(). (Zobacz moje porównanie String.split()iStringTokenizer .) Jest to również starsze od API wyrażeń regularnych, którego String.split()jest częścią.

Z moich czasów zauważysz, że na typowej maszynie String.split()wciąż można tokenizować tysiące łańcuchów w ciągu kilku milisekund . Ponadto ma tę zaletę StringTokenizer, że daje wynik w postaci tablicy ciągów, co zwykle jest tym, czego chcesz. Używanie an Enumeration, jak zapewnia StringTokenizer, jest przez większość czasu zbyt „skomplikowane składniowo”. Z tego punktu widzenia StringTokenizerw dzisiejszych czasach jest to trochę marnowanie miejsca i równie dobrze możesz po prostu użyć String.split().


8
Byłoby również interesujące zobaczyć wyniki Scanner w tych samych testach, które przeprowadziłeś na String.Split i StringTokenizer.
Dave,

2
Udzielił mi odpowiedzi na inne pytanie: „dlaczego odradza się używanie StringTokenizera, jak stwierdzono w uwagach dotyczących interfejsu API języka Java?”. Z tego tekstu wynika, że ​​odpowiedź brzmiałaby: „ponieważ String.split () jest wystarczająco szybki”.
Nogi

1
Czy więc StringTokenizer jest obecnie przestarzały?
Steve the Maker

czego użyć zamiast tego? Skaner?
Adrian

4
Zdaję sobie sprawę, że jest to odpowiedź na stare pytanie, ale jeśli muszę podzielić ogromny strumień tekstu na tokeny w locie, czy nie jest to StringTokenizernadal najlepszy zakład, ponieważ String.split()po prostu zabraknie mi pamięci?
Siergiej Tachenov

57

Zacznijmy od wyeliminowania StringTokenizer. Starzeje się i nie obsługuje nawet wyrażeń regularnych. Jego dokumentacja stwierdza:

StringTokenizerjest starszą klasą, która jest zachowywana ze względu na kompatybilność, chociaż jej użycie jest odradzane w nowym kodzie. Zaleca się, aby każdy, kto szuka tej funkcji, używał splitmetody Stringlub java.util.regexpakietu.

Więc wyrzućmy to od razu. To pozostawia split()i Scanner. Jaka jest między nimi różnica?

Po pierwsze, split()po prostu zwraca tablicę, co ułatwia korzystanie z pętli foreach:

for (String token : input.split("\\s+") { ... }

Scanner jest zbudowany bardziej jak strumień:

while (myScanner.hasNext()) {
    String token = myScanner.next();
    ...
}

lub

while (myScanner.hasNextDouble()) {
    double token = myScanner.nextDouble();
    ...
}

(Ma dość duże API , więc nie myśl, że zawsze ogranicza się do tak prostych rzeczy).

Ten interfejs w stylu strumieniowym może być przydatny do analizowania prostych plików tekstowych lub danych wejściowych konsoli, gdy nie masz (lub nie możesz uzyskać) wszystkich danych wejściowych przed rozpoczęciem analizowania.

Osobiście pamiętam tylko jeden raz, kiedy korzystałem Scannerz projektów szkolnych, kiedy musiałem uzyskać dane wejściowe użytkownika z wiersza poleceń. To sprawia, że ​​tego rodzaju operacja jest łatwa. Ale jeśli mam coś String, z czym chcę się rozstać, to prawie nie ma sensu split().


20
StringTokenizer jest 2x szybszy niż String.split (). Jeśli NIE MUSISZ używać wyrażeń regularnych, NIE WOLNO!
Alex Worden

Po prostu Scannerwykrywałem znaki nowego wiersza w danym String. Ponieważ znaki nowego wiersza mogą się różnić w zależności od platformy (spójrz na Patternjavadoc!), A ciąg wejściowy NIE gwarantuje zgodności System.lineSeparator(), uważam, że jest Scannerbardziej odpowiedni, ponieważ już wie, jakich znaków nowego wiersza szukać podczas wywoływania nextLine(). Ponieważ String.splitbędę musiał podać poprawny wzorzec wyrażenia regularnego, aby wykryć separatory wierszy, których nie znajduję w żadnej standardowej lokalizacji (najlepsze, co mogę zrobić, to skopiować je ze Scannerźródła klasy).
ADTC,

9

StringTokenizer był zawsze obecny. Jest najszybszy ze wszystkich, ale idiom przypominający wyliczenie może nie wyglądać tak elegancko jak inne.

split powstał na JDK 1.4. Wolniejszy niż tokenizer, ale łatwiejszy w użyciu, ponieważ można go wywołać z klasy String.

Skaner pojawił się na JDK 1.5. Jest najbardziej elastycznym i wypełnia istniejącą od dawna lukę w Java API, wspierając odpowiednik słynnej rodziny funkcji scanf Cs.


6

Jeśli masz obiekt String, który chcesz tokenizować, preferuj użycie metody split String zamiast StringTokenizer. Jeśli analizujesz dane tekstowe ze źródła spoza programu, na przykład z pliku lub od użytkownika, przydaje się skaner.


5
Tak po prostu, bez uzasadnienia, bez powodu?
jan.supol

6

Split jest wolny, ale nie tak wolny jak Scanner. StringTokenizer jest szybszy niż split. Jednak odkryłem, że mogę uzyskać dwukrotnie większą prędkość, handlując pewną elastycznością, aby uzyskać przyspieszenie, co zrobiłem na JFastParser https://github.com/hughperkins/jfastparser

Testowanie na łańcuchu zawierającym milion podwójnych:

Scanner: 10642 ms
Split: 715 ms
StringTokenizer: 544ms
JFastParser: 290ms

Jakiś Javadoc byłby miły, a co jeśli chcesz przeanalizować coś innego niż dane liczbowe?
NickJ,

Cóż, jest zaprojektowany z myślą o szybkości, a nie pięknie. Jest to dość proste, tylko kilka wierszy, więc jeśli chcesz, możesz dodać kilka dodatkowych opcji analizy tekstu.
Hugh Perkins

4

String.split wydaje się być znacznie wolniejszy niż StringTokenizer. Jedyną zaletą podziału jest to, że otrzymujesz tablicę tokenów. Możesz także użyć wyrażeń regularnych w podziale. org.apache.commons.lang.StringUtils ma metodę podziału, która działa znacznie szybciej niż dowolne z dwóch, a mianowicie. StringTokenizer lub String.split. Ale wykorzystanie procesora dla wszystkich trzech jest prawie takie samo. Potrzebujemy więc metody mniej obciążającej procesor, której nadal nie mogę znaleźć.


3
Ta odpowiedź jest nieco bezsensowna. Mówisz, że szukasz czegoś szybszego, ale „mniej obciążającego procesor”. Każdy program jest wykonywany przez CPU. Jeśli program nie wykorzystuje procesora w 100%, to musi czekać na coś innego, na przykład we / wy. Nie powinno to nigdy stanowić problemu podczas omawiania tokenizacji ciągów, chyba że robisz bezpośredni dostęp do dysku (czego w szczególności tutaj nie robimy).
Jolta,

4

Niedawno przeprowadziłem kilka eksperymentów dotyczących złej wydajności metody String.split () w sytuacjach o dużej wrażliwości na wydajność. Może ci się to przydać.

http://eblog.chrononsystems.com/hidden-evils-of-javas-stringsplit-and-stringr

Istota jest taka, że ​​String.split () kompiluje za każdym razem wzorzec wyrażenia regularnego i może w ten sposób spowolnić działanie programu w porównaniu do sytuacji, gdy używasz prekompilowanego obiektu Pattern i używasz go bezpośrednio do operacji na łańcuchu.


4
W rzeczywistości String.split () nie zawsze kompiluje wzorzec. Spójrz na źródło, jeśli 1.7 java, zobaczysz, że jest sprawdzenie, czy wzorzec jest pojedynczym znakiem, a nie znakiem ucieczki, podzieli łańcuch bez wyrażenia regularnego, więc powinien być dość szybki.
Krzysztof Krasoń

1

W przypadku domyślnych scenariuszy sugerowałbym również Pattern.split (), ale jeśli potrzebujesz maksymalnej wydajności (szczególnie na Androidzie wszystkie testowane przeze mnie rozwiązania są dość wolne) i wystarczy podzielić je tylko jednym znakiem, teraz używam własnej metody:

public static ArrayList<String> splitBySingleChar(final char[] s,
        final char splitChar) {
    final ArrayList<String> result = new ArrayList<String>();
    final int length = s.length;
    int offset = 0;
    int count = 0;
    for (int i = 0; i < length; i++) {
        if (s[i] == splitChar) {
            if (count > 0) {
                result.add(new String(s, offset, count));
            }
            offset = i + 1;
            count = 0;
        } else {
            count++;
        }
    }
    if (count > 0) {
        result.add(new String(s, offset, count));
    }
    return result;
}

Użyj „abc” .toCharArray (), aby pobrać tablicę znaków dla ciągu znaków. Na przykład:

String s = "     a bb   ccc  dddd eeeee  ffffff    ggggggg ";
ArrayList<String> result = splitBySingleChar(s.toCharArray(), ' ');

1

Jedną ważną różnicą jest to, że zarówno String.split (), jak i Scanner mogą tworzyć puste ciągi, ale StringTokenizer nigdy tego nie robi.

Na przykład:

String str = "ab cd  ef";

StringTokenizer st = new StringTokenizer(str, " ");
for (int i = 0; st.hasMoreTokens(); i++) System.out.println("#" + i + ": " + st.nextToken());

String[] split = str.split(" ");
for (int i = 0; i < split.length; i++) System.out.println("#" + i + ": " + split[i]);

Scanner sc = new Scanner(str).useDelimiter(" ");
for (int i = 0; sc.hasNext(); i++) System.out.println("#" + i + ": " + sc.next());

Wynik:

//StringTokenizer
#0: ab
#1: cd
#2: ef
//String.split()
#0: ab
#1: cd
#2: 
#3: ef
//Scanner
#0: ab
#1: cd
#2: 
#3: ef

Dzieje się tak, ponieważ separator dla String.split () i Scanner.useDelimiter () nie jest tylko łańcuchem, ale wyrażeniem regularnym. W powyższym przykładzie możemy zamienić separator „” na „+”, aby zachowywały się jak StringTokenizer.


-5

String.split () działa bardzo dobrze, ale ma swoje własne granice, na przykład jeśli chcesz podzielić ciąg, jak pokazano poniżej, w oparciu o symbol pojedynczej lub podwójnej kreski (|), nie działa. W takiej sytuacji możesz użyć StringTokenizer.

ABC | IJK


12
Właściwie możesz podzielić swój przykład za pomocą tylko „ABC | IJK” .split („\\ |”);
Tomo

Jednak "ABC || DEF ||" .split ("\\ |") tak naprawdę nie działa, ponieważ ignoruje dwie końcowe wartości puste, co sprawia, że ​​parsowanie jest bardziej skomplikowane niż powinno.
Armand
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.