Quelle est la façon la plus simple / la meilleure / la plus correcte d'itérer à travers les caractères d'une chaîne en Java?


341

StringTokenizer? Convertir le Stringen un char[]et répéter cela? Autre chose?




1
Voir aussi stackoverflow.com/questions/8894258/… Les repères montrent que String.charAt () est le plus rapide pour les petites chaînes, et l'utilisation de la réflexion pour lire directement le tableau de caractères est plus rapide pour les grandes chaînes.
Jonathan


Réponses:


363

J'utilise une boucle for pour itérer la chaîne et charAt()pour obtenir que chaque caractère l'examine. Puisque la chaîne est implémentée avec un tableau, la charAt()méthode est une opération à temps constant.

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

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

Voilà ce que je ferais. Cela me semble le plus simple.

En ce qui concerne l'exactitude, je ne crois pas que cela existe ici. Tout est basé sur votre style personnel.


3
Le compilateur intègre-t-il la méthode length ()?
Uri

7
cela pourrait inline length (), c'est-à-dire hisser la méthode derrière qui appelle quelques trames, mais il est plus efficace de le faire pour (int i = 0, n = s.length (); i <n; i ++) {char c = s.charAt (i); }
Dave Cheney

32
Encombrer votre code pour un gain de performances infime . Veuillez éviter cela jusqu'à ce que vous décidiez que ce domaine de code est critique en termes de vitesse.
slim

31
Notez que cette technique vous donne des caractères , pas des points de code , ce qui signifie que vous pouvez obtenir des substituts.
Gabe

2
@ikh charAt n'est pas O (1) : Comment en est-il ainsi? Le code de String.charAt(int)fait simplement value[index]. Je pense que vous confondez chatAt()avec autre chose qui vous donne des points de code.
antak

209

Deux options

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

ou

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

Le premier est probablement plus rapide, puis le 2e est probablement plus lisible.


26
plus un pour placer le s.length () dans l'expression d'initialisation. Si quelqu'un ne sait pas pourquoi, c'est parce que cela n'est évalué qu'une seule fois où s'il a été placé dans l'instruction de terminaison comme i <s.length (), alors s.length () serait appelé chaque fois qu'il bouclait.
Dennis

57
Je pensais que l'optimisation du compilateur s'en occupait pour vous.
Rhyous

4
@Matthias Vous pouvez utiliser le désassembleur de classe Javap pour voir que les appels répétés à s.length () dans pour l'expression de terminaison de boucle sont en effet évités. Notez que dans le code OP publié, l'appel à s.length () se trouve dans l'expression d'initialisation, de sorte que la sémantique du langage garantit déjà qu'il ne sera appelé qu'une seule fois.
prasopes

3
@prasopes Notez cependant que la plupart des optimisations java se produisent lors de l'exécution, PAS dans les fichiers de classe. Même si vous avez vu des appels répétés à length (), cela n'indique pas nécessairement une pénalité d'exécution.
Isaac

2
@Lasse, la raison putative est pour l'efficacité - votre version appelle la méthode length () à chaque itération, alors que Dave l'appelle une fois dans l'initialiseur. Cela dit, il est très probable que l'optimiseur JIT ("juste à temps") optimise l'appel supplémentaire, il ne s'agit donc probablement que d'une différence de lisibilité sans gain réel.
Steve

90

Notez que la plupart des autres techniques décrites ici se décomposent si vous traitez avec des caractères en dehors du BMP (Unicode Basic Multilingual Plane ), c'est-à-dire des points de code qui sont en dehors de la plage u0000-uFFFF. Cela ne se produira que rarement, car les points de code en dehors de celui-ci sont principalement affectés à des langues mortes. Mais il y a quelques caractères utiles en dehors de cela, par exemple certains points de code utilisés pour la notation mathématique, et certains utilisés pour coder les noms propres en chinois.

Dans ce cas, votre code sera:

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

La Character.charCount(int)méthode nécessite Java 5+.

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


1
Je ne comprends pas comment vous utilisez autre chose que le plan multilingue de base ici. curChar est toujours à 16 bits?
contrat du professeur Falken a été rompu

2
Soit vous utilisez un int pour stocker l'intégralité du point de code, soit chaque caractère ne stockera qu'une seule des deux paires de substitution qui définissent le point de code.
sk.

1
Je pense que je dois lire sur les points de code et les paires de substitution. Merci!
contrat du professeur Falken a été

6
+1 car cela semble être la seule réponse correcte pour les caractères Unicode en dehors du BMP
Jason S

A écrit du code pour illustrer le concept d'itération sur les points de code (par opposition aux caractères): gist.github.com/EmmanuelOga/…
Emmanuel Oga

26

Je suis d'accord que StringTokenizer est exagéré ici. En fait, j'ai essayé les suggestions ci-dessus et j'ai pris le temps.

Mon test était assez simple: créer un StringBuilder avec environ un million de caractères, le convertir en une chaîne et parcourir chacun d'eux avec charAt () / après la conversion en un tableau de char / avec un CharacterIterator mille fois (bien sûr en veillant à faire quelque chose sur la chaîne pour que le compilateur ne puisse pas optimiser toute la boucle :-)).

