電気ひつじ牧場

技術メモ

Java:パッケージに分けてコンパイル

最近JVM上で動く言語をJavaで作っているのですが、Javaのパッケージの分け方や分割コンパイルで生成されたclassファイルの実行方法をよく忘れるのでメモしておきます。

package

クラスの数が増えると関連するクラスはどこかにまとめた方が分かりやすくなります。その機能を提供するのがpackageです。
普通、パッケージ名は所有するドメインを逆順に並べたものを使用しますが、説明のためここではその慣習に従いません。

Apple.java

package fruit.bara;
public class Apple {...}

Orange.java

package fruit.mikan;
public class Orange {...}

これらのパッケージに属しているクラスを利用するときにはimport文を使います

Main.java

import fruit.bara.Apple;
import fruit.mikan.Orange;

public class Main {...}

import package名.class名ですね。こんな省略記法もあります。

Main.java

package Main;
import fruit.bara.*;
import fruit.mikan.*;

public class Main {...}

import package名.* です。これでfruit.baraパッケージとfruit.mikanパッケージに属する全てのクラスを利用できるようになります。
ただ、ファイルの配置を考えないと実行することはできません。後述します。

なお、以下のように書くことはできません。

Main.java(誤り)

import fruit.*; //エラー: シンボルを見つけられません

public class Main {...}

これでfruit.baraとfruit.mikanのパッケージに属するクラス全てが使えるようになりそうですが、コンパイルエラーになります。fruit.mikanとfruit.baraは共通の親パッケージfruitに属するパッケージのような気がしますが、パッケージには親子関係というものは存在しません。

コンパイル

$ javac -Xlint:all,-serial Apple.java Orange.java Main.java

lintの警告をserial関連を除き全て出力するようにしています。
コンパイルはできますがこのままjavaコマンドで実行するとエラーになります。

パッケージとディレクトリ階層

パッケージに所属するclassファイルは正しいディレクトリに格納しないと実行できません。ディレクトリ階層のルールは以下のようになります。
「クラスパスが通っているディレクトリ/package文で宣言したパッケージ名の'.'(カンマ)を'/'(スラッシュ)に読み替えてパスにしたもの」
サンプルコードの例で言うと、
package fruit.bara;だったので、
「クラスパスの通ったディレクトリ/fruit/bara」にApple.classを配置する必要があります。

クラスパス

クラスローダーがclassファイルを検索する起点となるパスのことです。
・ カレントディレクト
java -cp [path]のように実行時に指定されたパス
・OSの環境変数CLASSPATHに登録されているパス
ユーザーが指定できるクラスパスは以上の3つです。

実行

classファイルが入るディレクトリをbuildとします。

.
├── Apple.java
├── Main.java
├── Orange.java
└── build
    ├── Main
    │   └── Main.class
    └── fruit
        ├── bara
        │   └── Apple.class
        └── mikan
            └── Orange.class
$ ls
Main.java   Main.java   Orange.java  build
$ java -cp build/ Main.Main

cpオプションで./buildをクラスパスとして指定しています。javaコマンドの引数は起動したいクラス(=main関数のあるクラス)のFQCN(パッケージを含めたクラス名)です。FQCNを指定することにより、クラスパスを起点としてMain.Main.classが検索されます。

おまけ:-dオプション

javacコマンドに-dオプションをつけるとオプションの引数で指定したディレクトリにclassファイルを出力できるようになります。さらにすごいのが、上で行ったような正しいディレクトリへの格納作業も自動で行ってくれます。

$ javac -d ./build Apple.java Orange.java Main.java
$ tree
.
├── Apple.java
├── Main.java
├── Orange.java
└── build
    ├── Main
    │   └── Main.class
    └── fruit
        ├── bara
        │   └── Apple.class
        └── mikan
            └── Orange.class

これは便利・・・!

11/12追記

上記のプロジェクトでは*.javaがプロジェクトルートにそのまま配置されていますが、普通そのようなことはしません(コンパイルは通るが)。build/以下のように、「package文で宣言したパッケージ名の'.'(カンマ)を'/'(スラッシュ)に読み替えてパスにしたもの」に配置します。