Java 18 でデフォルトcharset が UTF-8 に。Windows環境のアプリ等は確認要

投稿: 2021年10月11日
タグ: 
  Developer(Java等)  Windows  ニュースネタ  技術メモ

Inside Java で『Java のデフォルト charset を UTF-8 にする』 JEP 400 についての記事が投稿されていました。 実際に動かして確認してみたいと思います。

JEP 400 and the Default Charset
https://inside.java/2021/10/04/the-default-charset-jep400/open_in_new
  • Java 18 でデフォルトcharset が UTF-8 に変更される(コンソール出力は除く)
  • 現状のデフォルトはプラットフォームOS、OS上の設定(ロケール・表示言語) に依存
  • 影響を受けそうなのは Windows。デフォルトが MS932 ⇒ UTF-8 に。
  • Java 18 が実システムで利用されるのは相当先と思われる
  • 日本語 Windows で MS932 が設定される仕組み
  • Java 18 で、デフォルトの charset がプラットフォームに依存せず UTF-8 に

    JEP 400 の修正で Java (のAPI) で使用するデフォルトcharset が UTF-8 に統一されるとのことです。 (但し、コンソールUI は除く)

    現状(~JDK17)、Java のデフォルトcharset は JVM 起動時に決定され、起動時のオプション(file.encoding) で指定がなければ JVM が動く OS によって決定されます。
    その結果、全く同一の Java コードで同一のファイルを読み書きしても、プラットフォームが異なれば、 エンコードする charset が別になり、一方は正常に読み書きでき、もう一方は文字化けする、ということが起こります。(多くの人が1度や2度見たことがあるかもしれません)

    といっても macOS や Linux では現状でもデフォルトcharset として UTF-8 が返ってくるので、 影響が発生するとすれば、基本的には Windows環境やWindowsとファイル連携している環境になると思います。

    ※ AIX や HP-UX とかでも影響があるかもしれませんが、ここでは忘れます。

    Windows環境で実際にデフォルトcharset を確認してみる

    既に openjdk の jdk18 の最新ビルドで修正が入っているので、Windows環境で変更を確認してみます。 ※ Windows 11 を利用していますが、Windows 10 でも同様の結果になると思います。

    まずは変更前の現状確認から。以下は、日本語 Windows 上の Java 17 で FileReader#getEncoding メソッドを実行した結果です。 デフォルトcharset が取得され MS932 が返ってきています。尚、バージョン番号の 17+35 というのは JDK17 正式リリース(GA) 時のバージョンです。

    Java 17
    jshell> var version = Runtime.version(); version ==> 17+35-2724 jshell> var encoding = new FileReader("C:\\temp\\test.txt").getEncoding(); encoding ==> "MS932"

    次は JDK18 の最新ビルド(18+18) で同じコードを実行してみます。Windows でも UTF8 が返ってきました。

    Java 18
    jshell> var version = Runtime.version(); version ==> 18-ea+18-1093 jshell> var encoding = new FileReader("C:\\temp\\test.txt").getEncoding(); encoding ==> "UTF8"

    文字化けシナリオを再現してみる

    おそらく、現時点での よくあるシナリオとしては『Linux/Unix OS で出力したファイル*1 を Windows環境にコピーして Javaアプリケーションで開こうとしたら*2 文字化けする』といったパターンではないかと思います。
    *1 の時点では UTF-8 でエンコードしてファイル出力することが多いと思います。 *2 は日本語 Windows だと MS932 がデフォルトになるので *1 と合わずに文字化け、ということになります。

    Java 18以降が混在すると一時的に Windows環境 同志でもこういう状況になりそうです。 Java 17 と Java 18 で再現してみます。

    まずは Java 18 でファイル出力します。エンコードする charset を指定していないのでデフォルトの UTF-8 で「あいうえお」と出力されます。

    Java 18
    jshell> FileWriter fw = new FileWriter("C:\\temp\\test.txt"); jshell> fw.write("あいうえお"); fw.flush(); fw.close();

    次に Java 17 で同じファイルを読み込みます。デフォルトの MS932 で読み込もうとして見事に文字化けしました。

    Java 17
    jshell> var output = new BufferedReader(new FileReader("C:\\temp\\test.txt")).readLine(); output ==> "縺ゅ>縺?縺医♀"

    UTF-8 でエンコードされたファイルを MS932 で開こうとしているために文字化けが発生しているので、 もちろん、UTF-8 を指定してファイルを開けば文字化けは解消されます。

    Java 17
    jshell> var output = new BufferedReader(new FileReader("C:\\temp\\test.txt", StandardCharsets.UTF_8)).readLine(); output ==> "あいうえお"

    同じ Windows環境でも Java 18 以降と Java 17 以前が混在する状況が生まれると上記のような事象が発生することになりそうです。

    でも、JDK18 を使う状況っていつ来るんでしょうか。

    Java SE 18 を含んだ LTS のバージョンは 2023年リリース予定の Java SE 21。実際の移行は相当先か

    冒頭の Inside Java のブログの中で以下のように記載されています。 UTF-8 をデフォルトにするというのは、個人的にも適切な対応だと思いますが、互換性の問題から対応が延期されてきたようです。

    Changing the default charset to UTF-8 is the right thing to do (and was long overdue too) but it does introduce some incompatible issues, especially for applications that are only deployed on Windows.

    [意訳] デフォルトcharset を UTF-8 に変更することは必要な対応ですが (そして、長期間滞ってもいましたが) 幾つかの非互換の問題を引き起こします。特に、Windows のみにデプロイされるアプリケーションで、です。

    今回、ついに対応、ということのようですが、LTS である Java 17 ではなく 18 に入れ込まれた点からもスケジュールの配慮を感じられます。 多くのユーザーが Java 18 を取り込むのは 2023年9月にリリース予定の Java 21(LTS) にアップデートするタイミングでしょうから、 現実的には最長で「Java 11 の Extended Support が切れる 2030年12月」か 「Java 17 の Extended Support が切れる 2029年9月」まで猶予があることになりそうです。

    Oracle Java SE Support Roadmap
    https://www.oracle.com/java/technologies/java-se-support-roadmap.htmlopen_in_new

    日本語 Windows環境のデフォルトcharset でなぜ MS932 が返ってくるのか

    現状(~17) の Java はデフォルトcharset をプラットフォームOS に確認する実装になっており、openjdk 17 だと 以下の java_props_md.c の実装で file.encoding が設定されていました。

    GitHub | openjdk | JDK17 file.encoding 設定箇所
    https://github.com/openjdk/jdk/blob/jdk-17+35/src/java.base/windows/native/libjava/java_props_md.copen_in_new

    上記実装の中で複数の Win32 API が呼ばれています。対象 Windows ユーザーのロケールを取得(GetUserDefaultLCID) し、 ロケールID を基にコードページを取得(GetLocaleInfo) していました。
    日本語 Windows の場合、ロケールID として 1041、コードページとして 932 が返ってきます。

    実際に呼ばれているか Java のプロセスにアタッチして Windows側から確認してみます。Java プロセス起動直後に呼ばれていました。

    WinDbg
    0:004> kn3 # Child-SP RetAddr Call Site 00 000000f2`e69fe838 00007ffb`464aadf5 KERNELBASE!GetUserDefaultLCID 01 000000f2`e69fe840 00007ffb`464a641c java!handleRead+0x955 02 000000f2`e69feb70 00000261`879fd621 java!Java_jdk_internal_util_SystemProps_00024Raw_platformProperties+0x1c

    ただ、Win32 API が返すのは「Windows Code Page」と呼ぶ数値のみです。日本語なら 932 が返ってきます。 「MS932」の頭についている MS は何かなと思ったら、Java 側で付与されていました。こんな実装になってるとは知りませんでした。

    Java 17
    case 932: /* 10:Japanese */ case 949: /* 12:Korean Extended Wansung */ case 950: /* 13:Chinese (Taiwan, Hongkong, Macau) */ case 1361: /* 15:Korean Johab */ ret[0] = 'M'; ret[1] = 'S'; break;
    https://github.com/openjdk/jdk/blob/jdk-17+35/src/java.base/windows/native/libjava/java_props_md.c#L87-L93open_in_new
    ファイルIO等で Encoding を指定するのはお作法の1つだと思うので、多くのシステムは影響を受けないと思いますが、 同時に、「そもそも charset なんて意識してない」システムもそれなりにあるだろうな、とも思います。

    リリーススケジュールを考えると最大限 配慮された、Oracleさんのやさしさを(勝手に) 感じるスケジュールですが、 対応が必要な人が腰を上げるのがギリギリになるのは通例なので 8年後ぐらいに騒ぎになるのかもしれません。