LoginSignup
8

More than 3 years have passed since last update.

1文字が2文字になる?toCharArrayメソッドの罠

Last updated at Posted at 2019-11-12

はじめに

以下は、@tak777 さんの【Java】文字列を一文字ずつ切り出しする方法という記事に書かれた、@saka1029 さんの以下のコメントです。

Javaのchar型は16ビットですが、Unicodeの文字集合の符号空間は0x0から0x10FFFFで16ビットを超えます。
Javaは内部コードとしてUTF-16という符号化方式を採用していて、16ビットを超える文字はchar2個で1文字を表現します。
例えば「𩸽(ほっけ)」のUnicode番号は0x29E3Dであり、Javaでは0xD867と0xDE3Dの連続した2個のcharから構成されます。
記事にあるコードでは1文字の半分を切り出してしまう可能性があることに注意する必要があります。

このコメントを読んで、サロゲートペアを用いて表された文字だとtoCharArrayメソッドがどのような結果になるのかが気になったので、実際に検証してみました。

テスト

テストコード

  • 【Java】文字列を一文字ずつ切り出しする方法に私がコメントとして書いたコードをベースとして、以下のテストコードを作成しました。
  • @saka1029 さんから「𩸽(ほっけ)」という字を例として挙げて頂いたので、「𩸽」を使った文言をテストに使ってみました。
    • この他に、「𠮷野家」の「𠮷」もサロゲートペアで表された文字だそうです。
CharArray.java
public class CharArray {
    public static void main(String[] args) {
        String text = "𩸽の煮付け";
        char[] charAry = text.toCharArray();

        for(char ch : charAry) {
            System.out.println(ch);
        }
    }
}

実行結果

標準出力
?
?
の
煮
付
け

@saka1029 さんが指摘した通り、「𩸽」という文字が2文字に分かれてしまい、正しく取得できませんでした...

解決策

色々と検索した結果、サロゲートペアを含む文字列を1文字ずつ切り出すには、StringクラスのcodePointAtメソッドやoffsetByCodePointsメソッドを使えば実現できることが分かりました。

この他に、BreakIteratorクラスを使う方法もあるそうですが、今回は割愛させて頂きます。

作成したコード

  • text.length()はサロゲートペアを考慮せずに文字列の長さを求めるため、lenには6が代入されます。
    • 𩸽という文字が2つのcharとして判定されるため、人間が見ると5文字でも、内部的には6文字としてカウントされます。
  • text.offsetByCodePoints(i,1)は、「text中の指定された位置(i)から、1コードポイント分だけオフセットされた位置のインデックスをを返しています。
    • text="𩸽の煮付け"の時、1文字目がサロゲートペアとなるためiの値が0,2,3,4,5と変化します。
    • 一方でtext="鰈の煮付け"となる場合は、サロゲートペアが存在しないのでiの値が0,1,2,3,4と変化します。
  • text.codePointAt(i)では、指定された位置のコードポイントを取得しています。
CharArray2.java
public class CharArray2 {
    public static void main(String[] args) {
        String text = "𩸽の煮付け";

        int len = text.length();

        for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) {
            System.out.println(Character.toChars(text.codePointAt(i)));
        }
    }
}

実行結果

  • サロゲートペアで表される文字についても、きちんと出力されました。
標準出力
𩸽
の
煮
付
け

補足

  • 以下のコードは @saka1029 さんにコメント欄で教えて頂いた方法で、上記のコードと同様にサロゲートペアを含む文字列を1文字ずつ切り出すことができます。
  • Java9以降ではString#codePoints()というメソッドが使えるため、以下のようにシンプルなコードにすることができます。
    • String#codePoints()を使うだけでなく、StreamAPIを利用してさらにスッキリしたコードになっています。
CharArray3.java
public class CharArray3 {
    public static void main(String[] args) throws Exception {
        String text = "𩸽の煮付け";
        text.codePoints()
            .forEach(c -> System.out.println(Character.toChars(c)));
    }
}

まとめ

  • これまでの業務ではサロゲートペアを考慮したコーディングをする機会が無かったので、とても良い勉強になりました。
  • IBMのサイトには「サロゲートペアを考慮したコードを定型処理の標準として採用すべき」という趣旨の記事もあるので、今後はこういった部分にもう少し気を付けてみようと思いました。
  • サッと書いた記事なので、記事に誤りなどがあればご指摘頂けると幸いです。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8