Javaで環境変数を取得するには System.getenv("名前") を使います。値が設定されていないときは null が返ります。 よく似たものに System.getProperty("名前") がありますが、こちらが読むのは「システムプロパティ」で、java コマンドの -D オプションや JVM 自身が設定する、環境変数とは別の入れ物です。

この記事では、2つの違いを比較表にまとめ、Linux で export と -D を指定して実行した出力を載せます。あわせて、user.dir・user.home・java.version・file.encoding など主要なシステムプロパティの実値つき一覧、getenv の Map に書き込んだときの UnsupportedOperationException、Optional での既定値の付け方、ProcessBuilder で子プロセスに環境変数を渡す方法、JDK 18 以降の file.encoding と native.encoding の違いまで扱います。

確認環境:Ubuntu 24.04 / OpenJDK 21.0.10。出力例のユーザー名とパスは、表示を環境に合わせて置き換えています(ユーザー名 taro、ホーム /home/taro、作業ディレクトリ /home/taro/demo)。

環境変数とシステムプロパティの早見表

やりたいこと 書き方 無いときの値
環境変数を1つ取得 System.getenv("APP_MODE") null
環境変数を全部取得 System.getenv()(読み取り専用の Map) ―
環境変数に既定値を付ける System.getenv().getOrDefault("APP_MODE", "dev") 既定値
システムプロパティを1つ取得 System.getProperty("app.mode") null
システムプロパティに既定値を付ける System.getProperty("app.mode", "dev") 既定値
システムプロパティを全部取得 System.getProperties() ―
数値のプロパティを取得 Integer.getInteger("app.port", 8080) 既定値
真偽値のプロパティを取得 Boolean.getBoolean("app.debug") false
実行時にプロパティを設定 System.setProperty("app.mode", "test") ―
環境変数を設定して起動 export APP_MODE=dev のあと java Main ―
プロパティを設定して起動 java -Dapp.mode=prod Main ―

2つの違いは次のとおりです。

項目 環境変数 システムプロパティ
取得メソッド System.getenv(name) System.getProperty(key)
設定する場所 OS・シェル(export NAME=値) java コマンド(-Dkey=値)と JVM 自身
名前の書き方の慣習 大文字とアンダースコア(APP_MODE) 小文字とドット(app.mode)
見える範囲 そのシェルから起動した全プロセスと、その子孫 その JVM の中だけ
実行中の変更 できない(読み取り専用) できる(System.setProperty)
無いとき null null(第2引数で既定値を指定できる)
既定値の指定 Optional や getOrDefault で書く getProperty(key, 既定値)
JVM が用意する値 なし(OS が用意する HOME・PATH など) あり(java.version、user.dir、os.name など)
大文字と小文字 UNIX 系では区別する。Windows では通常区別しない 区別する

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java 環境変数の取得方法|System.getenvとシステムプロパティ一覧」

System.getenv と System.getProperty の基本

まず、両方を同じプログラムで読んでみます。

public class EnvBasic {
    public static void main(String[] args) {
        // 環境変数:OS(シェル)から渡される
        System.out.println("APP_MODE (env)   = " + System.getenv("APP_MODE"));
        System.out.println("HOME (env)       = " + System.getenv("HOME"));
        System.out.println("NO_SUCH (env)    = " + System.getenv("NO_SUCH"));

        // システムプロパティ:java コマンドの -D や JVM が設定する
        System.out.println("app.mode (prop)  = " + System.getProperty("app.mode"));
        System.out.println("user.home (prop) = " + System.getProperty("user.home"));
        System.out.println("no.such (prop)   = " + System.getProperty("no.such"));
        System.out.println("no.such + 既定値 = " + System.getProperty("no.such", "default"));
    }
}

何も指定せずに実行する

java EnvBasic.java
APP_MODE (env)   = null
HOME (env)       = /home/taro
NO_SUCH (env)    = null
app.mode (prop)  = null
user.home (prop) = /home/taro
no.such (prop)   = null
no.such + 既定値 = default

設定していない APP_MODE と app.mode は、どちらも null です。空文字ではなく null なので、そのままメソッドを呼ぶと例外になります(後述)。HOME は OS が用意する環境変数、user.home は JVM が用意するシステムプロパティで、同じ場所を指しています。

export と -D を指定して実行する

