Qual é a maneira mais fácil / melhor / mais correta de iterar os caracteres de uma string em Java?

340

StringTokenizer? Converta Stringem a char[]e itere sobre isso? Algo mais?

Paul Wicks
fonte
duplicado: stackoverflow.com/questions/3925130/…
Emmanuel Oga
3
Veja também stackoverflow.com/questions/1527856/…
rogerdpack 16/15
11
Consulte também stackoverflow.com/questions/8894258/… Benchmarks show String.charAt () é o mais rápido para seqüências de caracteres pequenas, e usar reflexão para ler diretamente a matriz de caracteres é mais rápido para seqüências de caracteres grandes.
22415 Jonathan
Consulte também Como transformar uma String em um fluxo em java?
21416 Dangermouse

Respostas:

362

Eu uso um loop for para iterar a string e uso charAt()para que cada caractere a examine. Como a String é implementada com uma matriz, o charAt()método é uma operação de tempo constante.

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

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

Isso é o que eu faria. Parece o mais fácil para mim.

No que diz respeito à correção, não acredito que exista aqui. Tudo é baseado no seu estilo pessoal.

jjnguy
fonte
3
O compilador alinha o método length ()?
Uri
7
pode inline length (), que é içar o método por trás dessa chamada de alguns quadros, mas é mais eficiente fazer isso para (int i = 0, n = s.length (); i <n; i ++) {char c = s.charAt (i); }
Dave Cheney
32
Organizando seu código para obter um pequeno ganho de desempenho. Evite isso até que você decida que essa área de código é crítica à velocidade.
magro
31
Observe que essa técnica fornece caracteres , não pontos de código , o que significa que você pode obter substitutos.
Gabe
2
@ikh charAt não é O (1) : como é isso? O código para String.charAt(int)é apenas fazer value[index]. Eu acho que você está confundindo chatAt()com outra coisa que lhe dá pontos de código.
antak
208

Duas opções

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

ou

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

O primeiro é provavelmente mais rápido, e o segundo é provavelmente mais legível.

Dave Cheney
fonte
26
mais um para colocar o s.length () na expressão de inicialização. Se alguém não sabe o porquê, é porque isso é avaliado apenas uma vez em que se foi colocado na instrução de terminação como i <s.length (), então s.length () será chamado toda vez que ele fizer um loop.
Dennis
57
Eu pensei que a otimização do compilador cuidava disso para você.
Rhyous
4
@ Matthias Você pode usar o desmontador da classe Javap para ver se as chamadas repetidas para s.length () na expressão de terminação de loop são realmente evitadas. Observe que no código OP postado a chamada para s.length () está na expressão de inicialização, portanto a semântica do idioma já garante que será chamada apenas uma vez.
Prasopes
3
@prasopes Observe que a maioria das otimizações de java acontece no tempo de execução, NÃO nos arquivos de classe. Mesmo se você viu chamadas repetidas para length () que não indicam uma penalidade de tempo de execução, necessariamente.
Isaac
2
@Lasse, o motivo putativo é a eficiência - sua versão chama o método length () em todas as iterações, enquanto Dave o chama uma vez no inicializador. Dito isso, é muito provável que o otimizador JIT ("just in time") otimize a distância extra, portanto é provável que seja apenas uma diferença de legibilidade para nenhum ganho real.
Steve
90

Observe que a maioria das outras técnicas descritas aqui se decompõe se você estiver lidando com caracteres fora do BMP (Unicode Basic Multilingual Plane ), ou seja, pontos de código que estão fora do intervalo u0000-uFFFF. Isso só acontecerá raramente, uma vez que os pontos de código fora disso são atribuídos principalmente a idiomas mortos. Mas existem alguns caracteres úteis fora disso, por exemplo, alguns pontos de código usados ​​para notação matemática e outros usados ​​para codificar nomes próprios em chinês.

Nesse caso, seu código será:

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

O Character.charCount(int)método requer Java 5+.

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

sk.
fonte
11
Não entendo como você usa nada além do Plano Multilíngue Básico aqui. curChar ainda está 16 bits certo?
O contrato do Prof. Falken violou
2
Você usa um int para armazenar o ponto de código inteiro, ou então cada caractere armazena apenas um dos dois pares substitutos que definem o ponto de código.
sk.
11
Eu acho que preciso ler sobre pontos de código e pares substitutos. Obrigado!
O contrato do Prof. Falken violou
6
+1 uma vez que esta parece ser a única resposta que é correto para Unicode carboniza fora do BMP
Jason S
Escreveu algum código para ilustrar o conceito de iteração sobre pontos de código (em oposição a caracteres): gist.github.com/EmmanuelOga/...
Emmanuel OGA
26

Concordo que o StringTokenizer é um exagero aqui. Na verdade, eu tentei as sugestões acima e aproveitei o tempo.

