Qual è il modo più semplice / migliore / più corretto per scorrere i caratteri di una stringa in Java?


341

StringTokenizer? Convertire il Stringin a char[]e iterare su quello? Qualcos'altro?




1
Vedere anche stackoverflow.com/questions/8894258/... Benchmarks mostrare String.charAt () è più veloce per le piccole stringhe, e utilizzando la riflessione di leggere l'array di caratteri direttamente è il più veloce per stringhe di grandi dimensioni.
Jonathan,


Risposte:


363

Uso un ciclo for per iterare la stringa e uso charAt()per far esaminare ogni personaggio da ogni personaggio. Poiché la stringa è implementata con un array, il charAt()metodo è un'operazione a tempo costante.

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

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

Questo è quello che vorrei fare. Mi sembra il più semplice.

Per quanto riguarda la correttezza, non credo che esista qui. È tutto basato sul tuo stile personale.


3
Il compilatore incorpora il metodo length ()?
Uri,

7
potrebbe incorporare length (), che è il metodo dietro che richiama alcuni frame, ma è più efficiente farlo per (int i = 0, n = s.length (); i <n; i ++) {char c = s.charAt (i); }
Dave Cheney,

32
Disordinare il codice per un piccolo guadagno in termini di prestazioni. Si prega di evitare questo fino a quando non si decide che questa area di codice è critica in termini di velocità.
magro,

31
Nota che questa tecnica ti dà caratteri , non punti di codice , il che significa che potresti ottenere surrogati.
Gabe,

2
@ikh charAt is not O (1) : Com'è possibile? Il codice per String.charAt(int)sta semplicemente facendo value[index]. Penso che tu sia confuso chatAt()con qualcos'altro che ti dà punti di codice.
antak

209

Due opzioni

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

o

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

Il primo è probabilmente più veloce, quindi il secondo è probabilmente più leggibile.


26
più uno per posizionare s.length () nell'espressione di inizializzazione. Se qualcuno non sa perché, è perché viene valutato solo una volta se viene inserito nell'istruzione di terminazione come i <s.length (), quindi s.length () viene chiamato ogni volta che viene eseguito il ciclo.
Dennis,

57
Ho pensato che l'ottimizzazione del compilatore se ne occupasse per te.
Rhyous,

4
@Matthias È possibile utilizzare il disassemblatore della classe Javap per vedere che le chiamate ripetute a s.length () in per l'espressione di terminazione del loop sono effettivamente evitate. Si noti che nel codice OP pubblicato la chiamata a s.length () è nell'espressione di inizializzazione, quindi la semantica del linguaggio garantisce già che verrà chiamata una sola volta.
prasopes

3
@prasopes Si noti tuttavia che la maggior parte delle ottimizzazioni Java si verificano in fase di esecuzione, NON nei file di classe. Anche se hai visto ripetute chiamate a length () che non indicano necessariamente una penalità di runtime.
Isacco,

2
@Lasse, la ragione putativa è per l'efficienza: la tua versione chiama il metodo length () su ogni iterazione, mentre Dave lo chiama una volta nell'inizializzatore. Detto questo, è molto probabile che l'ottimizzatore JIT ("just in time") ottimizzi la chiamata extra, quindi è probabilmente solo una differenza di leggibilità per nessun guadagno reale.
Steve,

90

Nota che la maggior parte delle altre tecniche qui descritte si interrompe se hai a che fare con caratteri al di fuori del BMP (Unicode Basic Multilingual Plane ), ovvero punti di codice che sono al di fuori dell'intervallo u0000-uFFFF. Questo accadrà solo raramente, poiché i punti di codice al di fuori di questo sono per lo più assegnati a lingue morte. Ma ci sono alcuni caratteri utili al di fuori di questo, ad esempio alcuni punti di codice usati per la notazione matematica e alcuni usati per codificare nomi propri in cinese.

In tal caso il tuo codice sarà:

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

Il Character.charCount(int)metodo richiede Java 5+.

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


1
Non capisco come usi niente tranne il piano multilingue di base qui. curChar ha ancora 16 bit a destra?
contratto del Prof. Falken è stato violato

2
O si utilizza un int per memorizzare l'intero punto di codice, altrimenti ogni carattere memorizzerà solo una delle due coppie surrogate che definiscono il punto di codice.
sk.

1
Penso di aver bisogno di leggere su punti di codice e coppie surrogate. Grazie!
contratto del Prof. Falken è stato violato

6
+1 poiché questa sembra essere l'unica risposta corretta per i caratteri Unicode al di fuori del BMP
Jason S

Ha scritto del codice per illustrare il concetto di iterare sui punti di codice (al contrario dei caratteri): gist.github.com/EmmanuelOga/…
Emmanuel Oga

26

Sono d'accordo che StringTokenizer è eccessivo qui. In realtà ho provato i suggerimenti sopra e mi sono preso il tempo.

