daisukeの技術ブログ

AI、機械学習、最適化、Pythonなどについて、技術調査、技術書の理解した内容、ソフトウェア/ツール作成について書いていきます

Javaのコンパイル方法(仕組み)をパッケージ含めていろいろ試してみる

Javaのソースコード(.java)を、javacコマンドでコンパイルするときパスの指定や、実行時のパス指定がややこしいと感じたので、今回は、そのあたりを整理して、実際にいろいろなパターンで、コンパイル、実行していきます。

それではやっていきます!

参考文献

はじめに

「Javaでデザインパターンを学ぶ」の記事一覧です。良かったら参考にしてください。

Javaでデザインパターンの記事一覧

Javaのソースコード(.java)を、javacコマンドでコンパイルすると、中間コードやバイトコードなどと呼ばれるクラスファイル(.class)が作られます。このクラスファイルは、JavaVM(Java Virtual Machine)は、Java仮想マシンなどと呼ばれる、JVMの上で動きます。

通常のプログラム(ネイティブプログラム)は、直接CPUで実行できるプログラムですが、Javaの場合は、バイトコードという中間コードを作ることで、Windows、Linux、MACのどのプラットフォームでも動くようにしています。

Javaは、IDE(統合開発環境)を使って開発する場合、このようなことは意識しなくても出来てしまいます。しかし、統合開発環境でトラブルが起きたときは、その解決には、これらの知識が必要になります。

デザインパターンを学びながら、こういったトラブルを解決できる知識も身に着けていきたいと思います。

また、今回は Ubuntu22.04 を使います。ディレクトリ構造の表示に、treeコマンドを使っています。

インストールは、以下のようにします。

$ sudo apt install tree

また、通常のtreeコマンドの出力は文字化けする場合があるので、--charset=C を付けて実行します。毎回付けるのは面倒なので、エイリアスを設定しています。

# .bashrc
alias tree='tree --charset=C'

エンジニアグループのランキングに参加中です。

気楽にポチッとよろしくお願いいたします🙇

それでは、やっていきます。

JavaプログラムがJVMで実行されるまでの流れ

マニュアルを見ながらやっていきます。

docs.oracle.com

Javaのバージョンを確認します。

$ java -version
openjdk version "17.0.10" 2024-01-16
OpenJDK Runtime Environment (build 17.0.10+7-Ubuntu-122.04.1)
OpenJDK 64-Bit Server VM (build 17.0.10+7-Ubuntu-122.04.1, mixed mode, sharing)

簡単なJavaのアプリケーションで試します。

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

Main.javaをコンパイルします。

$ javac Main.java
$ ls
Main.class  Main.java

Main.classが生成されました。

Main.classを実行してみます。

$ java Main
Hello world !

ソースコードがカレントディレクトリではなく別ディレクトリにある場合

ディレクトリを作成します。先ほどのMain.classは削除しておきます。

mkdir -p src/com/example
rm Main.class

src/com/exampleにSub.javaを作ります。

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

Sub.javaをコンパイルします。

javac(コンパイラ)コマンドは、コマンドライン引数に、ソースファイルを指定する、ということを意識しておくと混乱しないと思います。

$ javac src/com/example/Sub.java
$ tree
.
|-- Main.java
`-- src
    `-- com
        `-- example
            |-- Sub.class
            `-- Sub.java

3 directories, 3 files

Sub.classが生成されました。

Sub.classを実行します。-cp は、クラスファイルのパスを指定するもので、-classpath でもよいです。

javaコマンドはクラスを引数に取るので、Sub.classとはならないことに注意です。

$ java -cp src/com/example Sub
Hello Sub !

パッケージに所属している場合

パッケージに所属したソースコードを作ります。

src/com/exampleにPac.javaを作ります。

package com.example;

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

Pac.javaをコンパイルします。ここまではSub.javaと同じですね。

$ javac src/com/example/Pac.java
$ tree
.
`-- src
    `-- com
        `-- example
            |-- Pac.class
            |-- Pac.java
            |-- Sub.class
            `-- Sub.java

3 directories, 4 files

Pac.classが生成されました。

Pac.classを実行します。この実行方法は、Sub.classと異なります。

$ java -cp src/ com.example.Pac
Hello Pac !

慣れないうちはややこしいですね。

クラスファイルを別ディレクトリに出力する場合

Pac.classとSub.classを削除しておきます。

クラスファイルを格納するディレクトリとして、classesを作成しておきます。

$ rm src/com/example/Pac.class src/com/example/Sub.class
$ mkdir classes
$ tree
.
|-- classes
`-- src
    `-- com
        `-- example
            |-- Pac.java
            `-- Sub.java

4 directories, 2 files

Sub.javaをコンパイルします。

