【Java】JARファイル[.jar]とは -使われ方とその必要性-

Java

JARファイルとは

JARファイルとは、Javaのクラスファイルを一つにまとめるための、ZIP形式のアーカイブファイルです。Javaアーカイブ( Java ARchive )の略であることからも、アーカイブファイルであることが分かります。( JARとは )

JARファイルは普及しているZIPファイル形式に基づくファイル形式で、多数のファイルを1つにまとめるために使用されます。 JAR ファイルは、基本的にはオプションのMETA-INFディレクトリを格納するZIPファイルです。

JARファイルの仕様

ZIPとは、圧縮機能を持つ(オプションとして選択できる)アーカイブファイルです。従ってZIPとは、圧縮ファイルではなく、アーカイブファイルです。

ZIPは複数のファイルを格納するシンプルなアーカイブフォーマットである。圧縮はzipアーカイブのオプションであり、圧縮が行われる場合はファイル単位に圧縮される。

ZIP > 技術的な情報

ここで重要なのは、JARファイルとは単なるZIP形式のアーカイブファイルであり、独自の仕様が定義された特別なファイル形式ではないということです。そのためJARファイルは、拡張子を .jar から .zip に手動で書き換えるだけで、通常のZIPファイルと同じように扱うことができるようになります。

これは例えば、一時的に中のクラスファイルを一部置き換えたい時や、含まれているクラスファイルのバージョンからそのJARファイルがいくつでビルドされたのかを確認したい時など、jarコマンドを使用して展開と再作成を行わなくてもGUI操作で対応できるため、簡易的な代替手段として便利です。

JARファイルとはオプションとしてマニフェストファイルを含むZIPファイルであるため、逆に通常のZIPファイルの拡張子を .jar に書き換えれば、それは定義上JARファイルと呼べる物になります。クラスファイルが一切含まれていなくても、後述 するマニフェストファイルが含まれていなくても(オプションのため必須ではない)、ZIP形式かつ拡張子が .jar でさえあれば良く、(クラスファイルがなければjavaコマンドで実行できないため全く意味のないファイルにはなりますが、)仕様上は何も変わらない同じ形式のファイルであるということです。

JARファイルとは
Javaのクラスファイルを一つにまとめるためのZIP形式のアーカイブファイル

従ってJARファイルをJARたらしめているのは、ZIP形式であることと拡張子が .jar であることの二点だけであると言えます。技術的にはZIPファイルとの間に構造上の違いはありません。語弊を恐れずに言えば、JARとはZIPの拡張ですらなく、単に拡張子が .jar になっているだけのZIPファイルそのものです。

例を挙げると次のようになります。中身にマニフェストファイルが含まれるか、クラスファイルが含まれるかは、JARファイルであるか否かに関係がありません。

ファイル内容ZIP形式か拡張子が .jarJARファイルであると..
jarコマンドで作成したexample.jarいえる
Mavenでビルドしたexample.jarいえる
通常のZIP形式ファイルexample.zip×いえない
通常のZIP形式ファイルexample.zipの拡張子をjarに書き換えたexample.jarいえる
テキストファイルexample.txtの拡張子をjarに書き換えたexample.jar×いえない

JARファイルの要件
ZIP形式であること
拡張子が .jar であること

JARファイルの使われ方

JARファイルは単なるZIPファイルであると説明しました。この拡張子を .jar に変えただけのZIPファイルが、JARファイルとしてどのように利用されるのか、次の具体例で確認していきます。

具体例①:実行用メインプログラムの配布(アーカイブのみ)

一つ目の利用メリットは、Javaのコンパイル単位の特性に対するアーカイブファイルでの配布の容易性です。

流れ1:複数の依存しあうソースファイルによるプログラムの作成

ここからの説明では、例として Launching a Multi-File Program を参考に、次のような Main.javaPersonService.javaPerson.java を使用します。MainPersonService を呼び出して Person を作成し、その name を出力するようなシンプルなプログラムです。

Main.java
package com.example;

import com.example.model.Person;
import com.example.service.PersonService;

public class Main {
    public static void main(String[] args) {
        PersonService service = new PersonService();
        Person person = service.createNewPerson();
        IO.println(person.printName() + " has been created!");
    }
}
PersonService.java
package com.example.service;

