在Java中,遍历字符串中的字符最简单/最好/最正确的方法是什么?

475

在Java中遍历字符串的方法有以下几种:

  1. 使用 StringTokenizer
  2. String 转换为 char[],然后遍历。

哪种方法是最容易/最好/最正确的遍历方式?


3
好的,我会尽力以简明扼要、准确无误的方式进行翻译,以下是需要翻译的内容:参见 https://dev59.com/7nI_5IYBdhLWcg3wF_B3 - rogerdpack
1
参见 https://dev59.com/hGox5IYBdhLWcg3w6odg。基准测试表明,对于小字符串,String.charAt() 是最快的,而直接使用反射读取 char 数组对于大字符串是最快的。 - Jonathan
Java 8: https://dev59.com/dnVC5IYBdhLWcg3wvT7g#47736566 - akhil_mittal
有无数种方法可以编写和实现Java中逐个字符遍历字符串的算法。哪一种是最正确、最简单、最简洁的,这是三个不同的问题,对于这三个问题的任何答案都取决于程序环境、字符串中的数据以及遍历字符串的原因。即使你给我所有这些信息,我所能给出的任何答案都只是一个观点,它将是我认为最容易、最正确——“最你所说的其他”的方式来完成它。 - JΛYDΞV
这篇帖子已经有400多个赞,但是没有一个人曾经想过应该标记此帖,这很遗憾...这个问题需要关闭直到它被编辑,但这也只是我的意见。 - JΛYDΞV
17个回答

478

我使用for循环来迭代字符串,并使用charAt()方法获取每个字符以便检查。由于String是用数组实现的,因此charAt()方法是一个常数时间操作。

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

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

这是我的做法,对我来说最容易。

至于正确性,我认为这里不存在。一切都基于你的个人风格。


5
编译器是否将 length() 方法内联? - Uri
14
它可能会内联 length() 方法,也就是将该调用后面的方法提升几个层级,但这种做法更有效率。为了翻译下方代码块中的内容:for(int i = 0, n = s.length() ; i < n ; i++) { char c = s.charAt(i); } - Dave Cheney
42
在代码中添加混乱的内容,只为了获得微小的性能提升。在您确定此代码区域对速度至关重要之前,请避免这样做。 - slim
35
请注意,这种技术提供的是“字符”而不是“码点”,这意味着您可能会得到代理项。 - Gabe
3
@ikh *charAt不是O(1)*:为什么会这样?String.charAt(int)的代码只是在执行value[index]。我认为你把chatAt()和其他返回码点的函数混淆了。 - antak
显示剩余8条评论

277

两个选项

for(int i = 0, n = s.length() ; i < n ; i++) { 
    char c = s.charAt(i); 
}
或者。
for(char c : s.toCharArray()) {
    // process c
}

第一种可能更快,而第二种可能更易读。


41
将 s.length() 放在初始化表达式中加一分。如果有人不知道原因,那是因为它只被计算一次,而如果将其放在终止语句中作为 i < s.length(),则每次循环都会调用 s.length()。 - Dennis
72
我以为编译器优化会替你处理那个。 - Rhyous
6
您可以使用Javap类反汇编器查看for循环终止表达式中对s.length()的重复调用已被避免。请注意,在OP发布的代码中,调用s.length()在初始化表达式中,因此语言语义已经保证它仅会被调用一次。 - brabec
4
请注意,大多数Java优化发生在运行时,而不是在类文件中。即使您看到了对length()的重复调用,这并不一定表示运行时有罚款。 - Isaac
3
在我看来,@Steve,实际上这种写法不太易读,因为(1)它是非常不常见的,会分散阅读代码的人的注意力(就像Lasse和许多评论者一样),(2)它将声明与使用分开。 - DavidS
显示剩余7条评论

106
注意,这里描述的大多数技术在处理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
}
< p> Character.charCount(int) 方法需要 Java 5+。

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


1
我不明白为什么你在这里使用除基本多文种平面以外的任何东西。curChar仍然是16位,对吧? - Prof. Falken
2
你可以使用int来存储整个代码点,否则每个字符只能存储定义代码点的两个代理对中的一个。 - sk.
2
我觉得我需要学习一下代码点和代理对。谢谢! - Prof. Falken
11
+1 因为这似乎是唯一一个正确处理 BMP 之外的 Unicode 字符的答案。 - Jason S
编写了一些代码来说明迭代代码点(而不是字符)的概念:https://gist.github.com/EmmanuelOga/48df70b27ead4d80234b#file-iteratecodepoints-java-L90 - Emmanuel Oga
重要的一点,特别是在这里被问到:https://dev59.com/7nI_5IYBdhLWcg3wF_B3?lq=1 - Ciro Santilli OurBigBook.com

44

Java 8中,我们可以这样解决:

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

方法chars()返回一个IntStream,正如文档中所述:

从此序列返回int值的流,用零扩展char值。 任何映射到代理代码点的 char 都会被未经解释地传递。 如果读取流时修改了序列,则结果是未定义的。

方法codePoints()也根据文档返回一个IntStream

从该序列返回代码点值的流。 在序列中遇到的任何代理对都会像通过Character.toCodePoint一样组合,结果会传递给流。 包括普通BMP字符、不成对代理和未定义代码单元在内的任何其他代码单元都将被零扩展为 int 值,然后传递给流。

