Java の文字化け対策 on Powershell

Java の文字化けって厄介ですよね。いろいろな要因が影響し、ひどいときには二重・三重に文字化けして見たこともないような化け方をします。しかもオブジェクトによってデフォルトの文字コードの決定方法が違ったりして、標準出力に複数の文字コードが混在したりします。

さらに困ったことに、PowerShell 上で動かしたりコンパイルしようとすると、問題は更に複雑になります。

この記事では文字化けの直し方は勿論、原因箇所の特定方法を含めて細かく解説させていただきます。

尚、この記事では基本的にすべてのエンコードを UTF-8 (BOM無し) に揃える前提で進めます。

# 実行環境
当記事執筆時に利用した Java の実行環境は以下です。
> openjdk 11.0.2 2019-01-15
> OpenJDK Runtime Environment 18.9 (build 11.0.2+9)
> OpenJDK 64-Bit Server VM 18.9 (build 11.0.2+9, mixed mode)

※ PowerShell は Windows PowerShell 5.1 および PowerShell 7.4.1 について記載しています。
※ Windows は日本語ロケールのものを利用しています。

# おすすめの解決方法
とりあえず結論を。
以下をすべて実施するのがおすすめです。

1. コントロールパネル > 時計と地域 > 地域 > 管理 > システム ロケールの変更... > ベータ：ワールドワイド言語サポートで Unicode UTF-8 を使用（U) にチェックを入れる ([参考](https://qiita.com/nanbuwks/items/8d6c5685b1d4d70c058b))
1. [PowerShell 7.4 をダウンロードして利用する](https://learn.microsoft.com/ja-jp/powershell/scripting/whats-new/migrating-from-windows-powershell-51-to-powershell-7?view=powershell-7.4)
1. java プログラム実行時に `-Dfile.encoding=utf-8` オプションを指定する (毎回指定するのが面倒なら `JAVA_TOOL_OPTIONS` 環境変数に `-Dfile.encoding=utf-8` を設定する)

とはいえ他のツールとの兼ね合いもありますし、場合によってはこれ以外にも問題が発生する可能性もあります。そのため、ぜひ以下を読んで個別原因と対処法を理解してください。

# PowerShell の文字化けに関する問題と対処法

## 変数やプロパティの設定
(Windows) PowerShell には、なぜか文字コードに関する設定が複数あります。
面倒ですがひととおり設定しておきましょう。
ただしおすすめの解決方法にも書いたように 『ワールドワイド言語サポートで Unicode UTF-8 を使用』 を有効にしておけば、この辺りは気にする必要がありません（デフォルトで UTF-8 になります）。

| 設定 | UTF-8 にする方法 | 概要 |
| --- | --- | --- |
| `$PSDefaultParameterValues` | `$PSDefaultParameterValues['*:Encoding'] = 'utf8'` | 各コマンドレットが出力に利用する文字コードを指定する。ただし PowerShell 7.1 より前までは BOM 有りになります。| 
| `$OutputEncoding` | `$OutputEncoding = [System.Text.UTF8Encoding]::new( $false )` | PowerShell が外部プログラムに出力するときに利用するエンコード。たとえば、`"あ" \| java SomeClass.java` としたとき、SomeClass.java の標準入力に "あ" をどんなエンコードでバイト列に変換して与えるかを制御します。 |
| `[System.Console]::OutputEncoding` | `[System.Console]::OutputEncoding = [System.Text.UTF8Encoding]::new( $false )` | プログラムの出力を PowerShell が表示するときに利用するエンコード。例えば Java プログラムが標準出力にバイナリ列を出力したとき、PowerShell はこのプロパティの設定に従ってバイナリ列を解釈して表示する。 |
| コードページ | `chcp 65001` | System.out.println の挙動などは一部この設定値に影響を受けるようです。コマンド プロンプト ではこの chcp さえ適切に設定しておけばほとんど常に UTF-8 で扱ってくれたので簡単でした。 |

なお、$OutputEncoding に Encoding オブジェクトを設定する際、`[System.Text.Encoding]::GetEncoding(65001)` や `[System.Text.Encoding]::GetEncoding('utf-8')` などとする方法がいろいろなサイトで紹介されていますが、この方法だと BOM 有りの UTF-8 になります。
余計なトラブルを避けるため、.Net の [System.Text.UTF8Encoding クラスのコンストラクタ](https://learn.microsoft.com/ja-jp/dotnet/api/system.text.utf8encoding.-ctor?view=net-8.0#system-text-utf8encoding-ctor(system-boolean))に BOM フラグ = false を与える (`[System.Text.UTF8Encoding]::new( $false )`) ほうが無難です。

## Windows PowerShell 5 のストリーム処理に関する問題
特に厄介なのが、最近の Windows にも標準で入っている Windows PowerShell 5.1 のパイプ出力やリダイレクトの挙動です。PowerShell 7.4 より前のバージョンではネイティブコマンドからのバイナリストリームをテキストとして扱い、勝手に特定の文字コードで解釈し、別の文字コードに変換しようとしてしまいます。(参考:[パイプラインでのネイティブ コマンドの使用](https://learn.microsoft.com/ja-jp/powershell/module/microsoft.powershell.core/about/about_pipelines?view=powershell-7.4#using-native-commands-in-the-pipeline))

これはどうしようもないので、PowerShell の 7.4 以降を利用しましょう。

試しに、以下の２つの Java プログラムをWindows PowerShell 5.1 上でパイプ (`java OutUtf8.java" | java InputCheck.java`) してみましょう。

```java:OutUtf8.java 
public class OutUtf8 {
  public static void main(String[] args) throws java.io.IOException {
    System.out.write(new byte[] { (byte)0xe3, (byte)0x81, (byte)0x82 });
      // UTF-8 の「あ」相当のバイト列
  }
}
```

```java:InputCheck.java
import java.nio.file.Files;
import java.nio.file.Paths;

public class InputCheck {
  public static void main(String[] args) throws java.io.IOException {
    var bytes = System.in.readAllBytes();
    for(var b : bytes) System.out.printf("%02X ", b); //16進で表示
    System.out.println();
    Files.write(Paths.get("out", "input.bin"), bytes);
  }
}
```

私の環境では、以下の結果が得られました。
> 3F 0D 0A

PowerShell に出力される内容も、out/input.bin をバイナリエディタで確認した内容も上記のとおりになっています。`0D, 0A` は Asciiコードでの CR, LF に相当します。その前の `3F` は文字 `?` の Ascii コードなので、OutUtf8.java の出力(`E3 81 82`)を文字として解釈できずに `?` へと差し替え、その `?` をテキストとして読み取ったうえで何らかの Ascii 互換文字コードで InputCheck.java の入力ストリームに渡したのでしょう。

さらに PoserShell 5.x の恐ろしい性質として、ファイルリダイレクト演算子 (`>`) や (`>>`) が出力を UTF-16 に変換します。この変換先のエンコードは基礎設定変数 `$PSDefaultParameterValues` を適切に設定することで変更できますが、なんと BOM 無しの UTF-8 をサポートしていません。(参考:[文字エンコードについて - PowerShell | Microsoft Learn](https://learn.microsoft.com/ja-jp/powershell/module/microsoft.powershell.core/about/about_character_encoding?view=powershell-7.4))

プログラムの出力を常に PowerShell のエンコードに合わせた文字データで出力し、その出力を PoserShell 上で適切に利用できる PowerShell 職人以外は、PowerShell 5.x を利用しないことをおすすめします。

PowerShell (v7.4 以降) では、BOM無しUTF-8 を利用できますし、ファイルリダイレクトの出力エンコードは規定値で BOM無しUTF-8 になっています。
また、ネイティブコマンドの stdout ストリームをネイティブコマンドの stdin ストリームにパイプする場合、ネイティブコマンドをファイルにリダイレクトする場合のどちらもバイナリ列を保持してくれます（試験的機能らしいですが……）。

実際、PowerShell7.4 上では意図したとおり、 `java OutUtf8.java | java InputCheck.java` は `E3 81 82` と出力されますし、 `java OutUtf8.java > out/out.txt` では out/out.txt に UTF-8 で `あ` (`E3 81 82`) が出力されます。

# Java の文字化けに影響する設定や変数
PowerShell の問題がひととおり片付いたら、今度は Java の問題をやっつけましょう。

以下の項目が文字化けに影響している可能性があります。

1. `javac -encoding` コマンドライン引数
1. `file.encoding` / [Charset.defaultCharset()](https://docs.oracle.com/javase/jp/8/docs/api/java/nio/charset/Charset.html#defaultCharset--) の値
1. `System.out` の charset 設定 (非公開プロパティ)

以降で一つずつ解説します。

## `javac -encoding` 引数
Java コンパイラがソースコードをどの文字コードで解釈するかを決定します。`javac -encoding UTF-8 SomeClass.java` とすことで、ソースを UTF-8 として解釈してくれます。

この引数を指定しなかった場合、JDK18より前のバージョンではプラットフォームに依存して決定した文字コードでソースを解釈します。
ただし JDK 18 以降はデフォルトで UTF-8 が利用されるようになったので、気にしなくてよくなりました ([JEP 400](https://openjdk.org/jeps/400))。

コマンドラインならコードページをUTF-8にしておけば(`chcp 65001`) JDK11 などでもデフォルトで UTF-8 で解釈してくれるようになりますが、残念ながら PowerShell ではうまくいきません。おすすめに記載したように、 ワールドワイド言語サポートで Unicode UTF-8 を使用 にチェックを入れるとデフォルトで UTF-8 になります。

コンパイル時に適切なエンコードで解釈されたかは、javap でコンパイルされた class ファイルを解析することで確認できます。
たとえば以下のようなファイルを UTF-8 で保存します。

```java:SystemOut.java
public class SystemOut {
  public static void main(String[] args) {
    System.out.println("漢"); //E6 BC a2
  }
}
```

このファイルをコンパイルして SystemOut.class ファイルを作成し、javap コマンドの詳細表示を ON にして (`-v` オプション) 表示します。

```powershell
javac SystemOut.java && javap -v SystemOut.class | findstr String
```

>    #3 = String             #18            // 貍｢
>    #4 = Methodref          #19.#20        // java/io/PrintStream.println:(Ljava/lang/String;)V
> ...

環境次第では上のように、文字化けしている行が見つかると思います。
上の `貍｢` は UTF-8 の `漢` (`E6 BC A2`) を Shift_JIS で解釈した場合の文字列です (`E6 BC` -> `貍`, `A2` -> `｢`)。 

※ PowerShell の設定が怪しい場合、クラスファイルの内部データが正しくても javap の出力が文字化けする可能性があります。不安な場合は SystemOut.class をバイナリエディタで開き、該当文字のバイナリ列があるかを確認します。Java のクラスファイルはリテラル値を UTF-8 で保持しているので、上記の例だと `E6 BC A2` の並びが出現すれば適切にソースを UTF-8 で解釈できています。

## `file.encoding` / [Charset.defaultCharset()](https://docs.oracle.com/javase/jp/8/docs/api/java/nio/charset/Charset.html#defaultCharset--) の値
Java 実行時の様々な挙動に影響してきます。 `java "-Dfile.encoding=UTF-8" SomeClass` のように実行時の引数に指定することで設定できます。

なお、Java の実行時引数は JAVA_TOOL_OPTIONS 環境変数で指定することもできます。
```powershell
 $Env:JAVA_TOOL_OPTIONS="-Dfile.encoding=UTF-8"
```

この `file.encoding` の値は、Java プログラム内では `System.getProperty("file.encoding")` で確認できます。
また、[`Charset.defaultCharset()`](https://docs.oracle.com/javase/jp/8/docs/api/java/nio/charset/Charset.html#defaultCharset--) の返り値もこの `file.encoding` の値に影響を受けます。

例えば、new String(byte[]) とした場合、ここで指定したエンコードで解釈して String インスタンスを構築します。

以下のコードを UTF-8 で保存し、`javac -encoding UTF-8 UtfToString.java` でコンパイルして下さい。
その後、`java "-Dfile.encoding=UTF-8" UtfToString` として実行すると、 out/out-str.txt は正しい UTF-8 のテキストとして出力されますが、`java "-Dfile.encoding=Shift_JIS" UtfToString` とすると `0xE6, 0xBC 0xA2` をShift_JIS として解釈して String のインスタンスを構築するため、文字化けします。

```java:UtfToString.java
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;

public class UtfToString {
  public static void main(String[] args) throws java.io.IOException {
    var str = new String(new byte[] { (byte)0xe6, (byte)0xbc, (byte)0xa2 }); // UTF-8 で漢
    Files.writeString(Paths.get("out", "out-str.txt"), str, StandardCharsets.UTF_8);
  }
}
```

## `System.out` の charset 設定
`System.out.println("漢");` などとした場合に標準出力への書き出しに利用されるエンコードは、上述の `file.encoding` のとおりになるとは限りません。

詳細な挙動は不明ですが、コードページを 65001 に設定し、かつ file.encoding を UTF-8 にして初めて UTF-8 が使われていました(どちらか一方ではだめ)。なお、[System.Console]::OutputEncoding が Shift_JIS になっていても、chcp と file.encoding の設定を UTF-8 にしていると出力は UTF-8 になってしまうので、普通に文字化けします。

OpenJDK 11 では、以下のプログラムでこの System.out.print 系処理が利用する文字コードを取得できました（リフレクションで非公開プロパティを無理やり読んでいるので、警告が出ます）

```java:SystemOutEncodeCheck.java
import java.io.OutputStreamWriter;
import java.nio.charset.CharsetEncoder;

public class SystemOutEncodeCheck {
  public static void main(String[] args) throws Exception {
    System.out.println(getSystemOutEncoding());
  }
  private static String getSystemOutEncoding() throws Exception {
    var charsetField = System.out.getClass().getDeclaredField("charOut");
    charsetField.setAccessible(true);
    var charOut = (OutputStreamWriter)getField(System.out, "charOut");
    var encoder = getField(charOut, "se");
    var innerEncode = (CharsetEncoder)getField(encoder, "encoder");
    return innerEncode.charset().toString();
  }

  private static Object getField(Object source, String fieldName) throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
    var field = source.getClass().getDeclaredField(fieldName);
    field.setAccessible(true);
    return field.get(source);
  }
}
```

> windows-31j

# まとめ
PowerShell, Java ともに文字コード周りは非常に難解な挙動をします。
システムロケールごと変更してしまう以外に、一気に解決する方法はないと思ってよさそうです。解説サイトに記載されているコマンドをよくわからないまま叩くと、別の問題と干渉して、下手をすれば問題がより複雑になります。焦らずに一つ一つ確認しながら対処していきましょう。

# おまけ
## PowerShell の起動パラメータに設定を入れ込む方法
例えば VSCode の terminal.integrated.profiles.windows 設定項目などに PowerShell を指定してインタラクティブシェルに設定する場合、できれば起動と同時にプロパティの設定を終えてしまいたいですね。

ワールドワイド言語サポートで Unicode UTF-8 を使用　にチェックを入れている場合は必要ないかと思いますが、そうでない場合は起動コマンドにプロパティの設定を入れ込んでしまいます。

例えば VSCode なら設定ファイルに以下のような項目を追加します。
```json:settings.json
{

"terminal.integrated.profiles.windows": {
  "PS7": {
    "path": "C:\\Program Files\\PowerShell\\7\\pwsh.exe",
    "icon": "terminal-powershell",
    "args": [
      "-NoExit",
      "-c",
      "[System.Console]::OutputEncoding = [System.Text.UTF8Encoding]::new( $false ); $OutputEncoding = [System.Text.UTF8Encoding]::new( $false ) ; chcp 65001"
    ],
  "overrideName": true
  }
}

}
```

`-NoExit` オプションでインタラクティブにし、`-c` オプションに続いて起動時に実行したいコマンドを記載します。起動時コマンドは一つしか設定できないので、`;` でつないで複数のコマンドをまとめてしまいます。

## Maven を利用する場合
Maven もエンコードを指定するプロパティが複数あり非常に複雑です。

project > properties 以下に指定する方法と、各プラグインの configuration に設定する方法があります。
ほとんどの場合、プラグラインは configuration のデフォルト値として properties に指定した値を読み取るようになっています。

注意したいのは、Maven を起動している JVM の起動オプションが関係する場合としない場合があるということです。
fork オプションなどで Maven と同じ JVM 上で処理が実行されるのか、それとも別プロセスで実行されるのかで、maven コマンド実行時の引数が影響してくる場合とそうでない場合が分かれるので注意してください。