import com.example.model.Person;

public class PersonService {
    public Person createNewPerson() {
        return new Person("Person01");
    }
}
Person.java
package com.example.model;

public class Person {

    private final String name;

    public Person(String name) {
        this.name = name;
    }

    public String printName() {
        return name;
    }
}

ディレクトリ構成は次のようになっています。これはパッケージ構成とディレクトリの階層を合わせる必要があるためです。

続いてこれらをjavacコマンドでコンパイルしますが、-dオプション を使用してbinという別のディレクトリにクラスファイルを出力することにします。この時、Javaはコンパイル結果としてファイルを一つにまとめるようなことはせず、ソースファイルと1対1でクラスファイルが生成される点に留意しておきます。

続いてクラスパスを通し、javaコマンドでプログラムを実行します。クラスパスとは対応するディレクトリの起点です。クラスファイルがbinディレクトリに出力されているため、-cpオプション を使用してbinディレクトリをクラスパスに指定します。

これにより、エントリポイントとなるMainクラスの完全修飾クラス名 com.example.Main を指定することで、プログラムを実行することができます。

# クラスファイルの出力先にbinディレクトリを指定してコンパイル
# binディレクトリにはソースファイルと1対1でクラスファイルが同じディレクトリ構成で出力される
> javac -d bin src\com\example\*.java src\com\example\model\*.java src\com\example\service\*.java

# クラスパスにbinディレクトリを指定してMainクラスを実行
# プログラムが正常終了し「Person01 has been created!」が出力される
> java -cp bin com.example.Main
Person01 has been created!

流れ2:複数ファイルをコンパイルしたプログラム配布時の注意点

さてここで、上記のプログラムを配布する必要が出てきました。別チームの人に利用してもらうだけで開発や機能追加を行う予定はないため、ソースファイル( .java )は不要であり、クラスファイルだけ( .class )を渡す想定です。( 実行に必要なのはクラスファイルのみ

しかし 前述 の通り、クラスファイルはソースファイルと1対1で生成され、例えば .exe のような単一の実行ファイルが作られることはありません。従って例えば、エントリポイントとなる Main.class だけを渡しても、受け取った側はこのプログラムを正常に実行することができません。

# 単にMain.classを渡されただけでは完全修飾クラス名と正しいディレクトリ構成が分からない
# カレントディレクトリに配置してそのまま実行してもエラーになる
> java Main
エラー: メイン・クラスMainを検出およびロードできませんでした
原因: java.lang.NoClassDefFoundError: Main (wrong name: com/example/Main)

# 上記のエラーログからパッケージ構成が不適切であることが分かる
# クラスファイルを com\example\Main.class に配置して再実行する
# 今度は依存関係のクラスが見つからないエラーが発生する
# ここで初めて依存する PersonService.class も必要であることが分かる
> java com.example.Main
Exception in thread "main" java.lang.NoClassDefFoundError: com/example/service/PersonService
        at com.example.Main.main(Main.java:8)
Caused by: java.lang.ClassNotFoundException: com.example.service.PersonService
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:580)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:490)
        ... 1 more

そのため上記のプログラムを配布する際は、Main.javaPersonService.javaPerson.java を全て、しかもパッケージ階層に合わせたディレクトリ構成を維持したままの状態で、渡す必要があります。

この、Javaではコンパイルによって .exe のような単一の実行ファイルが作られることはなく、生成された複数のクラスファイルをフォルダごと渡す必要がある、という点が重要です。

流れ3:複数ファイルを含むフォルダのアーカイブを展開して利用

では続いて、フォルダごと渡す場合を考えてみます。先程はクラスファイルの出力先をbinディレクトリにしていたため、これをそのまま配布すれば上手くいきます。受け取った側はbinディレクトリへ移動してデフォルトのクラスパスを利用するか、あるいはbinディレクトリをクラスパスに追加すれば正常に実行できます。

# 受け取ったbinフォルダまで移動
> cd bin

# クラスパス未指定の場合デフォルトはカレントディレクトリのため実行できる
> java com.example.Main
Person01 has been created!

# あるいはbinを配置したディレクトリ(ここではダウンロードフォルダ)へ移動
> cd C:\Users\xxx\Downloads

