Posted at

CLIで理解するJavaのコンパイルと実行

Javaプログラミングをする場合にIDEを使うことがほとんどだと思います。Javaプログラミングの学習を始める場合でもIDEの準備から求められることが大半です。しかし初学者の方がIDEでのJavaプログラミングから学習を初めると、IDE上でコードは書けるようになってもIDEがサポートしてくれる部分を深く理解できずにいたり、IDEに依存した作業しかできない状態から成長できません。

この記事は簡単なJavaのプログラムを、IDEを使わずにCLIで実行し理解を深める初学者向けの記事となります。プログラムの詳細な読み方や書き方までは言及しません。途中初学者には難しい言葉や概念などが登場するかもしれませんが、すぐに理解する必要はないので読み流しつつ徐々に理解していってください。


環境

今回作業するOSや利用するJavaのversionになります。本記事ではCentOSで作業していますが、vagrantの導入などができない場合はMacでも問題ないですし、UNIXコマンドを置き換えればWindowsのコマンドプロンプトでも作業可能だと思います(検証はしてません)。


OS

こちらのBOXを使ってVirtualBox上にvagrantでVMを起動し、その中で作業します。

$ cat /etc/os-release

NAME="CentOS Linux"
VERSION="7 (Core)"
ID="centos"
ID_LIKE="rhel fedora"
VERSION_ID="7"
PRETTY_NAME="CentOS Linux 7 (Core)"
ANSI_COLOR="0;31"
CPE_NAME="cpe:/o:centos:centos:7"
HOME_URL="https://www.centos.org/"
BUG_REPORT_URL="https://bugs.centos.org/"

CENTOS_MANTISBT_PROJECT="CentOS-7"
CENTOS_MANTISBT_PROJECT_VERSION="7"
REDHAT_SUPPORT_PRODUCT="centos"
REDHAT_SUPPORT_PRODUCT_VERSION="7"


Java

こちらのOpenJDKをyumでインストールして使います。

$ java -version

java 10.0.2 2018-07-17
Java(TM) SE Runtime Environment 18.3 (build 10.0.2+13)
Java HotSpot(TM) 64-Bit Server VM 18.3 (build 10.0.2+13, mixed mode)

$ javac -version
javac 10.0.2


Javaプログラム

Javaのプログラムは通常、実行前にjavacツールで.javaから.classというバイトコード(中間コードとも言う)にコンパイルします。そしてプログラム実行時にJVM上でバイトコードをインタプリタ方式で実行、もしくはJITコンパイラによってマシンコードに再コンパイルして実行されます。JITコンパイラはJRE(Javaランタイム環境)のコンポーネントの一つで、プログラムメソッドのマシンコードを最適化しパフォーマンス向上をする仕組みです。

ここからはCLIで実際にJavaプログラムのコンパイルと実行、アーカイブの作成までの作業を行います。


基本となるコンパイルと実行

まずはHello world.と出力するプログラムを書いて実行してみます。実行するプログラムファイルは下記となるのでviなどで用意してください。


App.java

class App {

public static void main(String[] args) {
System.out.println("Hello world.");
}
}

これをjavacでコンパイルします。

$ ls

App.java

$ javac App.java

$ ls
App.class App.java

.classファイルが生成されたことが確認できます。その後javaで実行します。引数はファイル名ではなくクラス名となるので注意してください。


$ java App
Hello world.

簡単でしたが、コンパイルとプログラムの実行をしました。ここまでは問題ないですね。


引数ありの実行

プログラム実行時に引数を渡してみます。先ほどのプログラムファイルに下記の修正を加えます。


App.java

class App {

public static void main(String[] args) {
for (String arg: args) {
System.out.println(String.format("Hello %s.", arg));
}
}
}

コンパイルして実行します。先ほどとは違い、実行時に引数を渡してみます。

$ java App Alice Bob Carol

Hello Alice.
Hello Bob.
Hello Carol.

引数を受け取れていることが確認できますね。


他クラスの利用

プログラム内で他のクラスにアクセスしてみます。まず他のクラスとして人間を表現するHumanクラスを別のファイルで作ります。


Human.java

class Human {

String name;
Human(String name) {
this.name = name;
}
void introduceMyself() {
System.out.println(String.format("My name is %s.", this.name));
}
}