export APP_MODE=dev
java -Dapp.mode=prod EnvBasic.java
APP_MODE (env)   = dev
HOME (env)       = /home/taro
NO_SUCH (env)    = null
app.mode (prop)  = prod
user.home (prop) = /home/taro
no.such (prop)   = null
no.such + 既定値 = default

export した値は getenv で、-D で渡した値は getProperty で取れます。 名前が似ていても別の入れ物なので、APP_MODE を export しても getProperty("app.mode") には入りません。

その1回の実行だけに環境変数を渡したいときは、コマンドの前に書きます。

APP_MODE=stg java EnvBasic.java
APP_MODE (env)   = stg

-D オプションの書き方と置く位置

-D の書き方には、つまずきやすい点が2つあります。

クラス名(ファイル名)より前に書く

java EnvBasic.java -Dapp.mode=prod
app.mode (prop)  = null

クラス名やファイル名より後ろに書いた -D は、プログラムの引数(main の args)として渡されます。 システムプロパティにはならないので null のままです。-D は必ず java の直後、クラス名より前に置きます。

空白を含む値は引用符で囲む

java -Dapp.title="Hello World" -Dapp.mode=prod -XshowSettings:properties -version 2>&1 | grep 'app\.'
    app.mode = prod
    app.title = Hello World

-XshowSettings:properties は、JVM が持っているシステムプロパティを一覧で表示するオプションです。プログラムを書かずに値を確かめたいときに便利です。

書き方 結果
java -Dapp.mode=prod Main app.mode が prod になる
java Main -Dapp.mode=prod args に入る。プロパティは null
java -Dapp.title="Hello World" Main 空白を含む値を渡せる
java -Dapp.debug Main 値を省くと空文字になる(null ではない)
java -Dapp.mode=a -Dapp.mode=b Main 同じキーを重ねると、後ろの b になった

最後の2行は、次のコマンドで確かめました。

java -Dapp.debug -Dapp.mode=a -Dapp.mode=b -XshowSettings:properties -version 2>&1 | grep 'app\.'
    app.debug = 
    app.mode = b

OpenJDK 21.0.10 では、値を省いた app.debug は空文字に、同じキーを重ねた app.mode は後ろの b になりました。なお、値が空文字のとき Boolean.getBoolean("app.debug") は false を返しました。フラグとして使うなら -Dapp.debug=true と値まで書きます。

環境変数から -D を渡す:JAVA_TOOL_OPTIONS

起動コマンドを書き換えられない場面では、環境変数 JAVA_TOOL_OPTIONS に -D を書いておくと、JVM が起動時に読み取ります。

JAVA_TOOL_OPTIONS="-Dapp.mode=tool" java EnvBasic.java
Picked up JAVA_TOOL_OPTIONS: -Dapp.mode=tool
app.mode (prop)  = tool

1行目の「Picked up JAVA_TOOL_OPTIONS: …」は、JVM が標準エラーに出すお知らせです。コマンドラインでも同じキーを指定した場合は、コマンドラインの値が使われました。

JAVA_TOOL_OPTIONS="-Dapp.mode=tool" java -Dapp.mode=cli EnvBasic.java
Picked up JAVA_TOOL_OPTIONS: -Dapp.mode=tool
app.mode (prop)  = cli

java コマンド専用の JDK_JAVA_OPTIONS でも同じことができます。

JDK_JAVA_OPTIONS="-Dapp.mode=jdkopt" java EnvBasic.java
NOTE: Picked up JDK_JAVA_OPTIONS: -Dapp.mode=jdkopt
app.mode (prop)  = jdkopt

「環境変数で指定したつもりなのにプロパティが変わる」「覚えのない Picked up の行が出る」ときは、この2つの環境変数を確かめると原因が見つかります。

主要なシステムプロパティ一覧(実値つき)

JVM が最初から用意しているシステムプロパティのうち、よく使うものを実際の値つきで並べます。説明は Java 21 の API ドキュメント(System.getProperties)の記載に沿っています。