# 相対パスでbinディレクトリをクラスパスに追加すれば実行できる
>java -cp bin com.example.Main
Person01 has been created!

このようにしてフォルダを他のコンピュータに送る場合、ZIP形式でアーカイブするのが一般的です。従って上記の例では、まず渡す側はbinディレクトリを bin.zip にアーカイブし、受け取った側はこの bin.zip を展開してbinフォルダに戻し、クラスパスを通してプログラムを実行する、という具合です。(これはつまり、クラスパスの活用場面で挙げたディレクトリの共有のことです。)

流れ4:アーカイブファイルをそのままクラスパスに追加して利用

しかし実際には、このように受け取ったZIPファイルを一度わざわざ展開して、フォルダに戻してからクラスパスに設定するということは通常あまり行いません。アーカイブファイルの指定でも触れている通り、クラスパスにはディレクトリの他、ZIPファイルを直接指定することが可能であるためです。

# 展開しなくてもZIPファイルのままクラスパスに追加すれば実行できる
> java -cp bin.zip com.example.Main
Person01 has been created!

ZIPファイルを作成する際、binディレクトリを直接選択してアーカイブすると、bin.zipの中にさらにbinディレクトリが存在する構成になってしまいます。この状態では、bin.zipにクラスパスを通しても、クラスファイルまでのパスとパッケージを含む完全修飾クラス名が一致せず、正常にプログラムを実行することができなくなってしまうため注意が必要です。

対応として例えば、その一つ下の(ここではcom)ディレクトリをアーカイブし、任意の名称にリネームしてZIPファイルを作成すること等が考えられます。

さて 初めの話 に戻ると、JARファイルとは拡張子が異なるだけのZIP形式アーカイブであると述べました。従ってこの bin.zip も、拡張子を書き換えて bin.jar というJARファイルにすることができます。

クラスパスにはフォルダやZIPファイルの他、JARファイルも指定可能です。そのため次のように拡張子を書き換えるだけで、全く同じZIP形式アーカイブを今度はJARファイルとしてクラスパスに追加し、プログラムを実行することができます。

# ZIPファイルをクラスパスに追加してプログラムを実行
> java -cp bin.zip com.example.Main
Person01 has been created!

# 拡張子を .zip から .jar に変更(ファイル名以外の変更は加えない)
> rename bin.zip bin.jar

# JARファイルをクラスパスに追加してプログラムを実行
> java -cp bin.jar com.example.Main
Person01 has been created!

ファイル名(拡張子)を変えただけで、ファイル自体には一切変更を加えていません。このようにJARファイルとは、単に拡張子が異なるだけのZIPファイルに過ぎないということが改めて分かります。

流れ5:アーカイブファイルの利用とエントリポイント連携の問題

このように、複数のクラスファイルをフォルダごと配布する必要性と、それに対するアーカイブファイルの利便性という観点から、ZIP形式アーカイブであるJARファイルは利用されます。そして同時に、技術的にはZIPファイルでも問題ないことが分かりました。

JARファイルの使われ方①

  • Javaはソースファイルと1対1でクラスファイルが生成される。
  • 単一の実行ファイルが生成されるわけではないため、
    1つのプログラムに対して必要なファイルが複数存在する。
  • プログラム配布の際にはこれらのファイルのアーカイブが求められる。

ZIPやJARを使えば、配布も容易でクラスパスにも直接追加可能、という点はここまでの具体例で確認した通りです。しかし .zip.jar も、あくまでクラスパスというオプションとして追加されているだけで、.exe のような実行対象そのものではありません。以下のように、javaコマンドの引数に指定されている実行対象は com.exmaple.Main というmainメソッドを持つクラスです。

> java -cp bin.zip com.example.Main
> java -cp bin.jar com.example.Main
#      ^^^^^^^^^^^ ^^^^^^^^^^^^^^^^
#   オプション <┘ └> 引数に指定されている実行対象はこちら

単にそれを実行すれば良い .exe と異なり、bin.zipbin.jar を受け取った利用者は、それを-cpオプションに追加した上で、正確にエントリポイントとなるクラスを指定しなくてはなりません。しかしJavaの場合、mainメソッドpublic static void main(String[]) と厳密に規定されている一方で、そのクラス名は定められておらず、この例のように必ずしも Main であるとは限らないため(またパッケージ次第で完全修飾クラス名も変わるため)、ZIPやJARからはそれを判別することができません。これはつまり、ZIPやJARを配布する時は常に、引数に指定する値として、エントリポイントとなるmainメソッドを持つクラスの完全修飾クラス名も併せて伝える必要があるということです。