先ほどのAppクラスのmainメソッド内でHumanクラスをインスタンス化します。


App.java

class App {

public static void main(String[] args) {
for (String arg: args) {
Human human = new Human(arg);
human.introduceMyself();
}
}
}

ではコンパイルしてみます。

$ ls

App.java Human.java

$ javac App.java

$ ls
App.class App.java Human.class Human.java

Appのコンパイルと一緒にHumanもコンパイルされたことが確認できます。

$ java App Alice Bob Carol

My name is Alice.
My name is Bob.
My name is Carol.

引数の数だけHumanインスタンスを生成しメソッドが実行されたことを確認しました。


パッケージ管理

パッケージ名を付与して、別々のパッケージのプログラムとしてコンパイルしてみます。まずはHumanクラスにパッケージ名を付与します。また、このクラスは別のパッケージからアクセスされるクラスなので、各修飾子を正しく付与しました。


Human.java

package jp.co.sample.lib;

public class Human {
private String name;
public Human(String name) {
this.name = name;
}
public void introduceMyself() {
System.out.println(String.format("My name is %s.", this.name));
}
}


続いてAppクラスですが、パッケージ名を付与すると共にHumanクラスにアクセスするためにimportも記述します。


App.java

package jp.co.sample;

import jp.co.sample.lib.Human;

class App {
public static void main(String[] args) {
for (String arg: args) {
Human human = new Human(arg);
human.introduceMyself();
}
}
}


パッケージ名の付与は完了しましたが、このままではコンパイルができません。Javaではパッケージ名と同様のディレクトリ構成にしてファイルを配置する必要があります。なのでディレクトリを下記のように作りファイルを移動させてください。

$ tree

.
└── jp
└── co
└── sample
├── App.java
└── lib
└── Human.java

4 directories, 2 files

ファイルを移動させたらコンパイルして実行してみます。

$ javac jp/co/sample/App.java

$ tree
.
└── jp
└── co
└── sample
├── App.class
├── App.java
└── lib
├── Human.class
└── Human.java

4 directories, 4 files

$ java jp.co.sample.App Alice Bob Carol
My name is Alice.
My name is Bob.
My name is Carol.

.javaファイルと同階層に.classファイルが作成され、プログラムが実行できたことを確認しました。


JARファイルの作成

作成した.classファイルを.jarにまとめアーカイブを作成します。.javaファイルは.jarには含めないので、srcディレクトリとして分けます。

$ tree

.
└── src
└── jp
└── co
└── sample
├── App.java
└── lib
└── Human.java

5 directories, 2 files

コンパイルをしてclassesディレクトリに.classファイルを出力します。パッケージ起点となるsrcディレクトリでない場所で実行する場合は-sourcepathオプションでパッケージ起点を指定する必要があります。

$ javac -sourcepath src -d classes src/jp/co/sample/App.java 

$ tree
.
├── classes
│   └── jp
│   └── co
│   └── sample
│   ├── App.class
│   └── lib
│   └── Human.class
└── src
└── jp
└── co
└── sample
├── App.java
└── lib
└── Human.java

10 directories, 4 files

classesディレクトリが作成され、その下に.classファイルがパッケージと同じディレクトリ構成で生成されたことが確認できます。

ちなみに実行時にパッケージ起点でない場所で実行する場合は-classpathオプションでパッケージ起点を指定する必要があります。

$ java -classpath classes jp.co.sample.App Alice Bob Carol

My name is Alice.
My name is Bob.
My name is Carol.

続いて.jar作成のためにMANIFESTファイルが必要となるので、下記のファイルを作成します。ここにはmainメソッドを持つクラス名をパッケージ名含め記載しておきます。最終行に空行を一行入れないとMANIFESTファイルとして認識してくれないのでお忘れなく。


manifest.mf

Main-Class: jp.co.sample.App


MANIFESTファイルを用意したらjarで.jarファイルを作成します。

$ jar cvfm sample.jar manifest.mf -C classes .

