daisukeの技術ブログ

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

Jarの作り方とJarを含んだJavaのコンパイル方法をパッケージ含めていろいろ試してみる

前回は、Javaのコンパイルと実行時のパスの関係を整理しました。

今回は、Jarを含めた場合をやっていきます。

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

参考文献

はじめに

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

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

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

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

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

docs.oracle.com

今回は Jarファイルを対象に加えます。

Jarファイルとは、複数のクラスファイルや、画像、サウンドなどのファイルをZIPで圧縮したアーカイブファイルです。適切なMANIFESTファイルを用意することで、Jarファイル自体でJavaを実行することもできます。

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

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

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

ベースとなる簡単なプログラム

Javaのバージョンを確認します。Ubuntu22.04を使ってます。

$ 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のアプリケーションを使います。

Main.javaは、パッケージに所属していない Sub.java と、パッケージに所属している Pac.java のクラスを利用しています。

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

4 directories, 3 files
// Main.java
import com.example.Pac;
public class Main {
  public static void main(String args[]){
    System.out.println( "Hello Main !" );
    String[] str = {"Main", "Sub"};
    Sub sub = new Sub();
    Pac pac = new Pac();
    sub.main(str);
    pac.main(str);
  }
}
// Sub.java
public class Sub {
  public static void main(String args[]){
    System.out.println( "Hello Sub !" );
  }
}
// Pac.java
package com.example;
public class Pac {
  public static void main(String args[]){
    System.out.println( "Hello Pac !" );
  }
}

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

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

4 directories, 6 files

Main.classが生成されました。合わせて、Sub.class と Pac.class も生成されました。

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

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

クラスパスの指定が複雑になっています。. はMainクラスに必要で、src はPacクラスに必要で、src/com/example はSubクラスに必要です。

本来、Javaはパッケージとディレクトリ構造を合わせるべきなので、以下のような構成にする方が普通だと思います。

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

3 directories, 3 files

こういう構成にした場合についても最後に説明します。

Jarファイルの作り方

マニュアルは簡単なことだけが書かれていました。マニュアルの中に、Javaチュートリアルがあり、そこにJarに関するチュートリアル(ただし、Java8用)があったので、それを見ながらやっていきます。

Jarファイルは、jarコマンドで作ります。たくさんのオプションがありますが、ここではJarファイルを新規作成する場合を対象にやっていきます。

今回使用するオプションについて説明します。

  • c:Jarファイルを新規作成します(fオプションが指定されている場合はファイルに出力され、指定されてない場合は標準出力に出力されます)
  • v:詳細な情報を標準出力に出力します
  • f jarfile:Jarファイルを指定します
  • e entrypoint:指定したエントリポイントで実行可能にします
  • t:Jarファイルの内容を一覧表示します
  • -C dir:一時的にdirにディレクトリを変更します

パッケージに所属していないクラスを利用する場合

まず、SubクラスだけをJarファイルに格納して、Main.javaから利用する形でやってみます。

Main.javaを、Pacクラスを使わないように変更しておきます。

//import com.example.Pac;

public class Main {
  public static void main(String args[]){
    System.out.println( "Hello Main !" );
    String[] str = {"Main", "Sub"};
    Sub sub = new Sub();
    //Pac pac = new Pac();
    sub.main(str);
    //pac.main(str);
  }
}

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

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

4 directories, 3 files

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

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

4 directories, 4 files

Sub.classで、Jarファイルを作成します。

ここで注意点です。Jarファイルを作成するとき、対象のクラスファイルが、カレントディレクトリにクラスファイルが見えている状態で、Jarファイルを作らないとうまくいきませんでした(後述しますが、Jarファイルはディレクトリ構成を保持するので、カレントディレクトリ以外のクラスファイルをJarファイルに格納すると、うまくいきませんでした)。

ということで、Sub.classファイルのあるディレクトリまで移動してから、Jarファイルを作成します。