Le résultat sur mon Powerbook 2,6 GHz (c'est un mac :-)) et JDK 1.5:

  • Test 1: charAt + String -> 3138msec
  • Test 2: chaîne convertie en tableau -> 9568 msec
  • Test 3: StringBuilder charAt -> 3536msec
  • Test 4: CharacterIterator et String -> 12151msec

Les résultats étant sensiblement différents, le moyen le plus simple semble également être le plus rapide. Fait intéressant, charAt () d'un StringBuilder semble être légèrement plus lent que celui de String.

BTW Je suggère de ne pas utiliser CharacterIterator car je considère son abus du caractère '\ uFFFF' comme une "fin d'itération" un hack vraiment horrible. Dans les grands projets, il y a toujours deux gars qui utilisent le même type de hack à deux fins différentes et le code se bloque vraiment mystérieusement.

Voici l'un des 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");

1
Cela a le même problème décrit ici: stackoverflow.com/questions/196830/…
Emmanuel Oga

22

Dans Java 8, nous pouvons le résoudre comme:

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

La méthode chars () renvoie un IntStreamcomme mentionné dans la doc :

Renvoie un flux d'int entier étendant les valeurs char de cette séquence. Tout caractère mappé à un point de code de substitution est transmis sans interprétation. Si la séquence est mutée pendant la lecture du flux, le résultat n'est pas défini.

La méthode codePoints()renvoie également un IntStreamdocument conforme à la doc:

Renvoie un flux de valeurs de points de code à partir de cette séquence. Toutes les paires de substitution rencontrées dans la séquence sont combinées comme si par Character.toCodePoint et le résultat est transmis au flux. Toutes les autres unités de code, y compris les caractères BMP ordinaires, les substituts non appariés et les unités de code non définies, sont étendues à zéro aux valeurs int qui sont ensuite transmises au flux.

En quoi le caractère et le point de code sont-ils différents? Comme mentionné dans cet article:

Unicode 3.1 a ajouté des caractères supplémentaires, ce qui porte le nombre total de caractères à plus de 216 caractères qui peuvent être distingués par un seul 16 bits char. Par conséquent, une charvaleur n'a plus de mappage un à un avec l'unité sémantique fondamentale dans Unicode. JDK 5 a été mis à jour pour prendre en charge le plus grand ensemble de valeurs de caractères. Au lieu de changer la définition du chartype, certains des nouveaux caractères supplémentaires sont représentés par une paire de substitution de deux charvaleurs. Pour réduire la confusion de dénomination, un point de code sera utilisé pour faire référence au nombre qui représente un caractère Unicode particulier, y compris les caractères supplémentaires.

Enfin pourquoi forEachOrderedet non forEach?

Le comportement de forEachest explicitement non déterministe alors que le forEachOrderedeffectue une action pour chaque élément de ce flux, dans l' ordre de rencontre du flux si le flux a un ordre de rencontre défini. forEachNe garantit donc pas que la commande sera conservée. Consultez également cette question pour en savoir plus.

Pour la différence entre un caractère, un point de code, un glyphe et un graphème, vérifiez cette question .


21

Il existe des classes dédiées à cela:

import java.text.*;

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

7
Ressemble à une surpuissance pour quelque chose d'aussi simple que d'itérer sur un tableau de caractères immuable.
ddimitrov

1
Je ne vois pas pourquoi c'est exagéré. Les itérateurs sont le moyen le plus java de faire quoi que ce soit ... itératif. Le StringCharacterIterator est destiné à tirer pleinement parti de l'immuabilité.
slim

2
D'accord avec @ddimitrov - c'est exagéré. La seule raison d'utiliser un itérateur serait de profiter de foreach, qui est un peu plus facile à "voir" qu'une boucle for. Si vous voulez quand même écrire une boucle for conventionnelle, vous pourriez tout aussi bien utiliser charAt ()
Rob Gilliam

3
L'utilisation de l'itérateur de caractères est probablement la seule façon correcte d'itérer sur les caractères, car Unicode nécessite plus d'espace qu'un Java n'en charfournit. Un Java charcontient 16 bits et peut contenir des caractères Unicode jusqu'à U + FFFF mais Unicode spécifie des caractères jusqu'à U + 10FFFF. L'utilisation de 16 bits pour coder Unicode entraîne un codage de caractères de longueur variable. La plupart des réponses sur cette page supposent que l'encodage Java est un encodage de longueur constante, ce qui est faux.
ceving

3
@ceving Il ne semble pas qu'un itérateur de personnage puisse vous aider avec les caractères non BMP: oracle.com/us/technologies/java/supplementary-142654.html
Bruno De Fraine

18

Si vous avez Guava sur votre chemin de classe, ce qui suit est une alternative assez lisible. La goyave a même une implémentation de liste personnalisée assez sensible dans ce cas, donc cela ne devrait pas être inefficace.

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

MISE À JOUR: Comme l'a noté @Alex, avec Java 8, il y a aussi CharSequence#charsà utiliser. Même le type est IntStream, il peut donc être mappé à des caractères comme:

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

Si vous devez faire quelque chose de complexe, optez pour la boucle for + goyave car vous ne pouvez pas muter les variables (par exemple, les entiers et les chaînes) définies en dehors de la portée de forEach à l'intérieur de forEach. Tout ce qui se trouve à l'intérieur de forEach ne peut pas non plus lever d'exceptions vérifiées, c'est donc parfois ennuyeux également.
sabujp

13

Si vous devez parcourir les points de code d'un String(voir cette réponse ), un moyen plus court / plus lisible consiste à utiliser la CharSequence#codePointsméthode ajoutée dans Java 8:

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

ou en utilisant le flux directement au lieu d'une boucle for:

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

Il y en a aussi CharSequence#charssi vous voulez un flux de personnages (bien que ce soit un IntStream, puisqu'il n'y en a pas CharStream).


3

Je n'utiliserais pas StringTokenizer car c'est l'une des classes du JDK qui est héritée.

Le javadoc dit:

StringTokenizerest une classe héritée qui est conservée pour des raisons de compatibilité, bien que son utilisation soit déconseillée dans le nouveau code. Il est recommandé que toute personne recherchant cette fonctionnalité utilise à la place la méthode de fractionnement Stringou le java.util.regexpackage.


Le tokenizer de chaîne est un moyen parfaitement valide (et plus efficace) pour itérer sur des jetons (c'est-à-dire des mots dans une phrase). C'est certainement une surpuissance pour itérer sur des caractères. Je sous-estime votre commentaire comme étant trompeur.
ddimitrov

3
ddimitrov: Je ne suis pas en train de comprendre comment souligner que StringTokenizer n'est pas recommandé INCLUANT une citation du JavaDoc ( java.sun.com/javase/6/docs/api/java/util/StringTokenizer.html ) pour cela en indiquant comme tel est trompeur. A voté pour compenser.
Powerlord

1
Merci M. Bemrose ... Je suppose que la citation de bloc citée aurait dû être limpide, où l'on devrait probablement déduire que les corrections de bogues actives ne seront pas validées dans StringTokenizer.
Alan

2

Si vous avez besoin de performances, vous devez tester sur votre environnement. Pas d'autre chemin.

Voici un exemple de code:

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

Sur Java en ligne, je reçois:

1 10349420
2 526130
3 484200
0

Sur Android x86 API 17, j'obtiens:

1 9122107
2 13486911
3 12700778
0

0

Voir Les tutoriels Java: chaînes .

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

Mettez la longueur dans int lenet utilisez la forboucle.


1
Je commence à me sentir un peu spammeur ... s'il y a un tel mot :). Mais cette solution a également le problème décrit ici: Cela a le même problème décrit ici: stackoverflow.com/questions/196830/…
Emmanuel Oga

0

StringTokenizer est totalement inadapté à la tâche de décomposer une chaîne en ses caractères individuels. Avec String#split()vous pouvez le faire facilement en utilisant une expression régulière qui ne correspond à rien, par exemple:

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

Mais StringTokenizer n'utilise pas d'expressions régulières, et il n'y a pas de chaîne de délimiteur que vous pouvez spécifier qui correspondra au rien entre les caractères. Il y a un petit hack mignon que vous pouvez utiliser pour accomplir la même chose: utilisez la chaîne elle-même comme chaîne de délimiteur (en faisant de chaque caractère un délimiteur) et demandez-lui de renvoyer les délimiteurs:

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

Cependant, je ne mentionne ces options que dans le but de les rejeter. Les deux techniques décomposent la chaîne d'origine en chaînes d'un caractère au lieu de primitives char, et impliquent toutes deux une surcharge importante sous forme de création d'objet et de manipulation de chaîne. Comparez cela à l'appel de charAt () dans une boucle for, qui n'entraîne pratiquement pas de surcharge.


0

Elaborer cette réponse et cette réponse .

Les réponses ci-dessus soulignent le problème de nombreuses solutions ici qui ne sont pas itérées par la valeur du point de code - elles auraient des problèmes avec les caractères de substitution . Les documents java décrivent également le problème ici (voir "Représentations de caractères Unicode"). Quoi qu'il en soit, voici un code qui utilise des caractères de substitution réelle de l'ensemble Unicode supplémentaire, et les convertit en arrière à une chaîne. Notez que .toChars () renvoie un tableau de caractères: si vous avez affaire à des substituts, vous aurez nécessairement deux caractères. Ce code devrait fonctionner pour tout caractère Unicode.

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

0

Cet exemple de code vous aidera!

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

Donc, généralement, il y a deux façons d'itérer à travers une chaîne en java à laquelle plusieurs personnes ont déjà répondu dans ce fil, en ajoutant simplement ma version de celui-ci

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

Si les performances sont en jeu, je recommanderai d'utiliser la première en temps constant, si ce n'est pas le cas, la seconde facilite votre travail compte tenu de l'immuabilité avec les classes de chaînes en java.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.