Как я могу выполнять итерацию по кодовым точкам Unicode строки Java?

Итак, я знаю о String#codePointAt(int), но он индексируется смещением char, а не смещением кодовой точки.

Я думаю о том, чтобы попробовать что-то вроде:

  • используя String#charAt(int), чтобы получить char по индексу
  • проверка того, находится ли char в диапазон высоких суррогатов
    • если это так, используйте String#codePointAt(int), чтобы получить код и увеличить индекс на 2
    • если нет, используйте заданное значение char в качестве кодового пункта и увеличивайте индекс на 1

Но мои проблемы -

  • Я не уверен, будут ли кодовые точки, которые естественно находятся в диапазоне высоких суррогатов, будут сохранены как два значения char или один
  • это похоже на ужасный дорогой способ итерации через символы
  • кто-то должен придумать что-то лучшее.

Ответ 1

Да, Java использует кодировку UTF-16-esque для внутренних представлений строк, и, да, она кодирует символы вне базовой многоязычной плоскости (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);
}

Ответ 2

Java 8 добавила CharSequence#codePoints, которая возвращает IntStream, содержащую коды. Вы можете напрямую использовать поток для перебора по ним:

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

или с циклом for, собирая поток в массив:

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

Эти пути, вероятно, дороже, чем решение Джонатана Фейнберга, но они быстрее читают/пишут, а разница в производительности обычно незначительна.

Ответ 3

Итерация над кодовыми точками подается как запрос функции на Sun.

См. Запись с ошибкой Sun

Также есть пример того, как итерации по String CodePoints есть.

Ответ 4

Думаю, я бы добавил метод обходного пути, который работает с циклами foreach (ref), плюс вы можете преобразовать его в java 8 new String # codePoints легко, когда вы переходите на java 8:

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

Затем вы можете использовать его с foreach следующим образом:

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

Или поочередно, если вы просто хотите преобразовать строку в массив int (который может использовать больше ОЗУ, чем описанный выше подход):

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