在Java中迭代字符串的字符最简单/最佳/最正确的方法是什么?


340

StringTokenizer?将转换Stringchar[]并对其进行迭代?还有吗




1
另请参见stackoverflow.com/questions/8894258/…基准测试显示String.charAt()对于小型字符串最快,而使用反射直接读取char数组对于大型字符串最快。
乔纳森(Jonathan)


Answers:


362

我使用for循环来迭代字符串,并使用它charAt()来获取每个字符以进行检查。由于String是通过数组实现的,因此该charAt()方法是恒定时间操作。

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

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

那就是我会做的。在我看来,这是最简单的。

就正确性而言,我不认为这是存在的。这完全取决于您的个人风格。


3
编译器是否内联length()方法?
Uri

7
它可能是内联的length(),这可以提升后面调用几个帧的方法,但是这样做对于(int i = 0,n = s.length(); i <n; i ++){char c = s.charAt(i); }
Dave Cheney

32
使代码杂乱无章,以获得微不足道的性能提升。请避免这种情况,直到您确定该代码区域对速度至关重要。
超薄

31
请注意,此技术为您提供字符,而不是代码点,这意味着您可能会获得替代。
加布

2
@ikh charAt不是O(1):怎么回事?的代码String.charAt(int)只是在做value[index]。我认为您会混淆chatAt()其他一些可以给您代码点的东西。
安塔克

208

两种选择

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

要么

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

第一个可能更快,然后第二个可能更具可读性。


26
再加上一个将s.length()放在初始化表达式中。如果任何人都不知道为什么,那是因为如果将它放在终止语句中的位置i <s.length()仅被评估一次,则每次循环时都会调用s.length()。
丹尼斯

57
我认为编译器优化会为您解决这一问题。
Rhyous 2012年

4
@Matthias您可以使用Javap类反汇编程序来查看,确实避免了对for循环终止表达式中的s.length()的重复调用。请注意,在发布的代码OP中,对s.length()的调用位于初始化表达式中,因此语言语义已保证将仅对其调用一次。
prasopes

3
@prasopes注意,尽管大多数Java优化在运行时发生,而不是在类文件中发生。即使您看到重复调用length()并不一定表示运行时损失。
艾萨克

2
@Lasse,假定的原因是为了提高效率-您的版本在每次迭代时都调用length()方法,而Dave在初始化程序中调用一次。就是说,极有可能JIT(“及时”)优化器将优化多余的调用,因此它可能只是可读性差异,而没有实际收益。
史蒂夫

90

请注意,如果您要处理BMP(Unicode 基本多语言平面)之外的字符(即,代码点超出u0000-uFFFF范围),则此处介绍的大多数其他技术都会崩溃。这种情况很少发生,因为在此之外的代码点大部分都分配给死语言。但是除此之外,还有一些有用的字符,例如一些用于数学记号的代码点,以及一些用于用中文编码专有名称的代码点。

在这种情况下,您的代码将是:

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

Character.charCount(int)方法需要Java 5+。

资料来源:http : //mindprod.com/jgloss/codepoint.html


1
除了基本的多语言平面之外,我什么都不用。curChar仍然是16位吗?
Falken教授的合同

2
您可以使用int来存储整个代码点,否则每个char将仅存储定义代码点的两个代理对中的一个。
sk。

1
我想我需要阅读代码点和代理对。谢谢!
Falken教授的合同

6
+1,因为这似乎是对BMP之外的Unicode字符唯一正确的答案
Jason S

编写一些代码来说明迭代代码点(而不是字符)的概念:gist.github.com/EmmanuelOga/…–
Emmanuel

26

我同意StringTokenizer在这里过大。实际上,我尝试了上述建议并花了一些时间。

我的测试非常简单:创建一个约有100万个字符的StringBuilder,将其转换为String,然后在转换为char数组后,使用CharacterIterator遍历charAt()/一千次(当然要确保对字符串执行某些操作,以使编译器无法优化整个循环:-))。

在我的2.6 GHz Powerbook(这是mac :-))和JDK 1.5上的结果:

  • 测试1:charAt + String-> 3138msec
  • 测试2:将字符串转换为数组-> 9568msec
  • 测试3:StringBuilder charAt-> 3536毫秒
  • 测试4:CharacterIterator和String-> 12151msec

由于结果明显不同,因此最直接的方法似乎也是最快的方法。有趣的是,StringBuilder的charAt()似乎比String慢一点。

顺便说一句,我建议不要使用CharacterIterator,因为我认为它滥用'\ uFFFF'字符作为“迭代结束”是一个非常糟糕的黑客。在大型项目中,总是有两个人出于两种不同目的使用相同类型的hack,并且代码确实神秘地崩溃。

这是测试之一:

    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
这有这里列出了同样的问题:stackoverflow.com/questions/196830/...
灵光男鹿

22

Java 8中,我们可以将其解决为:

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

chars()方法返回doc中IntStream提到的:

返回此序列的char值进行int零扩展的int流。映射到代理代码点的任何字符都将通过未解释传递。如果在读取流时序列发生突变,则结果不确定。

该方法codePoints()IntStream按文档返回一个:

返回此序列中的代码点值流。序列中遇到的所有代理对都像通过Character.toCodePoint进行组合,然后将结果传递到流中。任何其他代码单元(包括普通BMP字符,不成对的代理和未定义的代码单元)都将零扩展为int值,然后将其传递到流中。

字符和代码点有何不同?正如提到的这个文章:

Unicode 3.1添加了补充字符,使字符总数超过了可以用单个16位识别的216个字符char。因此,char值不再具有与Unicode中基本语义单元的一对一映射。JDK 5已更新,以支持更大的字符值集。代替更改char类型的定义,某些新的补充字符由两个char值的替代对表示。为了减少命名混乱,将使用代码点来表示代表特定Unicode字符的数字,包括补码。

最后为什么forEachOrdered而不是forEach

的行为forEach是明确地不确定性,其中作为forEachOrdered执行用于该流的每个元件的操作,在该流的遭遇顺序如果流具有规定的遭遇顺序。因此forEach,不能保证将保留订单。另请查看此问题以获取更多信息。

对于字符,代码点,字形和字形之间的区别,请检查此问题


21

为此有一些专用的类:

import java.text.*;

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

7
对于像迭代不可变char数组这样的简单操作来说,这似乎是一种过大的杀伤力。
ddimitrov

1
我不明白为什么这太过分了。迭代器是执行任何操作的最类似于Java的方式...迭代。StringCharacterIterator必将充分利用不变性。
超薄

2
同意@ddimitrov-这太过分了。使用迭代器的唯一原因是要利用foreach,它比for循环更容易“看到”。如果您仍然要编写常规的for循环,那么不妨使用charAt()
Rob Gilliam,2010年

3
使用字符迭代器可能是唯一的迭代字符的方法,因为Unicode比Java char提供的空间更多。Java char包含16位,并且可以将Unicode字符保留为U + FFFF,但是Unicode指定的字符可以保留为U + 10FFFF。使用16位编码Unicode会导致长度可变的字符编码。此页面上的大多数答案都假定Java编码是恒定长度编码,这是错误的。
ceving 2013年

3
@ceving似乎字符迭代器不会帮助您解决非BMP字符:oracle.com/us/technologies/java/supplementary-142654.html
Bruno De Fraine

18

如果您的类路径中包含番石榴,则以下是一种易于阅读的替代方法。对于这种情况,Guava甚至有一个相当明智的自定义List实现,因此这应该没有效率。

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

更新:正如@Alex指出的那样,在Java 8中也CharSequence#chars可以使用。即使类型是IntStream,也可以将其映射为以下字符:

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

如果您需要做一些复杂的事情,那么可以使用for循环+番石榴,因为您无法在forEach范围内的forEach范围之外定义变量(例如Integers和Strings)。forEach内部的任何内容也不会抛出检查异常,因此有时也很烦人。
sabujp

13

如果您需要遍历a的代码点String(请参见此答案),则一种更简短/更易读的方法是使用CharSequence#codePointsJava 8中添加的方法:

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

或直接使用流而不是for循环:

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

还有CharSequence#chars,如果你想要的字符流(虽然它是IntStream,因为没有CharStream)。


3

我不会使用,StringTokenizer因为它是JDK中遗留的类之一。

Javadoc说:

StringTokenizer是旧类,出于兼容性原因保留,尽管在新代码中不鼓励使用它。建议任何寻求此功能的人改用split方法Stringjava.util.regexpackage。


字符串令牌生成器是遍历令牌(即句子中的单词)的完全有效(且效率更高)的方法。对于遍历char而言,这绝对是一个过大的杀伤力。我拒绝您的评论有误导性。
ddimitrov

3
ddimitrov:我不遵循如何指出不建议使用StringTokenizer的方式,包括JavaDoc(java.sun.com/javase/6/docs/api/java/util/StringTokenizer.html)中的引号误导。提议抵消。
Powerlord

1
谢谢Bemrose先生...我认为引用的块引号应该很清楚,应该可以推断出不会将有效的错误修复提交给StringTokenizer。
艾伦(Alan)

2

如果需要性能,则必须在环境上进行测试。没有其他办法了。

下面是示例代码:

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

Java在线上,我得到:

1 10349420
2 526130
3 484200
0

在Android x86 API 17上,我得到:

1 9122107
2 13486911
3 12700778
0

0

请参阅Java教程:字符串

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

放入长度int len并使用for循环。


1
我开始有点垃圾邮件了……如果有这样的话:)。但这种解决方案也有问题,在这里概述:这有同样的问题在这里概述:stackoverflow.com/questions/196830/...
灵光男鹿

0

StringTokenizer完全不适合将字符串分成单个字符的任务。随着String#split()您可以通过使用符合什么,例如正则表达式做到这一点很容易:

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

但是StringTokenizer不使用正则表达式,并且您没有可以指定的分隔符字符串来匹配字符之间的所有字符。这里一个可爱的小砍你可以用它来完成同样的事情:使用字符串本身作为分隔符字符串(使得在它的每一个字符分隔符),并使其返回分隔符:

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

但是,我仅提及这些选项是为了消除它们。两种技术都将原始字符串分解为一个字符的字符串,而不是char基元,并且都涉及大量的对象创建和字符串操作形式的开销。相比之下,在for循环中调用charAt()几乎不会产生任何开销。


0

详细说明这个答案这个答案

上面的答案指出了这里的许多解决方案的问题,这些解决方案不会通过代码点的值进行迭代-它们会遇到任何替代字符的问题。Java文档还在此处概述了该问题(请参阅“ Unicode字符表示形式”)。无论如何,这是一些代码,它使用补充Unicode集中的一些实际替代字符,并将其转换字符串。请注意,.toChars()返回一个字符数组:如果要处理代理,则必须有两个字符。此代码应适用于任何 Unicode字符。

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

0

此示例代码将帮助您!

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

因此,通常有两种方法可以遍历java中的字符串,而该字符串已经在此线程中被多个人回答,只需添加我的版本即可。

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

如果性能受到威胁,那么我建议您恒定时间使用第一个,如果不这样做,那么考虑到Java中字符串类的不可变性,第二个就可以简化工作。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.