ChatGPT解决这个技术问题 Extra ChatGPT

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

在 Java 中遍历字符串字符的一些方法是:

使用 StringTokenizer?将字符串转换为 char[] 并对其进行迭代。

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

另请参阅 stackoverflow.com/questions/8894258/… 基准显示 String.charAt() 对于小字符串最快,使用反射直接读取 char 数组对于大字符串最快。
在 Java 中,有无数种方法可以编写和实现一种遍历字符串的算法,一个字符一个字符。哪一个是最正确、最简单和最简单的是 3 个不同的问题,这 3 个问题中任何一个的答案将取决于程序环境、字符串中的数据以及遍历字符串的原因。即使你给了我所有的信息,我能给你的任何答案,都将是一种意见,这将是我认为最简单最正确的方式——“你所说的大多数”——这样做的方式。

T
Thirumalai Parthasarathi

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

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

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

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

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


编译器是否内联 length() 方法?
它可能内联 length(),即提升调用几帧背后的方法,但这样做更有效 for(int i = 0, n = s.length() ; i < n ; i++) { char c = s.charAt(i); }
弄乱你的代码以获得微小的性能提升。请避免这种情况,直到您确定此代码区域对速度至关重要。
请注意,此技术为您提供字符,而不是代码点,这意味着您可能会得到代理。
@ikh charAt is not O(1):这是怎么回事? String.charAt(int) 的代码只是在做 value[index]。我认为您将 chatAt() 与其他给您代码点的东西混淆了。
j
jjnguy

两种选择

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

或者

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

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


加一个用于将 s.length() 放在初始化表达式中。如果有人不知道为什么,那是因为如果它作为 i < s.length() 放置在终止语句中,则只评估一次,那么每次循环时都会调用 s.length()。
我认为编译器优化会为您解决这个问题。
对此有什么进一步的想法吗?我们是否可以合理地期望编译器优化能够避免重复调用 s.length() 呢?
@Matthias您可以使用Javap类反汇编器来查看确实避免了在for循环终止表达式中重复调用s.length()。请注意,在 OP 发布的代码中,对 s.length() 的调用位于初始化表达式中,因此语言语义已经保证它只会被调用一次。
@prasopes 请注意,尽管大多数 java 优化发生在运行时,而不是在类文件中。即使您看到对 length() 的重复调用也不一定表示运行时惩罚。
B
Buhake Sindi

请注意,如果您正在处理 BMP (Unicode Basic Multilingual Plane) 之外的字符,即 u0000-uFFFF 范围之外的 code points,则此处描述的大多数其他技术都会失效。这只会很少发生,因为在此之外的代码点大多分配给死语言。但除此之外还有一些有用的字符,例如一些用于数学符号的代码点,以及一些用于编码中文专有名称的代码点。

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

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


除了基本多语言平面,我不明白你如何使用任何东西。 curChar 仍然是 16 位吗?
您要么使用 int 来存储整个代码点,否则每个 char 将只存储定义代码点的两个代理对中的一个。
我想我需要阅读代码点和代理对。谢谢!
+1,因为这似乎是唯一对 BMP 之外的 Unicode 字符正确的答案
编写了一些代码来说明迭代代码点(而不是字符)的概念:gist.github.com/EmmanuelOga/…
a
akhil_mittal

在 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

返回一个 int 流,对该序列中的 char 值进行零扩展。任何映射到代理代码点的字符都会未经解释地传递。如果在读取流时序列发生突变,则结果未定义。

方法 codePoints() 还根据文档返回 IntStream

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

char 和代码点有何不同?this文章中所述:

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

最后为什么是 forEachOrdered 而不是 forEach

forEach 的行为是明确的非确定性的,因为 forEachOrdered 对该流的每个元素执行操作,如果流具有已定义的遭遇顺序,则按照 流的遭遇顺序。所以 forEach 不保证订单会被保留。另请查看此 question 了解更多信息。

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


我认为这是这里最新的答案。
佚名

我同意 StringTokenizer 在这里是矫枉过正的。实际上我尝试了上面的建议并花时间。

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

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

测试 1:charAt + String --> 3138 毫秒

