原文はこちら。
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のことを忘れてしまうのは間違いないでしょう。