キー 意味 この環境での値
java.version Java のバージョン 21.0.10
java.version.date そのバージョンの日付 2026-01-20
java.vendor 提供元 Ubuntu
java.home Java のインストール先 /usr/lib/jvm/java-21-openjdk-amd64
java.vm.name JVM の名前 OpenJDK 64-Bit Server VM
java.specification.version Java 仕様のバージョン 21
java.class.version クラスファイルのバージョン 65.0
java.class.path クラスパス .(-cp で指定した値)
java.io.tmpdir 一時ファイルの既定の場所 /tmp
os.name OS の名前 Linux
os.arch CPU アーキテクチャ amd64
os.version OS のバージョン 6.18.44-fc-v64(カーネルの版)
user.name ユーザーのアカウント名 taro
user.home ホームディレクトリ /home/taro
user.dir 起動時の作業ディレクトリ /home/taro/demo
file.separator パスの区切り文字 /(UNIX 系)
path.separator パス一覧の区切り文字 :(UNIX 系)
line.separator 改行文字 \n(UNIX 系。長さ1)
file.encoding 既定の文字コード UTF-8
native.encoding OS・ロケール由来の文字コード UTF-8(ロケールが C.UTF-8 のとき)
stdout.encoding System.out の文字コード UTF-8(ロケールが C.UTF-8 のとき)
stderr.encoding System.err の文字コード UTF-8(ロケールが C.UTF-8 のとき)
user.language 言語 en
sun.java.command 起動したクラスと引数(OpenJDK の実装が設定) 実行したコマンドによる

一覧は次のプログラムで出力しました。改行のような見えない文字は、エスケープして表示しています。

public class PropList {
    public static void main(String[] args) {
        String[] keys = {
            "java.version", "java.version.date", "java.vendor", "java.home",
            "java.vm.name", "java.specification.version", "java.class.version", "java.class.path",
            "java.io.tmpdir", "os.name", "os.arch", "os.version",
            "user.name", "user.home", "user.dir",
            "file.separator", "path.separator", "line.separator",
            "file.encoding", "native.encoding", "stdout.encoding", "stderr.encoding",
            "user.language"
        };
        for (String k : keys) {
            String v = System.getProperty(k);
            // 改行などの見えない文字はエスケープして表示する
            String shown = (v == null) ? "null"
                    : "\"" + v.replace("\\", "\\\\").replace("\n", "\\n").replace("\r", "\\r") + "\"";
            System.out.printf("%-28s = %s%n", k, shown);
        }
        System.out.println("プロパティの総数: " + System.getProperties().size());
    }
}
java.version                 = "21.0.10"
java.version.date            = "2026-01-20"
java.vendor                  = "Ubuntu"
java.home                    = "/usr/lib/jvm/java-21-openjdk-amd64"
java.vm.name                 = "OpenJDK 64-Bit Server VM"
java.specification.version   = "21"
java.class.version           = "65.0"
java.class.path              = "."
java.io.tmpdir               = "/tmp"
os.name                      = "Linux"
os.arch                      = "amd64"
os.version                   = "6.18.44-fc-v64"
user.name                    = "taro"
user.home                    = "/home/taro"
user.dir                     = "/home/taro/demo"
file.separator               = "/"
path.separator               = ":"
line.separator               = "\n"
file.encoding                = "UTF-8"
native.encoding              = "UTF-8"
stdout.encoding              = "UTF-8"
stderr.encoding              = "UTF-8"
user.language                = "en"
プロパティの総数: 48

用途別の逆引き

知りたいこと 使うプロパティ 代わりに使える API
いまの作業ディレクトリ user.dir Path.of("").toAbsolutePath()
ホームディレクトリ user.home ―
一時ファイルの置き場所 java.io.tmpdir Files.createTempFile(...)
改行文字 line.separator System.lineSeparator()
パスの区切り file.separator File.separator、Path.of(a, b)
Java のバージョン java.version Runtime.version()
OS の判定 os.name ―
既定の文字コード file.encoding Charset.defaultCharset()

パスを組み立てるときは、file.separator を文字列でつなぐよりも Path.of("data", "in.csv") のように API に任せるほうが、OS の違いを意識せずに書けます。

システムプロパティは実行中に変更できる

システムプロパティは System.setProperty で変更でき、System.clearProperty で削除できます。

public class PropSet {
    public static void main(String[] args) {
        System.out.println("変更前: " + System.getProperty("app.mode"));
        String before = System.setProperty("app.mode", "test");   // 戻り値は直前の値
        System.out.println("setProperty の戻り値: " + before);
        System.out.println("変更後: " + System.getProperty("app.mode"));
        System.clearProperty("app.mode");
        System.out.println("clear後: " + System.getProperty("app.mode"));

        // user.dir を書き換えても、実際の作業ディレクトリは変わらない
        System.setProperty("user.dir", "/tmp");
        System.out.println("user.dir = " + System.getProperty("user.dir"));
        System.out.println("Path.of(\"\") = " + java.nio.file.Path.of("").toAbsolutePath());
    }
}

