Java 18 JEP 400 UTF-8 by Default の Windows 環境での影響

Java 18 の新機能として JEP 400 UTF-8 by Default が実装されました。Linux や Mac など、UTF-8 が標準の環境では影響はまずありませんが、Windowsのような UTF-8 が標準エンコーディングではない環境では対応が必要です。

# TL;DR
- Java 18 から、文字セット指定がない場合のソースコードを含むファイルの扱いが UTF-8 になる。
- Java 17 までの動作にするには Java 実行時にオプションとして `-Dfile.encoding=COMPAT` を指定する。
  - javac は `-J-Dfile.encoding=COMPAT` と指定

# 検証環境
- Windows 10 Pro 21H2
- OpenJDK JDK18 GA
- OpenJDK JDK17.0.2

# JEP 400 UTF-8 by Default
JEP 400 UTF-8 by Default で、標準 Java API の標準の文字セット(Charset) が UTF-8 に変更されました。JEP 400 のリンクは[こちら](https://openjdk.java.net/jeps/400)になります。非常に詳しく記述されていますので、一読をお勧めします。

JEP 400 で影響があるとされているクラスは以下のものです。
- java.io.InputStreamReader
- java.io.FileReader
- java.io.OutputStreamWriter
- java.io.FileWriter
- java.io.PrintStream
- java.util.Formatter
- java.util.Scanner
- java.net.URLEncoder
- java.net.URLDecoder

# ファイル操作への影響
文字セットの指定がない場合、これらのクラスの API は Java 17 までは、日本語 Windows では シフトJIS (MS932, 正式には windows-31j) で読み書きされていましたが、Java 18 からは UTF-8 として読み書きされます。

そのため、日本語 Windows 環境で、文字セットの指定なしでファイルへの入出力を行なっていたアプリケーションの Java 実行環境を Java 18 へ変更した場合、文字化けを起こすことになります。

jshell コマンドでの実行例です。Java 17 で作成したファイルを Java 18 で読み込んでみます。

```Java
// Java 17
jshell> var writer = new java.io.FileWriter("sjis.txt")
writer ==> java.io.FileWriter@5a2e4553
jshell> writer.write("こんにちは")
jshell> writer.close()
jshell> var br = new java.io.BufferedReader(new java.io.FileReader("sjis.txt"))
br ==> java.io.BufferedReader@7591083d
jshell> br.readLine()
$5 ==> "こんにちは"
```

ここで作成したファイル (sjis.txt) はシフトJISでエンコードされていますので、そのまま Java 18 で読むと文字化けします。

```Java
// Java 18
jshell> var br = new java.io.BufferedReader(new java.io.FileReader("sjis.txt"))
br ==> java.io.BufferedReader@7a4f0f29
jshell> br.readLine()
$2 ==> "????????"
```

もちろん文字セットを指定した場合は、その指定が使用されるので影響はありません。

ファイル名やパス名はシステムのエンコーディングのままです。変更になったのはファイルの中身を扱う文字セットになります。

nio2 の `Files.readAll(Path)` や `Files.write(Path, String)` 等は元々 UTF-8 で動作していますので、影響はありません。


# 互換オプション file.encoding=COMPAT
Java 17 までと同等の処理とするには、java コマンドの実行時オプションとしてシステムプロパティ `-Dfile.encoding=COMPAT` を指定します。

javac や javap などのツールに実行時オプションを指定する場合には、
`-J-Dfile.encoding=COMPAT` というように `-J` を頭につける必要があります。

Java 17 までは `-Dfile.encoding=COMPAT` という指定は有効ではありませんので、従来の実行環境に間違ってこのオプションが渡らないようにする必要があります。

コマンドのオプションとしてではなく環境変数で指定する場合には、`_JAVA_OPTIONS` 環境変数を使用することもできます。この環境変数が渡される全てのJava実行環境で有効となるので便利です。ただし、実行時に `Picked up _JAVA_OPTIONS: -Dfile.encoding=COMPAT` というメッセージが表示されてしまいます。

```Java:test.java
// test.java (シフトJISで保存)
public class test {
    public static void main(String[] args) {
        System.out.println("あ"); //あ
    }
}
```

```
>set _JAVA_OPTIONS=-Dfile.encoding=COMPAT

>javac test.java
Picked up _JAVA_OPTIONS: -Dfile.encoding=COMPAT

>java test
Picked up _JAVA_OPTIONS: -Dfile.encoding=COMPAT
あ
```

# コンパイル時の考慮点
ファイルのエンコーディングの扱いが変わりましたので、ソースコードのエンコーディングにも影響があります。文字データやコメントなどを日本語で書いていて、シフト JIS で保存している場合、`javac -J-Dfile.encoding=COMPAT ...` と指定するか、ソースコードを UTF-8 に変換する必要があります。ソースコードがシフト JIS とわかっている場合には、`javac -encoding MS932 ...` と指定することもできます。

以下の例で test.java は前節で使用しているものです。

```
// Java18 でコンパイル
>javac test.java
test.java:3: エラー: この文字(0x82)は、エンコーディングUTF-8にマップできません
        System.out.println("??"); //??
                            ^
test.java:3: エラー: この文字(0xA0)は、エンコーディングUTF-8にマップできません
        System.out.println("??"); //??
                             ^
test.java:3: エラー: この文字(0x82)は、エンコーディングUTF-8にマップできません
        System.out.println("??"); //??
                                    ^
test.java:3: エラー: この文字(0xA0)は、エンコーディングUTF-8にマップできません
        System.out.println("??"); //??
                                     ^
エラー4個

// 互換オプション指定
>javac -J-Dfile.encoding=COMPAT test.java
> (正常にコンパイル完了)
```

# コマンドプロンプトへの影響
System.out, System.err の出力は変わらずシフト JIS のままです。パイプや、リダイレクトを使用してもシフト JISとして出力されます。

# ネットワークへの影響
`java.io.InputStreamReader` を使用してバイトストリームを文字ストリームへ変換している場合などに影響を受けます。シフトJIS のファイルを直接ネットワーク経由で受信していた場合などが該当します。

先ほど作成したファイルを Web サーバー経由で読み込んでみます。

```Java
// Java 17
jshell> var conn = new java.net.URL("http://127.0.0.1:8000/sjis.txt").openConnection();
conn ==> sun.net.www.protocol.http.HttpURLConnection:http://127.0.0.1:8000/sjis.txt

jshell> var br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
br ==> java.io.BufferedReader@73c6c3b2

jshell> br.readLine();
$3 ==> "こんにちは"
```
```Java
// Java 18 (文字化け)
(省略)
jshell> br.readLine();
$3 ==> "??????????"
```

# 外部コマンド呼び出しへの影響
`Runtime.exec` で外部コマンドを実行し、`Process.getInputStream` で出力結果を取得する場合、`InputStreamReader` を経由することが多いと思います。ネットワークのケースと同じく `InputStreamReader` は影響を受けるクラスですので対応が必要です。

# コンパイル済みアプリケーションへの影響
Java 実行環境の変更になりますので、従来のバージョンでコンパイル済みのクラスであっても、Java 18 で実行した場合には JEP 400 の影響を受けます。

# 関連システムプロパティ
- **file.encoding**: ファイルの内容のエンコーディングを指定。
 Java18 で公式に値とされたのは "UTF-8" と "COMPAT" のみ。それ以外の値は非公式 (Document されておらず、Support されていない) の扱い。非公式ながら、日本語 Windows 環境 では `-Dfile.encoding=MS932` の指定が Java 17 までの動作。
- **native.encoding**: 環境のエンコーディングを保持しているシステムプロパティ。JEP 400 の準備として、Java 17 から導入された。
- **sun.jnu.encoding**: (非公式)ファイル名やパス名に使用するエンコーディング
- **sun.stdout.encoding**, **sun.stderr.encoding**: (非公式) stdout, stderr で使用するエンコーディングを指定する場合に使用。指定がない場合には native.encoding が使われる。

# おわりに
JEP 400 は日本語 Windows 環境ではとてもインパクトのある変更です。
Java 18 は Long Term Support (LTS) Release ではないので、本番環境で使われることはまずないと思われます。ただし、次の LTS には入ることになります。いつになるかは不明瞭ですが、Java 21 だとすると 2023/9 の予定ですので、その頃までに対応を考えておく必要がありそうです。

# 参考文献
- [JEP 400 UTF-8 by Default](https://openjdk.java.net/jeps/400)
- [Java 18 でデフォルトcharset が UTF-8 に。Windows環境のアプリ等は確認要](https://let-value.com/articles/2110/jep-400/)
- [Java 18新機能まとめ](https://qiita.com/nowokay/items/17d990aa8a5b1c5223c8)