Java | 基礎文法:main クラスの役割

Java Java
スポンサーリンク

main クラスの全体像

Java アプリは「エントリポイント」である main メソッドから始まります。main を持つクラス(便宜上「main クラス」)は、起動時に必要な初期化を行い、コマンドライン引数を受け取り、アプリの「最初の一歩」を指揮します。役割は「全てを書く」ことではなく、準備と振り分けに徹すること。実際のロジックは別クラスへ委ね、main クラスは「起動の配電盤」として小さく保つのが基本です。


main メソッドの仕様と正しい定義

正しいシグネチャと配置

main は必ず public static void main(String[] args) の形で定義し、どのクラスにも自由に置けます。起動時に JVM がこのシグネチャを探して呼び出します。

public class App {
    public static void main(String[] args) {
        System.out.println("Hello");
    }
}
Java
  • public: JVM から見える必要がある
  • static: インスタンス生成なしで呼ぶため
  • void: 戻り値は使われない(終了コードは別手段)
  • String[] args: コマンドライン引数を受け取る

引数の受け取り

起動時に与えた文字列が args に順番どおり入ります。不足してもコンパイルは通るため、存在チェックが必要です。

public static void main(String[] args) {
    if (args.length == 0) {
        System.err.println("usage: App <name>");
        System.exit(1); // 失敗の終了コード
    }
    System.out.println("Hi, " + args[0]);
}
Java

main クラスの責務(重要ポイントの深掘り)

責務の境界を明確にする

main クラスは次の「準備と分岐」に集中します。

public class App {
    public static void main(String[] args) {
        var cfg = Config.load();            // 設定読み込み
        var service = new Service(cfg);     // 依存の組み立て(DI)
        var exit = new Cli(service).run(args); // 引数に応じて処理を委譲
        System.exit(exit);                  // 終了コードで成否を伝える
    }
}
Java
  • 入力(引数・環境変数・設定ファイル)の読み込み
  • 依存の組み立て(サービスやリポジトリの生成)
  • 実処理クラスへの「起動モードの振り分け」
  • 成否の終了コードを返す

この「起動配電盤」設計により、ロジックはテスト可能な別クラスへ隔離されます。

例外の入口・出口を整える

main は「最後の防波堤」です。予期せぬ例外をエラーメッセージと終了コードに変換して、原因を失踪させないようにします。

public static void main(String[] args) {
    int code = 0;
    try {
        code = new Cli(new Service(Config.load())).run(args);
    } catch (Exception e) {
        System.err.println("fatal: " + e.getMessage());
        e.printStackTrace(System.err); // 重要ならスタックも
        code = 1;
    } finally {
        System.exit(code);
    }
}
Java

分離の型:main は薄く、ロジックは外へ

CLI パーサへ委譲して見通しを確保

引数解釈を main に書くと肥大化します。専用クラスへ移し、テストしやすくします。

final class Cli {
    private final Service svc;
    Cli(Service svc) { this.svc = svc; }
    int run(String[] args) {
        if (args.length == 0) return usage();
        switch (args[0]) {
            case "sum": return doSum(args);
            case "list": return doList(args);
            default: return usage();
        }
    }
    private int usage() { System.err.println("usage: sum|list"); return 1; }
    private int doSum(String[] args) { /* 引数チェック→svc呼び出し→結果表示 */ return 0; }
    private int doList(String[] args) { /* ... */ return 0; }
}
Java

依存の組み立てを「1箇所」に集約

設定・接続情報・キャッシュなどの初期化は main 直下のファクトリへ。後から差し替えやすくなります。

final class Bootstrap {
    static Service buildService() {
        var cfg = Config.load();
        var repo = new Repo(cfg.dbUrl());
        return new Service(repo, cfg);
    }
}
public static void main(String[] args) {
    var svc = Bootstrap.buildService();
    System.exit(new Cli(svc).run(args));
}
Java

終了コード・標準入出力・スレッドの扱い

終了コードで外側に成否を伝える

System.exit(0) は成功、0以外は失敗(慣例)。バッチやシェルから起動される場合、終了コードは重要な契約です。

int exit = taskSucceeded ? 0 : 2; // 2は「入力エラー」など用途別に決める
System.exit(exit);
Java

標準入出力の基本

System.out は通常出力、System.err はエラー。ユーザー向けの結果は out、失敗やヘルプは err へ分けると運用が楽になります。

System.out.println("result: " + value);
System.err.println("usage: App <args>");
Java

スレッドの起動と終了

メインスレッドが終わるとプロセスは終了します(非デーモンスレッドが残っていれば待機)。バックグラウンドを使うなら、終了条件と join を設計しましょう。

var t = new Thread(() -> doWork());
t.start();
t.join(); // 終了を待つ(例外処理も忘れずに)
Java

よくあるバッドパターンと回避

main が「何でも屋」になる

ファイルI/O、ネットワーク、計算、フォーマット、全部を main に書くと巨大メソッド化します。「配電盤」に徹し、詳細はクラス分割。

例外を握りつぶす

catch (Exception e) {} のような空キャッチは致命的。メッセージと終了コードに変換し、原因連鎖(e)を保持する。

終了コードを返さない

失敗でも System.exit(0) や何も返さないと、外部から成功に見えてしまう。用途別にコードを決め、統一する。

テスト不能な設計

main に直接ロジックが書かれているとテストが困難。Cli.run(String[]) のような純粋メソッドに落として、ユニットテスト可能にする。


例題で身につける:最小から配電盤へ

例 1: 最小の main と引数チェック

public class Hello {
    public static void main(String[] args) {
        if (args.length < 1) {
            System.err.println("usage: Hello <name>");
            System.exit(1);
        }
        System.out.println("Hello, " + args[0]);
        System.exit(0);
    }
}
Java

例 2: main は薄く、CLI へ委譲

public class App {
    public static void main(String[] args) {
        var svc = new Service(Config.load());
        int exit = new Cli(svc).run(args);
        System.exit(exit);
    }
}
Java
final class Service {
    private final Config cfg;
    Service(Config cfg) { this.cfg = cfg; }
    int sum(int... xs) { return java.util.Arrays.stream(xs).sum(); }
}
final class Cli {
    private final Service svc;
    Cli(Service svc) { this.svc = svc; }
    int run(String[] args) {
        if (args.length == 0) return usage();
        if ("sum".equals(args[0])) return sumCmd(args);
        return usage();
    }
    private int sumCmd(String[] args) {
        try {
            var xs = java.util.Arrays.stream(java.util.Arrays.copyOfRange(args, 1, args.length))
                                     .mapToInt(Integer::parseInt).toArray();
            System.out.println(svc.sum(xs));
            return 0;
        } catch (NumberFormatException e) {
            System.err.println("invalid number: " + e.getMessage());
            return 2;
        }
    }
    private int usage() { System.err.println("usage: App sum <ints...>"); return 1; }
}
Java

仕上げのアドバイス(重要部分のまとめ)

main クラスは「起動の配電盤」。設定読み込み、依存の組み立て、引数による振り分け、終了コードの決定だけに集中し、業務ロジックは別クラスへ委譲する。例外は最後の防波堤として捕捉し、メッセージと終了コードで外へ正しく伝える。標準出力と標準エラーを使い分け、スレッドやリソースのライフサイクルを明確にする——この型が身につけば、main は常に小さく、アプリはテストしやすく、運用に強くなります。

タイトルとURLをコピーしました