$ cd src/com/example/
$ jar cvf Sub.jar Sub.class
マニフェストが追加されました
Sub.classを追加中です(=411)(=283)(31%収縮されました)
$ cd -
$ mv src/com/example/Sub.jar classes/
$ tree
.
|-- Main.java
|-- classes
|   `-- Sub.jar
`-- src
    `-- com
        `-- example
            |-- Pac.java
            |-- Sub.class
            `-- Sub.java

4 directories, 5 files
$ jar tf classes/Sub.jar
META-INF/
META-INF/MANIFEST.MF
Sub.class

Sub.jarを移動したのは、同じディレクトリに、Sub.javaやSub.classがあると、それを使って、Main.javaがコンパイルできてしまうためです。

Main.javaをコンパイルします。Main.javaはSubクラスに依存しているので単体ではコンパイルできません。クラスパスに、先ほど作成したJarファイルを追加します。

$ javac Main.java
Main.java:7: エラー: シンボルを見つけられません
    Sub sub = new Sub();
    ^
  シンボル:   クラス Sub
  場所: クラス Main
Main.java:7: エラー: シンボルを見つけられません
    Sub sub = new Sub();
                  ^
  シンボル:   クラス Sub
  場所: クラス Main
エラー2個
$ javac -cp classes/Sub.jar Main.java
$ tree
.
|-- Main.class
|-- Main.java
|-- classes
|   `-- Sub.jar
`-- src
    `-- com
        `-- example
            |-- Pac.java
            |-- Sub.class
            `-- Sub.java

4 directories, 6 files

Jarファイルを使って、Main.javaをコンパイルすることが出来ました。

それでは、実行してみます。

クラスパスを指定しない場合は、カレントディレクトリを探してくれます(以下では、Hello Main !だけ実行できています)が、クラスパスを指定すると、カレントディレクトリは見てくれません。よって、Jarファイルがあるディレクトリと、Main.classがあるカレントディレクトリの両方をクラスパスに指定する必要があります。

$ java Main
Hello Main !
Exception in thread "main" java.lang.NoClassDefFoundError: Sub
        at Main.main(Main.java:7)
Caused by: java.lang.ClassNotFoundException: Sub
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:525)
        ... 1 more
$ java -cp classes/Sub.jar Main
エラー: メイン・クラスMainを検出およびロードできませんでした
原因: java.lang.ClassNotFoundException: Main
$ java -cp .:classes/Sub.jar Main
Hello Main !
Hello Sub !

パッケージに所属していないクラスを利用する場合で別ディレクトリのクラスファイルを利用する場合

次は、Jarファイルにディレクトリ構造を持たせる場合です。現状の私の知識では、コンパイルできませんでした

クラスファイルやJarファイルは削除した状態から始めます。Main.javaはSubクラスだけを参照しています。

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

4 directories, 3 files

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

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

4 directories, 4 files

別ディレクトリにクラスファイルがある状態で、Jarファイルを作ります。

$ jar cvf Sub.jar src/com/example/Sub.class
マニフェストが追加されました
src/com/example/Sub.classを追加中です(=411)(=283)(31%収縮されました)

Jarファイルの内容を確認します。Sub.classがディレクトリ構造を持っていることが分かると思います。

$ jar tf Sub.jar
META-INF/
META-INF/MANIFEST.MF
src/com/example/Sub.class
$ tree
.
|-- Main.java
|-- Sub.jar
|-- classes
`-- src
    `-- com
        `-- example
            |-- Pac.java
            |-- Sub.class
            `-- Sub.java

4 directories, 5 files

では、このJarファイルを使って、Main.javaをコンパイルしていきます。

Jarファイルをクラスパスに指定しただけでは失敗します。

$ javac -cp Sub.jar Main.java
Main.java:7: エラー: シンボルを見つけられません
    Sub sub = new Sub();
    ^
  シンボル:   クラス Sub
  場所: クラス Main
Main.java:7: エラー: シンボルを見つけられません
    Sub sub = new Sub();
                  ^
  シンボル:   クラス Sub
  場所: クラス Main
エラー2

いろいろやってみましたが、コンパイルできませんでした。

パッケージに所属しているクラスを利用する場合