$ javac -d classes src/com/example/Sub.java
$ tree
.
|-- classes
|   `-- Sub.class
`-- src
    `-- com
        `-- example
            |-- Pac.java
            `-- Sub.java

classesにクラスファイルが出力されました。

Sub.classを実行してみます。クラスパスの設定が先ほどと変わっているので注意です。

$ java -cp classes/ Sub
Hello Sub !

では、次は、Pac.javaをコンパイルします。

$ javac -d classes/ src/com/example/Pac.java
$ tree
.
|-- classes
|   |-- Sub.class
|   `-- com
|       `-- example
|           `-- Pac.class
`-- src
    `-- com
        `-- example
            |-- Pac.java
            `-- Sub.java

6 directories, 4 files

コンパイル方法は、Sub.javaと同じですが、クラスファイルはパッケージに合わせて、自動的に作成されたディレクトリに出力されました。

Pac.javaを実行します。

$ java -cp classes/ com.example.Pac
Hello Pac !

クラスパスの指定が変わってるだけで、あとは先ほどと同じですね。

クラス間に依存関係がある場合(パッケージに所属してない場合)

まず、classファイルを削除しておきます。

$ rm -rf classes/*

Main.javaを復活させて、Subクラスを使うように変更します。

Subクラスのmainメソッドをコールするとき、引数が必要なので、適当な文字列を作成しています。

public class Main {
  public static void main(String args[]){
    Sub sub = new Sub();
    String[] str = {"main", "sub"};
    sub.main(str);
  }
}

現在の状態です。

$ tree
.
|-- Main.java
|-- classes
`-- src
    `-- com
        `-- example
            |-- Pac.java
            `-- Sub.java

Main.javaをコンパイルします。Subクラスに依存しているので、ソースパスを指定します。

-cp(クラスパス指定)でも動作します。マニュアルによると、「ソースパスが指定されていない場合、クラスパスからソースファイルを検索する」とありました。ソースパスは、クラスパスのように短縮形がないので、クラスパスを使う人が多いのかもしれません(笑)

$ javac -sourcepath src/com/example Main.java
$ tree
.
|-- Main.class
|-- Main.java
|-- classes
`-- src
    `-- com
        `-- example
            |-- Pac.java
            |-- Sub.class
            `-- Sub.java

4 directories, 5 files

Main.classとともに、Sub.classも作成されています。

Mainクラスを実行します。クラスパスの指定が複数必要な場合は、: で繋ぎます。

$ java -cp .:src/com/example/ Main
Hello Sub !

Subクラスから出力されていますね、成功です。

次は、classesディレクトリにクラスファイルを出力するようにしてみます。

$ rm Main.class src/com/example/Sub.class
$ tree
.
|-- Main.java
|-- classes
`-- src
    `-- com
        `-- example
            |-- Pac.java
            `-- Sub.java

4 directories, 3 files
$ javac -d classes/ -sourcepath src/com/example Main.java
$ tree
.
|-- Main.java
|-- classes
|   |-- Main.class
|   `-- Sub.class
`-- src
    `-- com
        `-- example
            |-- Pac.java
            `-- Sub.java

4 directories, 5 files

Mainクラスを実行します。今度はクラスパスの指定が1か所だけでいいですね。

$ java -cp classes/ Main
Hello Sub !

クラス間に依存関係がある場合(パッケージに所属している場合)

次は、パッケージ版をやってみましょう。

まず、classファイルを削除しておきます。

$ rm -rf classes/*

Mainクラスで、Subクラスを呼び出していたところを、Pacクラスを呼び出すように変更します。パッケージなので、import文を使います。

import com.example.Pac;

public class Main {
  public static void main(String args[]){
    Pac pac = new Pac();
    String[] str = {"main", "sub"};
    pac.main(str);
  }
}

Main.javaをコンパイルします。

$ javac -sourcepath src Main.java
$ tree
.
|-- Main.class
|-- Main.java
|-- classes
`-- src
    `-- com
        `-- example
            |-- Pac.class
            |-- Pac.java
            `-- Sub.java

4 directories, 5 files

Subクラスのときと同様に、Pacクラスもコンパイルされました。

では、Mainクラスを実行します。クラスパスはMainクラスとPacクラスの両方が見えるようにします。

$ java -cp .:src Main
Hello Pac !

成功です。

では、最後に、classesディレクトリにクラスファイルを生成するようにしてみます。

まず、クラスファイルを削除しておきます。

$ rm Main.class src/com/example/Pac.class
$ tree
.
|-- Main.java
|-- classes
`-- src
    `-- com
        `-- example
            |-- Pac.java
            `-- Sub.java

4 directories, 3 files

Main.javaをコンパイルします。

$ javac -d classes/ -sourcepath src Main.java
$ tree
.
|-- Main.java
|-- classes
|   |-- Main.class
|   `-- com
|       `-- example
|           `-- Pac.class
`-- src
    `-- com
        `-- example
            |-- Pac.java
            `-- Sub.java

6 directories, 5 files

続いて、Mainクラスを実行します。クラスパスが1か所になるのがいいですね。

$ java -cp classes/ Main
Hello Pac !

成功です!やってるうちに慣れてきたと思います!

おわりに

パッケージが入ってくると、ややこしいですが、整理できたと思います。

今回の内容が参考になれば幸いです。

最後までお読みいただき、ありがとうございました。