Was ist der einfachste / beste / korrekteste Weg, um die Zeichen einer Zeichenfolge in Java zu durchlaufen?

340

StringTokenizer? Konvertieren Sie das Stringin ein char[]und iterieren Sie darüber? Etwas anderes?

Paul Wicks
quelle
3
Siehe auch stackoverflow.com/questions/1527856/…
Rogerdpack
1
Siehe auch stackoverflow.com/questions/8894258/… Benchmarks zeigen, dass String.charAt () für kleine Zeichenfolgen am schnellsten ist und die Verwendung von Reflektion zum direkten Lesen des char-Arrays für große Zeichenfolgen am schnellsten ist.
Jonathan

Antworten:

362

Ich benutze eine for-Schleife, um die Zeichenfolge zu iterieren und charAt()jedes Zeichen dazu zu bringen, sie zu untersuchen. Da der String mit einem Array implementiert ist, ist die charAt()Methode eine Operation mit konstanter Zeit.

String s = "...stuff...";

for (int i = 0; i < s.length(); i++){
    char c = s.charAt(i);        
    //Process char
}

Das würde ich tun. Es scheint mir am einfachsten zu sein.

Was die Korrektheit betrifft, glaube ich nicht, dass es das hier gibt. Es basiert alles auf Ihrem persönlichen Stil.

jjnguy
quelle
3
Inline der Compiler die length () -Methode?
Uri
7
Es könnte inline length () sein, dh die Methode dahinter, die einige Frames aufruft, aber es ist effizienter, dies für (int i = 0, n = s.length (); i <n; i ++) {char zu tun c = s.charAt (i); }
Dave Cheney
32
Überladen Sie Ihren Code für einen winzigen Leistungsgewinn. Bitte vermeiden Sie dies, bis Sie entscheiden, dass dieser Codebereich geschwindigkeitskritisch ist.
schlank
31
Beachten Sie, dass diese Technik Ihnen Zeichen und keine Codepunkte gibt , was bedeutet, dass Sie möglicherweise Ersatzzeichen erhalten.
Gabe
2
@ikh charAt ist nicht O (1) : Wie ist das so? Der Code für String.charAt(int)tut nur value[index]. Ich denke, Sie verwechseln chatAt()etwas anderes, das Ihnen Codepunkte gibt.
Antak
208

Zwei Optionen

for(int i = 0, n = s.length() ; i < n ; i++) { 
    char c = s.charAt(i); 
}

oder

for(char c : s.toCharArray()) {
    // process c
}

Der erste ist wahrscheinlich schneller, der zweite ist wahrscheinlich besser lesbar.

Dave Cheney
quelle
26
plus eins zum Platzieren der s.length () im Initialisierungsausdruck. Wenn jemand nicht weiß warum, liegt dies daran, dass dies nur einmal ausgewertet wird. Wenn es in der Beendigungsanweisung als i <s.length () platziert wurde, wird s.length () bei jeder Schleife aufgerufen.
Dennis
57
Ich dachte, die Compiler-Optimierung hat das für Sie erledigt.
Rhyous
4
@Matthias Mit dem Disassembler der Javap-Klasse können Sie feststellen, dass die wiederholten Aufrufe von s.length () in für den Ausdruck der Schleifenbeendigung tatsächlich vermieden werden. Beachten Sie, dass im Code OP der Aufruf von s.length () im Initialisierungsausdruck enthalten ist, sodass die Sprachsemantik bereits garantiert, dass er nur einmal aufgerufen wird.
Prasope
3
@prasopes Beachten Sie jedoch, dass die meisten Java-Optimierungen zur Laufzeit erfolgen, NICHT in den Klassendateien. Selbst wenn Sie wiederholte Aufrufe von length () gesehen haben, die nicht unbedingt eine Laufzeitstrafe anzeigen.
Isaac
2
@Lasse, der mutmaßliche Grund ist die Effizienz - Ihre Version ruft bei jeder Iteration die length () -Methode auf, während Dave sie im Initialisierer einmal aufruft. Es ist jedoch sehr wahrscheinlich, dass der JIT-Optimierer ("just in time") den zusätzlichen Anruf weg optimiert, sodass es sich wahrscheinlich nur um einen Lesbarkeitsunterschied ohne wirklichen Gewinn handelt.
Steve
90

