1. 概要

このチュートリアルでは、文字エンコードの基本とJavaでの処理方法について説明します。

2. 文字エンコードの重要性

ラテン語やアラビア語などの多様なライティングスクリプトを使用して、複数の言語に属するテキストを処理する必要があることがよくあります。 すべての言語のすべての文字は、何らかの方法で1と0のセットにマップする必要があります。 本当に、コンピューターが私たちのすべての言語を正しく処理できるのは不思議です。

これを適切に行うには、文字エンコードについて考える必要があります。そうしないと、データが失われたり、セキュリティの脆弱性が発生したりする可能性があります。

これをよりよく理解するために、Javaでテキストをデコードするメソッドを定義しましょう。

String decodeText(String input, String encoding) throws IOException {
    return 
      new BufferedReader(
        new InputStreamReader(
          new ByteArrayInputStream(input.getBytes()), 
          Charset.forName(encoding)))
        .readLine();
}

ここでフィードする入力テキストは、デフォルトのプラットフォームエンコーディングを使用していることに注意してください。

「ファサードパターンはソフトウェアデザインパターンです」という入力でこのメソッドを実行するとします。 「US-ASCII」としてエンコードすると、次のように出力されます。

The fa��ade pattern is a software design pattern.

まあ、私たちが期待したものとは正確には異なります。

何がうまくいかなかったのでしょうか? このチュートリアルの残りの部分では、これを理解して修正しようとします。

3. 基礎

ただし、深く掘り下げる前に、エンコーディング文字セット、およびコードポイントの3つの用語を簡単に確認しましょう。

3.1. エンコーディング

コンピューターは、10のようなバイナリ表現しか理解できません。 他のものを処理するには、実際のテキストからそのバイナリ表現への何らかのマッピングが必要です。 このマッピングは、文字エンコードまたは単にエンコードとして知られているものです。

たとえば、US-ASCII のメッセージの最初の文字「T」は、を「01010100」にエンコードします。

3.2. チャーセット

文字のバイナリ表現へのマッピングは、含まれる文字の点で大きく異なる可能性があります。 マッピングに含まれる文字の数は、実際に使用されている数文字からすべての文字までさまざまです。 マッピング定義に含まれる文字のセットは、正式にはcharsetと呼ばれます。

たとえば、ASCIIには128文字の文字セットがあります

3.3。 コードポイント

コードポイントは、文字を実際のエンコーディングから分離する抽象化です。 コードポイントは特定の文字への整数参照です。

整数自体は、10進数、または16進数や8進数などの代替ベースで表すことができます。 多数を参照しやすいように、代替ベースを使用します。

たとえば、Unicodeのメッセージの最初の文字Tには、コードポイント「U + 0054」(または10進数で84)があります。

4. エンコーディングスキームを理解する

文字エンコードは、エンコードする文字数に応じてさまざまな形式をとることができます。

エンコードされる文字数は、各表現の長さと直接的な関係があり、通常はバイト数として測定されます。 エンコードする文字が多いということは、本質的に、より長いバイナリ表現が必要であることを意味します。

今日実際に人気のあるエンコーディングスキームのいくつかを見てみましょう。

4.1. シングルバイトエンコーディング

ASCII (情報交換のためのアメリカ標準コード)と呼ばれる最も初期のエンコーディングスキームの1つは、シングルバイトエンコーディングスキームを使用します。 これは基本的に、ASCIIの各文字が7ビットの2進数で表されることを意味します。これでも、すべてのバイトに1ビットの空きがあります。

ASCIIの128文字セットは、小文字と大文字の英語のアルファベット、数字、および一部の特殊文字と制御文字をカバーしています。

Javaで、特定のエンコード方式で文字のバイナリ表現を表示する簡単なメソッドを定義しましょう。

String convertToBinary(String input, String encoding) 
      throws UnsupportedEncodingException {
    byte[] encoded_input = Charset.forName(encoding)
      .encode(input)
      .array();  
    return IntStream.range(0, encoded_input.length)
        .map(i -> encoded_input[i])
        .mapToObj(e -> Integer.toBinaryString(e ^ 255))
        .map(e -> String.format("%1$" + Byte.SIZE + "s", e).replace(" ", "0"))
        .collect(Collectors.joining(" "));
}

