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

Javaプログラミングをする場合にIDEを使うことがほとんどだと思います。Javaプログラミングの学習を始める場合でもIDEの準備から求められることが大半です。しかし初学者の方がIDEでのJavaプログラミングから学習を初めると、IDE上でコードは書けるようになってもIDEがサポートしてくれる部分を深く理解できずにいたり、IDEに依存した作業しかできない状態から成長できません。
この記事は簡単なJavaのプログラムを、IDEを使わずにCLIで実行し理解を深める初学者向けの記事となります。プログラムの詳細な読み方や書き方までは言及しません。途中初学者には難しい言葉や概念などが登場するかもしれませんが、すぐに理解する必要はないので読み流しつつ徐々に理解していってください。

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

## OS

[こちらのBOX](https://app.vagrantup.com/bento/boxes/centos-7)を使って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](http://openjdk.java.net/tools/)を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などで用意してください。

```java:App.java
class App {
    public static void main(String[] args) {
        System.out.println("Hello world.");
    }
}
```
これをjavacでコンパイルします。

```bash
$ ls
App.java

$ javac App.java

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

```bash

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

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

```java:App.java
class App {
    public static void main(String[] args) {
        for (String arg: args) {
            System.out.println(String.format("Hello %s.", arg));
        }
    }
}
```
コンパイルして実行します。先ほどとは違い、実行時に引数を渡してみます。

```bash
$ java App Alice Bob Carol
Hello Alice.
Hello Bob.
Hello Carol.
```
引数を受け取れていることが確認できますね。

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

```java: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クラスをインスタンス化します。

```java:App.java
class App {
    public static void main(String[] args) {
        for (String arg: args) {
            Human human = new Human(arg);
            human.introduceMyself();
        }
    }
}
```
ではコンパイルしてみます。

```bash
$ ls
App.java  Human.java

$ javac App.java

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

```bash
$ java App Alice Bob Carol
My name is Alice.
My name is Bob.
My name is Carol.
```
引数の数だけHumanインスタンスを生成しメソッドが実行されたことを確認しました。

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

```java: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も記述します。

```java: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ではパッケージ名と同様のディレクトリ構成にしてファイルを配置する必要があります。なのでディレクトリを下記のように作りファイルを移動させてください。

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

4 directories, 2 files
```
ファイルを移動させたらコンパイルして実行してみます。

```bash
$ 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ディレクトリとして分けます。

```bash
$ tree
.
└── src
    └── jp
        └── co
            └── sample
                ├── App.java
                └── lib
                    └── Human.java

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

```bash
$ 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`オプションでパッケージ起点を指定する必要があります。

```bash
$ 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ファイルとして認識してくれないのでお忘れなく。

```mf:manifest.mf
Main-Class: jp.co.sample.App

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

```bash
$ 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`ファイルを実行してみます。

```bash
$ 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"
```