Meu teste foi bastante simples: crie um StringBuilder com cerca de um milhão de caracteres, converta-o em String e percorra cada um deles com charAt () / depois de converter em um array de caracteres / com um CharacterIterator milhares de vezes (é claro, certifique-se de faça algo na string para que o compilador não possa otimizar todo o loop :-)).

O resultado no meu Powerbook de 2,6 GHz (que é um mac :-)) e no JDK 1.5:

  • Teste 1: charAt + String -> 3138msec
  • Teste 2: String convertida em array -> 9568msec
  • Teste 3: StringBuilder charAt -> 3536msec
  • Teste 4: CharacterIterator e String -> 12151msec

Como os resultados são significativamente diferentes, a maneira mais direta também parece ser a mais rápida. Curiosamente, charAt () de um StringBuilder parece ser um pouco mais lento que o de String.

BTW, sugiro não usar o CharacterIterator, pois considero o abuso do caracter '\ uFFFF' como "final da iteração" um truque realmente terrível. Em grandes projetos, sempre existem dois caras que usam o mesmo tipo de hack para dois propósitos diferentes e o código trava muito misteriosamente.

Aqui está um dos testes:

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

fonte
11
Isso tem o mesmo problema descrito aqui: stackoverflow.com/questions/196830/…
Emmanuel Oga
22

No Java 8 , podemos resolvê-lo da seguinte maneira:

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

O método chars () retorna um IntStreamcomo mencionado no doc :

Retorna um fluxo de int estendendo zero os valores de caracteres dessa sequência. Qualquer caractere mapeado para um ponto de código substituto é passado não interpretado. Se a sequência for alterada enquanto o fluxo estiver sendo lido, o resultado será indefinido.

O método codePoints() também retorna um IntStreamconforme o documento:

Retorna um fluxo de valores de pontos de código dessa sequência. Quaisquer pares substitutos encontrados na sequência são combinados como se por Character.toCodePoint e o resultado é passado para o fluxo. Quaisquer outras unidades de código, incluindo caracteres BMP comuns, substitutos não emparelhados e unidades de código indefinidas, são estendidos em zero aos valores int que são passados ​​para o fluxo.

Qual a diferença entre char e code point? Como mencionado em neste artigo:

O Unicode 3.1 adicionou caracteres suplementares, elevando o número total de caracteres para mais do que os 216 caracteres que podem ser distinguidos por um único 16 bits char. Portanto, um charvalor não possui mais um mapeamento individual para a unidade semântica fundamental no Unicode. O JDK 5 foi atualizado para suportar o conjunto maior de valores de caracteres. Em vez de alterar a definição do chartipo, alguns dos novos caracteres suplementares são representados por um par substituto de dois charvalores. Para reduzir a confusão de nomes, um ponto de código será usado para se referir ao número que representa um caractere Unicode específico, incluindo caracteres adicionais.

Finalmente, por que forEachOrderede não forEach?

O comportamento de forEaché explicitamente não determinístico, onde, quando ele forEachOrderedexecuta uma ação para cada elemento desse fluxo, na ordem de encontro do fluxo, se o fluxo tiver uma ordem de encontro definida. Portanto forEach, não garante que o pedido seja mantido. Verifique também esta pergunta para mais informações.

Para a diferença entre um caractere, um ponto de código, um glifo e um grafema, verifique esta questão .

akhil_mittal
fonte
21

Existem algumas aulas dedicadas para isso:

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
fonte
7
Parece um exagero para algo tão simples quanto iterar sobre uma matriz de caracteres imutável.
Ddimitrov 13/10/08
11
Não vejo por que isso é um exagero. Os iteradores são a maneira mais java-ish de fazer qualquer coisa ... iterativa. O StringCharacterIterator é obrigado a tirar o máximo proveito da imutabilidade.
magro
2
Concorde com @ddimitrov - isso é um exagero. A única razão para usar um iterador seria tirar proveito do foreach, que é um pouco mais fácil de "ver" do que um loop for. Se você quiser escrever um loop for convencional de qualquer maneira, use charAt ()
Rob Gilliam
3
O uso do iterador de caracteres é provavelmente a única maneira correta de iterar sobre os caracteres, porque o Unicode requer mais espaço do que o Java charfornece. Um Java charcontém 16 bits e pode conter caracteres Unicode até U + FFFF, mas o Unicode especifica caracteres até U + 10FFFF. Usar 16 bits para codificar Unicode resulta em uma codificação de caracteres de comprimento variável. A maioria das respostas nesta página assume que a codificação Java é uma codificação de comprimento constante, o que está errado.
ceving
3
@ceving Não parece que um personagem iterador vai ajudá-lo com caracteres não-BMP: oracle.com/us/technologies/java/supplementary-142654.html
Bruno De Fraine
18

Se você possui o Guava no caminho de classe, a seguir é uma alternativa bastante legível. A goiaba ainda tem uma implementação de lista personalizada bastante sensata para esse caso, portanto, isso não deve ser ineficiente.

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

ATUALIZAÇÃO: Como o @Alex observou, o Java 8 também CharSequence#charsdeve ser usado. Até o tipo é IntStream, portanto pode ser mapeado para caracteres como:

yourString.chars()
        .mapToObj(c -> Character.valueOf((char) c))
        .forEach(c -> System.out.println(c)); // Or whatever you want
Touko
fonte
Se você precisar fazer algo complexo, vá com o loop for + goiaba, pois você não pode alterar variáveis ​​(por exemplo, Inteiros e Strings) definidas fora do escopo do forEach dentro do forEach. O que quer que esteja dentro do forEach também não pode lançar exceções verificadas, então às vezes é irritante também.
sabujp 28/07/19
13

Se você precisar percorrer os pontos de código de um String (consulte esta resposta ), uma maneira mais curta / mais legível é usar o CharSequence#codePointsmétodo adicionado no Java 8:

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

ou usando o fluxo diretamente em vez de um loop for:

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

Também existe CharSequence#charsse você deseja um fluxo de caracteres (embora seja umIntStream , já que não existe CharStream).

Alex
fonte
3

Eu não usaria StringTokenizer , pois é uma das classes no JDK que é herdada.

O javadoc diz:

StringTokenizeré uma classe herdada que é mantida por motivos de compatibilidade, embora seu uso seja desencorajado em novo código. Recomenda-se que qualquer pessoa que procure essa funcionalidade use o método split Stringou o java.util.regexpacote.

Alan
fonte
O tokenizador de strings é uma maneira perfeitamente válida (e mais eficiente) para iterar sobre tokens (ou seja, palavras em uma frase.) É definitivamente um exagero para iterar sobre caracteres. Voto seu comentário como enganador.
Ddimitrov 13/10/08
3
ddimitrov: Eu não estou seguindo como apontando que StringTokenizer não é recomendada, incluindo uma citação do JavaDoc ( java.sun.com/javase/6/docs/api/java/util/StringTokenizer.html ) para ele declarar como tal, é enganoso. Votado para compensar.
Powerlord
11
Obrigado, Sr. Bemrose ... Entendo que a citação de bloco citada deveria ter sido clara, onde provavelmente se deve inferir que as correções de bugs ativas não serão confirmadas no StringTokenizer.
Alan
2

Se você precisar de desempenho, deverá testar em seu ambiente. Não há outro jeito.

Aqui exemplo de código:

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

No Java online , recebo:

1 10349420
2 526130
3 484200
0

No Android x86 API 17, recebo:

1 9122107
2 13486911
3 12700778
0
Enyby
fonte
0

Consulte Os tutoriais de Java: seqüências de caracteres .

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

Coloque o comprimento int lene use o forloop.

Eugene Yokota
fonte
11
Estou começando a me sentir um pouco spammerish ... se houver essa palavra :). Mas esta solução também tem o problema descrito aqui: Este tem o mesmo problema descrito aqui: stackoverflow.com/questions/196830/...
Emmanuel Oga
0

StringTokenizer é totalmente inadequado para a tarefa de quebrar uma string em seus caracteres individuais. Com String#split()você, você pode fazer isso facilmente usando uma regex que não corresponde a nada, por exemplo:

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

Mas o StringTokenizer não usa expressões regulares, e não há uma string delimitadora que você possa especificar que corresponda ao nada entre os caracteres. Não é um pouco bonito cortar você pode usar para realizar a mesma coisa: usar a própria string, como a cadeia de delimitador (fazendo com que cada personagem em que um delimitador) e tê-lo retornar os delimitadores:

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

No entanto, apenas menciono essas opções com o objetivo de descartá-las. Ambas as técnicas dividem a cadeia original em cadeias de um caractere em vez de primitivas de caracteres, e ambas envolvem uma grande sobrecarga na forma de criação de objetos e manipulação de cadeias. Compare isso com a chamada charAt () em um loop for, que incorre em praticamente nenhuma sobrecarga.

Alan Moore
fonte
0

Elaborando sobre esta resposta e esta resposta .

As respostas acima apontam o problema de muitas das soluções aqui que não iteram pelo valor do ponto de código - elas teriam problemas com quaisquer caracteres substitutos . Os documentos em java também descrevem o problema aqui (consulte "Representações de caracteres Unicode"). De qualquer forma, aqui está um código que usa alguns caracteres substitutos reais do conjunto Unicode suplementar e os converte novamente em uma String. Observe que .toChars () retorna uma matriz de caracteres: se você estiver lidando com substitutos, necessariamente terá dois caracteres. Este código deve funcionar para qualquer caractere Unicode.

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

Este código de exemplo irá ajudá-lo!

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
fonte
0

Então, tipicamente, existem duas maneiras de percorrer a string em java, que já foi respondida por várias pessoas aqui neste tópico, apenas adicionando minha versão dele. First is using

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 o desempenho estiver em risco, recomendarei usar o primeiro em tempo constante; caso contrário, o segundo facilita o seu trabalho, considerando a imutabilidade das classes de strings em java.

Sumit Kapoor
fonte