現在、文字「T」のコードポイントはUS-ASCIIでは84です(ASCIIはJavaではUS-ASCIIと呼ばれます)。

また、ユーティリティメソッドを使用すると、そのバイナリ表現を確認できます。

assertEquals(convertToBinary("T", "US-ASCII"), "01010100");

これは、予想どおり、文字「T」の7ビットのバイナリ表現です。

元のASCIIは、すべてのバイトの最上位ビットを未使用のままにしました。同時に、ASCIIは、特に英語以外の言語の場合、表現されていない文字をかなり多く残していました。

これは、その未使用のビットを利用し、追加の128文字を含める努力につながりました。

長い間提案され採用されたASCIIエンコーディングスキームにはいくつかのバリエーションがありました。これらは大まかに「ASCII拡張」と呼ばれるようになりました。

ASCII拡張機能の多くは成功のレベルが異なりますが、多くの文字がまだ表現されていないため、これは広く採用するには十分ではありませんでした。

最も人気のあるASCII拡張機能の1つは、ISO-8859-1 で、「ISOラテン1」とも呼ばれます。

4.2. マルチバイトエンコーディング

ますます多くの文字に対応する必要性が高まるにつれて、ASCIIのようなシングルバイトエンコーディングスキームは持続可能ではありませんでした。

これにより、必要なスペースが増えるという犠牲を払っても、はるかに優れた容量を持つマルチバイトエンコーディングスキームが生まれました。

BIG5とSHIFT-JISは、マルチバイト文字エンコード方式の例であり、より広い文字セットを表すために1バイトと2バイトを使用し始めました。 これらのほとんどは、文字数が大幅に多い中国語および同様のスクリプトを表す必要があるために作成されました。

ここで、メソッド convertToBinary を呼び出し、 input を「語」、漢字、encodingを「Big5」とします。

assertEquals(convertToBinary("語", "Big5"), "10111011 01111001");

上記の出力は、Big5エンコーディングが文字「語」を表すために2バイトを使用することを示しています。

文字エンコーディングの包括的なリストとそのエイリアスは、InternationalNumberAuthorityによって管理されています。

5. Unicode

エンコードは重要ですが、表現を理解するためにはデコードも同様に重要であることを理解するのは難しいことではありません。 これは、一貫性のある、または互換性のあるエンコーディングスキームが広く使用されている場合にのみ実際に可能です。

単独で開発され、地域で実践されているさまざまなエンコーディングスキームは、困難になり始めました。

この課題により、世界で可能なすべての文字を処理できるUnicodeと呼ばれる単一のエンコーディング標準が生まれました。 これには、使用中のキャラクターだけでなく、機能しなくなったキャラクターも含まれます。

さて、それは各文字を格納するために数バイトを必要とする必要がありますか? 正直なところそうですが、Unicodeには独創的な解決策があります。

標準としてのUnicodeは、世界中のすべての可能な文字のコードポイントを定義します。Unicodeの文字「T」のコードポイントは10進数で84です。 通常、これをUnicodeでは「U + 0054」と呼びます。これは、U+の後に16進数が続くことに他なりません。

Unicodeのコードポイントのベースとして16進数を使用します。これは、1,114,112ポイントがあるためです。これは、10進数で便利に通信するには、かなり大きな数値です。

これらのコードポイントがビットにエンコードされる方法は、Unicode内の特定のエンコードスキームに任されています。これらのエンコードスキームの一部については、以下のサブセクションで説明します。

5.1. UTF-32

UTF-32は、 Unicodeのエンコード方式であり、Unicodeで定義されたすべてのコードポイントを表すために4バイトを使用します。 明らかに、すべての文字に4バイトを使用することはスペース効率が悪くなります。

‘T’のような単純な文字がUTF-32でどのように表現されるかを見てみましょう。 前に紹介したメソッドconvertToBinaryを使用します。

