Java におけるエンコーディング関連システムプロパティの最近の変遷について


はじめに

JDK 18 の、JEP 400: UTF-8 by Default 前後で、エンコーディング関連のシステムプロパティにつらつらと変更があり、混乱するので、ここにまとめておきます。

JDK 19 以降のエンコーディング関連システムプロパティは、なんと7個も存在します。

  • sun.jnu.encoding
  • native.encoding
  • file.encoding
  • stdout.encoding
  • stdout.encoding
  • sun.stderr.encoding
  • sun.stdout.encoding

現状のシステムプロパティは多くて良くわからなくなるので、JDK 16 時代の内部実装から順を追って見ていきましょう。


JDK16におけるエンコーディング関連システムプロパティ

JEP 400: UTF-8 by Default 以前はシンプルな時代でした。

エンコーディング関連システムプロパティは以下があります。

  • file.encoding
  • sun.jnu.encoding
  • sun.stdout.encoding
  • sun.stderr.encoding

この中の sun.* は非公式オプションで、大抵は file.encoding だけを気にすれば良かったです。

実装はどうなっているかと言えば、これらのシステムプロパティは jdk/src/java.base/share/classes/jdk/internal/util/SystemProps.java で以下のように設定されます。

// Platform defined encoding cannot be overridden on the command line
put(props, "sun.jnu.encoding", raw.propDefault(Raw._sun_jnu_encoding_NDX));

// Add properties that have not been overridden on the cmdline
putIfAbsent(props, "file.encoding",
        ((raw.propDefault(Raw._file_encoding_NDX) == null)
                ? raw.propDefault(Raw._sun_jnu_encoding_NDX)
                : raw.propDefault(Raw._file_encoding_NDX)));

// Use platform values if not overridden by a commandline -Dkey=value
// In no particular order
putIfAbsent(props, "sun.stdout.encoding", raw.propDefault(Raw._sun_stdout_encoding_NDX));
putIfAbsent(props, "sun.stderr.encoding", raw.propDefault(Raw._sun_stderr_encoding_NDX));

raw はネイティブ側で設定された配列を表し、jdk/src/java.base/share/native/libjava/System.c で以下のように設定されたものです。

PUTPROP(propArray, _sun_jnu_encoding_NDX, sprops->sun_jnu_encoding);
PUTPROP(propArray, _file_encoding_NDX, sprops->encoding);
PUTPROP(propArray, _sun_stdout_encoding_NDX, sprops->sun_stdout_encoding);
PUTPROP(propArray, _sun_stderr_encoding_NDX, sprops->sun_stderr_encoding);

sprops がシステムプロパティを表し、これは jdk/src/java.base/share/native/libjava/java_props.h にて以下のように定義された構造体です。

typedef struct {
    // ...
    char *encoding;
    char *sun_jnu_encoding;
    char *stdout_encoding;
    char *stderr_encoding;
    // ...
} java_props_t;

この構造体 sprops は、ホスト環境に応じて以下のネイティブコードで構築されます。

  • jdk/src/java.base/unix/native/libjava/java_props_md.c
  • jdk/src/java.base/windows/native/libjava/java_props_md.c

主に、Linux の場合は nl_langinfo()、Windows の場合は GetLocaleInfo() などのAPIにより、ホスト環境のロケール情報からエンコーディングのプロパティが設定されます。

java_props_t Linux Windows
encoding nl_langinfo()から取得 GetLocaleInfo()から取得
sun_jnu_encoding encoding の値 GetLocaleInfo()から取得
stdout_encoding 未割当 コンソール割り当てがあればGetConsoleCP()から取得
stderr_encoding 未割当 コンソール割り当てがあればGetConsoleCP()から取得

Windows の場合の sun.stdout.encoding sun.stderr.encoding について補足しておきます。

jdk/src/java.base/windows/native/libjava/java_props_md.c の以下のコードで sun.stdout.encodingsun.stderr.encoding が設定されます。