この問題に対してJARファイルでは、mainメソッドがどのクラスに定義されているかという情報をアーカイブファイル内に持たせることが可能です。次の具体例では、この情報を記述するためのマニフェストファイルという物をJARファイルに含める場合を説明します。

具体例②:実行用メインプログラムの配布(マニフェストあり)

二つ目の利用メリットは、マニフェストファイルの存在です。JARファイルの仕様として定められたマニフェストファイルを含めることで、メタ情報を持たせたり、実行時のオプション情報を用いて単なるアーカイブファイルを実行ファイルのように扱えるようにしたりできます。

マニフェストファイル

JARファイルの仕様ではオプションとして、マニフェストファイルと呼ばれるKey-Value形式のテキストファイルを含めることができるようになっています。必須項目は存在しないため、定められた属性を必要に応じて記述することで、JARファイル自体のメタ情報や、実行時に必要な情報を付加することが可能です。マニフェストファイルは、決められた書式で情報を記述し、決められたファイル名で、JARファイル内の決められたディレクトリに、正しく配置する必要があります。

マニフェストファイル

  • 概要
    • Key-Value形式のテキストファイル
    • JARファイル自体のセキュリティおよび構成情報やメイン属性などを定義
  • ルール

マニフェストファイルに記述できる属性はいくつかありますが、代表的なものとしてMain-Class属性が挙げられます。これはmainメソッドが定義されたクラスを記述しておくことで、実行時にメインクラスの完全修飾クラス名を指定しなくても良くなるというものです。

  • スタンドアロン・アプリケーション用に定義された属性: これらの属性は、実行可能jarファイルにバンドルされているスタンドアロン・アプリケーションによって使用され、”java -jar x.jar“を実行してjavaランタイムによって直接起動できます。
    • Main-Class: この属性の値は、起動時に起動ツールによってロードされるメイン・アプリケーション・クラスのクラス名である。 値に.class拡張子をクラス名に追加することはできません。

JARマニフェスト > 主な属性

そして、Main-Class属性が記述されたマニフェストファイルを持ち、java -jar x.jar で実行できるJARファイルのことを、特に 実行可能JARファイル と呼びます。

実行可能JARファイル

実行可能JARファイルとは、エントリポイントとなるmainメソッドが定義されたクラスが、マニフェストファイルのMain-Class属性に記述されたJARファイルのことです。

前述 のただアーカイブしただけのJARファイルの場合は、-cpオプションでクラスパス追加を行い、メインクラスを引数に指定する必要がありました。実行可能JARファイルの場合は、マニフェストファイルからメインクラスの情報が読み取られるため、-jarオプションでJARファイルを直接指定するだけで実行できます。

# 単にアーカイブしただけのJARファイルを使用する場合
> java -cp bin.jar com.example.Main

# 実行可能JARファイルを使用する場合
> java -jar bin.jar

マニフェストファイルにMain-Class属性が記述されたJARファイル

ここからは順を追って、マニフェストファイルを追加し、bin.jar を実行可能JARファイルにしてみます。

まず、現状の bin.jar はマニフェストファイル自体が存在していないため次のようなエラーになります。

# マニフェストファイルがない状態でもクラスパス追加による実行は可能
> java -cp bin.jar com.example.Main
Person01 has been created!

# マニフェストファイルが無いため -jar オプションによる実行はエラー
> java -jar bin.jar
Error: Invalid or corrupt jarfile bin.jar

そこで 前述 のルールに従い、bin.jar の直下に META-INF ディレクトリを作成し、そこへ以下の MANIFEST.MF を配置します。これがマニフェストファイルです。bin.jar を一時的に bin.zip に書き換えて作業することで、容易にディレクトリ追加とファイル配置を行えます。

MANIFEST.MF
Main-Class: com.example.Main

マニフェストファイルの仕様上、必須項目というものは無いため、ここではMain-Class属性のみ記述しています。しかし、jarコマンドMaven JAR Plugin によって作成されるマニフェストファイルからも分かる通り、通常は最低限 Manifest-Version属性Created-By属性 が記載されます。