マニフェストが追加されました
jp/を追加中です(=0)(=0)(0%格納されました)
jp/co/を追加中です(=0)(=0)(0%格納されました)
jp/co/sample/を追加中です(=0)(=0)(0%格納されました)
jp/co/sample/App.classを追加中です(=469)(=343)(26%収縮されました)
jp/co/sample/lib/を追加中です(=0)(=0)(0%格納されました)
jp/co/sample/lib/Human.classを追加中です(=595)(=382)(35%収縮されました)

$ ls
classes manifest.mf sample.jar src

$ jar -tf sample.jar
META-INF/
META-INF/MANIFEST.MF
jp/
jp/co/
jp/co/sample/
jp/co/sample/App.class
jp/co/sample/lib/
jp/co/sample/lib/Human.class

.jarファイルの中にMANIFESTファイルと.classファイルを内包していることが確認できます。

では最後に.jarファイルを実行してみます。

$ java -jar sample.jar Alice Bob Carol

My name is Alice.
My name is Bob.
My name is Carol.


まとめ

本記事ではプログラムを書いてコンパイルし、その後.jarファイルを作成し実行するところまで作業してみました。IDEでの作業とCLIでコマンドを叩いてコンパイルや実行をするのとでは作業内容が大きく違うと感じたことでしょう。UIのあるIDEでは直感的に作業ができるのに対し、CLIでの作業は一つ一つコマンドを理解して実行する必要があると思います。本記事の内容を理解することが、IDEでの作業理解にも活きてくると思います。


おまけ

OpenJDKには、コンパイルした.classファイルを逆アセンブルできるツールが内包されており、プログラムの詳細な命令文を追うことができるので、余裕がある方は見てみると良いでしょう。

$ javap -v -classpath classes jp.co.sample.App

Classfile /home/vagrant/java_test/classes/jp/co/sample/App.class
Last modified 2019/04/30; size 469 bytes
MD5 checksum 7ad6f96dd09200ac12a4c48cadb71ea8
Compiled from "App.java"
class jp.co.sample.App
minor version: 0
major version: 54
flags: (0x0020) ACC_SUPER
this_class: #5 // jp/co/sample/App
super_class: #6 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #6.#17 // java/lang/Object."<init>":()V
#2 = Class #18 // jp/co/sample/lib/Human
#3 = Methodref #2.#19 // jp/co/sample/lib/Human."<init>":(Ljava/lang/String;)V
#4 = Methodref #2.#20 // jp/co/sample/lib/Human.introduceMyself:()V
#5 = Class #21 // jp/co/sample/App
#6 = Class #22 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 StackMapTable
#14 = Class #23 // "[Ljava/lang/String;"
#15 = Utf8 SourceFile
#16 = Utf8 App.java
#17 = NameAndType #7:#8 // "<init>":()V
#18 = Utf8 jp/co/sample/lib/Human
#19 = NameAndType #7:#24 // "<init>":(Ljava/lang/String;)V
#20 = NameAndType #25:#8 // introduceMyself:()V
#21 = Utf8 jp/co/sample/App
#22 = Utf8 java/lang/Object
#23 = Utf8 [Ljava/lang/String;
#24 = Utf8 (Ljava/lang/String;)V
#25 = Utf8 introduceMyself
{
jp.co.sample.App();
descriptor: ()V
flags: (0x0000)
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 5: 0

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=6, args_size=1
0: aload_0
1: astore_1
2: aload_1
3: arraylength
4: istore_2
5: iconst_0
6: istore_3
7: iload_3
8: iload_2
9: if_icmpge 39
12: aload_1
13: iload_3
14: aaload
15: astore 4
17: new #2 // class jp/co/sample/lib/Human
20: dup
21: aload 4
23: invokespecial #3 // Method jp/co/sample/lib/Human."<init>":(Ljava/lang/String;)V
26: astore 5
28: aload 5
30: invokevirtual #4 // Method jp/co/sample/lib/Human.introduceMyself:()V
33: iinc 3, 1
36: goto 7
39: return
LineNumberTable:
line 7: 0
line 8: 17
line 9: 28
line 7: 33
line 11: 39
StackMapTable: number_of_entries = 2
frame_type = 254 /* append */
offset_delta = 7
locals = [ class "[Ljava/lang/String;", int, int ]
frame_type = 248 /* chop */
offset_delta = 31
}
SourceFile: "App.java"