Beachten Sie, dass die meisten anderen hier beschriebenen Techniken nicht funktionieren , wenn Sie Zeichen außerhalb des BMP (Unicode Basic Multilingual Plane ) verwenden, dh Codepunkte , die außerhalb des Bereichs u0000-uFFFF liegen. Dies kommt nur selten vor, da die Codepunkte außerhalb meist toten Sprachen zugeordnet sind. Es gibt jedoch einige nützliche Zeichen außerhalb davon, zum Beispiel einige Codepunkte, die für die mathematische Notation verwendet werden, und einige, die zum Codieren von Eigennamen auf Chinesisch verwendet werden.

In diesem Fall lautet Ihr Code:

String str = "....";
int offset = 0, strLen = str.length();
while (offset < strLen) {
  int curChar = str.codePointAt(offset);
  offset += Character.charCount(curChar);
  // do something with curChar
}

Die Character.charCount(int)Methode erfordert Java 5+.

Quelle: http://mindprod.com/jgloss/codepoint.html

sk.
quelle
1
Ich verstehe hier nicht, wie Sie etwas anderes als die mehrsprachige Grundebene verwenden. curChar ist immer noch 16 Bit richtig?
Prof. Falken Vertrag verletzt
2
Sie verwenden entweder ein int, um den gesamten Codepunkt zu speichern, oder jedes Zeichen speichert nur eines der beiden Ersatzpaare, die den Codepunkt definieren.
sk.
1
Ich denke, ich muss mich über Codepunkte und Ersatzpaare informieren. Vielen Dank!
Prof. Falken Vertrag verletzt
6
+1, da dies die einzige Antwort zu sein scheint, die für Unicode-Zeichen außerhalb des BMP korrekt ist
Jason S
Schrieb einen Code, um das Konzept der Iteration über Codepunkte (im Gegensatz zu Zeichen) zu veranschaulichen
Emmanuel Oga
26

Ich bin damit einverstanden, dass StringTokenizer hier übertrieben ist. Eigentlich habe ich die obigen Vorschläge ausprobiert und mir die Zeit genommen.

Mein Test war ziemlich einfach: Erstellen Sie einen StringBuilder mit ungefähr einer Million Zeichen, konvertieren Sie ihn in einen String und durchlaufen Sie jeden von ihnen mit charAt () / nach tausendmaliger Konvertierung in ein char-Array / mit einem CharacterIterator (natürlich stellen Sie dies sicher) Mach etwas an der Zeichenfolge, damit der Compiler nicht die gesamte Schleife optimieren kann :-)).

Das Ergebnis auf meinem 2,6 GHz Powerbook (das ist ein Mac :-)) und JDK 1.5:

  • Test 1: charAt + String -> 3138 ms
  • Test 2: In Array konvertierter String -> 9568 ms
  • Test 3: StringBuilder charAt -> 3536 ms
  • Test 4: CharacterIterator und String -> 12151 ms

Da die Ergebnisse erheblich voneinander abweichen, scheint der einfachste Weg auch der schnellste zu sein. Interessanterweise scheint charAt () eines StringBuilder etwas langsamer zu sein als das von String.

Übrigens schlage ich vor, CharacterIterator nicht zu verwenden, da ich den Missbrauch des Zeichens '\ uFFFF' als "Ende der Iteration" für einen wirklich schrecklichen Hack halte. In großen Projekten gibt es immer zwei Leute, die dieselbe Art von Hack für zwei verschiedene Zwecke verwenden, und der Code stürzt wirklich mysteriös ab.

Hier ist einer der Tests:

    int count = 1000;
    ...

    System.out.println("Test 1: charAt + String");
    long t = System.currentTimeMillis();
    int sum=0;
    for (int i=0; i<count; i++) {
        int len = str.length();
        for (int j=0; j<len; j++) {
            if (str.charAt(j) == 'b')
                sum = sum + 1;
        }
    }
    t = System.currentTimeMillis()-t;
    System.out.println("result: "+ sum + " after " + t + "msec");

quelle
1
Dies hat das gleiche Problem hier beschrieben: stackoverflow.com/questions/196830/…
Emmanuel Oga
22

In Java 8 können wir es lösen als:

String str = "xyz";
str.chars().forEachOrdered(i -> System.out.print((char)i));
str.codePoints().forEachOrdered(i -> System.out.print((char)i));

Die Methode chars () gibt ein IntStreamwie in doc erwähnt zurück :

Gibt einen Stream von int Null zurück, der die Zeichenwerte aus dieser Sequenz erweitert. Jedes Zeichen, das einem Ersatzcodepunkt zugeordnet ist, wird nicht interpretiert durchlaufen. Wenn die Sequenz während des Lesens des Streams mutiert ist, ist das Ergebnis undefiniert.

Die Methode gibt codePoints()außerdem ein IntStreamDokument gemäß Dokument zurück:

Gibt einen Strom von Codepunktwerten aus dieser Sequenz zurück. Alle in der Sequenz angetroffenen Ersatzpaare werden wie von Character.toCodePoint kombiniert und das Ergebnis an den Stream übergeben. Alle anderen Codeeinheiten, einschließlich gewöhnlicher BMP-Zeichen, ungepaarter Ersatzzeichen und undefinierter Codeeinheiten, werden auf int-Werte erweitert, die dann an den Stream übergeben werden.

Wie unterscheiden sich char und code point? Wie in diesem Artikel erwähnt:

Unicode 3.1 fügte zusätzliche Zeichen hinzu und erhöhte die Gesamtzahl der Zeichen auf mehr als die 216 Zeichen, die durch ein einzelnes 16-Bit unterschieden werden können char. Daher ist ein charWert nicht mehr eins zu eins der grundlegenden semantischen Einheit in Unicode zugeordnet. JDK 5 wurde aktualisiert, um den größeren Satz von Zeichenwerten zu unterstützen. Anstatt die Definition des charTyps zu ändern , werden einige der neuen Zusatzzeichen durch ein Ersatzpaar aus zwei charWerten dargestellt. Um die Verwirrung bei der Benennung zu verringern, wird ein Codepunkt verwendet, um auf die Nummer zu verweisen, die ein bestimmtes Unicode-Zeichen darstellt, einschließlich zusätzlicher Zeichen.

Endlich warum forEachOrderedund nicht forEach?

Das Verhalten von forEachist explizit nicht deterministisch, wenn der forEachOrderedBenutzer eine Aktion für jedes Element dieses Streams in der Begegnungsreihenfolge des Streams ausführt, wenn der Stream eine definierte Begegnungsreihenfolge hat. So forEachgarantiert nicht , dass die Reihenfolge gehalten werden würde. Überprüfen Sie auch diese Frage für mehr.

Für Unterschied zwischen einem Charakter, ein Codepunkt, eine Glyphe und einem Graphem dieser überprüfen Frage .

akhil_mittal
quelle
21

Hierfür gibt es einige spezielle Klassen:

import java.text.*;

final CharacterIterator it = new StringCharacterIterator(s);
for(char c = it.first(); c != CharacterIterator.DONE; c = it.next()) {
   // process c
   ...
}
Bruno De Fraine
quelle
7
Sieht aus wie ein Overkill für etwas so Einfaches wie das Iterieren über ein unveränderliches Char-Array.
ddimitrov
1
Ich verstehe nicht, warum das übertrieben ist. Iteratoren sind die Java-artigste Art, etwas zu tun ... iterativ. Der StringCharacterIterator muss die Unveränderlichkeit voll ausnutzen.
schlank
2
Stimmen Sie mit @ddimitrov überein - das ist übertrieben. Der einzige Grund, einen Iterator zu verwenden, besteht darin, foreach zu nutzen, das etwas einfacher zu "sehen" ist als eine for-Schleife. Wenn Sie sowieso eine konventionelle for-Schleife schreiben wollen, können Sie auch charAt ()
Rob Gilliam
3
Die Verwendung des Zeicheniterators ist wahrscheinlich die einzig richtige Methode, um Zeichen zu durchlaufen, da Unicode mehr Speicherplatz benötigt, als Java charbereitstellt. Ein Java charenthält 16 Bit und kann Unicode-Zeichen bis U + FFFF enthalten, Unicode gibt jedoch Zeichen bis U + 10FFFF an. Die Verwendung von 16 Bit zum Codieren von Unicode führt zu einer Zeichencodierung mit variabler Länge. Die meisten Antworten auf dieser Seite gehen davon aus, dass die Java-Codierung eine Codierung mit konstanter Länge ist, was falsch ist.
18.
3
@ceving Es scheint nicht, dass ein Zeicheniterator Ihnen bei Nicht-BMP-Zeichen helfen wird
Bruno De Fraine
18

Wenn Sie Guava in Ihrem Klassenpfad haben, ist das Folgende eine ziemlich lesbare Alternative. Guava hat sogar eine ziemlich vernünftige benutzerdefinierte Listenimplementierung für diesen Fall, daher sollte dies nicht ineffizient sein.

for(char c : Lists.charactersOf(yourString)) {
    // Do whatever you want     
}

UPDATE: Wie @Alex bemerkte, gibt es mit Java 8 auch CharSequence#charszu verwenden. Sogar der Typ ist IntStream, daher kann er Zeichen wie den folgenden zugeordnet werden:

