メインコンテンツにスキップ

JavaからHello Worldお出力するまで 1

· 16分の読み時間
Haril Song
Owner, Software Engineer at 42dot

banner

プログラミングの世界では、常に「Hello World」という文を出力することから始まります。それはまるで不文律のようです。

# hello.py
print("Hello World")
python hello.py
// Hello World

Python?素晴らしい。

// hello.js
console.log("Hello World");
node hello.js
// Hello World

JavaScript?悪くない。

public class VerboseLanguage {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
javac VerboseLanguage.java
java VerboseLanguage
// Hello World

しかし、Javaはまるで別の世界から来たように感じます。クラス名がファイル名と一致しなければならないことさえまだ触れていません。

publicとは何か、classとは何か、staticとは何か、voidmainString[]、そしてSystem.out.printlnを経て、ようやく文字列「Hello World」にたどり着きます。さあ、別の言語を学びましょう。1

単に「Hello World」を出力するだけでも、Javaはかなりの背景知識を要求します。なぜJavaはこんなにも冗長なプロセスを必要とするのでしょうか?

このシリーズは3つの章に分かれています。目標は、2つの単語「Hello World」を出力するために裏で何が起こっているのかを詳しく探ることです。各章の具体的な内容は以下の通りです:

  • 第1章では、Hello Worldを出発点とする理由を紹介します。
  • 第2章では、コンパイルされたクラスファイルとコンピュータがJavaコードを解釈し実行する方法を検討します。
  • 最後に、JVMがpublic static void mainをロードして実行する方法とその動作原理を探ります。

3つの章の内容を組み合わせることで、ようやく「Hello World」の概念を理解することができます。かなり長い旅ですが、深呼吸して始めましょう。

第1章. なぜ?

JavaでHello Worldを出力する前に、いくつかの「なぜ」の瞬間を考慮する必要があります。

なぜクラス名はファイル名と一致しなければならないのか?

より正確には、publicクラスの名前がファイル名と一致しなければならないのです。なぜでしょうか?

Javaプログラムはコンピュータに直接理解されるものではありません。JVMという仮想マシンがプログラムの実行を助けます。Javaプログラムをコンピュータで実行可能にするためには、いくつかのステップを経てJVMが解釈できる機械語に変換する必要があります。最初のステップは、コンパイラを使用してプログラムをJVMが解釈できるバイトコードに変換することです。変換されたバイトコードはJVM内部のインタープリタを通じて機械語に翻訳され、実行されます。

コンパイルプロセスを簡単に見てみましょう。

public class Outer {
public static void main(String[] args) {
System.out.println("This is Outer class");
}

private class Inner {
}
}
javac Outer.java
Permissions Size User   Date Modified Name
.rw-r--r-- 302 haril 30 Nov 16:09 Outer$Inner.class
.rw-r--r-- 503 haril 30 Nov 16:09 Outer.class
.rw-r--r-- 159 haril 30 Nov 16:09 Outer.java

Javaはコンパイル時に各クラスごとに.classファイルを生成します

さて、JVMはプログラムを実行するためにmainメソッドを見つける必要があります。どうやってmainメソッドを見つけるのでしょうか?

なぜmain()を見つける必要があるのか?もう少し待ってください。

もしJavaファイル名がパブリッククラス名と一致しない場合、Javaインタープリタはmainメソッドを見つけるためにすべてのクラスファイルを読み込む必要があります。ファイル名がパブリッククラス名と一致している場合、Javaインタープリタは解釈する必要のあるファイルをよりよく特定できます。

1000クラスが含まれるJava1000というファイルを想像してみてください。1000クラスの中からmain()がどこにあるのかを特定するために、インタープリタはすべてのクラスファイルを調べる必要があります。

しかし、ファイル名がパブリッククラス名と一致している場合、main()により迅速にアクセスでき(mainはパブリッククラスに存在するため)、すべてのロジックがmain()から始まるため、他のクラスにも簡単にアクセスできます。

なぜpublicでなければならないのか?

JVMはクラス内のmainメソッドを見つける必要があります。クラスの外部からクラスにアクセスするJVMがクラス内のメソッドを見つけるためには、そのメソッドがpublicでなければなりません。実際、アクセス修飾子をprivateに変更すると、mainpublicとして宣言するように指示するエラーメッセージが表示されます。

Error: Main method not found in class VerboseLanguage, please define the main method as:
public static void main(String[] args)

なぜstaticでなければならないのか?

JVMはpublic main()メソッドを見つけました。しかし、このメソッドを呼び出すためには、まずオブジェクトを作成する必要があります。JVMはこのオブジェクトを必要とするのでしょうか?いいえ、JVMはただmainを呼び出すだけでよいのです。staticとして宣言することで、JVMは不要なオブジェクトを作成する必要がなくなり、メモリを節約できます。

なぜvoidでなければならないのか?

mainメソッドの終了はJavaの実行の終了を意味します。JVMはmainの戻り値を使用することができないため、戻り値の存在は意味がありません。したがって、voidとして宣言するのが自然です。

なぜmainという名前でなければならないのか?

mainというメソッド名は、JVMがアプリケーションの実行エントリーポイントを見つけるために設計されたものです。

「設計」という言葉は大げさに聞こえますが、実際にはmainという名前のメソッドを見つけるようにハードコーディングされています。もし見つける名前がmainではなくharilだった場合、harilという名前のメソッドを探していたでしょう。もちろん、Javaの作成者たちはmainを選んだ理由があったのでしょうが、それがすべてです。

mainClassName = GetMainClassName(env, jarfile);
mainClass = LoadClass(env, classname);

// Find the main method
mainID = (*env)->GetStaticMethodID(env, mainClass, "main", "([Ljava/lang/String;)V");

jbject obj = (*env)->ToReflectedMethod(env, mainClass, mainID, JNI_TRUE);

なぜargsが必要なのか?

これまで、main()String[] argsについては触れていませんでした。なぜこの引数が必要で、なぜ省略するとエラーが発生するのでしょうか?

public static void main(String[] args)はJavaアプリケーションのエントリーポイントであるため、この引数はJavaアプリケーションの外部から渡される必要があります。

標準入力のすべての型は文字列として入力されます。

これがargsが文字列配列として宣言される理由です。考えてみれば納得です。Javaアプリケーションが実行される前に、カスタムオブジェクトタイプを直接作成できますか?🤔

では、なぜargsが必要なのでしょうか?

外部から内部に引数を簡単に渡すことで、Javaアプリケーションの動作を変更することができます。このメカニズムは、Cプログラミングの初期からプログラムの動作を制御するために広く使用されてきました。特にシンプルなアプリケーションにとって、この方法は非常に効果的です。Javaはこの広く使用されている方法を採用しただけです

String[] argsを省略できない理由は、Javaがエントリーポイントとしてpublic static void main(String[] args)のみを許可しているためです。Javaの作成者たちは、argsを宣言して使用しない方が、省略を許可するよりも混乱が少ないと考えたのです。

System.out.println

最後に、出力に関連するメソッドについて話し始めることができます。

もう一度言いますが、Pythonではprint("Hello World")でした。2

Javaプログラムはオペレーティングシステム上で直接実行されるのではなく、JVMという仮想マシン上で実行されます。これにより、Javaプログラムはオペレーティングシステムに関係なくどこでも実行できるようになりますが、オペレーティングシステムが提供する特定の機能を使用するのが難しくなります。これが、JavaでCLIを作成したり、OSのメトリクスを収集したりするのが難しい理由です。

しかし、限られたOS機能を活用する方法(JNI)があり、Systemはこの機能を提供します。主な機能のいくつかは次のとおりです:

  • 標準入力
  • 標準出力
  • 環境変数の設定
  • 実行中のアプリケーションを終了し、ステータスコードを返す

Hello Worldを出力するために、Systemの標準出力機能を使用しています。

実際、System.out.printlnの流れを追うと、nativeキーワードが付いたwriteBytesメソッドに出会い、この操作をCコードに委譲し、標準出力に転送することがわかります。

// FileOutputStream.java
private native void writeBytes(byte b[], int off, int len, boolean append)
throws IOException;

nativeキーワードが付いたメソッドの呼び出しは、Java Native Interface(JNI)を通じて動作します。これは後の章で取り上げます。

String

Javaの文字列は少し特別です。いや、かなり特別なようです。文字列は別のメモリ空間に割り当てられ、特別に扱われていることがわかります。なぜでしょうか?

文字列の次の特性に注目することが重要です:

  • 非常に大きくなる可能性がある。
  • 比較的頻繁に再利用される。

したがって、文字列は一度作成されたら再利用することに重点を置いて設計されています。大きな文字列データがメモリ内でどのように管理されるかを完全に理解するには、後で取り上げるトピックの理解が必要です。ここでは、メモリ空間の節約の原則について簡単に触れておきます。

まず、Javaで文字列がどのように宣言されるかを見てみましょう。

String greeting = "Hello World";

内部的には次のように動作します:

文字列はString Constant Poolに作成され、不変の特性を持っています。一度文字列が作成されると変更されず、新しい文字列を作成する際にConstant Poolに同じ文字列が見つかると、それが再利用されます。

次の章では、JVMスタック、フレーム、ヒープについて取り上げます。

もう一つの文字列の宣言方法はインスタンス化です。

String greeting = new String("Hello World");

この方法は内部動作に違いがあるため、あまり使用されません。以下のように動作します。

newキーワードを使用せずに文字列を直接使用すると、String Constant Poolに作成され、再利用されます。しかし、newキーワードでインスタンス化すると、Constant Poolには作成されません。これにより、同じ文字列が複数回作成され、メモリ空間が無駄になる可能性があります。

まとめ

この章では、次の質問に答えました:

  • なぜ.javaファイル名はクラス名と一致しなければならないのか?
  • なぜpublic static void main(String[] args)でなければならないのか?
  • 出力操作の流れ
  • 文字列の特性とその作成および使用の基本原則

次の章では、Javaコードを自分でコンパイルし、バイトコードがどのように生成されるか、そのメモリアリアとの関係などを探ります。

参考文献

Footnotes

  1. Life Coding Python

  2. Life Coding Python