Il mio test è stato abbastanza semplice: creare un StringBuilder con circa un milione di caratteri, convertirlo in una stringa e attraversare ciascuno di essi con charAt () / dopo la conversione in un array di caratteri / con un CharacterIterator mille volte (ovviamente assicurandosi di fai qualcosa sulla stringa in modo che il compilatore non possa ottimizzare l'intero ciclo :-)).

Il risultato sul mio Powerbook a 2,6 GHz (che è un mac :-)) e JDK 1.5:

  • Test 1: charAt + String -> 3138msec
  • Test 2: stringa convertita in array -> 9568msec
  • Test 3: StringBuilder charAt -> 3536msec
  • Test 4: CharacterIterator e String -> 12151msec

Poiché i risultati sono significativamente diversi, anche il modo più semplice sembra essere il più veloce. È interessante notare che charAt () di StringBuilder sembra essere leggermente più lento di quello di String.

A proposito, suggerisco di non usare CharacterIterator poiché considero il suo abuso del carattere '\ uFFFF' come "fine dell'iterazione" un hack davvero terribile. Nei grandi progetti ci sono sempre due ragazzi che usano lo stesso tipo di hack per due scopi diversi e il codice si arresta in modo davvero misterioso.

Ecco uno dei test:

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

1
Questo ha lo stesso problema delineato qui: stackoverflow.com/questions/196830/…
Emmanuel Oga,

22

In Java 8 possiamo risolverlo come:

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

Il metodo chars () restituisce un IntStreamcome menzionato nel doc :

Restituisce un flusso di int zero che estende i valori del carattere da questa sequenza. Qualsiasi carattere associato a un punto di codice surrogato viene passato senza interpretazione. Se la sequenza viene modificata durante la lettura dello stream, il risultato non è definito.

Il metodo codePoints()restituisce anche un IntStreamsecondo documento:

Restituisce un flusso di valori di punti di codice da questa sequenza. Tutte le coppie surrogate incontrate nella sequenza vengono combinate come da Character.toCodePoint e il risultato viene passato allo stream. Qualsiasi altra unità di codice, inclusi i normali caratteri BMP, surrogati non accoppiati e unità di codice non definite, viene estesa a zero ai valori int che vengono quindi passati allo stream.

In cosa differiscono char e code point? Come menzionato in questo articolo:

Unicode 3.1 ha aggiunto caratteri supplementari, portando il numero totale di caratteri a oltre i 216 caratteri che possono essere distinti da un singolo 16 bit char. Pertanto, un charvalore non ha più un mapping uno a uno all'unità semantica fondamentale in Unicode. JDK 5 è stato aggiornato per supportare il set più ampio di valori di carattere. Invece di modificare la definizione del chartipo, alcuni dei nuovi caratteri supplementari sono rappresentati da una coppia surrogata di due charvalori. Per ridurre la confusione dei nomi, verrà utilizzato un punto di codice per fare riferimento al numero che rappresenta un particolare carattere Unicode, inclusi quelli supplementari.

Finalmente perché forEachOrderede no forEach?

Il comportamento di forEachè esplicitamente non deterministico dove come forEachOrderedesegue un'azione per ciascun elemento di questo flusso, nell'ordine di incontro del flusso se il flusso ha un ordine di incontro definito. Quindi forEachnon garantisce che l'ordine verrà mantenuto. Controlla anche questa domanda per ulteriori informazioni.

Per la differenza tra un carattere, un punto di codice, un glifo e un grafema controlla questa domanda .


21

Ci sono alcune lezioni dedicate per questo:

import java.text.*;

final CharacterIterator it = new StringCharacterIterator(s);
for(char c = it.first(); c != CharacterIterator.DONE; c = it.next()) {
   // process c
   ...
}

7
Sembra esagerato per qualcosa di semplice come l'iterazione su un array di caratteri immutabile.
ddimitrov,

1
Non vedo perché questo sia eccessivo. Gli iteratori sono il modo più java di fare qualsiasi cosa ... iterativo. StringCharacterIterator è destinato a sfruttare appieno l'immutabilità.
magro,

2
Concordo con @ddimitrov: questo è eccessivo. L'unico motivo per usare un iteratore sarebbe di sfruttare foreach, che è un po 'più facile da "vedere" di un ciclo for. Se hai intenzione di scrivere un ciclo convenzionale per comunque, allora potresti anche usare charAt ()
Rob Gilliam

3
L'uso dell'iteratore di caratteri è probabilmente l'unico modo corretto per scorrere i caratteri, poiché Unicode richiede più spazio di quello charfornito da Java . Un Java charcontiene 16 bit e può contenere caratteri Unicode su U + FFFF ma Unicode specifica caratteri fino a U + 10FFFF. L'uso di 16 bit per codificare Unicode produce una codifica di caratteri a lunghezza variabile. La maggior parte delle risposte in questa pagina presuppone che la codifica Java sia una codifica di lunghezza costante, che è errata.
ceving il

3
@ceving Non sembra che un iteratore di personaggi ti aiuterà con personaggi non BMP: oracle.com/us/technologies/java/supplementary-142654.html
Bruno De Fraine,

18

Se hai Guava sul tuo percorso di classe, la seguente è un'alternativa piuttosto leggibile. Guava ha anche un'implementazione dell'elenco personalizzata abbastanza ragionevole per questo caso, quindi questo non dovrebbe essere inefficiente.

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