char和code point有什么区别?文章所述:

Unicode 3.1 添加了补充字符,使得字符总数超过了可以由单个 16 位 char 区分的65536个字符(2^16)。 因此,char 值不再具有与 Unicode 中基本语义单位的一对一映射关系。 JDK 5已更新以支持更大集合的字符值。 该定义并未更改 char 类型,而是一些新的补充字符由两个 char 值的代理对表示。为了减少名称混淆,代码点将用于引用表示特定Unicode字符的数字,包括补充字符。

最后为什么是forEachOrdered而不是forEach

forEach的行为明确是非确定性的,而forEachOrdered为此流的每个元素执行一个操作,如果流具有已定义的遇到顺序,则按照流的遇到顺序执行。 因此,forEach不能保证保持顺序。 如需更多信息,请参见此问题

有关字符、代码点、字形和字素之间区别的详细信息,请参见此问题


1
我认为这是目前最新的答案。 - Daniel Fleck

33
我认为在这里使用StringTokenizer过于复杂。实际上,我尝试了上面的建议并测试了时间。
我的测试非常简单:创建一个大约有一百万个字符的StringBuilder,将其转换为String,并使用charAt() /转换为char数组/使用CharacterIterator遍历每个字符一千次(当然要确保对字符串进行一些操作,以防编译器优化整个循环 :-) )。
在我的2.6 GHz Powerbook(这是一台Mac :-))和JDK 1.5上的结果如下:
  • 测试1:charAt + String --> 3138毫秒
  • 测试2:String转换为数组 --> 9568毫秒
  • 测试3:StringBuilder.charAt --> 3536毫秒
  • 测试4:CharacterIterator和String --> 12151毫秒
由于结果显着不同,最直接的方法似乎也是最快的方法。有趣的是,StringBuilder的charAt()似乎比String的charAt()稍微慢一些。
顺便说一句,我建议不要使用CharacterIterator,因为我认为其滥用'\uFFFF'字符作为“迭代结束”的方式是一种真正可怕的hack。在大型项目中,总会有两个人为了两个不同的目的使用相同的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
这个问题与此处概述的问题相同:https://dev59.com/dnVC5IYBdhLWcg3wvT7g#B5-dEYcBWogLw_1bGjYo - Emmanuel Oga

21

这方面有一些专门的类:

import java.text.*;

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

10
似乎为了遍历不可变字符数组这么简单的操作而采用过度设计了。 - ddimitrov
1
我不明白为什么这会过度设计。迭代器是做任何迭代操作的最Java风格的方式... StringCharacterIterator一定会充分利用不可变性。 - slim
3
同@ddimitrov所说,这太过了。使用迭代器的唯一原因是利用foreach,它比for循环更容易“看到”。如果您无论如何都要编写传统的for循环,那么还不如使用charAt()。 - Rob Gilliam
4
使用字符迭代器可能是遍历字符的唯一正确方式,因为Unicode所需的空间比Java的char提供的空间更多。Java的char包含16位,可以容纳Unicode字符高达U+FFFF,但Unicode规定了字符可达到U+10FFFF。使用16位编码Unicode会导致变长字符编码。本页上的大多数答案都假设Java编码是定长编码,这是错误的。 - ceving
4
似乎字符迭代器不能帮助您处理非BMP字符:http://www.oracle.com/us/technologies/java/supplementary-142654.html - Bruno De Fraine
显示剩余2条评论

19
如果你的类路径上有Guava,下面是一个相当易读的替代方案。甚至在这种情况下,Guava还提供了一个相当明智的自定义列表实现,因此这不应该是低效的。
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循环+guava是一个不错的选择,因为你不能改变在forEach之外定义的变量(例如整数和字符串)。此外,forEach内部的任何内容也不能抛出已检查的异常,有时这也很烦人。 - sabujp

14
如果你需要迭代一个字符串的代码点 (参见这个 answer),一种更短、更易读的方法是使用 Java 8 中添加的 CharSequence#codePoints 方法:
for(int c : string.codePoints().toArray()){
    ...
}

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

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

如果您想要一个字符流,也可以使用CharSequence#chars(尽管它是一个IntStream,因为没有CharStream)。


4

如果您需要性能,那么必须在您的环境上进行测试。别无选择。

这里是示例代码:

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

1
这样的基准测试并不可靠,因为JVM的工作方式(例如优化和JIT)会影响结果。您需要使用JMH才能获得有用的数据。 - andrebrait

3

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

javadoc说:

StringTokenizer是一个保留的遗留类,用于兼容性考虑,尽管在新代码中不鼓励使用。建议任何需要此功能的人改用String的split方法或java.util.regex包。


字符串分词器是迭代标记(即句子中的单词)的完全有效(且更高效)方法。但对于字符迭代来说,这绝对是一种过度设计。我会将您的评论评为误导性,并给予反对票。 - ddimitrov
4
我不太明白指出StringTokenizer不被推荐使用,并包含一段引用自JavaDoc(http://java.sun.com/javase/6/docs/api/java/util/StringTokenizer.html)的内容来证明这一点是如何具有误导性的。我已经点赞以抵消。 - Powerlord
1
谢谢Bemrose先生...我理解引用的代码块应该是非常清晰的,其中一个可能推断出来的是,活跃的错误修复不会提交到StringTokenizer。 - Alan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接