1. エンコーディング(encoding)の概念
まず最初の問いから始めましょう: エンコーディングとは何ですか?
国際会議に参加したと想像してください。みんなそれぞれの言語で話しますが、互いに理解したい。そこで必要なのが、英語の "hello" はロシア語の "privet"、スペイン語の "hola" に当たる、と知っている翻訳者です。コンピュータの世界でのエンコーディングは、その翻訳者に相当します。
エンコーディングは、文字をバイト列で表現する方法です
コンピュータは単純な機械で、理解できるのは0と1、つまりビットとバイトだけです。しかし人間は文字や数字、絵文字、さらには(なんてことだ!)漢字のような表意文字も見たい。コンピュータが「文字を書き込む」には、各文字にどのバイト列を対応させるか取り決める必要があります。
エンコーディング(encoding)とは、文字(文字、数字、句読点、絵文字など)を保存や送信のためにバイトに変換し、逆に表示のためにバイトを文字へ戻すための規則の集合です。
例: さまざまなエンコーディングにおける文字 'A'(キリル文字)
- UTF-8 では、'A'(キリル文字)は2バイトで表されます: 0xD0 0x90。
- Windows-1251 では、同じ文字は1バイトです: 0xC0。
- 一方、ラテン文字の 'A' は、ほとんどの一般的なエンコーディングで 0x41 です。
ファイルを誤ったエンコーディングで読むと、文字は「文字化け」(意味不明な記号や「?」)になります。
2. なぜエンコーディングが必要か
なぜ文字をそのまま保存できないのですか?
コンピュータが理解するのは数(0と1)だけだからです。どの数値がどの文字に対応するか——それこそがエンコーディングの本質です。
例: ディスク上の「Privet」
ファイルに "Privet" という単語を書き込むと、コンピュータにとっては単なるバイト列です。これらのバイトをどう解釈するかは、エンコーディング次第です。
- ファイルが UTF-8 で記録されている場合、先頭のキリル文字(例: U+041F)も次の小文字(例: U+0440)もそれぞれ2バイトになります。
- Windows-1251 の場合は各文字が1バイトですが、バイト値は別物です。
どこでエンコーディングが必要?
- テキストをファイルに書き込むとき: 後で正しく読み取れるようにするため。
- テキストをファイルから読むとき: バイト列を正しく文字に戻すため。
- ネットワークでテキストを送るとき(例: HTTP、e‑mail)。
- データベースを扱うとき: テキストがどのエンコーディングで保存されているかを理解する必要があります。
エンコーディングを指定しないと…
未知の言語のテキストを読もうとするのと同じです。その言語が分かっていれば成功率は上がりますが、分からなければ、うまくいっても意味が取れず、悪ければ支離滅裂な文字列になります。
3. 正しいエンコーディングがない場合の問題
文字化けとデータ損失
初心者(だけでなく)プログラマのよくある嘆き: 「"Privet" のはずが "Привет" や『?』だらけになるのはなぜ?」
これは、ファイルがあるエンコーディングで保存され、別のエンコーディングで読まれているときに起こります。たとえば古い Windows で Windows-1251 で作ったファイルを、デフォルトが UTF-8 の Linux で開いた、あるいはその逆です。
例
- Windows-1251 で保存: キリル文字 U+041F のバイトは 0xCF。
- UTF-8 で開く: プログラムはキリル文字が2バイトだと期待しているのに1バイトしか来ない。すべてが崩れます。
データ損失
保存時に選んだエンコーディングがその文字をサポートしていないと(例えば ASCII に絵文字を保存しようとした場合など)、その文字は消えるか「?」に置き換わります。エンコーディングに「収まらない」ものは失われます。
ファイル交換時の問題
あるエンコーディングで保存されたファイルは、別のデフォルトエンコーディングを使う他のコンピュータでは正しく表示されないことがあります。Windows と Linux 間でのファイル交換や、古いファイルを開くときによく起こります。
4. Java におけるエンコーディング: 内部表現と外部表現
JVM の内部: いつでも Unicode(UTF-16)
Java の文字列(String)は、プログラム内部では常に Unicode(正確には UTF-16)で保持されます。つまり、世界中のどの言語の文字列でも変数に代入でき、Java は問題なく扱えます。
String hello = "こんにちは、世界! 😀";
JVM のメモリ上では、このテキストは 16 ビット値(char)の集合として保持され、各文字には Unicode 表の固有のコードが割り当てられています。
豆知識
Java の char は 16 ビット(2 バイト)です。しかし一部の文字(たとえば稀な漢字や絵文字)は char を2つ必要とします——これが「サロゲートペア」です。
入出力では: エンコーディングが重要!
外部の世界(ファイル、ネットワーク)に文字列を読み込む/書き出すとき、Java は内部表現(UTF-16)をバイト列に変換します。ここでエンコーディングが必要になります。
- エンコーディングを明示しない場合、Java はシステムのデフォルトを使います(ロシア語環境の Windows では Windows-1251、Linux では UTF-8 など)。
- これは危険です。別のコンピュータでは結果が変わるかもしれません。
例: エンコーディングを指定せずにファイルを読み書きする
// 悪い例! エンコーディングが明示されていません。
FileReader reader = new FileReader("data.txt");
FileWriter writer = new FileWriter("data.txt");
この場合、Java はシステムのエンコーディングを使用します。ファイルが別のシステムで書かれていたら——「文字化け」になります。
良い実践: いつもエンコーディングを指定する
// 良い例! エンコーディングを明示しています。
BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream("data.txt"), StandardCharsets.UTF_8));
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream("data.txt"), StandardCharsets.UTF_8));
5. 短いイラスト: エンコーディングの舞台裏で何が起きているか
図: ファイルからプログラムへ、そして戻るまでの文字列の道のり
[ディスク上のファイル(バイト列、エンコーディング X)]
|
V
[Java がバイトを読み、エンコーディング X を使って String(UTF-16)へ変換]
|
V
[あなたのプログラム内で文字列を扱う]
|
V
[Java が String をバイト列へ書き出す(エンコーディング Y を使用)]
|
V
[ディスク上のファイル(バイト列、エンコーディング Y)]
X と Y が一致していれば問題は起きません。異なるとトラブルの可能性があります。
6. エンコーディング小史(知っておくと役立つ)
ASCII
ASCII は最古参のエンコーディングのひとつで、1 文字を 1 バイトで表し、英字・数字・基本記号のみを扱います。他の文字体系は対象外です。
Windows-1251、ISO-8859-1 などの「古参」たち
これは文字体系ごとに用意された 1 バイト系エンコーディングです: キリル文字、ラテン文字、ギリシャ文字など。各所が独自に選んだ結果、混乱が生じました。
Unicode と UTF ファミリー
- Unicode は世界中の文字のためのグローバルな表です。
- UTF-8、UTF-16、UTF-32 は、Unicode の文字をバイト列で表現する異なる方法です。
- UTF-8 は Web、ファイル、システム間連携の標準になりました。
7. 実践: エンコーディングがファイル操作に与える影響
異なるエンコーディングでの文字列の書き込みと読み込みの小さな例を見てみましょう。
例: 異なるエンコーディングで書いてから読む
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
public class EncodingDemo {
public static void main(String[] args) throws IOException {
String text = "こんにちは、世界! 😀";
// ファイルを UTF-8 で書き出す
try (Writer writer = new OutputStreamWriter(
new FileOutputStream("utf8.txt"), StandardCharsets.UTF_8)) {
writer.write(text);
}
// 次に、間違ったエンコーディングで読んでみる
try (Reader reader = new InputStreamReader(
new FileInputStream("utf8.txt"), Charset.forName("Windows-1251"))) {
int c;
while ((c = reader.read()) != -1) {
System.out.print((char) c);
}
}
// 画面には文字化けが表示されます!
}
}
結論: エンコーディングが一致しないと、テキストは歪みます。
8. エンコーディングと他システムとの連携
実プロジェクトでは、異なる言語で書かれ、異なる OS 上で動くプログラム間でファイルをやり取りすることがよくあります。各プログラムが期待するエンコーディングはそれぞれ異なりがちです。事前に取り決めておかないと、「文字化け」と原因不明のバグを招きます。典型的なケース: データベースは UTF-8 でテキストを保存しているのに、プログラムが元ファイルを Windows-1251 として読み込み、そのまま DB に投入してしまう——文字の破損が確定です。
9. エンコーディングでよくあるミス
ミス No.1: ファイルの読み書き時にエンコーディングを指定しない。
その結果、「自分のマシンでは動く」のに、同僚の環境では「文字化け」になります。
ミス No.2: 古いコンストラクタ(FileReader、FileWriter)を使う。
これらは常にシステムのエンコーディングを使用します——初心者にとっての落とし穴です。
ミス No.3: 元ファイルのエンコーディングが間違っている。
ファイルがあるエンコーディングで保存され、別のエンコーディングで読まれると、一部の文字が歪んだり「?」に置き換わったりします。
ミス No.4: エンコーディング間の変換での文字欠落。
対象のエンコーディングがすべての文字をサポートしていない場合(例えば UTF-8 の代わりに ASCII を使うなど)、一部のテキストは単に消えます。
GO TO FULL VERSION