2026年1月18日日曜日

【Java】「1文字」は本当に「1」なのか?サロゲートペアの罠と、Javaが用意したスマートな解決策

1. 導入:「文字数制限」の落とし穴

ユーザー名の登録画面などで「10文字以内」と決めてバリデーションを実装したのに、ある特定の文字を入力するとエラーになったり、データベースで溢れたりする……。そんな経験はありませんか?

「文字の長さ」は、プログラミングにおいて最も誤解しやすい概念の一つです。今回は、私が実際に遭遇した「サロゲートペア」の罠と、それを自前で判定せずにJavaの力で解決する方法を探求します。


2. 遭遇した罠:Javaの .length() が「2」を返す瞬間

Javaの String.length() は、厳密には「文字数」ではなく「char(16ビット単位)の数」を返します。そのため、一部の特殊な文字で計算が狂います。

【検証コード:SurrogateTest.java】

Java
public class SurrogateTest {
    public static void main(String[] args) {
        String normal = "あ";   // 1文字
        String special = "𠮷";  // 1文字(サロゲートペア:つちよし)

        System.out.println("「あ」のlength: " + normal.length());  // 1
        System.out.println("「𠮷」のlength: " + special.length()); // 2 !?
    }
}

この「𠮷」のように、16ビット(1つのchar)に収まらず、2つのcharをペアにして表現する文字をサロゲートペアと呼びます。


3. 探求:自前で判定……の前に知っておきたいこと

「バイト単位で処理したいし、サロゲートペアの判定ロジックを自作しなきゃいけないかな?」と最初は身構えました。上位サロゲートがどう、下位サロゲートがどう……とビットを追うのは大変です。

しかし、Javaにはこれらをスマートに扱うメソッドが既に備わっていました。

【解決コード:CodePointTest.java】

Java
String s = "𠮷野家";

// 1. 人間が見たままの「正しい文字数」を取得する
int realLength = s.codePointCount(0, s.length());
System.out.println("本当の文字数: " + realLength); // 3 と出力される

// 2. 指定した位置の文字(コードポイント)を取得する
int cp = s.codePointAt(0); // 0文字目の「𠮷」を一つの塊として取得
System.out.println("0文字目のコードポイント: " + Integer.toHexString(cp));

// 3. 判定もメソッド一つ
if (Character.isSurrogatePair(s.charAt(0), s.charAt(1))) {
    System.out.println("これはサロゲートペアです。");
}


4. 納得ポイント:Javaの設計を信じる

実際に触ってみて得られた「納得感」は以下の通りです。

  • バイト単位の処理でも「コードポイント」を意識する: UTF-8でバイト配列に変換する前に、Javaのメソッドで「コードポイント単位(本当の文字単位)」で切り出せば、文字が途中で泣き別れ(文字化け)するのを防げます。

  • 「車輪の再発明」は不要: サロゲートペアの判定を自前で実装しようとすると、境界値のチェックなどバグを埋め込みやすいですが、Character.isSurrogatePaircodePointAt を使えば、Javaが内部で正しく処理してくれます。


5. まとめ:道具を正しく知れば、コードはもっと誠実になる

「1文字は1文字でしょ」という思い込みが、いかに危険かを痛感しました。

同時に、Javaがこうした多言語対応の複雑さをメソッド一つに隠蔽してくれていることに、深い感謝と「嬉しさ」を感じました。

「なぜ動くのか」を知りつつ、提供されている強力な武器(メソッド)を使いこなす。これが、堅牢なシステムを作るためのプロの近道なのだと納得しました。


6. おまけ:使い方早見表


確認したいこと 使うべきメソッド 結果の例 ("𠮷野家") 主な活用シーン
内部的なchar数 s.length() 4 Java内部のメモリ管理、char単位の処理
見たままの文字数 s.codePointCount(0, s.length()) 3 画面上の入力制限、文字数バリデーション
UTF-8バイト数 s.getBytes("UTF-8").length 10 DBのサイズ制限(BYTE)、通信、ファイル出力
サロゲートペア判定 Character.isSurrogatePair(...) true / false 文字を1字ずつ安全に切り出すロジック等


0 件のコメント:

コメントを投稿

【Java】「文字化け」の悲劇を繰り返さない。ファイルのエンコーディング誤認を防ぐ鉄則

1. 導入:開いた瞬間の「」にさようなら UTF-8だと思って読み込んだファイルが、実はWindows標準のShift-JISだった……。 コンソールやログに並ぶ「(豆腐)」や、意味不明な漢字の羅列。誰しも一度は経験する絶望的な瞬間です。 Javaでファイル処理を行う際、もっとも...