java -Dapp.mode=prod PropSet.java の実行結果です。

変更前: prod
setProperty の戻り値: prod
変更後: test
clear後: null
user.dir = /tmp
Path.of("") = /home/taro/demo

最後の2行に注目してください。user.dir の値を書き換えても、実際の作業ディレクトリは移動しません。 プロパティの文字列が変わるだけで、Path の相対パスは起動時のディレクトリを基準に解決されました。JVM が用意したプロパティは「読むためのもの」と考え、自分で定義したキー(app. で始まるものなど)だけを変更するのが分かりやすい使い方です。

環境変数を全部取得する|getenv() の Map

引数なしの System.getenv() は、すべての環境変数を Map で返します。

import java.util.Map;
import java.util.TreeMap;

public class EnvAll {
    public static void main(String[] args) {
        Map<String, String> env = System.getenv();
        System.out.println("件数: " + env.size());
        // 名前順に並べ、APP_ で始まるものだけ表示
        new TreeMap<>(env).forEach((k, v) -> {
            if (k.startsWith("APP_")) System.out.println(k + "=" + v);
        });
        System.out.println("クラス: " + env.getClass().getName());
    }
}

APP_MODE=dev APP_PORT=9000 java EnvAll.java の実行結果です(件数は環境によって変わります)。

件数: 194
APP_MODE=dev
APP_PORT=9000
クラス: java.util.Collections$UnmodifiableMap

Map の順序は決まっていないので、一覧で出すときは TreeMap に入れて名前順にすると読みやすくなります。全件をそのままログに出すと、トークンやパスワードが入った環境変数まで記録されてしまうので、表示するのは必要な名前だけに絞るのがおすすめです。

getenv の Map に書き込むと UnsupportedOperationException

返ってくる Map は、クラス名のとおり変更できない Mapです。put してみます。

import java.util.Map;

public class EnvPut {
    public static void main(String[] args) {
        Map<String, String> env = System.getenv();
        env.put("APP_MODE", "test");   // 書き込もうとすると…
        System.out.println(env.get("APP_MODE"));
    }
}
Exception in thread "main" java.lang.UnsupportedOperationException
    at java.base/java.util.Collections$UnmodifiableMap.put(Collections.java:1660)
    at EnvPut.main(EnvPut.java:6)

Java には、自分のプロセスの環境変数を書き換える API がありません。 値を切り替えたいときの方法は次の3つです。

やりたいこと 方法
起動時に値を変えたい シェルで export する、または NAME=値 java ... で起動する
実行中に切り替えられる設定がほしい システムプロパティ(System.setProperty)や、自分の設定クラスを使う
子プロセスにだけ別の値を渡したい ProcessBuilder.environment() に put する(次の章)

大文字・小文字と空文字

public class Case {
    public static void main(String[] args) {
        System.out.println(System.getenv("APP_MODE") + " / " + System.getenv("app_mode") + " / " + System.getenv("App_Mode"));
        System.out.println("[" + System.getenv("APP_EMPTY") + "] length=" + System.getenv("APP_EMPTY").length());
        System.out.println("lineSeparator length=" + System.lineSeparator().length());
    }
}

app_mode=lower APP_MODE=UPPER APP_EMPTY= java Case.java の実行結果です。

UPPER / lower / null
[] length=0
lineSeparator length=1

Linux では APP_MODE と app_mode は別の変数として扱われました。API ドキュメントには、UNIX 系では通常大文字と小文字を区別し、Windows では通常区別しない、と書かれています。また、APP_EMPTY= のように値を空にして設定した変数は、null ではなく空文字で返ります。「設定されていない」と「空で設定されている」は別の状態です。

null への備え|Optional で既定値を付ける

設定されていない環境変数は null なので、そのままメソッドを呼ぶと NullPointerException になります。

public class Npe {
    public static void main(String[] args) {
        String mode = System.getenv("APP_MODE");   // 未設定なら null
        System.out.println(mode.toUpperCase());
    }
}
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.toUpperCase()" because "<local1>" is null
    at Npe.main(Npe.java:4)

既定値の付け方を、用途別にまとめたコードです。

import java.util.Optional;