yourString.chars()
        .mapToObj(c -> Character.valueOf((char) c))
        .forEach(c -> System.out.println(c)); // Or whatever you want
Touko
quelle
Wenn Sie etwas Komplexes tun müssen, wählen Sie die for-Schleife + Guave, da Sie keine Variablen (z. B. Ganzzahlen und Zeichenfolgen) mutieren können, die außerhalb des Bereichs von forEach innerhalb von forEach definiert sind. Was auch immer sich im forEach befindet, kann auch keine geprüften Ausnahmen auslösen, was manchmal auch ärgerlich ist.
Sabujp
13

Wenn Sie die Codepunkte von a String(siehe diese Antwort ) durchlaufen müssen, können Sie die CharSequence#codePointsin Java 8 hinzugefügte Methode kürzer / besser lesen :

for(int c : string.codePoints().toArray()){
    ...
}

oder verwenden Sie den Stream direkt anstelle einer for-Schleife:

string.codePoints().forEach(c -> ...);

Es gibt auch, CharSequence#charswenn Sie einen Stream der Zeichen wollen (obwohl es ein ist IntStream, da es keine gibt CharStream).

Alex
quelle
3

Ich würde es nicht verwenden, StringTokenizerda es sich um eine der Klassen im JDK handelt, die Legacy sind.

Der Javadoc sagt:

StringTokenizerist eine Legacy-Klasse, die aus Kompatibilitätsgründen beibehalten wird, obwohl von ihrer Verwendung in neuem Code abgeraten wird. Es wird empfohlen, dass jeder, der diese Funktionalität sucht, die Split-Methode von verwendetStringjava.util.regex , stattdessen oder das Paket verwendet.

Alan
quelle
Der String-Tokenizer ist eine absolut gültige (und effizientere) Methode zum Durchlaufen von Token (dh Wörtern in einem Satz). Es ist definitiv ein Overkill zum Durchlaufen von Zeichen. Ich stimme Ihrem Kommentar als irreführend zu.
ddimitrov
3
ddimitrov: Ich verfolge nicht, wie darauf hingewiesen wird, dass StringTokenizer nicht empfohlen wird, EINSCHLIESSLICH eines Zitats aus dem JavaDoc ( java.sun.com/javase/6/docs/api/java/util/StringTokenizer.html ), das als solches angegeben wird irreführend. Upvoted zum Offset.
Powerlord
1
Vielen Dank, Herr Bemrose. Ich gehe davon aus, dass das zitierte Blockzitat kristallklar sein sollte, wobei man wahrscheinlich darauf schließen sollte, dass aktive Fehlerkorrekturen nicht an StringTokenizer übergeben werden.
Alan
2

Wenn Sie Leistung benötigen, müssen Sie diese in Ihrer Umgebung testen . Kein anderer Weg.

Hier Beispielcode:

int tmp = 0;
String s = new String(new byte[64*1024]);
{
    long st = System.nanoTime();
    for(int i = 0, n = s.length(); i < n; i++) {
        tmp += s.charAt(i);
    }
    st = System.nanoTime() - st;
    System.out.println("1 " + st);
}

{
    long st = System.nanoTime();
    char[] ch = s.toCharArray();
    for(int i = 0, n = ch.length; i < n; i++) {
        tmp += ch[i];
    }
    st = System.nanoTime() - st;
    System.out.println("2 " + st);
}
{
    long st = System.nanoTime();
    for(char c : s.toCharArray()) {
        tmp += c;
    }
    st = System.nanoTime() - st;
    System.out.println("3 " + st);
}
System.out.println("" + tmp);

Auf Java online bekomme ich:

1 10349420
2 526130
3 484200
0

Auf Android x86 API 17 bekomme ich:

1 9122107
2 13486911
3 12700778
0
Enyby
quelle
0

Siehe Die Java-Tutorials: Strings .

public class StringDemo {
    public static void main(String[] args) {
        String palindrome = "Dot saw I was Tod";
        int len = palindrome.length();
        char[] tempCharArray = new char[len];
        char[] charArray = new char[len];

        // put original string in an array of chars
        for (int i = 0; i < len; i++) {
            tempCharArray[i] = palindrome.charAt(i);
        } 

        // reverse array of chars
        for (int j = 0; j < len; j++) {
            charArray[j] = tempCharArray[len - 1 - j];
        }

        String reversePalindrome =  new String(charArray);
        System.out.println(reversePalindrome);
    }
}

Geben Sie die Länge ein int lenund verwenden Sie die forSchleife.