assertEquals(convertToBinary("T", "UTF-32"), "00000000 00000000 00000000 01010100");

上記の出力は、最初の3バイトが単にスペースを浪費している文字「T」を表すための4バイトの使用法を示しています。

5.2. UTF-8

UTF-8は、 Unicodeの別のエンコード方式であり、可変長のバイトを使用してエンコードします。 通常、1バイトを使用して文字をエンコードしますが、必要に応じてより多くのバイトを使用できるため、スペースを節約できます。

入力を「T」、エンコードを「UTF-8」として、メソッドconvertToBinaryをもう一度呼び出しましょう。

assertEquals(convertToBinary("T", "UTF-8"), "01010100");

出力は、1バイトだけを使用するASCIIとまったく同じです。 実際、UTF-8はASCIIと完全な下位互換性があります。

入力を「語」、エンコードを「UTF-8」として、メソッドconvertToBinaryをもう一度呼び出しましょう。

assertEquals(convertToBinary("語", "UTF-8"), "11101000 10101010 10011110");

ここでわかるように、UTF-8は文字「語」を表すために3バイトを使用します。 これは可変幅エンコーディングとして知られています。

UTF-8は、スペース効率が高いため、Webで使用される最も一般的なエンコーディングです。

6. Javaでのエンコーディングサポート

Javaは、さまざまなエンコーディングとそれらの相互変換をサポートしています。 クラスCharsetは、Javaプラットフォームのすべての実装でサポートが義務付けられている標準エンコーディングのセットを定義します。

これには、US-ASCII、ISO-8859-1、UTF-8、UTF-16などが含まれます。 Javaの特定の実装は、オプションで追加のエンコーディングをサポートする場合があります

Javaが使用する文字セットを取得する方法には微妙な点がいくつかあります。 それらをさらに詳しく見ていきましょう。

6.1. デフォルトの文字セット

Javaプラットフォームは、デフォルトの文字セットと呼ばれるプロパティに大きく依存しています。 Java仮想マシン(JVM)は、起動時にデフォルトの文字セットを決定します

これは、JVMが実行されている基盤となるオペレーティングシステムのロケールと文字セットによって異なります。 たとえば、MacOSでは、デフォルトの文字セットはUTF-8です。

デフォルトの文字セットを決定する方法を見てみましょう。

Charset.defaultCharset().displayName();

このコードスニペットをWindowsマシンで実行すると、次の出力が得られます。

windows-1252

現在、「windows-1252」は英語でのWindowsプラットフォームのデフォルトの文字セットであり、この場合、Windowsで実行されているJVMのデフォルトの文字セットを決定します。

6.2. デフォルトの文字セットを使用するのは誰ですか?

Java APIの多くは、JVMによって決定されたデフォルトの文字セットを利用します。 いくつか例を挙げると:

  • InputStreamReaderおよびFileReader
  • OutputStreamWriterおよびFileWriter
  • フォーマッターおよびスキャナー
  • URLEncoderおよびURLDecoder

つまり、これは、文字セットを指定せずに例を実行した場合、次のことを意味します。

new BufferedReader(new InputStreamReader(new ByteArrayInputStream(input.getBytes()))).readLine();

次に、デフォルトの文字セットを使用してデコードします。

また、デフォルトでこれと同じ選択を行うAPIがいくつかあります。

したがって、デフォルトの文字セットは、安全に無視できない重要性を想定しています。

6.3. デフォルトの文字セットに関する問題

これまで見てきたように、Javaのデフォルトの文字セットは、JVMの起動時に動的に決定されます。 これにより、異なるオペレーティングシステムで使用した場合に、プラットフォームの信頼性が低下したり、エラーが発生しやすくなります。

たとえば、

new BufferedReader(new InputStreamReader(new ByteArrayInputStream(input.getBytes()))).readLine();

macOSでは、UTF-8を使用します。

Windowsで同じスニペットを試してみると、Windows-1252を使用して同じテキストをデコードします。

または、macOSでファイルを書き込んでから、Windowsで同じファイルを読み取ることを想像してみてください。