public class Defaults {
    /** 環境変数 → システムプロパティ → 既定値 の順に探す */
    static String config(String envName, String propName, String defaultValue) {
        return Optional.ofNullable(System.getenv(envName))
                .filter(s -> !s.isBlank())                       // 空文字は設定なしと同じ扱いにする
                .or(() -> Optional.ofNullable(System.getProperty(propName)))
                .orElse(defaultValue);
    }

    public static void main(String[] args) {
        // 1) Optional で既定値
        String mode = Optional.ofNullable(System.getenv("APP_MODE")).orElse("dev");
        System.out.println("APP_MODE = " + mode);

        // 2) 数値に変換して既定値
        int port = Optional.ofNullable(System.getenv("APP_PORT"))
                .map(Integer::parseInt)
                .orElse(8080);
        System.out.println("APP_PORT = " + port);

        // 3) JDK 標準の getOrDefault(Map として扱う)
        System.out.println("APP_NAME = " + System.getenv().getOrDefault("APP_NAME", "sample"));

        // 4) システムプロパティは第2引数で既定値
        System.out.println("app.mode = " + System.getProperty("app.mode", "dev"));

        // 5) 数値・真偽値のプロパティ用メソッド
        System.out.println("app.port = " + Integer.getInteger("app.port", 8080));
        System.out.println("app.debug = " + Boolean.getBoolean("app.debug"));

        // 6) 環境変数 → プロパティ → 既定値
        System.out.println("config   = " + config("APP_MODE", "app.mode", "dev"));

        // 7) 必須の設定は、無ければ早めに知らせる
        try {
            String url = Optional.ofNullable(System.getenv("APP_DB_URL"))
                    .orElseThrow(() -> new IllegalStateException("環境変数 APP_DB_URL を設定してください"));
            System.out.println(url);
        } catch (IllegalStateException e) {
            System.out.println("例外: " + e.getMessage());
        }
    }
}

何も設定せずに java Defaults.java を実行した結果です。

APP_MODE = dev
APP_PORT = 8080
APP_NAME = sample
app.mode = dev
app.port = 8080
app.debug = false
config   = dev
例外: 環境変数 APP_DB_URL を設定してください

値を設定して実行します。

APP_MODE=stg APP_PORT=9001 APP_NAME=shop APP_DB_URL=jdbc:postgresql://localhost/app \
  java -Dapp.mode=prod -Dapp.port=9002 -Dapp.debug=true Defaults.java
APP_MODE = stg
APP_PORT = 9001
APP_NAME = shop
app.mode = prod
app.port = 9002
app.debug = true
config   = stg
jdbc:postgresql://localhost/app
書き方 向いている場面
Optional.ofNullable(System.getenv(x)).orElse(既定値) 文字列の環境変数
.map(Integer::parseInt).orElse(8080) 数値に変換したい環境変数
System.getenv().getOrDefault(x, 既定値) 短く書きたいとき
System.getProperty(key, 既定値) システムプロパティ
Integer.getInteger(key, 既定値) 数値のシステムプロパティ
Boolean.getBoolean(key) 真偽値のシステムプロパティ("true" のときだけ true)
.orElseThrow(...) 必須の設定。無いことを起動時に知らせる

数値への変換では、数字以外が入っていた場合も考えておきます。APP_PORT=abc java Defaults.java を実行すると、次の例外になりました。

Exception in thread "main" java.lang.NumberFormatException: For input string: "abc"
    at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
    at java.base/java.lang.Integer.parseInt(Integer.java:662)

設定値の読み込みはアプリケーションの起動時に1か所へまとめ、不正な値はその場でメッセージを出して止めると、原因がすぐ分かります。

子プロセスに環境変数を渡す|ProcessBuilder

Java から別のコマンドを起動するときは、ProcessBuilder の environment() で、子プロセスに渡す環境変数を変更できます。こちらの Map は書き込めます。

import java.util.Map;

public class Child {
    public static void main(String[] args) throws Exception {
        // sh -c で、子プロセス側の環境変数を表示する
        ProcessBuilder pb = new ProcessBuilder("sh", "-c",
                "echo \"APP_MODE=$APP_MODE / APP_TOKEN=$APP_TOKEN / LANG=${LANG:-unset}\"");
        Map<String, String> env = pb.environment();   // 親の環境変数のコピー(書き込める)
        env.put("APP_MODE", "child");                 // 追加・上書き
        env.put("APP_TOKEN", "abc123");
        env.remove("LANG");                           // 子には渡さない
        pb.inheritIO();                               // 子の出力をそのまま表示
        int code = pb.start().waitFor();
        System.out.println("exit = " + code);

        // 親(この Java プロセス)の環境変数は変わらない
        System.out.println("親の APP_MODE = " + System.getenv("APP_MODE"));
        System.out.println("親の APP_TOKEN = " + System.getenv("APP_TOKEN"));

        // 子の Java にシステムプロパティを渡すときは -D を引数に並べる
        new ProcessBuilder("java", "-Dapp.mode=child", "-XshowSettings:properties", "-version")
                .redirectErrorStream(true)
                .start().inputReader().lines()
                .filter(l -> l.contains("app.mode"))
                .forEach(System.out::println);
    }
}