MANIFEST.MF は最終行を改行コードで終了させる必要があります。これを誤ると記述された属性が正しく認識されず、エラーの原因となります。

> java -jar bin.jar
bin.jarにメイン・マニフェスト属性がありません

マニフェストファイル(/META-INF/MANIFEST.MF)を追加すると、次のように-jarオプションにJARファイルを指定するだけで実行できるようになります。これが実行可能JARファイルです。

# 実行可能JARファイルの場合は -jar オプションで実行可能
> java -jar bin.zip
Person01 has been created!

# JARファイルであることに変わりはないため -cp オプション指定も可能
> java -cp bin.jar com.example.Main
Person01 has been created!

メインクラスを指定せず > java -jar xxx.jar だけで実行可能

元々 bin.jar はその下にある com 以下を走査させるために必要なただの起点でした。従って名称が bin.jar である必要はないため、分かりやすい名前、例えばアプリケーション名などに変更してみます。JARファイル名を変更しても、正しく指定さえすれば問題なく実行できることが分かります。

# bin.jar は実行可能JAR
> java -jar bin.jar
Person01 has been created!

# bin.jar を sample-app.jar にリネーム
> rename bin.jar sample-app.jar

# 指定するファイル名が正しければ問題なく実行可能
> java -jar sample-app.jar
Person01 has been created!

実行可能JARファイルは、-jarオプションによるコマンドラインでの実行の他、JARファイルをダブルクリックすることでも起動可能です。(※今回のサンプルプログラムの場合は、コマンドプロンプトが起動して文字列が出力された後にすぐ終了するだけのため、何も起きていないように見えます。)

デスクトップ上でJARファイルをダブルクリックすればjavaw -jarによりJARファイルが自動的に実行されます。

実行可能JARファイル

ファイル名に指定はなくダブルクリックでも起動可能

実行可能JARファイルとすることで、ダブルクリックで起動できる sample-app.jar という、実行ファイルのような振る舞いをする単一のファイルを用意することができました。

実行可能JARファイル

  • 実行ファイルのような振る舞いをする単一のJARファイル
  • マニフェストファイルにMain-Class属性が記述されたJARファイル
  • メインクラスを指定せず > java -jar xxx.jar だけで実行可能
  • ファイル名に指定はなくダブルクリックでも起動可能

JARファイルとZIPファイル

このように、本来は複数のクラスファイルが生成されそれらをまとめて取り扱わなければならないJavaプログラムを、アーカイブして配布しやすくしたり、単一の実行ファイルであるかのように扱えるようにしたりすることが、JARファイルの利用により可能となります。

一方で、これらは全てZIPファイルでも実現可能です。クラスパス追加については 前述 の通りですが、実行可能JARファイルについても、拡張子を .zip にしたところで同様に問題なく機能します。

# 実行可能JARファイル
> java -jar sample-app.jar
Person01 has been created!

# 拡張子を .jar から .zip に変更
> rename sample-app.jar sample-app.zip

# -jar の引数がZIPファイルでも実行可能
> java -jar sample-app.zip
Person01 has been created!

マニフェストファイル自体はJARファイルの仕様で規定されている物です。従ってZIPファイルにとって、マニフェストファイルのディレクトリ構成や記述内容の規約などは知らされていません。-jarオプションの側が指定された引数ファイルを解釈する際、JARファイルを読み取るように作られており、実体としては同じであるZIPファイルも技術的には解釈可能であるため実行できているという状況です。

例えばマニフェストファイルが存在しないエラーの場合、ZIPファイルを指定していても Invalid or corrupt jarfile とうメッセージが出力され zipfile のようにはなっていません。このことからも技術仕様上は同じなので動いているだけで、ZIPファイルの指定が想定されているわけではないということが分かります。

# マニフェストファイルがないJARファイルを実行しようとした場合
> java -jar sample-app.jar
Error: Invalid or corrupt jarfile sample-app.jar

# マニフェストファイルがないZIPファイルを実行しようとした場合
# Invalid or corrupt zipfile のようにはなっておらずJARファイルの時と同じ文言
> java -jar sample-app.zip
Error: Invalid or corrupt jarfile sample-app.zip