测试 2:字符串转换为数组 --> 9568 毫秒

测试 3:StringBuilder charAt --> 3536msec

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

这与此处概述的问题相同:stackoverflow.com/questions/196830/…
B
Bruno De Fraine

为此有一些专门的课程:

import java.text.*;

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

对于像迭代不可变字符数组这样简单的事情来说,这看起来有点过头了。
我不明白为什么这是矫枉过正。迭代器是做任何事情的最java-ish方式......迭代。 StringCharacterIterator 必然会充分利用不变性。
同意@ddimitrov - 这是矫枉过正。使用迭代器的唯一原因是利用 foreach,它比 for 循环更容易“看到”。如果您无论如何要编写常规的 for 循环,那么不妨使用 charAt()
使用字符迭代器可能是迭代字符的唯一正确方法,因为 Unicode 需要比 Java char 提供的更多空间。 Java char 包含 16 位,最多可以容纳 U+FFFF 的 Unicode 字符,但 Unicode 指定最多 U+10FFFF 的字符。使用 16 位对 Unicode 进行编码会产生可变长度的字符编码。此页面上的大多数答案都假定 Java 编码是恒定长度编码,这是错误的。
@ceving 字符迭代器似乎不会帮助您处理非 BMP 字符:oracle.com/us/technologies/java/supplementary-142654.html
h
husayt

如果您的类路径中有 Guava,则以下是一个非常易读的替代方案。对于这种情况,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 范围之外改变定义的变量(例如整数和字符串)。 forEach 中的任何内容也不能抛出已检查的异常,所以这有时也很烦人。
C
Community

如果您需要遍历 String 的代码点(请参阅此 answer),一种更短/更易读的方法是使用 Java 8 中添加的 CharSequence#codePoints 方法:

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

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

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

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


E
Enyby

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

这里示例代码:

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 online 我得到:

1 10349420
2 526130
3 484200
0

在 Android x86 API 17 上,我得到:

1 9122107
2 13486911
3 12700778
0

A
Alan

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

javadoc 说:

StringTokenizer 是一个遗留类,出于兼容性原因保留,但不鼓励在新代码中使用它。建议任何寻求此功能的人改用 String 的 split 方法或 java.util.regex 包。


字符串标记器是迭代标记(即句子中的单词)的完全有效(并且更有效)的方式。它绝对是迭代字符的过度杀伤力。我不赞成您的评论具有误导性。
ddimitrov:我没有关注如何指出不建议使用 StringTokenizer,包括从 JavaDoc (java.sun.com/javase/6/docs/api/java/util/StringTokenizer.html) 中引用的内容,因为它是误导性的。赞成抵消。
谢谢 Bemrose 先生……我认为引用的块引用应该非常清楚,人们可能应该推断出活动的错误修复不会提交给 StringTokenizer。
u
unpluggeDloop
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

E
Eugene Yokota

请参阅The Java Tutorials: Strings

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


我开始觉得有点垃圾邮件......如果有这样的话:)。但是此解决方案也存在此处列出的问题:这与此处列出的问题相同:stackoverflow.com/questions/196830/…
A
Alan Moore

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

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

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

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

但是,我仅出于消除它们的目的而提及这些选项。这两种技术都将原始字符串分解为单字符字符串而不是 char 原语,并且都涉及对象创建和字符串操作形式的大量开销。将其与在 for 循环中调用 charAt() 进行比较,这几乎不会产生任何开销。


C
Community

详细说明 this answerthis answer

上面的答案指出了这里许多不按代码点值迭代的解决方案的问题——它们对任何 surrogate chars 都会有问题。 Java 文档还概述了问题 here(请参阅“Unicode 字符表示”)。无论如何,这里有一些代码使用补充 Unicode 集中的一些实际代理字符,并将它们 back 转换为字符串。请注意, .toChars() 返回一个字符数组:如果您正在处理代理项,则必须有两个字符。此代码适用于 any Unicode 字符。

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

d
devDeejay

此示例代码将为您提供帮助!

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

v
v8-E

所以通常有两种方法可以遍历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 中字符串类的不变性,使用第二个会让你的工作更容易。