APP_MODE=parent LANG=C.UTF-8 java Child.java の実行結果です。

APP_MODE=child / APP_TOKEN=abc123 / LANG=unset
exit = 0
親の APP_MODE = parent
親の APP_TOKEN = null
    app.mode = child
  • environment() は、親の環境変数のコピーから始まります。 put で追加・上書き、remove で削除できます。
  • 変更は子プロセスにだけ反映されます。 親の APP_MODE は parent のままで、APP_TOKEN も親には存在しません。
  • システムプロパティは子に引き継がれません。 子も Java なら、-D を引数として並べて渡します。

API ドキュメントには、Java の子プロセスへ情報を渡すときは、環境変数よりもシステムプロパティが一般に好ましい、と書かれています。環境変数は子孫のプロセス全体に見えるため影響範囲が広い、というのが理由です。

file.encoding と native.encoding の違い(JDK 18 以降)

文字コードに関するプロパティは、JDK 18 で意味が変わりました。JEP 400(UTF-8 by Default)で、標準 API の既定の文字コードが、OS やロケールに関係なく UTF-8 になったためです。関連するプロパティを整理します(JEP 400 と Java 21 の API ドキュメントの記載です)。

プロパティ 意味 指定できる値
file.encoding 既定の文字コード(Charset.defaultCharset())。JDK 18 以降の既定は UTF-8 コマンドラインで UTF-8 か COMPAT
native.encoding OS・ロケールから決まる文字コード。JDK 17 で追加 設定しても効果なし(読むためのもの)
stdout.encoding System.out の文字コード 起動時に UTF-8 を指定できる
stderr.encoding System.err の文字コード 起動時に UTF-8 を指定できる

-Dfile.encoding=COMPAT を付けると、JDK 17 以前と同じ「OS・ロケールから決める」動きに戻ります。ロケールを変えながら確かめます。

import java.nio.charset.Charset;

public class Enc {
    public static void main(String[] args) {
        System.out.println("file.encoding    = " + System.getProperty("file.encoding"));
        System.out.println("native.encoding  = " + System.getProperty("native.encoding"));
        System.out.println("stdout.encoding  = " + System.getProperty("stdout.encoding"));
        System.out.println("defaultCharset() = " + Charset.defaultCharset());
        System.out.println("System.out       = " + System.out.charset());
        System.out.println("日本語の表示");
    }
}

ロケールが C.UTF-8 のとき

LC_ALL=C.UTF-8 java Enc.java
file.encoding    = UTF-8
native.encoding  = UTF-8
stdout.encoding  = UTF-8
defaultCharset() = UTF-8
System.out       = UTF-8
日本語の表示

ロケールが C(UTF-8 ではない)のとき

LANG=C java Enc.java
file.encoding    = UTF-8
native.encoding  = ANSI_X3.4-1968
stdout.encoding  = ANSI_X3.4-1968
defaultCharset() = UTF-8
System.out       = US-ASCII
??????

file.encoding は UTF-8 のままですが、native.encoding はロケール由来の ANSI_X3.4-1968(ASCII)になり、System.out もそれに従います。 そのため、ファイルの読み書きは UTF-8 で行われる一方、画面への日本語の表示は「?」になりました。コンテナや cron のようにロケールが設定されていない場所で日本語のログが「?」になるときは、この状態です。

表示を直す2つの方法

1つ目は、ロケールを UTF-8 のものにする方法(上の C.UTF-8 の例)です。2つ目は、System.out の文字コードを起動時に指定する方法です。

LANG=C java -Dstdout.encoding=UTF-8 Enc.java
file.encoding    = UTF-8
native.encoding  = ANSI_X3.4-1968
stdout.encoding  = UTF-8
defaultCharset() = UTF-8
System.out       = UTF-8
日本語の表示

COMPAT を指定したとき

