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();
}

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

  • 「ファサードパターンはソフトウェア設計パターン」として

    input

    を使用し、「US-ASCII」として

    encoding

    ** を使用してこのメ​​ソッドを実行すると、次のように出力されます。

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

まあ、私たちがまさに期待したことではありません。

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


3基礎

しかし、より深く掘り下げる前に、3つの用語を簡単に見てみましょう。


encoding



charsets

、および

code point


3.1. エンコーディング

コンピュータは

1



0

のようなバイナリ表現しか理解できません。

それ以外のものを処理するには、実際のテキストからそのバイナリ表現へのある種のマッピングが必要です。

このマッピングは、私たちが

文字エンコーディング

または単に

エンコーディング


として知っているものです。

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


3.2. 文字セット

それらのバイナリ表現への文字のマッピングは、それらが含む文字に関して非常に異なり得る。マッピングに含まれる文字数は、実際に使用される文字数のごくわずかからすべての文字までさまざまです。

マッピング定義に含まれる文字の集合は、正式には

charset


と呼ばれています。

たとえば、http://ee.hawaii.edu/~tep/EE160/Book/chap4/subsection2.1.1.1.html[ASCIIの文字セットは128文字]


3.3.

コードポイント

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

整数そのものを、通常の10進数または16進数や8進数のような代替基数で表すことができます。大量の参照を容易にするために、代替ベースを使用します。

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

** 4符号化方式について

文字エンコードは、エンコードする文字数に応じてさまざまな形式を取ります。

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

今日実際に使用されている一般的なエンコード方式のいくつかを見てみましょう。


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

ASCII(情報交換用米国標準コード)と呼ばれる最も初期のエンコード方式の1つは、1バイトのエンコード方式を使用します。これは本質的に** ASCIIの各文字は7ビットの2進数で表現されることを意味します。

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ビット2進表現です。

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

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

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

ASCII拡張の多くは異なるレベルの成功を収めましたが、明らかに、多くの文字がまだ表されていなかったので、これはより広い採用には十分ではありませんでした。

  • 最も普及しているASCII拡張の1つはISO-8859-1 ** であり、「ISO Latin 1」とも呼ばれます。


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

ますます多くの文字を収容する必要性が高まるにつれて、ASCIIのようなシングルバイト符号化方式は持続可能ではなかった。

これは、スペース要件の増加を犠牲にして、はるかに優れた容量を有するマルチバイト符号化方式をもたらした。

BIG5とSHIFT-JISは、より広い文字セットを表すために1バイトと2バイトを使い始めた

マルチバイト文字コード体系の例です

。これらのほとんどは、かなり多くの文字数を持つ中国語などの文字を表す必要があるために作成されました。

それでは、

input

を「語​​」、漢字、

encoding

を「Big5」として、メソッド

convertToBinary

を呼び出します。

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

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


包括的なリスト

の文字エンコーディングとそのエイリアスは、International Number Authorityによって管理されています。


5 Unicode

エンコードは重要ですが、表現を理解するにはデコードも同様に重要であることを理解するのは難しくありません。これは実際には、一貫性のある、あるいは互換性のある符号化方式が広く使われている場合にのみ可能です。

単独で開発され、地域で実践されているさまざまなエンコード方式が困難になり始めました。

この課題により、** Unicodeと呼ばれる単一のエンコード規格が生まれました。これは、世界中のあらゆる文字に対応する能力を備えています。これは使われているキャラクター、そして使われなくなったキャラクターさえ含みます!

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

  • 標準としてのUnicodeは、世界中のすべての可能な文字のコードポイントを定義しています** Unicodeの文字 ‘T’のコードポイントは、10進数で84です。私たちはこれをUnicodeでは「U + 0054」と呼んでいます。

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

  • これらのコードポイントがどのようにビットにエンコードされるかは、Unicode内の特定のエンコード方式に任されています。


5.1. UTF-32

UTF-32は、Unicodeで定義されているすべてのコードポイントを表すために4バイトを使用する、Unicodeのエンコード方式です。明らかに、各文字に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つのエンコード方式です。一般的に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

はhttps://docs.oracle.com/javase/8/docs/technotes/guides/intl/encoding.doc.html[標準エンコーディングのセット]を定義し、これはJavaプラットフォームのすべての実装でサポートが義務付けられています。

これには、US-ASCII、ISO-8859-1、UTF-8、およびUTF-16が含まれます。

Javaの特定の実装はオプションで追加のエンコーディングをサポートするかもしれません

Javaが扱う文字セットを選ぶ方法には、いくつかの微妙な点があります。もっと詳しく見ていきましょう。


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

Javaプラットフォームは、デフォルトのcharsetというプロパティに大きく依存しています。

Java仮想マシン(JVM)は起動時にデフォルトの文字セットを決定します

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

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

Charset.defaultCharset().displayName();

Windowsマシンでこのコードスニペットを実行すると、出力は次のようになります。

windows-1252

現在、「windows-1252」は英語でのWindowsプラットフォームのデフォルト文字セットです。


6.2. 誰がデフォルトの文字セットを使用しますか?

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


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はこれを解決しないのですか?

ロケールおよびオペレーティングシステムの文字セットではなく「UTF-8」をJavaのデフォルトの文字セットとして使用することを規定したhttp://openjdk.java.net/jeps/8187041[Java Enhancement Proposal](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は、それらのコンストラクターによるエンコード方式の設定をサポートしています。


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での誤った文字エンコードの使用例を取り上げ、その正しい方法を見ました。最後に、文字エンコーディングに関連した他の一般的なエラーシナリオについて説明しました。

いつものように、例のコードはhttps://github.com/eugenp/tutorials/tree/master/core-java[GitHubで利用可能]です。