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

105

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

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

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

Но меня беспокоит

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

Ответы:

143

Да, 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);
}
Джонатан Файнберг
источник
2
Что касается того, «дорого» это или нет, что ж ... другого способа встроить в Java нет. Но если вы имеете дело только с латинскими / европейскими / кириллическими / греческими / ивритами / арабскими шрифтами, тогда вы просто s.charAt () сколько душе угодно. :)
Джонатан Файнберг
24
Но не стоит. Например, если ваша программа выводит XML и если кто-то дает ей какой-то непонятный математический оператор, внезапно ваш XML может оказаться недействительным.
Механическая улитка
2
Я бы использовал offset = s.offsetByCodePoints(offset, 1);. Есть ли польза от использования offset += Character.charCount(codepoint);вместо этого?
Пол Грок,
3
@Mechanicalsnail Я не понимаю вашего комментария. Почему вывод XML может привести к неправильному поведению этого ответа?
Gili
3
@Gili, ответ в порядке. Он имел в виду комментарий @Jonathan Feinberg, в котором он выступает за использование charAt()плохой идеи
RecursiveExceptionException
73

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

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

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

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

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

Алекс - GlassEditor.com
источник
3
for (int c : (Iterable<Integer>) () -> string.codePoints().iterator())тоже работает.
saka1029
2
Чуть более короткая версия кода @ saka1029: s: for (int c : (Iterable<Integer>) string.codePoints()::iterator) ...
Lii
7

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

Видеть Запись об Sun

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

Александр Эггер
источник
6
В Java 8 теперь есть метод codePoints (), встроенный в String: docs.oracle.com/javase/8/docs/api/java/lang/…
Дов Вассерман
7

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

Вы можете использовать его с 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 (который может использовать больше ОЗУ, чем описанный выше подход):

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

К счастью, использует «codePoints», безопасно обрабатывает суррогатную пару UTF-16 (внутреннее строковое представление Java).

Роджердпак
источник