AGGIORNAMENTO: Come notato da @Alex, con Java 8 c'è anche CharSequence#charsda usare. Anche il tipo è IntStream, quindi può essere mappato su caratteri come:

yourString.chars()
        .mapToObj(c -> Character.valueOf((char) c))
        .forEach(c -> System.out.println(c)); // Or whatever you want

Se è necessario eseguire operazioni complesse, utilizzare for loop + guava poiché non è possibile mutare le variabili (ad es. Numeri interi e stringhe) definite al di fuori dell'ambito di forEach all'interno di forEach. Qualunque cosa sia dentro ogni for, inoltre, non può generare eccezioni verificate, quindi a volte è anche fastidioso.
sabujp,

13

Se è necessario scorrere i punti di codice di un String(vedere questa risposta ), un modo più breve / più leggibile consiste nell'utilizzare il CharSequence#codePointsmetodo aggiunto in Java 8:

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

o usando direttamente lo stream invece di un ciclo for:

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

C'è anche CharSequence#charsse vuoi un flusso di personaggi (anche se è un IntStream, poiché non c'è CharStream).


3

Non userei StringTokenizerperché è una delle classi del JDK che è legacy.

Il javadoc dice:

StringTokenizerè una classe legacy che viene conservata per motivi di compatibilità sebbene il suo utilizzo sia sconsigliato nel nuovo codice. Si consiglia a chiunque cerchi questa funzionalità di utilizzare invece il metodo split Stringo il java.util.regexpacchetto.


Il tokenizer di stringhe è un modo perfettamente valido (e più efficiente) per iterare su token (cioè parole in una frase). È sicuramente un overkill per iterare su caratteri. Sto valutando il tuo commento come fuorviante.
ddimitrov,

3
ddimitrov: non sto seguendo come sottolineare che StringTokenizer non è raccomandato COMPRENDENDO una citazione da JavaDoc ( java.sun.com/javase/6/docs/api/java/util/StringTokenizer.html ) per affermare che tale è ingannevole. Eseguito l'upgrade per compensare.
Powerlord,

1
Grazie Mr. Bemrose ... Immagino che la citazione del blocco citata avrebbe dovuto essere cristallina, dove si dovrebbe probabilmente dedurre che le correzioni di bug attive non verranno commesse su StringTokenizer.
Alan,

2

Se hai bisogno di prestazioni, devi testare sul tuo ambiente. Nessun altro modo.

Ecco un esempio di codice:

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

Su Java online ottengo:

1 10349420
2 526130
3 484200
0

Su Android x86 API 17 ottengo:

1 9122107
2 13486911
3 12700778
0

0

Vedi Tutorial Java: Stringhe .

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

Inserisci la lunghezza int lene usa il forloop.


1
Sto iniziando a sentirmi un po 'spammerish ... se c'è una parola del genere :). Ma questa soluzione ha anche il problema delineato qui: Questo ha lo stesso problema delineato qui: stackoverflow.com/questions/196830/…
Emmanuel Oga,

0

StringTokenizer è totalmente inadatto al compito di spezzare una stringa nei suoi singoli caratteri. Con String#split()te puoi farlo facilmente usando un regex che non corrisponde a nulla, ad esempio:

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

Ma StringTokenizer non usa regex e non esiste una stringa del delimitatore che puoi specificare che non corrisponderà al nulla tra i caratteri. V'è un poco carino incidere è possibile utilizzare per realizzare la stessa cosa: utilizzare la stringa stessa come delimitatore per la stringa (rendendo ogni personaggio in esso un delimitatore) e farlo tornare i delimitatori:

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

Tuttavia, cito solo queste opzioni allo scopo di respingerle. Entrambe le tecniche suddividono la stringa originale in stringhe di un carattere anziché in caratteri primitivi, ed entrambe comportano un notevole sovraccarico sotto forma di creazione di oggetti e manipolazione di stringhe. Confrontalo con la chiamata a charAt () in un ciclo for, che non comporta praticamente alcun sovraccarico.


0

Elaborazione di questa risposta e questa risposta .

Le risposte sopra evidenziano il problema di molte delle soluzioni qui che non ripetono il valore del punto di codice: avrebbero problemi con qualsiasi carattere surrogato . Anche i documenti java delineano qui il problema (vedi "Rappresentazioni dei caratteri Unicode"). In ogni caso, ecco qualche codice che utilizza alcuni caratteri surrogati reale dal set Unicode supplementare, e li converte indietro in una stringa. Nota che .toChars () restituisce una matrice di caratteri: se hai a che fare con surrogati, avrai necessariamente due caratteri. Questo codice dovrebbe funzionare per qualsiasi carattere Unicode.

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

0

Questo codice di esempio ti aiuterà!

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

0

Quindi in genere ci sono due modi per iterare attraverso la stringa in Java che ha già ricevuto risposta da più persone qui in questo thread, solo aggiungendo la mia versione di essa per prima cosa sta usando

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

Se la prestazione è in gioco, allora consiglierò di usare il primo a tempo costante, se non lo è con il secondo, semplifica il tuo lavoro considerando l'immutabilità con le classi di stringhe in Java.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.