SJ blog
backend
A

信頼度ランク

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-8UTF-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 の charUTF-16 の 1 コード単位(16ビット)。U+FFFF まで
サロゲートペアU+FFFF 超の文字を char 2 個で表現する仕組み
String.length()char の数(コードポイント数ではない)
String.codePointCount()実際の文字(コードポイント)の数

「文字列を 1 文字ずつ処理する」コードは charAt ではなく codePoints() を使うのが正確だ。絵文字や一部の漢字(U+20000 以上の CJK 拡張漢字)が入ると charAt は壊れる。