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

在Java中迭代字符串字符的方法有:

  1. 使用StringTokenizer ?
  2. String转换为char[]并在其上迭代。

最简单/最好/最正确的迭代方法是什么?

644406 次浏览

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

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


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

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

至于正确性,我不相信这里存在。这完全取决于你的个人风格。

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

javadoc说:

StringTokenizer是遗留类 出于兼容性原因而保留 尽管在new中不鼓励使用 代码。建议任何人 查找此功能,请使用 String

. java.util.regex

看到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循环。

这里有一些专门的类:

import java.text.*;


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

两个选项

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

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

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

StringTokenizer完全不适合将字符串分解为单个字符的任务。对于String#split(),你可以通过使用一个不匹配的正则表达式轻松做到这一点,例如:

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

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

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

但是,我只是为了排除它们而提到这些选项。这两种技术都将原始字符串分解为单字符字符串,而不是char原语,并且都以对象创建和字符串操作的形式涉及大量开销。与在for循环中调用charAt()相比,后者几乎没有开销。

我同意StringTokenizer在这里是多余的。事实上,我尝试了上面的建议,并花了时间。

我的测试相当简单:创建一个带有大约一百万个字符的StringBuilder,将其转换为String,并在转换为char数组/使用CharacterIterator一千次之后使用charAt()遍历每个字符(当然要确保对字符串做一些事情,这样编译器就不能优化掉整个循环:-))。

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

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

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

顺便说一句,我建议不要使用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");

请注意,如果您处理的字符不在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

如果你的类路径上有番石榴,下面是一个相当可读的替代方法。对于这种情况,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

如果你需要遍历String的代码点(参见此回答),一个更短/更可读的方法是使用Java 8中添加的CharSequence#codePoints方法:

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

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

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

如果你想要一个字符流,还有CharSequence#chars(尽管它是IntStream,因为没有CharStream)。

详细阐述了这个答案这个答案

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

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

这个示例代码将帮助你!

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 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 0类型的流,从this扩展char值 序列。将传递映射到代理代码点的任何字符 通过粗略的。如果序列发生突变,而流发生突变

.读取时,结果为undefined

方法codePoints()也会根据每个doc返回IntStream:

返回该序列的码点值流。任何 序列中遇到的代理项对被组合,就像由 的性格。toCodePoint并将结果传递给流。任何 其他代码单位,包括未配对的普通BMP字符 代理和未定义的代码单元从0扩展到int值 然后传递给流。

字符和码点有什么不同?正如在文章中提到的:

Unicode 3.1增加了补充字符,带来了总数 大于2^16 = 65536个字符的 由单个16位char来区分。因此,char的值为no 长有一个一对一的映射到基本语义单位在 Unicode。JDK 5已更新为支持更大的字符集 值。而不是改变char类型的定义,一些 新的补充字符由代理对表示 两个char值。为了减少命名混乱,代码点将是 用于指代表特定Unicode的数字 字符,包括补充字符

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

forEach的行为是显式的不确定的,其中forEachOrdered对该流的每个元素执行操作,如果流具有定义的遇到顺序,则在遇到水流的顺序中。所以forEach不能保证顺序被保持。请检查问题以获取更多信息。

对于字符、码位、字形和字素之间的区别,检查这个问题

如果你需要性能,你可以在你的环境上必须测试。没有别的办法。

下面是示例代码:

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
所以通常有两种方法在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 the character array

如果性能受到威胁,那么我会建议在常数时间内使用第一个,如果不是,那么考虑到java中字符串类的不可变性,那么使用第二个会使您的工作更容易。

public class Main {


public static void main(String[] args) {
String myStr = "Hello";
String myStr2 = "World";
      

for (int i = 0; i < myStr.length(); i++) {
char result = myStr.charAt(i);
System.out.println(result);
}
        

for (int i = 0; i < myStr2.length(); i++) {
char result = myStr2.charAt(i);
System.out.print(result);
}
}
}

输出:

H
e
l
l
o
World