LANG=C java -Dfile.encoding=COMPAT Enc.java
file.encoding    = ANSI_X3.4-1968
native.encoding  = ANSI_X3.4-1968
stdout.encoding  = ANSI_X3.4-1968
defaultCharset() = US-ASCII
System.out       = US-ASCII
??????????????????

COMPAT では、file.encoding が native.encoding の値に置き換わり、既定の文字コードもロケール由来になりました。4つの実行結果をまとめます。

起動方法 file.encoding native.encoding System.out 日本語の表示
ロケール C.UTF-8 UTF-8 UTF-8 UTF-8 表示される
ロケール C UTF-8 ANSI_X3.4-1968 US-ASCII ? になる
ロケール C + -Dstdout.encoding=UTF-8 UTF-8 ANSI_X3.4-1968 UTF-8 表示される
ロケール C + -Dfile.encoding=COMPAT ANSI_X3.4-1968 ANSI_X3.4-1968 US-ASCII ? になる

覚えておきたいのは、file.encoding は「ファイルなどを読み書きする標準 API の既定」、native.encoding は「OS 側の文字コード」、画面への出力は stdout.encoding という役割分担です。なお、ファイルの読み書きでは Files.readString(path, StandardCharsets.UTF_8) のように文字コードを明示しておくと、どの設定でも同じ結果になります。

環境変数とシステムプロパティの使い分け

場面 向いているもの 理由
コンテナや CI など、外から設定を差し込む 環境変数 起動コマンドを変えずに値を切り替えられる
PATH や HOME など OS が決めた値を読む 環境変数 OS との共通の約束ごと
Java のプログラムだけに関係する設定 システムプロパティ その JVM の中だけに見える。既定値も書きやすい
テストで一時的に値を変えたい システムプロパティ setProperty と clearProperty で切り替えられる
子の Java プロセスに設定を渡す システムプロパティ(-D) API ドキュメントが一般に好ましいとしている
Java のバージョンや OS を調べる システムプロパティ JVM が用意している

両方に対応したいときは、Defaults の例にある config メソッドのように「環境変数 → システムプロパティ → 既定値」の順に探す形にしておくと、どちらで指定しても動きます。

よくある質問

Q. System.getenv と System.getProperty の違いは何ですか? A. getenv は OS の環境変数(export で設定するもの)、getProperty は JVM のシステムプロパティ(-D で設定するものと、java.version など JVM が用意するもの)を読みます。別の入れ物なので、片方に設定した値はもう片方からは取れません。

Q. Java のプログラムの中で環境変数を設定できますか? A. 自分のプロセスの環境変数は変更できません。System.getenv() の Map に put すると UnsupportedOperationException になります。実行中に切り替えたい設定はシステムプロパティに、子プロセスに渡したい値は ProcessBuilder の environment() に設定します。

Q. export したのに getenv が null を返します。 A. export したシェルと、Java を起動したシェルやツールが別になっていることが多いです。IDE やサービスとして起動している場合は、その起動設定に環境変数を書きます。System.getenv() を全件出力して、名前(大文字・小文字を含む)が合っているかを確かめるのが近道です。

Q. システムプロパティを全部見るにはどうすればよいですか? A. プログラムなしで見るなら java -XshowSettings:properties -version が手軽です。プログラムの中では System.getProperties() を使います。

まとめ

  • 環境変数は System.getenv("名前")、システムプロパティは System.getProperty("キー")。 無いときはどちらも null
  • export した値は getenv、-D で渡した値は getProperty で取れる。 -D はクラス名より前に書く
  • システムプロパティは第2引数で既定値を指定できる。 環境変数は Optional や getOrDefault で既定値を付ける
  • getenv() の Map は読み取り専用。 put すると UnsupportedOperationException になる
  • 子プロセスに渡す環境変数は ProcessBuilder の environment() で変更する。 親の環境変数は変わらない
  • user.dir・user.home・java.version・os.name・java.io.tmpdir・line.separator は JVM が用意するプロパティ
  • JDK 18 以降、file.encoding の既定は UTF-8。 OS 側の文字コードは native.encoding、画面出力は stdout.encoding で決まる

参考・出典

  • Java SE 21 API ドキュメント:System(getenv / getProperty / getProperties)、ProcessBuilder(docs.oracle.com)
  • JEP 400: UTF-8 by Default(openjdk.org)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java 環境変数の取得方法|System.getenvとシステムプロパティ一覧」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?