JARファイルの使われ方②

  • マニフェストファイルが配置され実行可能JARファイルとして利用される。

実行可能JARファイルに限って言えば、同じファイルでも拡張子が異なっているメリットが明確に一つ挙がります。前述 の通り実行可能JARはダブルクリックにより起動が可能ですが、これは同じファイルでも拡張子が異なれば挙動を変えることができます。つまり、sample-app.jar をダブルクリックした場合はプログラムの起動、sample-app.zip をダブルクリックした場合はエクスプローラで中身を表示、といった具合です。技術仕様上は同一ファイルでも、拡張子が異なることで関連付けが変わります。

とはいえ、必ずしも全てのJARファイルが実行可能JARファイルであるというわけではありません。エントリポイントとなるメインクラスを持たない、ライブラリ用として配布されているJARファイルも多数存在します。

具体例③:ライブラリ用プログラムの配布(メインクラスなし)

ライブラリとして作成されたJARファイルの場合、他のプログラムから呼び出されることが前提のため、メインクラスを持たないことがほとんどです。そのため実行可能JARファイルにする必要はなく、(通常はメタ情報などを含みますが、)マニフェストファイルは無くても問題ありません。

JARファイルの使われ方③

  • JavaライブラリはJARファイルの形式で配布される。

このようなJARファイルの場合、ZIPファイルのままクラスパスに追加すれば良いため、拡張子が .jar である必要性はありません。技術的にはJARファイルの存在は不要とも言えます。

それでもJavaのシステムで標準的にJARファイルが使用されている理由は、例えばJavaライブラリの配布のされ方などを見て推し量ることができます。

JAVAファイルの必要性

JavaのOSSライブラリである Apache Commons Lang は、多数のクラスファイルをJARファイルにまとめて配布されています。これを自身のプログラムで利用したい場合、commons-lang3-x.x.x.jar をダウンロードしてクラスパスを通せば、それだけでそこに格納されている全てのクラスを使用できるようになります。

Commons Lang のダウンロードページを見てみると、.tar.gz.zip といった一般的なアーカイブ形式が並んでいますが、Javaアーカイブである .jar はありません。しかし commons-lang3-x.x.x-bin.zip をダウンロードして展開してみると、その中には commons-lang3-x.x.x.jarcommons-lang3-x.x.x-javadoc.jar といったJARファイルがいくつも格納されています。

ここでは同じZIP形式アーカイブが明確に使い分けられています。そして利用者は特に何の説明もなしに、ダウンロードしたZIPファイルを展開し、中に含まれているJARファイルがライブラリ本体である、ということを瞬時に理解できます。これが、JARファイルが必要な理由です。

ダウンロードページには送受信用に一般的なアーカイブ形式のZIPが、その中に含まれるJavaシステムにライブラリとして使用するためのファイルにはJavaアーカイブであるJARが、それぞれ使われているということです。従ってJARファイルとは、物理的にはZIPファイルと同一であるものの、論理的にZIPファイルと区別するために異なる拡張子 .jar を持つファイルであると言えます。

これは丁度、全く同じファイルであっても、ダブルクリックした時に、拡張子が .zip ならエクスプローラで中身を表示し、拡張子が .jar ならJavaプログラムを起動するという、実行可能JARに対する Windows の挙動と同様です。開発者も拡張子という目印を基に判断して、そのファイルの取り扱い方を決定できます。

JARファイルの必要性
ZIPファイルと物理的に同一のファイルを論理的に区別する
配置されたファイルが一目見てJava用のライブラリであると判別できるようにする目印

JARファイルの要点

  • JARファイルとは
    • Javaのクラスファイルを一つにまとめるためのZIP形式のアーカイブファイル
  • JARファイルの要件
    • ZIP形式であること
    • 拡張子が .jar であること
  • JARファイルの使われ方
    • 複数クラスファイルを配布する際にアーカイブファイルにまとめる
    • マニフェストファイルが配置され実行可能JARファイルとして利用される
    • 一般的なライブラリ(=複数クラスファイル)もJARファイル形式で配布される
  • JARファイルの必要性
    • ZIPファイルと物理的に同一のファイルを論理的に区別する
    • 配置されたファイルが一目見てJavaライブラリと判別できるようにする目印