如何遍历Java字符串的Unicode码点?

114
我知道String#codePointAt(int),但它是由char偏移量索引的,而不是由codepoint偏移量索引的。
我正在考虑尝试以下内容:
- 使用String#charAt(int)获取索引处的char - 测试char是否在high-surrogates range - 如果是,则使用String#codePointAt(int)获取codepoint,并将索引增加2 - 如果不是,则使用给定的char值作为codepoint,并将索引增加1
但我的担忧是:
- 我不确定自然位于高代理范围内的codepoints是否会存储为两个char值还是一个值。 - 这似乎是一种非常昂贵的迭代字符的方式。 - 肯定有人想出了更好的方法。
4个回答

158

是的,Java使用类似于UTF-16的编码来表示字符串的内部结构,并使用代理项方案对基本多文种平面(BMP)之外的字符进行编码。

如果您知道将处理BMP之外的字符,则以下是迭代Java字符串字符的规范方式:

final int length = s.length();
for (int offset = 0; offset < length; ) {
   final int codepoint = s.codePointAt(offset);

   // do something with the codepoint

   offset += Character.charCount(codepoint);
}

25
但是你不应该这么做。比如,如果你的程序输出XML格式的内容,然后有人给它输入一些奇怪的数学符号,那么你的XML可能会变得无效。 - Mechanical snail
2
我本来会使用 offset = s.offsetByCodePoints(offset, 1);。但是使用 offset += Character.charCount(codepoint); 有什么好处吗? - Paul Groke
4
是的,有一个函数叫做 offsetByCodePoints(它重定向到Character.offsetByCodePoints),大约有 50 行循环等内容;而 charCount 只有一行代码和一个条件判断语句。因此,我认为这会导致性能损失较大。 - Sipka
3
@Mechanicalsnail,我不理解你的评论。为什么输出XML会导致这个答案表现异常? - Gili
5
@Gili的回答没问题。他是在回应@Jonathan Feinberg的评论,后者提倡使用charAt(),这是一个不好的主意。 - RecursiveExceptionException
显示剩余9条评论

87

Java 8增加了CharSequence#codePoints方法,它返回一个包含代码点的IntStream流。 您可以直接使用该流来迭代这些代码点:

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

或者使用 for 循环将流收集到数组中:

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

这些方法可能比Jonathan Feinbergs的解决方案更昂贵,但它们读写更快,性能差异通常会微不足道。


3
for (int c : (Iterable<Integer>) () -> string.codePoints().iterator()) 也可以工作。 (说明:该句是Java代码,意思是对字符串中的字符进行迭代,并将其转化为整数类型的Unicode码点) - user4910279
2
@saka1029的代码稍微简短一些:for (int c : (Iterable<Integer>) string.codePoints()::iterator) ... - Lii

9

我认为可以增加一种使用foreach循环的解决方法(参考链接),并且当你使用Java 8时,可以很容易地将它转换为Java 8新的String#codePoints 方法:

你可以像这样在foreach中使用它:

 for(int codePoint : codePoints(myString)) {
   ....
 }

这里是方法:

public static Iterable<Integer> codePoints(final String string) {
  return new Iterable<Integer>() {
    public Iterator<Integer> iterator() {
      return new Iterator<Integer>() {
        int nextIndex = 0;
        public boolean hasNext() {
          return nextIndex < string.length();
        }
        public Integer next() {
          int result = string.codePointAt(nextIndex);
          nextIndex += Character.charCount(result);
          return result;
        }
        public void remove() {
          throw new UnsupportedOperationException();
        }
      };
    }
  };
}

或者,如果您只想将字符串转换为int代码点数组(如果您的代码更容易使用代码点int数组)(可能会使用比上述方法更多的RAM):

 public static List<Integer> stringToCodePoints(String in) {
    if( in == null)
      throw new NullPointerException("got null");
    List<Integer> out = new ArrayList<Integer>();
    final int length = in.length();
    for (int offset = 0; offset < length; ) {
      final int codepoint = in.codePointAt(offset);
      out.add(codepoint);
      offset += Character.charCount(codepoint);
    }
    return out;
  }

感谢使用“codePointAt”,它可以安全地处理UTF-16(Java内部字符串表示形式)的代理对。


6

在Sun公司,迭代代码点被归类为一个功能请求。

请参见Bug报告

那里还有一个关于如何迭代字符串代码点的示例。


7
Java 8现在内置了一个codePoints()方法,可用于String类型:http://docs.oracle.com/javase/8/docs/api/java/lang/CharSequence.html#codePoints - Dov Wasserman

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