Eugene Yokota
quelle
1
Ich fange an, mich ein bisschen spammerisch zu fühlen ... wenn es so ein Wort gibt :). Aber diese Lösung hat auch das hier beschriebene Problem: Dies hat das gleiche Problem, das hier beschrieben wird: stackoverflow.com/questions/196830/…
Emmanuel Oga
0

StringTokenizer ist völlig ungeeignet für die Aufgabe, einen String in seine einzelnen Zeichen zu zerlegen. Mit können String#split()Sie dies einfach tun, indem Sie einen regulären Ausdruck verwenden, der mit nichts übereinstimmt, z.

String[] theChars = str.split("|");

StringTokenizer verwendet jedoch keine regulären Ausdrücke, und Sie können keine Trennzeichenfolge angeben, die mit dem Nichts zwischen den Zeichen übereinstimmt. Es gibt einen niedlichen kleinen Hack, mit dem Sie dasselbe erreichen können: Verwenden Sie die Zeichenfolge selbst als Trennzeichen (wodurch jedes Zeichen darin ein Trennzeichen wird) und lassen Sie die Trennzeichen zurückgeben:

StringTokenizer st = new StringTokenizer(str, str, true);

Ich erwähne diese Optionen jedoch nur, um sie abzulehnen. Beide Techniken unterteilen die ursprüngliche Zeichenfolge in Zeichenfolgen mit einem Zeichen anstelle von Zeichenprimitiven, und beide erfordern einen hohen Overhead in Form der Objekterstellung und der Zeichenfolgenmanipulation. Vergleichen Sie dies mit dem Aufruf von charAt () in einer for-Schleife, die praktisch keinen Overhead verursacht.

Alan Moore
quelle
0

Ausarbeitung dieser Antwort und dieser Antwort .

Die obigen Antworten weisen auf das Problem vieler der hier aufgeführten Lösungen hin, die nicht nach Codepunktwerten iterieren - sie hätten Probleme mit Ersatzzeichen . In den Java-Dokumenten wird das Problem auch hier beschrieben (siehe "Unicode-Zeichendarstellungen"). Wie auch immer, hier ist ein Code, der einige tatsächliche Ersatzzeichen aus dem zusätzlichen Unicode-Satz verwendet und diese wieder in einen String konvertiert . Beachten Sie, dass .toChars () ein Array von Zeichen zurückgibt: Wenn Sie mit Ersatzzeichen arbeiten, haben Sie notwendigerweise zwei Zeichen. Dieser Code sollte für jedes Unicode-Zeichen funktionieren .

    String supplementary = "Some Supplementary: 𠜎𠜱𠝹𠱓";
    supplementary.codePoints().forEach(cp -> 
            System.out.print(new String(Character.toChars(cp))));
Hawkeye Parker
quelle
0

Dieser Beispielcode hilft Ihnen dabei!

import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

public class Solution {
    public static void main(String[] args) {
        HashMap<String, Integer> map = new HashMap<String, Integer>();
        map.put("a", 10);
        map.put("b", 30);
        map.put("c", 50);
        map.put("d", 40);
        map.put("e", 20);
        System.out.println(map);

        Map sortedMap = sortByValue(map);
        System.out.println(sortedMap);
    }

    public static Map sortByValue(Map unsortedMap) {
        Map sortedMap = new TreeMap(new ValueComparator(unsortedMap));
        sortedMap.putAll(unsortedMap);
        return sortedMap;
    }

}

class ValueComparator implements Comparator {
    Map map;

    public ValueComparator(Map map) {
        this.map = map;
    }

    public int compare(Object keyA, Object keyB) {
        Comparable valueA = (Comparable) map.get(keyA);
        Comparable valueB = (Comparable) map.get(keyB);
        return valueB.compareTo(valueA);
    }
}
devDeejay
quelle
0

Daher gibt es normalerweise zwei Möglichkeiten, um in Java durch Zeichenfolgen zu iterieren, die hier in diesem Thread bereits von mehreren Personen beantwortet wurden. Fügen Sie einfach meine Version hinzu, die First verwendet

String s = sc.next() // assuming scanner class is defined above
for(int i=0; i<s.length; i++){
     s.charAt(i)   // This being the first way and is a constant time operation will hardly add any overhead
  }

char[] str = new char[10];
str = s.toCharArray() // this is another way of doing so and it takes O(n) amount of time for copying contents from your string class to character array

Wenn die Leistung auf dem Spiel steht, empfehle ich, die erste in konstanter Zeit zu verwenden. Wenn dies nicht der Fall ist, erleichtert die zweite Ihre Arbeit angesichts der Unveränderlichkeit mit String-Klassen in Java.

Sumit Kapoor
quelle