hStdOutErr = GetStdHandle(STD_OUTPUT_HANDLE);
if (hStdOutErr != INVALID_HANDLE_VALUE &&
    GetFileType(hStdOutErr) == FILE_TYPE_CHAR) {
    sprops.sun_stdout_encoding = getConsoleEncoding();
}
hStdOutErr = GetStdHandle(STD_ERROR_HANDLE);
if (hStdOutErr != INVALID_HANDLE_VALUE &&
    GetFileType(hStdOutErr) == FILE_TYPE_CHAR) {
    if (sprops.sun_stdout_encoding != NULL)
        sprops.sun_stderr_encoding = sprops.sun_stdout_encoding;
    else
        sprops.sun_stderr_encoding = getConsoleEncoding();
}

GetFileType(hStdOutErr) == FILE_TYPE_CHAR では、ファイルが文字ファイルであるかを判定しており、文字ファイルの場合、対象のハンドルは通常、コンソールまたは LPT デバイスとなります(コンソールの割り当て有無を判定)。

つまり、コンソール割り当てがある場合、getConsoleEncoding() の中で WIN API GetConsoleCP() から取得したコードポイントを設定します。

Linux の場合は sun.stdout.encoding sun.stderr.encoding の割り当ては行われません。

なお、sun_jnu_encoding は、MacOSの場合 UTF-8 で固定されています。

#ifdef MACOSX
    sprops.sun_jnu_encoding = "UTF-8";
#else
    sprops.sun_jnu_encoding = sprops.encoding;
#endif


JDK17におけるエンコーディング関連システムプロパティ

JDK17 では UTF-8 by Default の前準備として JDK-8265989 System property for the native character encoding name でシステムプロパティ native.encoding が追加されました。

jdk/src/java.base/share/classes/jdk/internal/util/SystemProps.java には、native.encoding の設定が追加され、その設定値は file.encoding と同じ値になっています(file.encodingCOMPAT 指定された場合のエンコーディングとなる)。

// Platform defined encoding cannot be overridden on the command line
put(props, "sun.jnu.encoding", raw.propDefault(Raw._sun_jnu_encoding_NDX));
var nativeEncoding = ((raw.propDefault(Raw._file_encoding_NDX) == null)
        ? raw.propDefault(Raw._sun_jnu_encoding_NDX)
        : raw.propDefault(Raw._file_encoding_NDX));
put(props, "native.encoding", nativeEncoding);

// Add properties that have not been overridden on the cmdline
putIfAbsent(props, "file.encoding", nativeEncoding);

putIfAbsent(props, "sun.stdout.encoding", raw.propDefault(Raw._sun_stdout_encoding_NDX));
putIfAbsent(props, "sun.stderr.encoding", raw.propDefault(Raw._sun_stderr_encoding_NDX));

native.encoding はJava側だけのシステムプロパティであり、ネイティブ側のコードに変更はありません。


JDK17では、さらに、JDK-8264208 Console charset APIConsoleCharset の変更が追加されました。

この変更似合わせて、jdk/src/java.base/unix/native/libjava/java_props_md.c に以下が追加されました。

if (isatty(STDOUT_FILENO) == 1) {
    sprops.sun_stdout_encoding = sprops.encoding;
}
if (isatty(STDERR_FILENO) == 1) {
    sprops.sun_stderr_encoding = sprops.encoding;
}

Linux において、今までは未設定だった sun.stdout.encoding sun.stderr.encoding が、コンソールの接続がある場合(isatty() == 1)、 に sprops.encoding が設定されるようになりました(プラットフォームのロケールからの値)。

Windows側の jdk/src/java.base/windows/native/libjava/java_props_md.c にも以下のような変更が入っており、エンコーディングのプロパティの文字列値が変更されています。

