Jak mogę iterować przez punkty kodowe Unicode ciągu Java?

105

Wiem o tym String#codePointAt(int), ale jest indeksowany przez charprzesunięcie, a nie przez przesunięcie punktu kodowego.

Myślę o spróbowaniu czegoś takiego:

Ale moje obawy są

  • Nie jestem pewien, czy punkty kodowe, które naturalnie znajdują się w zakresie z wysokim surogatem, będą przechowywane jako dwie charwartości, czy jedna
  • wydaje się to okropnie kosztownym sposobem na iterację postaci
  • ktoś musiał wymyślić coś lepszego.
rampion
źródło

Odpowiedzi:

143

Tak, Java używa kodowania UTF-16-esque dla wewnętrznych reprezentacji ciągów i tak, koduje znaki poza podstawową płaszczyzną wielojęzyczną ( BMP ) przy użyciu schematu zastępczego.

Jeśli wiesz, że będziesz mieć do czynienia ze znakami spoza BMP, oto kanoniczny sposób na iterację znaków ciągu 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);
}
Jonathan Feinberg
źródło
2
Jeśli chodzi o to, czy jest to „drogie”, cóż ... nie ma innego sposobu wbudowania w Javę. Ale jeśli masz do czynienia tylko ze skryptami łacińskimi / europejskimi / cyrylickimi / greckimi / hebrajskimi / arabskimi, po prostu s.charAt (), ile chcesz. :)
Jonathan Feinberg
24
Ale nie powinieneś. Na przykład, jeśli twój program generuje XML i jeśli ktoś poda mu jakiś niejasny operator matematyczny, nagle twój XML może być nieprawidłowy.
Ślimak mechaniczny
2
Bym użył offset = s.offsetByCodePoints(offset, 1);. Czy używanie offset += Character.charCount(codepoint);zamiast tego przynosi jakieś korzyści ?
Paul Groke
3
@Mechanicalsnail Nie rozumiem twojego komentarza. Dlaczego wyjście XML miałoby spowodować, że ta odpowiedź źle się zachowuje?
Gili
3
@Gili odpowiedź jest w porządku. Nawiązał do komentarza @Jonathana Feinberga, w którym opowiada się za użyciem, charAt()co jest złym pomysłem
RecursiveExceptionException,
72

Dodano Java 8, CharSequence#codePointsktóra zwraca plik IntStreamzawierający punkty kodowe. Możesz użyć strumienia bezpośrednio, aby je iterować:

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

lub z pętlą for, zbierając strumień do tablicy:

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

Te sposoby są prawdopodobnie droższe niż rozwiązanie Jonathana Feinbergsa , ale są szybsze do odczytu / zapisu, a różnica w wydajności będzie zwykle nieistotna.

Alex - GlassEditor.com
źródło
3
for (int c : (Iterable<Integer>) () -> string.codePoints().iterator())również działa.
saka1029
2
Nieco krótsza wersja kodu @ saka1029: s:for (int c : (Iterable<Integer>) string.codePoints()::iterator) ...
Lii
7

Pomyślałem, że dodam metodę obejścia, która działa z pętlami foreach ( ref ), a ponadto możesz łatwo przekonwertować ją na nową metodę String # codePoints języka Java 8, gdy przejdziesz do java 8:

Możesz go używać z każdym w ten sposób:

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

Oto metoda pomocnicza:

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

Lub alternatywnie, jeśli chcesz po prostu przekonwertować ciąg na tablicę int (która może zużywać więcej pamięci RAM niż powyższe podejście):

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

Na szczęście używa "codePoints" bezpiecznie obsługuje zastępczą parę UTF-16 (wewnętrzna reprezentacja ciągu java).

rogerdpack
źródło