エンコード方式が異なるため、データの損失や破損につながる可能性があることを理解するのは難しくありません。

6.4. デフォルトの文字セットを上書きできますか?

Javaでのデフォルトの文字セットの決定は、2つのシステムプロパティにつながります。

  • file.encoding :このシステムプロパティの値は、デフォルトの文字セットの名前です
  • sun.jnu.encoding :このシステムプロパティの値は、ファイルパスのエンコード/デコード時に使用される文字セットの名前です。

これで、コマンドライン引数を使用してこれらのシステムプロパティをオーバーライドするのが直感的になりました。

-Dfile.encoding="UTF-8"
-Dsun.jnu.encoding="UTF-8"

ただし、これらのプロパティはJavaでは読み取り専用であることに注意してください。 上記の使用法はドキュメントにはありません。 これらのシステムプロパティをオーバーライドすると、望ましい動作や予測可能な動作が得られない場合があります。

したがって、Javaのデフォルトの文字セットをオーバーライドすることは避けてください。

6.5. Javaがこれを解決しないのはなぜですか?

ロケールとオペレーティングシステムの文字セットに基づくのではなく、Javaのデフォルトの文字セットとして「UTF-8」を使用することを規定するJava拡張提案(JEP)があります。

このJEPは現在ドラフト状態であり、(うまくいけば!)通過すると、前に説明した問題のほとんどが解決されます。

java.nio.file.Files にあるような新しいAPIは、デフォルトの文字セットを使用しないことに注意してください。 これらのAPIのメソッドは、デフォルトの文字セットではなく、UTF-8として文字セットを使用して文字ストリームを読み書きします。

6.6. 私たちのプログラムでこの問題を解決する

通常、デフォルト設定に依存するのではなく、テキストを処理するときに文字セットを指定することを選択する必要があります。 文字からバイトへの変換を処理するクラスで使用するエンコーディングを明示的に宣言できます。

幸い、この例ではすでに文字セットを指定しています。 適切なものを選択し、残りはJavaに任せる必要があります。

‘ç’のようなアクセント付き文字はエンコーディングスキーマASCIIに存在しないため、それらを含むエンコーディングが必要であることを理解しておく必要があります。 おそらく、UTF-8?

それを試してみましょう。ここで、メソッド decodeText を、「UTF-8」と同じ入力でエンコードして実行します。

The façade pattern is a software-design pattern.

ビンゴ! 私たちが今見たいと思っていた出力を見ることができます。

ここでは、InputStreamReaderのコンストラクターでニーズに最も適していると思われるエンコーディングを設定しました。 これは通常、Javaで文字とバイト変換を処理する最も安全な方法です。

同様に、 OutputStreamWriter および他の多くのAPIは、コンストラクターを介したエンコードスキームの設定をサポートしています。

6.7. MalformedInputException

バイトシーケンスをデコードする場合、特定の Charset に対して正当でない場合、または正当な16ビットUnicodeではない場合があります。 つまり、指定されたバイトシーケンスには、指定されたCharsetにマッピングがありません。

入力シーケンスに不正な形式の入力がある場合、3つの事前定義された戦略(または CodingErrorAction )があります。

  • IGNORE は不正な形式の文字を無視し、コーディング操作を再開します
  • REPLACE は、出力バッファー内の不正な形式の文字を置き換え、コーディング操作を再開します
  • REPORTMalformedInputExceptionをスローします

CharsetDecoderのデフォルトのmalformedInputActionはREPORT、であり、InputStreamReaderのデフォルトデコーダーのデフォルトのmalformedInputActionREPLACEです。[ X199X]

指定されたCharset CodingErrorAction type、およびデコードされる文字列を受け取るデコード関数を定義しましょう。

String decodeText(String input, Charset charset, 
  CodingErrorAction codingErrorAction) throws IOException {
    CharsetDecoder charsetDecoder = charset.newDecoder();
    charsetDecoder.onMalformedInput(codingErrorAction);
    return new BufferedReader(
      new InputStreamReader(
        new ByteArrayInputStream(input.getBytes()), charsetDecoder)).readLine();
}