switch (codepage) {
case 0:
case 65001:  // <- 追加
    strcpy(ret, "UTF-8");
    break;


JDK18 UTF-8 by Default

JDK18 では、デフォルトの文字コードが UTF-8 となりました(JEP 400: UTF-8 by Default)。

これにより Charset.defaultCharset() が(Windowsプラットフォームであろうと)UTF-8を返すようになりました。

ただし、コンソールI/O に関しては別で、System.outSystem.err の文字セットは、旧来通り環境依存になります。

UTF-8 by Default では、file.encodingCOMPAT を設定した場合は、従来互換の文字セットが採用されます。これは jdk/src/java.base/share/classes/jdk/internal/util/SystemProps.java で以下のように実装されています。

// Platform defined encoding cannot be overridden on the command line
put(props, "sun.jnu.encoding", raw.propDefault(Raw._sun_jnu_encoding_NDX));
var nativeEncoding = ((raw.propDefault(Raw._file_encoding_NDX) == null)
        ? raw.propDefault(Raw._sun_jnu_encoding_NDX)
        : raw.propDefault(Raw._file_encoding_NDX));
put(props, "native.encoding", nativeEncoding);

// "file.encoding" defaults to "UTF-8", unless specified in the command line
// where "COMPAT" designates the native encoding.
var fileEncoding = props.getOrDefault("file.encoding", "UTF-8");
if ("COMPAT".equals(fileEncoding)) {
    put(props, "file.encoding", nativeEncoding);
} else {
    putIfAbsent(props, "file.encoding", fileEncoding);
}

putIfAbsent(props, "sun.stdout.encoding", raw.propDefault(Raw._sun_stdout_encoding_NDX));
putIfAbsent(props, "sun.stderr.encoding", raw.propDefault(Raw._sun_stderr_encoding_NDX));

jdk/src/java.base/share/native/libjava/System.c は以下のようになりました。

#ifdef MACOSX
    /*
     * Since sun_jnu_encoding is now hard-coded to UTF-8 on Mac, we don't
     * want to use it to overwrite file.encoding
     */
    PUTPROP(propArray, _file_encoding_NDX, sprops->encoding);
#else
    PUTPROP(propArray, _file_encoding_NDX, sprops->sun_jnu_encoding);
#endif

    PUTPROP(propArray, _sun_jnu_encoding_NDX, sprops->sun_jnu_encoding);

    /*
     * file encoding for stdout and stderr
     */
    PUTPROP(propArray, _sun_stdout_encoding_NDX, sprops->sun_stdout_encoding);
    PUTPROP(propArray, _sun_stderr_encoding_NDX, sprops->sun_stderr_encoding);

そもそも jdk/src/java.base/unix/native/libjava/java_props_md.c で以下のようになっているので、上の条件付きコンパイルは意味を成していないと思われますが。

#ifdef MACOSX
    sprops.sun_jnu_encoding = "UTF-8";
#else
    sprops.sun_jnu_encoding = sprops.encoding;
#endif
    if (isatty(STDOUT_FILENO) == 1) {
        sprops.sun_stdout_encoding = sprops.encoding;
    }
    if (isatty(STDERR_FILENO) == 1) {
        sprops.sun_stderr_encoding = sprops.encoding;
    }


JDK19 におけるコンソールI/Oの文字コード

JDK 19 では、JDK-8283620 System.out does not use the encoding/charset specified in the Javadoc により、旧来の非公式オプションであった sun.stdout.encodingsun.stderr.encoding が以下のように名前を変えて公式オプションになりました。

  • stdout.encoding : 標準出力ストリーム(System.out)で使用されるエンコーディング
  • stderr.encoding : 標準エラーストリーム(System.err)で使用されるエンコーディング

旧来の sun.stdout.encodingsun.stderr.encodingstdout.encodingstderr.encoding にフォールバックされるため、sum.* を使い続けることもできます。

jdk/src/java.base/share/classes/jdk/internal/util/SystemProps.java は以下のようになりました。 JDK16 時代と比べるとぐちゃぐちゃです。

// Platform defined encoding cannot be overridden on the command line
put(props, "sun.jnu.encoding", raw.propDefault(Raw._sun_jnu_encoding_NDX));
var nativeEncoding = ((raw.propDefault(Raw._file_encoding_NDX) == null)
        ? raw.propDefault(Raw._sun_jnu_encoding_NDX)
        : raw.propDefault(Raw._file_encoding_NDX));
put(props, "native.encoding", nativeEncoding);

// "file.encoding" defaults to "UTF-8", unless specified in the command line
// where "COMPAT" designates the native encoding.
var fileEncoding = props.getOrDefault("file.encoding", "UTF-8");
if ("COMPAT".equals(fileEncoding)) {
    put(props, "file.encoding", nativeEncoding);
} else {
    putIfAbsent(props, "file.encoding", fileEncoding);
}

// "stdout/err.encoding", prepared for System.out/err. For compatibility
// purposes, substitute them with "sun.*" if they don't exist. If "sun.*" aren't
// available either, fall back to "native.encoding".
putIfAbsent(props, "stdout.encoding", props.getOrDefault("sun.stdout.encoding",
        raw.propDefault(Raw._stdout_encoding_NDX)));
putIfAbsent(props, "stdout.encoding", nativeEncoding);
putIfAbsent(props, "stderr.encoding", props.getOrDefault("sun.stderr.encoding",
        raw.propDefault(Raw._stderr_encoding_NDX)));
putIfAbsent(props, "stderr.encoding", nativeEncoding);

ネイティブ側の実装からは sun.stdout.encodingsun.stderr.encoding は消え去り、以下の構造体定義となっています。

typedef struct {

    char *encoding;
    char *sun_jnu_encoding;
    char *stdout_encoding;  // 旧 sun_stdout_encoding
    char *stderr_encoding;   // 旧 sun_stderr_encoding

} java_props_t;

これに合わせて、sun_stdout_encoding sun_stderr_encodingstdout.encodingstderr.encoding にリネームされています。

stdout.encodingstderr.encoding は、System.outSystem.err の初期化で以下のように設定されます(src/java.base/share/classes/java/lang/System.java)。

setOut0(newPrintStream(fdOut, props.getProperty("stdout.encoding")));
setErr0(newPrintStream(fdErr, props.getProperty("stderr.encoding")));

setOut0()setErr0() はネイティブコードを経由して System.outSystem.errPrintStream インスタンスを割り当てます(static final な変数のためネイティブに設定)。

Console クラスも GetPropertyAction.privilegedGetProperty("stdout.encoding") として取得した文字セットが設定されます。

System.outSystem.errConsole の何れも、stdout.encodingstderr.encoding に値が無い場合(コンソールの接続が無い場合)は、JDK17 で追加された native.encoding の値(デフォルトは file.encoding と同じ値)が設定されます。

現在のJDK24のソースでは、native.encoding の値ではなく、UTF-8 を直接設定するように変更されています。


まとめ

JDK17以降、エンコーディング関連のシステムプロパティは色々と変更されてきました。

システムプロパティ 説明
sun.jnu.encoding nl_langinfo()/GetLocaleInfo()から取得したロケール情報から設定(非公開)。主にファイルパスやProcess コマンドのエンコーディングに使用される
native.encoding JDK17で追加。基本的に sun.jnu.encoding と同じ。COMPAT 指定された場合のエンコーディング。
file.encoding UTF-8 固定。引数でCOMPATと指定された場合は native.encoding の値
stdout.encoding コンソール接続がある場合、nl_langinfo()/GetConsoleCP() の値から設定
stderr.encoding コンソール接続がある場合、nl_langinfo()/GetConsoleCP() の値から設定
sun.stdout.encoding stdout.encodingに置き換え(非公開)
sun.stderr.encoding stderr.encodingに置き換え(非公開)

ファイル入出力では Charset defaultCharset() が使われ、この値は基本的に file.encoding の値(デフォルトUTF-8)が使われます。

標準出力 System.outSystem.err 及び Console には、stdout.encoding stderr.encoding の値が使われます。この値は、Linuxの場合はロケール情報(nl_langinfo())、Windows では、接続されているコンソールのコードポイント値(GetConsoleCP()) を元にして設定されます。