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

What is the easiest/best/most correct way to iterate through the characters of a string in Java?

EDOCX1?0?把String转换成char[]并迭代?还有别的吗?


我使用for循环迭代字符串,并使用charAt()让每个字符检查它。由于字符串是用数组实现的,因此charAt()方法是一个固定时间操作。

1
2
3
4
5
6
String s ="...stuff...";

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

这就是我要做的。这对我来说似乎是最简单的。

就正确性而言,我不相信这存在。这都是基于你的个人风格。


两个选择

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

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

第一个可能更快,第二个可能更可读。


注意,如果您处理的是BMP(Unicode基本多语言平面)之外的字符,即U0000 UFFFF范围之外的代码点,那么这里描述的大多数其他技术都会分解。这种情况很少发生,因为代码点之外的代码大多被分配给死语言。但除此之外还有一些有用的字符,例如一些用于数学符号的代码点,以及一些用于用中文编码专有名称的代码点。

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

1
2
3
4
5
6
7
String str ="....";
int offset = 0, strLen = str.length();
while (offset < strLen) {
  int curChar = str.codePointAt(offset);
  offset += Character.charCount(curChar);
  // do something with curChar
}

EDCOX1×6的方法需要Java 5 +。

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


我同意StringTokenizer在这里的杀伤力太大了。实际上,我尝试了上面的建议,并花了时间。

我的测试相当简单:用大约一百万个字符创建一个StringBuilder,将其转换为字符串,并在转换为char数组/使用characterIterator 1000次后使用char a t()/遍历每个字符串(当然要确保对字符串做些什么,这样编译器就无法优化整个循环:-)。

我的2.6 GHz PowerBook(Mac:-)和JDK 1.5上的结果是:

  • 测试1:charat+string->3138msec
  • 测试2:字符串转换为数组--->9568msec
  • 测试3:stringbuilder charat->3536msec
  • 测试4:CharacterIterator和字符串--->12151msec

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

顺便说一句,我建议不要使用characterIterator,因为我认为它滥用了'uffff'字符作为"迭代结束"是一个非常可怕的黑客。在大型项目中,总是有两个家伙使用相同的黑客来实现两个不同的目的,代码崩溃真的很神秘。

下面是一个测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    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
2
3
4
5
6
7
import java.text.*;

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


如果您的类路径上有guava,那么下面是一个非常易读的选项。对于这种情况,guava甚至有一个相当合理的自定义列表实现,因此这不应该是低效的。

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

更新:正如@亚历克斯所指出的,在Java 8中也使用EDCOX1(7)来使用。即使类型是intstream,也可以映射到如下字符:

10


如果需要迭代一个EDCOX1的代码点0(参见此答案),一个更短的/更可读的方式是使用在Java 8中添加的EDCOX1×1的方法:

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

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

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

如果需要字符流,也有CharSequence#chars(尽管它是IntStream,因为没有CharStream)。


在Java 8中,我们可以把它分解为:

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

方法chars()返回一个IntStream,如doc:

Returns a stream of int zero-extending the char values from this
sequence. Any char which maps to a surrogate code point is passed
through uninterpreted. If the sequence is mutated while the stream is
being read, the result is undefined.

方法codePoints()还返回一个IntStream,根据doc:

Returns a stream of code point values from this sequence. Any
surrogate pairs encountered in the sequence are combined as if by
Character.toCodePoint and the result is passed to the stream. Any
other code units, including ordinary BMP characters, unpaired
surrogates, and undefined code units, are zero-extended to int values
which are then passed to the stream.

字符和代码点有什么不同?如本文所述:

Unicode 3.1 added supplementary characters, bringing the total number
of characters to more than the 216 characters that can be
distinguished by a single 16-bit char. Therefore, a char value no
longer has a one-to-one mapping to the fundamental semantic unit in
Unicode. JDK 5 was updated to support the larger set of character
values. Instead of changing the definition of the char type, some of
the new supplementary characters are represented by a surrogate pair
of two char values. To reduce naming confusion, a code point will be
used to refer to the number that represents a particular Unicode
character, including supplementary ones.

最后为什么是forEachOrdered而不是forEach呢?

forEach的行为是明确的不确定性的,当forEachOrdered对该流的每个元素执行一个动作时,如果该流有一个定义的相遇顺序,则按该流的相遇顺序执行。因此,forEach并不能保证订单的有效性。还要检查这个问题了解更多信息。

对于字符、代码点、字形和字形之间的差异,请检查此问题。


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

JavaDoc说:

StringTokenizer is a legacy class that
is retained for compatibility reasons
although its use is discouraged in new
code. It is recommended that anyone
seeking this functionality use the
split method of String or the
java.util.regex package instead.


如果您需要性能,那么您必须在您的环境中进行测试。没有别的办法。

这里是示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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
2
3
4
1 10349420
2 526130
3 484200
0

在Android x86 API 17上,我得到:

1
2
3
4
1 9122107
2 13486911
3 12700778
0

这个示例代码将帮助您!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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);
    }
}


阐述这个答案和这个答案。

上面的答案指出了这里的许多解决方案的问题,这些解决方案不按代码点值迭代——它们在使用任何代理字符时都会遇到问题。Java文档还概述了这里的问题(参见Unicode字符表示)。无论如何,这里有一些代码使用一些来自补充Unicode集的实际代理字符,并将它们转换回字符串。请注意,.tochars()返回一个字符数组:如果要处理代理,则必须有两个字符。此代码适用于任何Unicode字符。

1
2
3
    String supplementary ="Some Supplementary: ????????";
    supplementary.codePoints().forEach(cp ->
            System.out.print(new String(Character.toChars(cp))));

StringTokenizer完全不适合将字符串拆分为单个字符的任务。使用String#split()可以通过使用不匹配的regex轻松做到这一点,例如:

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

但是StringTokenizer不使用正则表达式,并且没有可以指定的分隔符字符串来匹配字符之间的零。有一个小技巧可以用来完成相同的事情:使用字符串本身作为分隔符字符串(使其中的每个字符都成为分隔符),并让它返回分隔符:

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

然而,我只是为了解雇他们才提到这些选择。这两种技术都将原始字符串分解为一个字符串而不是char原语,并且都涉及大量的开销,比如对象创建和字符串操作。将其与for循环中调用charat()进行比较,后者几乎不产生开销。


参见Java教程:字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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环。