信頼度ランク
| S | 公式ソース確認済み |
| A | 成功実績多数・失敗例少数 |
| B | 賛否両論 |
| C | 動作未確認・セキュリティリスク高 |
| Z | 個人所感 |
文字コード・Unicode・UTF-8 を正確に理解する ── Java の char が 16 ビットな理由
ASCII から Unicode へ、UTF-8 と UTF-16 の違い、Java の char が 65535 文字しか表せない理由、絵文字が 2 つの char になるサロゲートペア。文字コードの歴史を辿って Java の文字処理を根本から理解する。
一言結論
Java の char は UTF-16 の 1 コード単位(16 ビット)。Unicode の全文字(絵文字含む)を扱うには int(コードポイント)や String を使う必要がある。この背景を理解すると文字化けや charAt の落とし穴が見えてくる。
char c = 'A';
System.out.println((int) c); // 65
String emoji = "😀";
System.out.println(emoji.length()); // 2 ← なぜ?
char half = emoji.charAt(0);
System.out.println(half); // ? ← 文字化け
"😀" の length が 2 になる理由、charAt(0) が文字化けする理由。これは Java のバグではなく、文字コードの歴史から来た必然だ。
1. コンピュータは文字をどう扱うか
コンピュータが扱えるのはビット(0 と 1)だけだ。文字を扱うには「文字 → 数値」の対応表が必要だ。これが**文字コード(Character Encoding)**だ。
2. ASCII ── 最初の標準
1963 年に制定された ASCII(American Standard Code for Information Interchange) は、英数字・記号・制御文字を 7 ビット(128 種類)で定義した。
'A' → 65 (01000001)
'B' → 66 (01000010)
'a' → 97 (01100001)
'0' → 48 (00110000)
' ' → 32 (00100000)
ASCII で表せる文字は 128 種類。英語圏には十分だったが、日本語・中国語・アラビア語など非英語圏には全く足りなかった。
3. 各国の独自規格が乱立 ── 文字化け時代
ASCII の不足を各国が独自に補った:
- Shift_JIS(日本): 日本語を 2 バイトで表現
- EUC-JP(日本): Unix 系での日本語エンコーディング
- GB2312(中国): 中国語の文字コード
- ISO-8859-1(西欧): ヨーロッパ言語向け
問題は互換性がないことだ。Shift_JIS で書いたファイルを EUC-JP として読むと文字化けする。インターネットの普及でこれが深刻な問題になった。
Shift_JIS で "日本語" → バイト列: 0x93FA 0x96{7B 0x8CEA
EUC-JP で読む → 文字化け(全く別の文字に見える)
4. Unicode ── 世界中の文字を 1 つの規格に
1991 年、「世界中の文字を 1 つの規格でまとめる」プロジェクト Unicode が始動した。
Unicode は各文字に コードポイント(Code Point) という番号を割り当てる。U+XXXX の形式で表記される。
'A' → U+0041 (10進数: 65)
'あ' → U+3042 (10進数: 12354)
'漢' → U+6F22 (10進数: 28450)
'😀' → U+1F600 (10進数: 128512)
現在 Unicode は約 14 万文字以上をカバーしている(絵文字・古代文字・数学記号含む)。
5. Unicode の符号化方式 ── UTF-8 と UTF-16
コードポイントを実際のバイト列にする方法(符号化方式)が複数ある。主要なのが UTF-8 と UTF-16 だ。
UTF-8 ── 可変長・ASCII 互換
| コードポイント範囲 | バイト数 |
|---|---|
| U+0000 〜 U+007F(ASCII の範囲) | 1 バイト |
| U+0080 〜 U+07FF(ラテン拡張など) | 2 バイト |
| U+0800 〜 U+FFFF(日本語など) | 3 バイト |
| U+10000 以上(絵文字など) | 4 バイト |
'A' (U+0041) → 0x41 (1バイト、ASCII と同じ)
'あ' (U+3042) → 0xE3 0x81 0x82 (3バイト)
'😀' (U+1F600) → 0xF0 0x9F 0x98 0x80 (4バイト)
UTF-8 のメリット: ASCII と完全互換(既存の ASCII テキストはそのまま UTF-8 として読める)。Web の標準エンコーディング。
UTF-16 ── 固定 2 バイト(一部例外あり)
基本的に 1 文字を 2 バイト(16 ビット)で表現する。ただし U+FFFF を超える文字(絵文字など)は 4 バイト(サロゲートペア)になる。
'A' (U+0041) → 0x0041 (2バイト)
'あ' (U+3042) → 0x3042 (2バイト)
'😀' (U+1F600) → 0xD83D 0xDE00 (4バイト、サロゲートペア)
6. Java の char が 16 ビットな理由
Java は 1995 年に設計された。当時の Unicode は「65536 文字(U+0000〜U+FFFF)あれば全世界の文字を表現できる」と考えられていた。そのため Java は char を 16 ビット(UTF-16 の 1 単位) として設計した。
char c = 'A'; // 16ビット = 2バイト
(int) c = 65 // コードポイント
char c2 = 'あ'; // U+3042 = 12354
(int) c2 = 12354
しかし Unicode 2.0(1996 年)で 65536 文字を超える文字が追加されてしまった。Java の char では足りなくなった。
7. サロゲートペア ── char 2 個で 1 文字
U+FFFF を超えるコードポイント(絵文字・一部の漢字など)は、2 つの char(サロゲートペア) で表現される。
String emoji = "😀"; // U+1F600
System.out.println(emoji.length()); // 2 ← char の数(コード単位数)
System.out.println(emoji.codePointCount(0, emoji.length())); // 1 ← 実際の文字数
char high = emoji.charAt(0); // サロゲートの前半(High Surrogate)
char low = emoji.charAt(1); // サロゲートの後半(Low Surrogate)
System.out.println(Character.isSurrogate(high)); // true
これが "😀".length() == 2 の理由だ。
文字数を正確に数えるには
String s = "Hello😀World";
// ❌ char の数(サロゲートペアを 2 と数える)
System.out.println(s.length()); // 13
// ✅ 実際のコードポイント数
System.out.println(s.codePointCount(0, s.length())); // 12
// コードポイント単位で処理する
s.codePoints().forEach(cp -> {
System.out.println(new String(Character.toChars(cp)));
});
8. Java における文字処理のまとめ
// char: UTF-16 の 1 コード単位(U+FFFF まで安全)
char c = 'あ'; // OK
char c2 = '😀'; // コンパイルエラー(サロゲートペアが必要)
// int(コードポイント): すべての Unicode 文字を表現できる
int cp = "😀".codePointAt(0); // 128512 (U+1F600)
String s = new String(Character.toChars(cp)); // "😀"
// String: UTF-16 の char の列として内部保存
// ただし Java 9 以降は Compact Strings により、
// ASCII のみの文字列は 1 バイト/文字に最適化される
実務での注意点
// ❌ charAt でコードポイント単位処理(絵文字で壊れる)
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i); // サロゲートペアの片割れが来る可能性
}
// ✅ codePoints() でコードポイント単位処理
s.codePoints().forEach(cp -> {
// cp はすべてのUnicode文字を正しく表現できる
});
// 文字列の比較: 正規化を考慮する場合は Normalizer を使う
// (同じ見た目でも別のコードポイント列の場合がある)
import java.text.Normalizer;
String normalized = Normalizer.normalize(s, Normalizer.Form.NFC);
まとめ
| 概念 | 正体 |
|---|---|
| コードポイント | Unicode が各文字に割り当てた番号(U+XXXX) |
| UTF-8 | 可変長(1〜4バイト)。ASCII 互換。Web の標準 |
| UTF-16 | 基本 2 バイト。U+FFFF 超はサロゲートペア(4バイト) |
Java の char | UTF-16 の 1 コード単位(16ビット)。U+FFFF まで |
| サロゲートペア | U+FFFF 超の文字を char 2 個で表現する仕組み |
String.length() | char の数(コードポイント数ではない) |
String.codePointCount() | 実際の文字(コードポイント)の数 |
「文字列を 1 文字ずつ処理する」コードは charAt ではなく codePoints() を使うのが正確だ。絵文字や一部の漢字(U+20000 以上の CJK 拡張漢字)が入ると charAt は壊れる。