したがって、「ファサードパターンはソフトウェアデザインパターンです」とデコードすると、 US_ASCII を使用すると、各戦略の出力が異なります。 まず、不正な文字をスキップするCodingErrorAction.IGNOREを使用します。

Assertions.assertEquals(
  "The faade pattern is a software design pattern.",
  CharacterEncodingExamples.decodeText(
    "The façade pattern is a software design pattern.",
    StandardCharsets.US_ASCII,
    CodingErrorAction.IGNORE));

2番目のテストでは、 CodingErrorAction.REPLACE を使用して、不正な文字の代わりに�を配置します。

Assertions.assertEquals(
  "The fa��ade pattern is a software design pattern.",
  CharacterEncodingExamples.decodeText(
    "The façade pattern is a software design pattern.",
    StandardCharsets.US_ASCII,
    CodingErrorAction.REPLACE));

3番目のテストでは、 CodingErrorAction.REPORT を使用して、 MalformedInputException:をスローします。

Assertions.assertThrows(
  MalformedInputException.class,
    () -> CharacterEncodingExamples.decodeText(
      "The façade pattern is a software design pattern.",
      StandardCharsets.US_ASCII,
      CodingErrorAction.REPORT));

7. エンコーディングが重要なその他の場所

プログラミング中に文字エンコードを考慮する必要はありません。 他の多くの場所では、テキストが最終的に間違ってしまう可能性があります。

これらの場合の問題の最も一般的な原因は、あるエンコーディングスキームから別のへのテキストの変換であり、それによってデータ損失が発生する可能性があります。

テキストのエンコードまたはデコード時に問題が発生する可能性のあるいくつかの場所を簡単に見ていきましょう。

7.1. テキストエディタ

ほとんどの場合、テキストエディタはテキストの出所です。 vi、メモ帳、MS Wordなど、人気のあるテキストエディタが多数あります。 これらのテキストエディタのほとんどでは、エンコードスキームを選択できます。 したがって、処理するテキストに適切であることを常に確認する必要があります。

7.2. ファイルシステム

エディターでテキストを作成したら、それらをファイルシステムに保存する必要があります。 ファイルシステムは、それが実行されているオペレーティングシステムによって異なります。 ほとんどのオペレーティングシステムは、複数のエンコーディングスキームを固有にサポートしています。 ただし、エンコーディング変換によってデータが失われる場合があります。

7.3. 通信網

ファイル転送プロトコル(FTP)などのプロトコルを使用してネットワーク経由で転送される場合、テキストには文字エンコード間の変換も含まれます。 Unicodeでエンコードされたものについては、変換での損失のリスクを最小限に抑えるために、バイナリとして転送するのが最も安全です。 ただし、ネットワークを介してテキストを転送することは、データ破損の頻度が低い原因の1つです。

7.4. データベース

OracleやMySQLなどの一般的なデータベースのほとんどは、データベースのインストールまたは作成時に文字エンコード方式の選択をサポートしています。 データベースに保存する予定のテキストに従って、これを選択する必要があります。 これは、エンコーディング変換が原因でテキストデータの破損が発生することが多い場所の1つです。

7.5. ブラウザ

最後に、ほとんどのWebアプリケーションでは、テキストを作成し、ブラウザーなどのユーザーインターフェイスで表示することを目的として、テキストをさまざまなレイヤーに渡します。 ここでも、文字を適切に表示できる適切な文字エンコードを選択することが不可欠です。 Chrome、Edgeなどの最も一般的なブラウザでは、設定から文字エンコードを選択できます。

8. 結論

この記事では、プログラミング中にエンコーディングがどのように問題になる可能性があるかについて説明しました。

さらに、エンコーディングや文字セットなどの基本についても説明しました。 さらに、さまざまなエンコーディングスキームとその使用法を試しました。

また、Javaでの誤った文字エンコードの使用例を取り上げ、それを正しく行う方法を確認しました。 最後に、文字エンコードに関連する他の一般的なエラーシナリオについて説明しました。

いつものように、例のコードはGitHubから入手できます。