次は、パッケージに所属しているクラス(Pac.java)を利用する場合です。

まず、Main.javaをSubクラスは使わず、Pacクラスを使うように変更します。

import com.example.Pac;

public class Main {
  public static void main(String args[]){
    System.out.println( "Hello Main !" );
    String[] str = {"Main", "Sub"};
    //Sub sub = new Sub();
    Pac pac = new Pac();
    //sub.main(str);
    pac.main(str);
  }
}

ソースファイルだけの状態から始めます。

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

4 directories, 3 files

まず、Pac.javaをコンパイルします。

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

4 directories, 4 files

Pac.javaは「package com.example」なので、srcディレクトリに移動してJarファイルを作る必要があります。

$ cd src/
$ jar cvf Pac.jar com/example/Pac.class
マニフェストが追加されました
com/example/Pac.classを追加中です(=423)(=294)(30%収縮されました)
$ jar tf Pac.jar
META-INF/
META-INF/MANIFEST.MF
com/example/Pac.class
$ cd ..
$ tree
.
|-- Main.java
|-- classes
`-- src
    |-- Pac.jar
    `-- com
        `-- example
            |-- Pac.class
            |-- Pac.java
            `-- Sub.java

4 directories, 5 files

それでは、Main.javaをコンパイルします。

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

4 directories, 6 files

コンパイルできたので、実行してみます。

$ java -cp .:src/Pac.jar Main
Hello Main !
Hello Pac !

成功しました。

-Cオプションを使って、ディレクトリを移動せずにJarファイルを作る方法

jarコマンドの-Cオプションを使うと、-Cオプションで指定したディレクトリに移動したことと同じ効果があります。

ソースファイル(.java)以外は、あらかじめ削除しておきます。

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

4 directories, 3 files

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

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

4 directories, 4 files

-Cオプションでsrcディレクトリの位置でJarファイルを作ります。クラスファイルの指定が、srcディレクトリからの指定になることに注意です。

$ jar cvf classes/Pac.jar -C src/ com/example/Pac.class
マニフェストが追加されました
com/example/Pac.classを追加中です(=423)(=294)(30%収縮されました)
$ tree
.
|-- Main.java
|-- classes
|   `-- Pac.jar
`-- src
    `-- com
        `-- example
            |-- Pac.class
            |-- Pac.java
            `-- Sub.java

4 directories, 5 files

それでは、Main.javaをコンパイルします。

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

4 directories, 6 files

では、実行してみます。

$ java -cp .:classes/Pac.jar Main
Hello Main !
Hello Pac !

自然なディレクトリ構成の場合

本来、Javaはパッケージとディレクトリ構造を合わせるべきなので、以下のような構成にする方が普通です。

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

3 directories, 3 files

こういう構成にした場合ののコンパイルです。オプションを指定する必要がありません。

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

3 directories, 6 files

実行してみます。

$ java Main
Hello Main !
Hello Sub !
Hello Pac !

さらに、実行可能なJarファイルを作成してみます。

オプションの f(Jarファイルを新規作成する)と、e:指定したエントリポイントで実行可能にする)の順序と、classes/Main.jar(Jarファイル)と Main(エントリポイント)の順序は同じにする必要があるので注意です。

$ jar cvfe classes/Main.jar Main ./*.class com/example/Pac.class
マニフェストが追加されました
Main.classを追加中です(=566)(=386)(31%収縮されました)
Sub.classを追加中です(=411)(=283)(31%収縮されました)
com/example/Pac.classを追加中です(=423)(=294)(30%収縮されました)
$ tree
.
|-- Main.class
|-- Main.java
|-- Sub.class
|-- Sub.java
|-- classes
|   `-- Main.jar
`-- com
    `-- example
        |-- Pac.class
        `-- Pac.java

3 directories, 7 files

実行してみます。

$ java -jar classes/Main.jar
Hello Main !
Hello Sub !
Hello Pac !

成功です!

おわりに

Jarファイルが入ってくると、さらにややこしくなりましたが、理解が深まりました!

一部、出来なかったところもありますが、解決したら、また追記したいと思います。

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

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