JEP 400 and the Default Charset

原文はこちら。
The original article was written by Naoto Sato (Principal Member of Technical Staff, Java Platform Group at Oracle).
https://inside.java/2021/10/04/the-default-charset-jep400/

TL;DR: JDK 18から、UTF-8がプラットフォーム全体でデフォルトの文字セット (charset) になります。特にWindows上でアプリケーションを実行する場合は、必ずアプリケーションをテストしてください。

デフォルトの文字セット (default charset) について疑問に思ったことはありますか? Charset.defaultCharsetのjavadocによると、以下のようです。

The default charset is determined during virtual-machine startup and typically depends upon the locale and charset of the underlying operating system. (デフォルトの文字セットは仮想マシンの起動時に決定されますが、それは通常、OSのロケールと文字セットによって決まります)

「OSのロケールと文字セットによって決まる」という表現は少し漠然としていますね。なぜそうなのでしょうか。Javaが誕生した25年以上前には、デフォルトの文字セットというものはありませんでした。当時、Java言語仕様 (Java Language Specification) がjava.lang.Characterクラスの基礎としてUnicodeを採用したのは素晴らしい選択でした。時を戻して今日では、Unicodeは以前よりも一般的になりました。今日、UTF-8エンコーディングはほぼすべての場所で優勢であり、とりわけWebの世界では95%以上のコンテンツがUTF-8でエンコードされています。

(参考資料)Usage of character encodings broken down by ranking
https://w3techs.com/technologies/cross/character_encoding/ranking

WikipediaではUTF-8のこの数年間の成長を裏付けています。

UTF-8
https://en.wikipedia.org/wiki/UTF-8
https://ja.wikipedia.org/wiki/UTF-8

新しいプログラミング言語(GoやRustなど)では、デフォルトのテキストエンコーディングとしてUTF-8を採用しました。Javaでは、OSやユーザーの環境に応じて任意の文字セットを返すCharset.defaultCharset()メソッドが、ユーザーの技術的負債としてしばしば指摘されてきました。新たに参加した開発者は、そのような歴史的負債を背負う必要はないはずです。

別の視点、つまり「デフォルトの文字セットはどこで使われているのか」という視点から見てみましょう。最も典型的な使い方は、おそらく java.io.InputStreamReader クラスの暗黙のデコーダでしょう。InputStreamReaderのサブクラスであるjava.io.FileReaderを見てみましょう。UTF-8でエンコードされた日本語テキストファイルを、明示的な文字コードを指定せずに作成したFileReaderインスタンスで読み込むとします。

java.io.FileReader("test.txt")  "こんにちは" (macOS)
java.io.FileReader("test.txt")  "ã?“ã‚“ã?«ã?¡ã? ̄" (Windows (en-US))

ここで問題が顕在化します。macOSでは、基盤のOSが使用するデフォルトのエンコーディングはUTF-8であるため、ファイルの内容は正しく読み取られます(デコードされます)。一方、Windows(US)で同じテキストファイルを読むと、内容が文字化けしてしまいます。これは、FileReaderオブジェクトが、システムロケール English(United States) のWindowsで使用されるデフォルトエンコーディングであるコードページ1252 (CP-1252) エンコーディングでテキストを読み取るからです。同じOSであっても、ユーザーの設定によって結果が異なる場合があります。もし、そのWindowsホストのユーザーがシステムロケールをJapanese (Japan)に変更した場合、そのユーザーの環境では次のようにテキストが読み取られます。

java.io.FileReader("test.txt")  "縺薙s縺ォ縺。縺ッ" (Windows (ja-JP))

Making UTF-8 the Default Charset

この長年の問題に対処するため、JEP 400は、JDK 18でデフォルトの文字セットをUTF-8に変更します。

JEP 400: UTF-8 by Default
https://openjdk.java.net/jeps/400

これは実際、明示的に文字セットを設定しない場合にUTF-8がデフォルトの文字セットになる、java.nio.file.Files クラスの既存の newBufferedReader/Writer メソッドに整合します。

jshell> Files.newBufferedReader(Path.of("test.txt")).readLine()
$1 ==> "こんにちは"

上記の例は、JDK17以降、ホストやユーザーの設定に関係なく、java.nio.file.FilesメソッドでUTF-8エンコードのテキストファイルが読めることを示しています。

UTF-8をデフォルトの文字セットとすることで、JDKのI/O APIは常に同じ、予測可能な方法で動作するようになり、ホストやユーザーの環境に注意を払う必要はありません。かつては一貫した動作を必要とするアプリケーションでは、非サポートのシステムプロパティfile.encodingを指定する必要がありましたが、もはやこれは不要になりました!

jshell> new BufferedReader(new FileReader("test.txt")).readLine()
$2 ==> "こんにちは"

上記の例は、JDK 18のホストおよび/またはユーザーの設定に関係なく、FileReaderクラスが新しいFilesメソッドで一貫して動作することができることを示しています。

1点考慮しなければならないことがあります。それは、基礎となるホストおよび/またはユーザーの環境に従うstdout/err(標準出力および標準エラー出力)に直接接続されているSystem.out/errです。このエンコーディングをUTF-8に変更すると、System.out/errへの出力は直ちに影響を受け、一部の環境(Windowsなど)では文字化けを起こす可能性があります。そのため、これらのI/Oで使用されるエンコーディングは、JDK 17で導入されたjava.io.Console.charset()と同等のものがそのまま使用されます。

Compatibility & Mitigation Strategies

デフォルトの文字セットをUTF-8に変更することは、正しいことです(そして、長い間待たされたことでもあります)。しかしながら、特にWindowsにのみデプロイされるアプリケーションでは、非互換の問題が発生する可能性があります。デフォルトの文字セットがホストとユーザーの環境に依存する、以前の動作を期待するユーザーがいらっしゃることを理解しています。そのようなアプリケーションを安定して動作させるために、以下の2つの緩和策を用意しました。

1. Source Code Recompilation

ソースコードを再コンパイルできるのであれば、影響を受けるコードを変更して、文字セットを明示的に指定してください。例えば、上記の例では、java.io.FileReader("test.txt", "UTF-8") のように、文字コードを指定しないコンストラクタを、文字コードを指定するコンストラクタに置き換えてください。こうすることで、動作が統一されます。文字コードが分からないけれども以前のような動作が必要な場合は、JDK 17で導入されたnative.encodingシステムプロパティを使用します。例えば、WindowsでEnglish (United States)のシステムデフォルトロケールを使っている場合、次のようになります。

jshell> System.getProperty("native.encoding")
$3 ==> "Cp1252"

したがって、FileReader のコンストラクタに Cp1252 を指定する必要があります。修正すると次のようになります。

String encoding = System.getProperty("native.encoding"); // Populated on Java 18 and later
Charset cs = (encoding != null) ? Charset.forName(encoding) : Charset.defaultCharset();
var reader = new FileReader("file.txt", cs);

コンパイルといえば、javacコマンドもデフォルトの文字セットに依存します。したがって、ソースファイルがどのようなエンコーディングで保存されたか、それがUTF-8であるかどうかを知った上で、javac-encodingオプションで指定する必要があるのです。

2. No Recompilation

JDK18では、file.encodingがシステムプロパティとしてサポートされました(つまり、javadocに記述され、サポート対象になった、ということです)。このシステムプロパティの値は、UTF-8かCOMPATのいずれかであり、それ以外の場合、動作は未定義です。アプリケーションをコマンドラインオプション -Dfile.encoding=COMPAT で起動した場合、デフォルトエンコーディングを以前の JDK リリースで使用されていた方法で決定するため、、互換性が保たれます。

Preparing for JEP 400 – Call to Action

JEP 400はいささか破壊的なエンハンスメントゆえ、既存の環境でアプリケーションをテストすることを強くお勧めします。file.encodingシステムプロパティを使用すれば、JDK 8からこれまでにリリースされたJDKでこのJEPの正確な効果を簡単に再現できます。そのため、コマンドラインオプション -Dfile.encoding=UTF-8 を指定してアプリケーションを実行し、動作を確認してください。macOSとLinuxではデフォルトのエンコーディングがすでにUTF-8ゆえ、特に問題はないものと思われます。Windowsでは、特に中国語/日本語/韓国語などの東アジアのロケールでは、互換性のない動作が予想されます。そのような場合は、上記で説明した緩和策を試してみてください。

もちろん、JDK 18 Early Access ビルド(JEP 400 はbuild 13 で統合されています)を使って JEP 400 を試すことも可能です。

JDK 18 Early-Access Builds
https://jdk.java.net/18/

Wrap-up

JEP 400 は、待望されていたものの、破壊的なエンハンスメントゆえ、私たちはその評判を気にしていました。JEP 400がCandidate (候補) 状態に昇格したとき、私たちは外部から多数のフィードバックを受けましたが、そのほとんどが非常に肯定的であることがわかりました。このことは、今回の機能強化の方向性を補強するものです。長い目で見れば、コモディティ化が進んで開発者がJEP 400のことを忘れてしまうのは間違いないでしょう。

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中